Commit b2800f26 authored by edy's avatar edy

refactor(ui): reduce app renderer prop assembly

parent 625ebaeb
...@@ -1355,10 +1355,13 @@ export default function App() { ...@@ -1355,10 +1355,13 @@ export default function App() {
const conversationWorkspaceProps = { const conversationWorkspaceProps = {
viewMode, viewMode,
workspaceRef: conversationWorkspaceRef, workspaceRef: conversationWorkspaceRef,
messageListRef, status: {
attachmentInputRef,
skillMenuRef,
panelActions, panelActions,
showInlineStartupNotice,
chatLaunchState,
startupCurtainStatus
},
emptyState: {
selectedSkillBadge, selectedSkillBadge,
selectedSkillIsDefault: selectedSkillId === DEFAULT_SKILL.id, selectedSkillIsDefault: selectedSkillId === DEFAULT_SKILL.id,
homeLeadIcon: <LobsterClawIcon />, homeLeadIcon: <LobsterClawIcon />,
...@@ -1367,10 +1370,6 @@ export default function App() { ...@@ -1367,10 +1370,6 @@ export default function App() {
activeExpertVisualKey, activeExpertVisualKey,
activeExpertGuide, activeExpertGuide,
expertWorkspaceLogo, expertWorkspaceLogo,
renderExpertIcon,
showInlineStartupNotice,
chatLaunchState,
startupCurtainStatus,
homeIntentSuggestion: pendingHomeIntentSuggestion?.suggestion ?? null, homeIntentSuggestion: pendingHomeIntentSuggestion?.suggestion ?? null,
homeIntentDecisionPending, homeIntentDecisionPending,
homeIntentLabels: { homeIntentLabels: {
...@@ -1380,9 +1379,6 @@ export default function App() { ...@@ -1380,9 +1379,6 @@ export default function App() {
continue: ui.suggestionContinue, continue: ui.suggestionContinue,
switchAction: ui.suggestionSwitchAction switchAction: ui.suggestionSwitchAction
}, },
renderIntentIcon: getIntentSuggestionIcon,
onContinueHomeIntent: continuePendingHomePromptInHome,
onSwitchHomeIntent: switchExpertAndContinuePendingHomePrompt,
showBindEntry, showBindEntry,
bindEntry: { bindEntry: {
lobsterKeyDraft, lobsterKeyDraft,
...@@ -1396,8 +1392,10 @@ export default function App() { ...@@ -1396,8 +1392,10 @@ export default function App() {
noExpertsLabel: expertsPageCopy.noExperts, noExpertsLabel: expertsPageCopy.noExperts,
starterQuestionsHint: ui.starterQuestionsHint, starterQuestionsHint: ui.starterQuestionsHint,
homeEmptyTitle: homeChatCopy.emptyTitle, homeEmptyTitle: homeChatCopy.emptyTitle,
homePrompts: homeChatCopy.prompts, homePrompts: homeChatCopy.prompts
onStarterPrompt: applyStarterPrompt, },
messages: {
messageListRef,
messages, messages,
showEmptyState, showEmptyState,
messageTraces, messageTraces,
...@@ -1412,17 +1410,11 @@ export default function App() { ...@@ -1412,17 +1410,11 @@ export default function App() {
copyIcon: <CopyIcon />, copyIcon: <CopyIcon />,
copiedIcon: <CheckIcon />, copiedIcon: <CheckIcon />,
deleteIcon: <TrashIcon />, deleteIcon: <TrashIcon />,
regenerateIcon: <RefreshIcon />, regenerateIcon: <RefreshIcon />
renderThumbIcon: (direction) => <ThumbIcon direction={direction} />, },
renderMarkdownContent, composer: {
buildDouyinVideoStatusCard, attachmentInputRef,
formatMessageTimestamp, skillMenuRef,
onMessageListScroll: handleMessageListScroll,
onCopyText: handleCopyText,
onDeleteMessage: deleteMessage,
onTraceExpandedChange: setMessageTraceExpanded,
onRegenerateAssistantMessage: regenerateAssistantMessage,
onToggleMessageReaction: toggleMessageReaction,
prompt, prompt,
isBound, isBound,
canSend, canSend,
...@@ -1441,7 +1433,24 @@ export default function App() { ...@@ -1441,7 +1433,24 @@ export default function App() {
skills: effectiveSkills, skills: effectiveSkills,
skillMenuOpen, skillMenuOpen,
attachmentIcon: <AttachmentIcon />, attachmentIcon: <AttachmentIcon />,
submitIcon: sendPhase !== "idle" ? <StopIcon /> : <ArrowUpIcon />, submitIcon: sendPhase !== "idle" ? <StopIcon /> : <ArrowUpIcon />
},
actions: {
renderExpertIcon,
renderIntentIcon: getIntentSuggestionIcon,
onContinueHomeIntent: continuePendingHomePromptInHome,
onSwitchHomeIntent: switchExpertAndContinuePendingHomePrompt,
onStarterPrompt: applyStarterPrompt,
renderThumbIcon: (direction) => <ThumbIcon direction={direction} />,
renderMarkdownContent,
buildDouyinVideoStatusCard,
formatMessageTimestamp,
onMessageListScroll: handleMessageListScroll,
onCopyText: handleCopyText,
onDeleteMessage: deleteMessage,
onTraceExpandedChange: setMessageTraceExpanded,
onRegenerateAssistantMessage: regenerateAssistantMessage,
onToggleMessageReaction: toggleMessageReaction,
onSubmit: sendPrompt, onSubmit: sendPrompt,
onCancel: cancelActiveStream, onCancel: cancelActiveStream,
onPromptChange: setPrompt, onPromptChange: setPrompt,
...@@ -1459,6 +1468,7 @@ export default function App() { ...@@ -1459,6 +1468,7 @@ export default function App() {
onResizePointerDown: handleComposerResizePointerDown, onResizePointerDown: handleComposerResizePointerDown,
onResizePointerMove: handleComposerResizePointerMove, onResizePointerMove: handleComposerResizePointerMove,
onResizePointerEnd: handleComposerResizePointerEnd onResizePointerEnd: handleComposerResizePointerEnd
}
} satisfies ComponentProps<typeof ConversationWorkspaceView>; } satisfies ComponentProps<typeof ConversationWorkspaceView>;
const settingsStatusHint = showSettingsStatusHint const settingsStatusHint = showSettingsStatusHint
? <div className={"inline-hint settings-runtime-hint" + (chatLaunchState === "error" ? " error" : "")}>{startupMessage}</div> ? <div className={"inline-hint settings-runtime-hint" + (chatLaunchState === "error" ? " error" : "")}>{startupMessage}</div>
......
...@@ -28,13 +28,14 @@ interface HomeStarterPrompt { ...@@ -28,13 +28,14 @@ interface HomeStarterPrompt {
prompt: string prompt: string
} }
interface ConversationWorkspaceViewProps { interface ConversationWorkspaceStatusProps {
viewMode: ViewMode
workspaceRef: RefObject<HTMLDivElement | null>
messageListRef: RefObject<HTMLDivElement | null>
attachmentInputRef: RefObject<HTMLInputElement | null>
skillMenuRef: RefObject<HTMLDivElement | null>
panelActions: ReactNode panelActions: ReactNode
showInlineStartupNotice: boolean
chatLaunchState: ChatLaunchState
startupCurtainStatus: string
}
interface ConversationWorkspaceEmptyStateProps {
selectedSkillBadge: string selectedSkillBadge: string
selectedSkillIsDefault: boolean selectedSkillIsDefault: boolean
homeLeadIcon: ReactNode homeLeadIcon: ReactNode
...@@ -43,10 +44,6 @@ interface ConversationWorkspaceViewProps { ...@@ -43,10 +44,6 @@ interface ConversationWorkspaceViewProps {
activeExpertVisualKey: ExpertVisualKey activeExpertVisualKey: ExpertVisualKey
activeExpertGuide: ExpertGuideContent activeExpertGuide: ExpertGuideContent
expertWorkspaceLogo: ReactNode expertWorkspaceLogo: ReactNode
renderExpertIcon: (expertKey: ExpertVisualKey) => ReactNode
showInlineStartupNotice: boolean
chatLaunchState: ChatLaunchState
startupCurtainStatus: string
homeIntentSuggestion: ProjectIntentSuggestion | null homeIntentSuggestion: ProjectIntentSuggestion | null
homeIntentDecisionPending: boolean homeIntentDecisionPending: boolean
homeIntentLabels: { homeIntentLabels: {
...@@ -56,9 +53,6 @@ interface ConversationWorkspaceViewProps { ...@@ -56,9 +53,6 @@ interface ConversationWorkspaceViewProps {
continue: string continue: string
switchAction: string switchAction: string
} }
renderIntentIcon: (projectId: string) => ReactNode
onContinueHomeIntent: () => void | Promise<unknown>
onSwitchHomeIntent: () => void | Promise<unknown>
showBindEntry: boolean showBindEntry: boolean
bindEntry: { bindEntry: {
lobsterKeyDraft: string lobsterKeyDraft: string
...@@ -73,7 +67,10 @@ interface ConversationWorkspaceViewProps { ...@@ -73,7 +67,10 @@ interface ConversationWorkspaceViewProps {
starterQuestionsHint: string starterQuestionsHint: string
homeEmptyTitle: string homeEmptyTitle: string
homePrompts: readonly HomeStarterPrompt[] homePrompts: readonly HomeStarterPrompt[]
onStarterPrompt: (prompt: string) => void }
interface ConversationWorkspaceMessagesProps {
messageListRef: RefObject<HTMLDivElement | null>
messages: MessageListMessage[] messages: MessageListMessage[]
showEmptyState: boolean showEmptyState: boolean
messageTraces: Record<string, MessageTraceState> messageTraces: Record<string, MessageTraceState>
...@@ -89,6 +86,38 @@ interface ConversationWorkspaceViewProps { ...@@ -89,6 +86,38 @@ interface ConversationWorkspaceViewProps {
copiedIcon: ReactNode copiedIcon: ReactNode
deleteIcon: ReactNode deleteIcon: ReactNode
regenerateIcon: ReactNode regenerateIcon: ReactNode
}
interface ConversationWorkspaceComposerProps {
attachmentInputRef: RefObject<HTMLInputElement | null>
skillMenuRef: RefObject<HTMLDivElement | null>
prompt: string
isBound: boolean
canSend: boolean
isComposerDragOver: boolean
isComposerResizeActive: boolean
composerShellStyle: CSSProperties
attachmentAccept: string
attachments: ChatAttachment[]
composerPlaceholder: string
sendButtonLabel: string
skillMenuTitle: string
defaultChatLabel: string
defaultSkillId: string
selectedSkillId: string
selectedSkillName: string
skills: Array<{ id: string; name: string; description: string }>
skillMenuOpen: boolean
attachmentIcon: ReactNode
submitIcon: ReactNode
}
interface ConversationWorkspaceActionsProps {
renderExpertIcon: (expertKey: ExpertVisualKey) => ReactNode
renderIntentIcon: (projectId: string) => ReactNode
onContinueHomeIntent: () => void | Promise<unknown>
onSwitchHomeIntent: () => void | Promise<unknown>
onStarterPrompt: (prompt: string) => void
renderThumbIcon: (direction: MessageReaction) => ReactNode renderThumbIcon: (direction: MessageReaction) => ReactNode
renderMarkdownContent: ( renderMarkdownContent: (
content: string, content: string,
...@@ -110,25 +139,6 @@ interface ConversationWorkspaceViewProps { ...@@ -110,25 +139,6 @@ interface ConversationWorkspaceViewProps {
onTraceExpandedChange: (messageId: string, expanded: boolean) => void onTraceExpandedChange: (messageId: string, expanded: boolean) => void
onRegenerateAssistantMessage: (messageId: string) => void | Promise<void> onRegenerateAssistantMessage: (messageId: string) => void | Promise<void>
onToggleMessageReaction: (messageId: string, reaction: MessageReaction) => void onToggleMessageReaction: (messageId: string, reaction: MessageReaction) => void
prompt: string
isBound: boolean
canSend: boolean
isComposerDragOver: boolean
isComposerResizeActive: boolean
composerShellStyle: CSSProperties
attachmentAccept: string
attachments: ChatAttachment[]
composerPlaceholder: string
sendButtonLabel: string
skillMenuTitle: string
defaultChatLabel: string
defaultSkillId: string
selectedSkillId: string
selectedSkillName: string
skills: Array<{ id: string; name: string; description: string }>
skillMenuOpen: boolean
attachmentIcon: ReactNode
submitIcon: ReactNode
onSubmit: () => void | Promise<void> onSubmit: () => void | Promise<void>
onCancel: () => void | Promise<void> onCancel: () => void | Promise<void>
onPromptChange: (value: string) => void onPromptChange: (value: string) => void
...@@ -148,154 +158,82 @@ interface ConversationWorkspaceViewProps { ...@@ -148,154 +158,82 @@ interface ConversationWorkspaceViewProps {
onResizePointerEnd: (event: ReactPointerEvent<HTMLDivElement>) => void onResizePointerEnd: (event: ReactPointerEvent<HTMLDivElement>) => void
} }
interface ConversationWorkspaceViewProps {
viewMode: ViewMode
workspaceRef: RefObject<HTMLDivElement | null>
status: ConversationWorkspaceStatusProps
emptyState: ConversationWorkspaceEmptyStateProps
messages: ConversationWorkspaceMessagesProps
composer: ConversationWorkspaceComposerProps
actions: ConversationWorkspaceActionsProps
}
export function ConversationWorkspaceView({ export function ConversationWorkspaceView({
viewMode, viewMode,
workspaceRef, workspaceRef,
messageListRef, status,
attachmentInputRef, emptyState,
skillMenuRef,
panelActions,
selectedSkillBadge,
selectedSkillIsDefault,
homeLeadIcon,
activeExpertName,
activeExpertKey,
activeExpertVisualKey,
activeExpertGuide,
expertWorkspaceLogo,
renderExpertIcon,
showInlineStartupNotice,
chatLaunchState,
startupCurtainStatus,
homeIntentSuggestion,
homeIntentDecisionPending,
homeIntentLabels,
renderIntentIcon,
onContinueHomeIntent,
onSwitchHomeIntent,
showBindEntry,
bindEntry,
hasExpertProjects,
noExpertsLabel,
starterQuestionsHint,
homeEmptyTitle,
homePrompts,
onStarterPrompt,
messages, messages,
showEmptyState, composer,
messageTraces, actions
messageReactions,
copiedToken,
sending,
messageLabels,
copyIcon,
copiedIcon,
deleteIcon,
regenerateIcon,
renderThumbIcon,
renderMarkdownContent,
buildDouyinVideoStatusCard,
formatMessageTimestamp,
onMessageListScroll,
onCopyText,
onDeleteMessage,
onTraceExpandedChange,
onRegenerateAssistantMessage,
onToggleMessageReaction,
prompt,
isBound,
canSend,
isComposerDragOver,
isComposerResizeActive,
composerShellStyle,
attachmentAccept,
attachments,
composerPlaceholder,
sendButtonLabel,
skillMenuTitle,
defaultChatLabel,
defaultSkillId,
selectedSkillId,
selectedSkillName,
skills,
skillMenuOpen,
attachmentIcon,
submitIcon,
onSubmit,
onCancel,
onPromptChange,
onTextareaKeyDown,
onAttachmentSelection,
onOpenAttachmentPicker,
onRemoveAttachment,
onToggleSkillMenu,
onClearSelectedSkill,
onChooseSkill,
onDragEnter,
onDragOver,
onDragLeave,
onDrop,
onResizePointerDown,
onResizePointerMove,
onResizePointerEnd
}: ConversationWorkspaceViewProps) { }: ConversationWorkspaceViewProps) {
const homeMicrocopyStatus = selectedSkillIsDefault ? "默认工作区" : "已切换" const homeMicrocopyStatus = emptyState.selectedSkillIsDefault ? "默认工作区" : "已切换"
const panelLead = viewMode === "chat" ? ( const panelLead = viewMode === "chat" ? (
<div className="home-microcopy" aria-label={`工作区,${selectedSkillBadge}`}> <div className="home-microcopy" aria-label={`工作区,${emptyState.selectedSkillBadge}`}>
<span className="home-microcopy-icon-shell" aria-hidden="true"> <span className="home-microcopy-icon-shell" aria-hidden="true">
<span className="home-microcopy-icon"> <span className="home-microcopy-icon">
{homeLeadIcon} {emptyState.homeLeadIcon}
</span> </span>
<span className="home-microcopy-icon-beam" /> <span className="home-microcopy-icon-beam" />
</span> </span>
<span className="home-microcopy-body"> <span className="home-microcopy-body">
<strong className="home-microcopy-title">{selectedSkillBadge}</strong> <strong className="home-microcopy-title">{emptyState.selectedSkillBadge}</strong>
</span> </span>
<span className={"home-microcopy-status" + (selectedSkillIsDefault ? " brand" : "")}> <span className={"home-microcopy-status" + (emptyState.selectedSkillIsDefault ? " brand" : "")}>
{homeMicrocopyStatus} {homeMicrocopyStatus}
</span> </span>
</div> </div>
) : expertWorkspaceLogo ? ( ) : emptyState.expertWorkspaceLogo ? (
<div className={"expert-hero-heading expert-brand-card expert-brand-card-" + activeExpertKey}> <div className={"expert-hero-heading expert-brand-card expert-brand-card-" + emptyState.activeExpertKey}>
{expertWorkspaceLogo} {emptyState.expertWorkspaceLogo}
<span className="expert-hero-body"> <span className="expert-hero-body">
<strong className="expert-hero-title">{activeExpertName}</strong> <strong className="expert-hero-title">{emptyState.activeExpertName}</strong>
</span> </span>
</div> </div>
) : ( ) : (
<div className="conversation-panel-kicker expert-hero-kicker"> <div className="conversation-panel-kicker expert-hero-kicker">
<span className={"expert-hero-icon expert-hero-icon-" + activeExpertVisualKey} aria-hidden="true"> <span className={"expert-hero-icon expert-hero-icon-" + emptyState.activeExpertVisualKey} aria-hidden="true">
{renderExpertIcon(activeExpertVisualKey)} {actions.renderExpertIcon(emptyState.activeExpertVisualKey)}
</span> </span>
<span className="expert-hero-copy"> <span className="expert-hero-copy">
<strong>{activeExpertName}</strong> <strong>{emptyState.activeExpertName}</strong>
</span> </span>
</div> </div>
) )
const activeEmptyState = viewMode === "experts" ? ( const activeEmptyState = viewMode === "experts" ? (
<ExpertEmptyState <ExpertEmptyState
activeExpertName={activeExpertName} activeExpertName={emptyState.activeExpertName}
activeExpertKey={activeExpertKey} activeExpertKey={emptyState.activeExpertKey}
activeExpertGuide={activeExpertGuide} activeExpertGuide={emptyState.activeExpertGuide}
starterQuestionsHint={starterQuestionsHint} starterQuestionsHint={emptyState.starterQuestionsHint}
onStarterPrompt={onStarterPrompt} onStarterPrompt={actions.onStarterPrompt}
/> />
) : ( ) : (
<div className="empty-state home-empty-state"> <div className="empty-state home-empty-state">
<div className="home-empty-copy"> <div className="home-empty-copy">
<strong className="home-empty-title">{homeEmptyTitle}</strong> <strong className="home-empty-title">{emptyState.homeEmptyTitle}</strong>
</div> </div>
<div className="starter-prompt-list" aria-label="可选任务入口"> <div className="starter-prompt-list" aria-label="可选任务入口">
{homePrompts.map((item) => ( {emptyState.homePrompts.map((item) => (
<button <button
key={item.prompt} key={item.prompt}
type="button" type="button"
className="starter-prompt" className="starter-prompt"
title={item.prompt} title={item.prompt}
aria-label={item.prompt} aria-label={item.prompt}
onClick={() => onStarterPrompt(item.prompt)} onClick={() => actions.onStarterPrompt(item.prompt)}
> >
<span className="starter-prompt-title">{item.title}</span> <span className="starter-prompt-title">{item.title}</span>
<span className="starter-prompt-desc">{item.description}</span> <span className="starter-prompt-desc">{item.description}</span>
...@@ -307,116 +245,116 @@ export function ConversationWorkspaceView({ ...@@ -307,116 +245,116 @@ export function ConversationWorkspaceView({
const statusNotice = ( const statusNotice = (
<ConversationStatusNotice <ConversationStatusNotice
show={showInlineStartupNotice} show={status.showInlineStartupNotice}
chatLaunchState={chatLaunchState} chatLaunchState={status.chatLaunchState}
status={startupCurtainStatus} status={status.startupCurtainStatus}
/> />
) )
const intentNotice = viewMode === "chat" ? ( const intentNotice = viewMode === "chat" ? (
<HomeIntentSuggestionNotice <HomeIntentSuggestionNotice
suggestion={homeIntentSuggestion} suggestion={emptyState.homeIntentSuggestion}
decisionPending={homeIntentDecisionPending} decisionPending={emptyState.homeIntentDecisionPending}
labels={homeIntentLabels} labels={emptyState.homeIntentLabels}
renderIcon={renderIntentIcon} renderIcon={actions.renderIntentIcon}
onContinue={onContinueHomeIntent} onContinue={actions.onContinueHomeIntent}
onSwitch={onSwitchHomeIntent} onSwitch={actions.onSwitchHomeIntent}
/> />
) : null ) : null
const bodyContent = showBindEntry const bodyContent = emptyState.showBindEntry
? ( ? (
<BindEntry <BindEntry
lobsterKeyDraft={bindEntry.lobsterKeyDraft} lobsterKeyDraft={emptyState.bindEntry.lobsterKeyDraft}
workspaceApiKeyConfigured={bindEntry.workspaceApiKeyConfigured} workspaceApiKeyConfigured={emptyState.bindEntry.workspaceApiKeyConfigured}
saving={bindEntry.saving} saving={emptyState.bindEntry.saving}
bindingLabel={bindEntry.bindingLabel} bindingLabel={emptyState.bindEntry.bindingLabel}
onLobsterKeyChange={bindEntry.onLobsterKeyChange} onLobsterKeyChange={emptyState.bindEntry.onLobsterKeyChange}
onSave={bindEntry.onSave} onSave={emptyState.bindEntry.onSave}
/> />
) )
: viewMode === "experts" && !hasExpertProjects : viewMode === "experts" && !emptyState.hasExpertProjects
? <div className="empty-state">{noExpertsLabel}</div> ? <div className="empty-state">{emptyState.noExpertsLabel}</div>
: ( : (
<MessageList <MessageList
messages={messages} messages={messages.messages}
viewMode={viewMode} viewMode={viewMode}
showBindEntry={showBindEntry} showBindEntry={emptyState.showBindEntry}
showEmptyState={showEmptyState} showEmptyState={messages.showEmptyState}
emptyState={activeEmptyState} emptyState={activeEmptyState}
messageListRef={messageListRef} messageListRef={messages.messageListRef}
messageTraces={messageTraces} messageTraces={messages.messageTraces}
messageReactions={messageReactions} messageReactions={messages.messageReactions}
copiedToken={copiedToken} copiedToken={messages.copiedToken}
sending={sending} sending={messages.sending}
activeExpertKey={activeExpertKey} activeExpertKey={emptyState.activeExpertKey}
labels={messageLabels} labels={messages.messageLabels}
copyIcon={copyIcon} copyIcon={messages.copyIcon}
copiedIcon={copiedIcon} copiedIcon={messages.copiedIcon}
deleteIcon={deleteIcon} deleteIcon={messages.deleteIcon}
regenerateIcon={regenerateIcon} regenerateIcon={messages.regenerateIcon}
renderThumbIcon={renderThumbIcon} renderThumbIcon={actions.renderThumbIcon}
renderMarkdownContent={renderMarkdownContent} renderMarkdownContent={actions.renderMarkdownContent}
buildDouyinVideoStatusCard={buildDouyinVideoStatusCard} buildDouyinVideoStatusCard={actions.buildDouyinVideoStatusCard}
formatMessageTimestamp={formatMessageTimestamp} formatMessageTimestamp={actions.formatMessageTimestamp}
onScroll={onMessageListScroll} onScroll={actions.onMessageListScroll}
onCopyText={onCopyText} onCopyText={actions.onCopyText}
onDeleteMessage={onDeleteMessage} onDeleteMessage={actions.onDeleteMessage}
onTraceExpandedChange={onTraceExpandedChange} onTraceExpandedChange={actions.onTraceExpandedChange}
onRegenerateAssistantMessage={onRegenerateAssistantMessage} onRegenerateAssistantMessage={actions.onRegenerateAssistantMessage}
onToggleMessageReaction={onToggleMessageReaction} onToggleMessageReaction={actions.onToggleMessageReaction}
/> />
) )
const composerContent = ( const composerContent = (
<ChatComposer <ChatComposer
prompt={prompt} prompt={composer.prompt}
isBound={isBound} isBound={composer.isBound}
sending={sending} sending={messages.sending}
canSend={canSend} canSend={composer.canSend}
isDragOver={isComposerDragOver} isDragOver={composer.isComposerDragOver}
isResizeActive={isComposerResizeActive} isResizeActive={composer.isComposerResizeActive}
viewMode={viewMode} viewMode={viewMode}
shellStyle={composerShellStyle} shellStyle={composer.composerShellStyle}
attachmentInputRef={attachmentInputRef} attachmentInputRef={composer.attachmentInputRef}
skillMenuRef={skillMenuRef} skillMenuRef={composer.skillMenuRef}
attachmentAccept={attachmentAccept} attachmentAccept={composer.attachmentAccept}
attachments={attachments} attachments={composer.attachments}
placeholder={composerPlaceholder} placeholder={composer.composerPlaceholder}
sendButtonLabel={sendButtonLabel} sendButtonLabel={composer.sendButtonLabel}
skillMenuTitle={skillMenuTitle} skillMenuTitle={composer.skillMenuTitle}
defaultChatLabel={defaultChatLabel} defaultChatLabel={composer.defaultChatLabel}
defaultSkillId={defaultSkillId} defaultSkillId={composer.defaultSkillId}
selectedSkillId={selectedSkillId} selectedSkillId={composer.selectedSkillId}
selectedSkillName={selectedSkillName} selectedSkillName={composer.selectedSkillName}
skills={skills} skills={composer.skills}
skillMenuOpen={skillMenuOpen} skillMenuOpen={composer.skillMenuOpen}
attachmentIcon={attachmentIcon} attachmentIcon={composer.attachmentIcon}
submitIcon={submitIcon} submitIcon={composer.submitIcon}
onSubmit={onSubmit} onSubmit={actions.onSubmit}
onCancel={onCancel} onCancel={actions.onCancel}
onPromptChange={onPromptChange} onPromptChange={actions.onPromptChange}
onTextareaKeyDown={onTextareaKeyDown} onTextareaKeyDown={actions.onTextareaKeyDown}
onAttachmentSelection={onAttachmentSelection} onAttachmentSelection={actions.onAttachmentSelection}
onOpenAttachmentPicker={onOpenAttachmentPicker} onOpenAttachmentPicker={actions.onOpenAttachmentPicker}
onRemoveAttachment={onRemoveAttachment} onRemoveAttachment={actions.onRemoveAttachment}
onToggleSkillMenu={onToggleSkillMenu} onToggleSkillMenu={actions.onToggleSkillMenu}
onClearSelectedSkill={onClearSelectedSkill} onClearSelectedSkill={actions.onClearSelectedSkill}
onChooseSkill={onChooseSkill} onChooseSkill={actions.onChooseSkill}
onDragEnter={onDragEnter} onDragEnter={actions.onDragEnter}
onDragOver={onDragOver} onDragOver={actions.onDragOver}
onDragLeave={onDragLeave} onDragLeave={actions.onDragLeave}
onDrop={onDrop} onDrop={actions.onDrop}
onResizePointerDown={onResizePointerDown} onResizePointerDown={actions.onResizePointerDown}
onResizePointerMove={onResizePointerMove} onResizePointerMove={actions.onResizePointerMove}
onResizePointerEnd={onResizePointerEnd} onResizePointerEnd={actions.onResizePointerEnd}
/> />
) )
return ( return (
<ChatWorkspace <ChatWorkspace
panelLead={panelLead} panelLead={panelLead}
panelActions={panelActions} panelActions={status.panelActions}
workspaceRef={workspaceRef} workspaceRef={workspaceRef}
statusNotice={statusNotice} statusNotice={statusNotice}
intentNotice={intentNotice} intentNotice={intentNotice}
......
...@@ -258,7 +258,7 @@ ...@@ -258,7 +258,7 @@
- Modify: `apps/ui/src/features/settings/SettingsPanels.tsx` - Modify: `apps/ui/src/features/settings/SettingsPanels.tsx`
- Create or modify focused hooks only under existing `apps/ui/src/features/*` - Create or modify focused hooks only under existing `apps/ui/src/features/*`
- [ ] **Step 1: 拆出纯 props 组装** - [x] **Step 1: 拆出纯 props 组装**
Move only pure prop-building logic from `App.tsx` into focused helpers or hooks when all inputs are already available. Move only pure prop-building logic from `App.tsx` into focused helpers or hooks when all inputs are already available.
...@@ -274,7 +274,12 @@ ...@@ -274,7 +274,12 @@
- No IPC calls move across ownership boundaries in this step. - No IPC calls move across ownership boundaries in this step.
- `App.tsx` line count decreases, but line count is not the success metric; clearer ownership is. - `App.tsx` line count decreases, but line count is not the success metric; clearer ownership is.
- [ ] **Step 2: Keep high-risk flows in place** Progress note:
- Completed on 2026-05-25 by grouping `ConversationWorkspaceView` props into focused `status`, `emptyState`, `messages`, `composer`, and `actions` objects.
- IPC calls, stream lifecycle, composer submit/cancel, session actions, startup overlay actions, and smoke hooks remain in `App.tsx`.
- [x] **Step 2: Keep high-risk flows in place**
Do not move these during this phase: Do not move these during this phase:
...@@ -289,7 +294,11 @@ ...@@ -289,7 +294,11 @@
- Existing smoke tests do not need selector or action rewrites. - Existing smoke tests do not need selector or action rewrites.
- [ ] **Step 3: Reduce oversized component props** Progress note:
- Confirmed during the prop grouping pass; no smoke contract or high-risk flow was moved.
- [x] **Step 3: Reduce oversized component props**
For `ConversationWorkspaceView`, group related props into typed objects only when ownership is obvious: For `ConversationWorkspaceView`, group related props into typed objects only when ownership is obvious:
...@@ -304,7 +313,11 @@ ...@@ -304,7 +313,11 @@
- Component call site becomes easier to read. - Component call site becomes easier to read.
- Existing child component props stay unchanged unless required. - Existing child component props stay unchanged unless required.
- [ ] **Step 4: Validate App.tsx encoding and tests** Progress note:
- `ConversationWorkspaceView` now receives typed grouped props; child components still receive their existing prop shapes.
- [x] **Step 4: Validate App.tsx encoding and tests**
Run: Run:
...@@ -320,6 +333,12 @@ ...@@ -320,6 +333,12 @@
- Diff contains no mojibake and no unrelated Chinese copy changes. - Diff contains no mojibake and no unrelated Chinese copy changes.
- UI typecheck passes. - UI typecheck passes.
Progress note:
- Passed on 2026-05-25: BOM check printed `b'imp'`.
- Inspected `git diff -- apps/ui/src/App.tsx apps/ui/src/features/chat/ConversationWorkspaceView.tsx`; no mojibake or unrelated Chinese copy changes.
- Passed on 2026-05-25: `corepack pnpm --filter @qjclaw/ui typecheck`; `corepack pnpm build`.
## 6. Phase 4 - 修正核心工作流 UI/UX ## 6. Phase 4 - 修正核心工作流 UI/UX
**目标:** 优先改善用户每天会反复使用的界面:聊天、任务、自动化、设置。 **目标:** 优先改善用户每天会反复使用的界面:聊天、任务、自动化、设置。
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment