Commit b2800f26 authored by edy's avatar edy

refactor(ui): reduce app renderer prop assembly

parent 625ebaeb
......@@ -1355,110 +1355,120 @@ export default function App() {
const conversationWorkspaceProps = {
viewMode,
workspaceRef: conversationWorkspaceRef,
messageListRef,
attachmentInputRef,
skillMenuRef,
panelActions,
selectedSkillBadge,
selectedSkillIsDefault: selectedSkillId === DEFAULT_SKILL.id,
homeLeadIcon: <LobsterClawIcon />,
activeExpertName,
activeExpertKey,
activeExpertVisualKey,
activeExpertGuide,
expertWorkspaceLogo,
renderExpertIcon,
showInlineStartupNotice,
chatLaunchState,
startupCurtainStatus,
homeIntentSuggestion: pendingHomeIntentSuggestion?.suggestion ?? null,
homeIntentDecisionPending,
homeIntentLabels: {
title: ui.suggestionSwitchTitle,
prefix: ui.suggestionSwitchPrefix,
suffix: ui.suggestionSwitchSuffix,
continue: ui.suggestionContinue,
switchAction: ui.suggestionSwitchAction
status: {
panelActions,
showInlineStartupNotice,
chatLaunchState,
startupCurtainStatus
},
renderIntentIcon: getIntentSuggestionIcon,
onContinueHomeIntent: continuePendingHomePromptInHome,
onSwitchHomeIntent: switchExpertAndContinuePendingHomePrompt,
showBindEntry,
bindEntry: {
lobsterKeyDraft,
workspaceApiKeyConfigured: Boolean(workspace?.apiKeyConfigured),
saving,
bindingLabel: ui.binding,
onLobsterKeyChange: setLobsterKeyDraft,
onSave: () => void saveLobsterKey()
emptyState: {
selectedSkillBadge,
selectedSkillIsDefault: selectedSkillId === DEFAULT_SKILL.id,
homeLeadIcon: <LobsterClawIcon />,
activeExpertName,
activeExpertKey,
activeExpertVisualKey,
activeExpertGuide,
expertWorkspaceLogo,
homeIntentSuggestion: pendingHomeIntentSuggestion?.suggestion ?? null,
homeIntentDecisionPending,
homeIntentLabels: {
title: ui.suggestionSwitchTitle,
prefix: ui.suggestionSwitchPrefix,
suffix: ui.suggestionSwitchSuffix,
continue: ui.suggestionContinue,
switchAction: ui.suggestionSwitchAction
},
showBindEntry,
bindEntry: {
lobsterKeyDraft,
workspaceApiKeyConfigured: Boolean(workspace?.apiKeyConfigured),
saving,
bindingLabel: ui.binding,
onLobsterKeyChange: setLobsterKeyDraft,
onSave: () => void saveLobsterKey()
},
hasExpertProjects: Boolean(expertPageProjects.length),
noExpertsLabel: expertsPageCopy.noExperts,
starterQuestionsHint: ui.starterQuestionsHint,
homeEmptyTitle: homeChatCopy.emptyTitle,
homePrompts: homeChatCopy.prompts
},
hasExpertProjects: Boolean(expertPageProjects.length),
noExpertsLabel: expertsPageCopy.noExperts,
starterQuestionsHint: ui.starterQuestionsHint,
homeEmptyTitle: homeChatCopy.emptyTitle,
homePrompts: homeChatCopy.prompts,
onStarterPrompt: applyStarterPrompt,
messages,
showEmptyState,
messageTraces,
messageReactions,
copiedToken,
sending,
messageLabels: {
thinking: ui.thinking,
hideTrace: ui.hideTrace,
traceCollapsed: ui.traceCollapsed
messages: {
messageListRef,
messages,
showEmptyState,
messageTraces,
messageReactions,
copiedToken,
sending,
messageLabels: {
thinking: ui.thinking,
hideTrace: ui.hideTrace,
traceCollapsed: ui.traceCollapsed
},
copyIcon: <CopyIcon />,
copiedIcon: <CheckIcon />,
deleteIcon: <TrashIcon />,
regenerateIcon: <RefreshIcon />
},
copyIcon: <CopyIcon />,
copiedIcon: <CheckIcon />,
deleteIcon: <TrashIcon />,
regenerateIcon: <RefreshIcon />,
renderThumbIcon: (direction) => <ThumbIcon direction={direction} />,
renderMarkdownContent,
buildDouyinVideoStatusCard,
formatMessageTimestamp,
onMessageListScroll: handleMessageListScroll,
onCopyText: handleCopyText,
onDeleteMessage: deleteMessage,
onTraceExpandedChange: setMessageTraceExpanded,
onRegenerateAssistantMessage: regenerateAssistantMessage,
onToggleMessageReaction: toggleMessageReaction,
prompt,
isBound,
canSend,
isComposerDragOver,
isComposerResizeActive,
composerShellStyle,
attachmentAccept,
attachments: composerAttachments,
composerPlaceholder,
sendButtonLabel,
skillMenuTitle: ui.skillMenuTitle,
defaultChatLabel: ui.defaultChat,
defaultSkillId: DEFAULT_SKILL.id,
selectedSkillId,
selectedSkillName: selectedSkill.name,
skills: effectiveSkills,
skillMenuOpen,
attachmentIcon: <AttachmentIcon />,
submitIcon: sendPhase !== "idle" ? <StopIcon /> : <ArrowUpIcon />,
onSubmit: sendPrompt,
onCancel: cancelActiveStream,
onPromptChange: setPrompt,
onTextareaKeyDown: handleComposerKeyDown,
onAttachmentSelection: handleAttachmentSelection,
onOpenAttachmentPicker: openAttachmentPicker,
onRemoveAttachment: removeComposerAttachment,
onToggleSkillMenu: () => setSkillMenuOpen((current) => !current),
onClearSelectedSkill: clearSelectedSkill,
onChooseSkill: chooseSkill,
onDragEnter: handleComposerDragEnter,
onDragOver: handleComposerDragOver,
onDragLeave: handleComposerDragLeave,
onDrop: handleComposerDrop,
onResizePointerDown: handleComposerResizePointerDown,
onResizePointerMove: handleComposerResizePointerMove,
onResizePointerEnd: handleComposerResizePointerEnd
composer: {
attachmentInputRef,
skillMenuRef,
prompt,
isBound,
canSend,
isComposerDragOver,
isComposerResizeActive,
composerShellStyle,
attachmentAccept,
attachments: composerAttachments,
composerPlaceholder,
sendButtonLabel,
skillMenuTitle: ui.skillMenuTitle,
defaultChatLabel: ui.defaultChat,
defaultSkillId: DEFAULT_SKILL.id,
selectedSkillId,
selectedSkillName: selectedSkill.name,
skills: effectiveSkills,
skillMenuOpen,
attachmentIcon: <AttachmentIcon />,
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,
onCancel: cancelActiveStream,
onPromptChange: setPrompt,
onTextareaKeyDown: handleComposerKeyDown,
onAttachmentSelection: handleAttachmentSelection,
onOpenAttachmentPicker: openAttachmentPicker,
onRemoveAttachment: removeComposerAttachment,
onToggleSkillMenu: () => setSkillMenuOpen((current) => !current),
onClearSelectedSkill: clearSelectedSkill,
onChooseSkill: chooseSkill,
onDragEnter: handleComposerDragEnter,
onDragOver: handleComposerDragOver,
onDragLeave: handleComposerDragLeave,
onDrop: handleComposerDrop,
onResizePointerDown: handleComposerResizePointerDown,
onResizePointerMove: handleComposerResizePointerMove,
onResizePointerEnd: handleComposerResizePointerEnd
}
} satisfies ComponentProps<typeof ConversationWorkspaceView>;
const settingsStatusHint = showSettingsStatusHint
? <div className={"inline-hint settings-runtime-hint" + (chatLaunchState === "error" ? " error" : "")}>{startupMessage}</div>
......
......@@ -28,13 +28,14 @@ interface HomeStarterPrompt {
prompt: string
}
interface ConversationWorkspaceViewProps {
viewMode: ViewMode
workspaceRef: RefObject<HTMLDivElement | null>
messageListRef: RefObject<HTMLDivElement | null>
attachmentInputRef: RefObject<HTMLInputElement | null>
skillMenuRef: RefObject<HTMLDivElement | null>
interface ConversationWorkspaceStatusProps {
panelActions: ReactNode
showInlineStartupNotice: boolean
chatLaunchState: ChatLaunchState
startupCurtainStatus: string
}
interface ConversationWorkspaceEmptyStateProps {
selectedSkillBadge: string
selectedSkillIsDefault: boolean
homeLeadIcon: ReactNode
......@@ -43,10 +44,6 @@ interface ConversationWorkspaceViewProps {
activeExpertVisualKey: ExpertVisualKey
activeExpertGuide: ExpertGuideContent
expertWorkspaceLogo: ReactNode
renderExpertIcon: (expertKey: ExpertVisualKey) => ReactNode
showInlineStartupNotice: boolean
chatLaunchState: ChatLaunchState
startupCurtainStatus: string
homeIntentSuggestion: ProjectIntentSuggestion | null
homeIntentDecisionPending: boolean
homeIntentLabels: {
......@@ -56,9 +53,6 @@ interface ConversationWorkspaceViewProps {
continue: string
switchAction: string
}
renderIntentIcon: (projectId: string) => ReactNode
onContinueHomeIntent: () => void | Promise<unknown>
onSwitchHomeIntent: () => void | Promise<unknown>
showBindEntry: boolean
bindEntry: {
lobsterKeyDraft: string
......@@ -73,7 +67,10 @@ interface ConversationWorkspaceViewProps {
starterQuestionsHint: string
homeEmptyTitle: string
homePrompts: readonly HomeStarterPrompt[]
onStarterPrompt: (prompt: string) => void
}
interface ConversationWorkspaceMessagesProps {
messageListRef: RefObject<HTMLDivElement | null>
messages: MessageListMessage[]
showEmptyState: boolean
messageTraces: Record<string, MessageTraceState>
......@@ -89,6 +86,38 @@ interface ConversationWorkspaceViewProps {
copiedIcon: ReactNode
deleteIcon: 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
renderMarkdownContent: (
content: string,
......@@ -110,25 +139,6 @@ interface ConversationWorkspaceViewProps {
onTraceExpandedChange: (messageId: string, expanded: boolean) => void
onRegenerateAssistantMessage: (messageId: string) => void | Promise<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>
onCancel: () => void | Promise<void>
onPromptChange: (value: string) => void
......@@ -148,154 +158,82 @@ interface ConversationWorkspaceViewProps {
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({
viewMode,
workspaceRef,
messageListRef,
attachmentInputRef,
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,
status,
emptyState,
messages,
showEmptyState,
messageTraces,
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
composer,
actions
}: ConversationWorkspaceViewProps) {
const homeMicrocopyStatus = selectedSkillIsDefault ? "默认工作区" : "已切换"
const homeMicrocopyStatus = emptyState.selectedSkillIsDefault ? "默认工作区" : "已切换"
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">
{homeLeadIcon}
{emptyState.homeLeadIcon}
</span>
<span className="home-microcopy-icon-beam" />
</span>
<span className="home-microcopy-body">
<strong className="home-microcopy-title">{selectedSkillBadge}</strong>
<strong className="home-microcopy-title">{emptyState.selectedSkillBadge}</strong>
</span>
<span className={"home-microcopy-status" + (selectedSkillIsDefault ? " brand" : "")}>
<span className={"home-microcopy-status" + (emptyState.selectedSkillIsDefault ? " brand" : "")}>
{homeMicrocopyStatus}
</span>
</div>
) : expertWorkspaceLogo ? (
<div className={"expert-hero-heading expert-brand-card expert-brand-card-" + activeExpertKey}>
{expertWorkspaceLogo}
) : emptyState.expertWorkspaceLogo ? (
<div className={"expert-hero-heading expert-brand-card expert-brand-card-" + emptyState.activeExpertKey}>
{emptyState.expertWorkspaceLogo}
<span className="expert-hero-body">
<strong className="expert-hero-title">{activeExpertName}</strong>
<strong className="expert-hero-title">{emptyState.activeExpertName}</strong>
</span>
</div>
) : (
<div className="conversation-panel-kicker expert-hero-kicker">
<span className={"expert-hero-icon expert-hero-icon-" + activeExpertVisualKey} aria-hidden="true">
{renderExpertIcon(activeExpertVisualKey)}
<span className={"expert-hero-icon expert-hero-icon-" + emptyState.activeExpertVisualKey} aria-hidden="true">
{actions.renderExpertIcon(emptyState.activeExpertVisualKey)}
</span>
<span className="expert-hero-copy">
<strong>{activeExpertName}</strong>
<strong>{emptyState.activeExpertName}</strong>
</span>
</div>
)
const activeEmptyState = viewMode === "experts" ? (
<ExpertEmptyState
activeExpertName={activeExpertName}
activeExpertKey={activeExpertKey}
activeExpertGuide={activeExpertGuide}
starterQuestionsHint={starterQuestionsHint}
onStarterPrompt={onStarterPrompt}
activeExpertName={emptyState.activeExpertName}
activeExpertKey={emptyState.activeExpertKey}
activeExpertGuide={emptyState.activeExpertGuide}
starterQuestionsHint={emptyState.starterQuestionsHint}
onStarterPrompt={actions.onStarterPrompt}
/>
) : (
<div className="empty-state home-empty-state">
<div className="home-empty-copy">
<strong className="home-empty-title">{homeEmptyTitle}</strong>
<strong className="home-empty-title">{emptyState.homeEmptyTitle}</strong>
</div>
<div className="starter-prompt-list" aria-label="可选任务入口">
{homePrompts.map((item) => (
{emptyState.homePrompts.map((item) => (
<button
key={item.prompt}
type="button"
className="starter-prompt"
title={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-desc">{item.description}</span>
......@@ -307,116 +245,116 @@ export function ConversationWorkspaceView({
const statusNotice = (
<ConversationStatusNotice
show={showInlineStartupNotice}
chatLaunchState={chatLaunchState}
status={startupCurtainStatus}
show={status.showInlineStartupNotice}
chatLaunchState={status.chatLaunchState}
status={status.startupCurtainStatus}
/>
)
const intentNotice = viewMode === "chat" ? (
<HomeIntentSuggestionNotice
suggestion={homeIntentSuggestion}
decisionPending={homeIntentDecisionPending}
labels={homeIntentLabels}
renderIcon={renderIntentIcon}
onContinue={onContinueHomeIntent}
onSwitch={onSwitchHomeIntent}
suggestion={emptyState.homeIntentSuggestion}
decisionPending={emptyState.homeIntentDecisionPending}
labels={emptyState.homeIntentLabels}
renderIcon={actions.renderIntentIcon}
onContinue={actions.onContinueHomeIntent}
onSwitch={actions.onSwitchHomeIntent}
/>
) : null
const bodyContent = showBindEntry
const bodyContent = emptyState.showBindEntry
? (
<BindEntry
lobsterKeyDraft={bindEntry.lobsterKeyDraft}
workspaceApiKeyConfigured={bindEntry.workspaceApiKeyConfigured}
saving={bindEntry.saving}
bindingLabel={bindEntry.bindingLabel}
onLobsterKeyChange={bindEntry.onLobsterKeyChange}
onSave={bindEntry.onSave}
lobsterKeyDraft={emptyState.bindEntry.lobsterKeyDraft}
workspaceApiKeyConfigured={emptyState.bindEntry.workspaceApiKeyConfigured}
saving={emptyState.bindEntry.saving}
bindingLabel={emptyState.bindEntry.bindingLabel}
onLobsterKeyChange={emptyState.bindEntry.onLobsterKeyChange}
onSave={emptyState.bindEntry.onSave}
/>
)
: viewMode === "experts" && !hasExpertProjects
? <div className="empty-state">{noExpertsLabel}</div>
: viewMode === "experts" && !emptyState.hasExpertProjects
? <div className="empty-state">{emptyState.noExpertsLabel}</div>
: (
<MessageList
messages={messages}
messages={messages.messages}
viewMode={viewMode}
showBindEntry={showBindEntry}
showEmptyState={showEmptyState}
showBindEntry={emptyState.showBindEntry}
showEmptyState={messages.showEmptyState}
emptyState={activeEmptyState}
messageListRef={messageListRef}
messageTraces={messageTraces}
messageReactions={messageReactions}
copiedToken={copiedToken}
sending={sending}
activeExpertKey={activeExpertKey}
labels={messageLabels}
copyIcon={copyIcon}
copiedIcon={copiedIcon}
deleteIcon={deleteIcon}
regenerateIcon={regenerateIcon}
renderThumbIcon={renderThumbIcon}
renderMarkdownContent={renderMarkdownContent}
buildDouyinVideoStatusCard={buildDouyinVideoStatusCard}
formatMessageTimestamp={formatMessageTimestamp}
onScroll={onMessageListScroll}
onCopyText={onCopyText}
onDeleteMessage={onDeleteMessage}
onTraceExpandedChange={onTraceExpandedChange}
onRegenerateAssistantMessage={onRegenerateAssistantMessage}
onToggleMessageReaction={onToggleMessageReaction}
messageListRef={messages.messageListRef}
messageTraces={messages.messageTraces}
messageReactions={messages.messageReactions}
copiedToken={messages.copiedToken}
sending={messages.sending}
activeExpertKey={emptyState.activeExpertKey}
labels={messages.messageLabels}
copyIcon={messages.copyIcon}
copiedIcon={messages.copiedIcon}
deleteIcon={messages.deleteIcon}
regenerateIcon={messages.regenerateIcon}
renderThumbIcon={actions.renderThumbIcon}
renderMarkdownContent={actions.renderMarkdownContent}
buildDouyinVideoStatusCard={actions.buildDouyinVideoStatusCard}
formatMessageTimestamp={actions.formatMessageTimestamp}
onScroll={actions.onMessageListScroll}
onCopyText={actions.onCopyText}
onDeleteMessage={actions.onDeleteMessage}
onTraceExpandedChange={actions.onTraceExpandedChange}
onRegenerateAssistantMessage={actions.onRegenerateAssistantMessage}
onToggleMessageReaction={actions.onToggleMessageReaction}
/>
)
const composerContent = (
<ChatComposer
prompt={prompt}
isBound={isBound}
sending={sending}
canSend={canSend}
isDragOver={isComposerDragOver}
isResizeActive={isComposerResizeActive}
prompt={composer.prompt}
isBound={composer.isBound}
sending={messages.sending}
canSend={composer.canSend}
isDragOver={composer.isComposerDragOver}
isResizeActive={composer.isComposerResizeActive}
viewMode={viewMode}
shellStyle={composerShellStyle}
attachmentInputRef={attachmentInputRef}
skillMenuRef={skillMenuRef}
attachmentAccept={attachmentAccept}
attachments={attachments}
placeholder={composerPlaceholder}
sendButtonLabel={sendButtonLabel}
skillMenuTitle={skillMenuTitle}
defaultChatLabel={defaultChatLabel}
defaultSkillId={defaultSkillId}
selectedSkillId={selectedSkillId}
selectedSkillName={selectedSkillName}
skills={skills}
skillMenuOpen={skillMenuOpen}
attachmentIcon={attachmentIcon}
submitIcon={submitIcon}
onSubmit={onSubmit}
onCancel={onCancel}
onPromptChange={onPromptChange}
onTextareaKeyDown={onTextareaKeyDown}
onAttachmentSelection={onAttachmentSelection}
onOpenAttachmentPicker={onOpenAttachmentPicker}
onRemoveAttachment={onRemoveAttachment}
onToggleSkillMenu={onToggleSkillMenu}
onClearSelectedSkill={onClearSelectedSkill}
onChooseSkill={onChooseSkill}
onDragEnter={onDragEnter}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
onResizePointerDown={onResizePointerDown}
onResizePointerMove={onResizePointerMove}
onResizePointerEnd={onResizePointerEnd}
shellStyle={composer.composerShellStyle}
attachmentInputRef={composer.attachmentInputRef}
skillMenuRef={composer.skillMenuRef}
attachmentAccept={composer.attachmentAccept}
attachments={composer.attachments}
placeholder={composer.composerPlaceholder}
sendButtonLabel={composer.sendButtonLabel}
skillMenuTitle={composer.skillMenuTitle}
defaultChatLabel={composer.defaultChatLabel}
defaultSkillId={composer.defaultSkillId}
selectedSkillId={composer.selectedSkillId}
selectedSkillName={composer.selectedSkillName}
skills={composer.skills}
skillMenuOpen={composer.skillMenuOpen}
attachmentIcon={composer.attachmentIcon}
submitIcon={composer.submitIcon}
onSubmit={actions.onSubmit}
onCancel={actions.onCancel}
onPromptChange={actions.onPromptChange}
onTextareaKeyDown={actions.onTextareaKeyDown}
onAttachmentSelection={actions.onAttachmentSelection}
onOpenAttachmentPicker={actions.onOpenAttachmentPicker}
onRemoveAttachment={actions.onRemoveAttachment}
onToggleSkillMenu={actions.onToggleSkillMenu}
onClearSelectedSkill={actions.onClearSelectedSkill}
onChooseSkill={actions.onChooseSkill}
onDragEnter={actions.onDragEnter}
onDragOver={actions.onDragOver}
onDragLeave={actions.onDragLeave}
onDrop={actions.onDrop}
onResizePointerDown={actions.onResizePointerDown}
onResizePointerMove={actions.onResizePointerMove}
onResizePointerEnd={actions.onResizePointerEnd}
/>
)
return (
<ChatWorkspace
panelLead={panelLead}
panelActions={panelActions}
panelActions={status.panelActions}
workspaceRef={workspaceRef}
statusNotice={statusNotice}
intentNotice={intentNotice}
......
......@@ -258,7 +258,7 @@
- Modify: `apps/ui/src/features/settings/SettingsPanels.tsx`
- 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.
......@@ -274,7 +274,12 @@
- 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.
- [ ] **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:
......@@ -289,7 +294,11 @@
- 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:
......@@ -304,7 +313,11 @@
- Component call site becomes easier to read.
- 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:
......@@ -320,6 +333,12 @@
- Diff contains no mojibake and no unrelated Chinese copy changes.
- 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
**目标:** 优先改善用户每天会反复使用的界面:聊天、任务、自动化、设置。
......
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