diff --git a/src/components/ChatMessagesPanel/ChatAlerts.tsx b/src/components/ChatMessagesPanel/ChatAlerts.tsx new file mode 100644 index 00000000..1907d914 --- /dev/null +++ b/src/components/ChatMessagesPanel/ChatAlerts.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { AlertTriangle } from 'lucide-react'; +import { Button } from '../ui/Button'; +import { Icon } from '../ui/Icon'; + +interface ChatAlertsProps { + chatError: string | null; + isServerConnected: boolean; + onClose?: () => void; +} + +export const ChatAlerts: React.FC = ({ + chatError, + isServerConnected, + onClose, +}) => ( + <> + {chatError && ( +
+ {chatError} +
+ )} + + {!isServerConnected && ( +
+ + + Server not running — Chat is read-only + + {onClose && ( + + )} +
+ )} + +); diff --git a/src/components/ChatMessagesPanel/ChatComposer.tsx b/src/components/ChatMessagesPanel/ChatComposer.tsx new file mode 100644 index 00000000..253854db --- /dev/null +++ b/src/components/ChatMessagesPanel/ChatComposer.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { + ComposerPrimitive, + useComposerRuntime, +} from '@assistant-ui/react'; +import { Button } from '../ui/Button'; +import { DeepResearchToggle } from '../DeepResearch'; +import type { UseDeepResearchReturn } from '../../hooks/useDeepResearch'; + +interface ChatComposerProps { + isServerConnected: boolean; + isThreadRunning: boolean; + isDeepResearchEnabled: boolean; + toggleDeepResearch: () => void; + deepResearch: Pick; + stopDeepResearch: () => void; + onDeepResearchSubmit: (query: string) => void; + onStopGeneration: () => void; +} + +export const ChatComposer: React.FC = ({ + isServerConnected, + isThreadRunning, + isDeepResearchEnabled, + toggleDeepResearch, + deepResearch, + stopDeepResearch, + onDeepResearchSubmit, + onStopGeneration, +}) => { + const composerRuntime = useComposerRuntime({ optional: true }); + + return ( +
+ {isThreadRunning && !deepResearch.isRunning && ( +
Assistant is thinking…
+ )} + {deepResearch.isRunning && ( +
Researching… This may take a few minutes.
+ )} + + +
+ + {isThreadRunning && !deepResearch.isRunning && ( + + )} + {isDeepResearchEnabled ? ( + + ) : ( + + + + )} +
+
+
+ ); +}; diff --git a/src/components/ChatMessagesPanel/ChatHeader.tsx b/src/components/ChatMessagesPanel/ChatHeader.tsx new file mode 100644 index 00000000..af836f76 --- /dev/null +++ b/src/components/ChatMessagesPanel/ChatHeader.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { Download, Mic, MicOff, Pencil, RotateCcw, Sparkles } from 'lucide-react'; +import { Button } from '../ui/Button'; +import { Icon } from '../ui/Icon'; +import { Input } from '../ui/Input'; +import { ToolsPopover } from '../ToolsPopover'; +import { ToolSupportIndicator } from '../ToolSupportIndicator'; +import { cn } from '../../utils/cn'; +import { getToolRegistry } from '../../services/tools'; +import type { UseVoiceModeReturn } from '../../hooks/useVoiceMode'; + +interface ChatHeaderProps { + title: string | undefined; + isRenaming: boolean; + titleDraft: string; + setTitleDraft: (value: string) => void; + startRenaming: () => void; + commitRename: () => void; + cancelRenaming: () => void; + isGeneratingTitle: boolean; + generateTitle: () => void; + isThreadRunning: boolean; + activeConversationId: number | null; + serverPort: number; + supportsToolCalls: boolean | null; + toolFormat: string | null | undefined; + voice: UseVoiceModeReturn | undefined; + onClearConversation: () => void; + onExportConversation: () => void; +} + +export const ChatHeader: React.FC = ({ + title, + isRenaming, + titleDraft, + setTitleDraft, + startRenaming, + commitRename, + cancelRenaming, + isGeneratingTitle, + generateTitle, + isThreadRunning, + activeConversationId, + serverPort, + supportsToolCalls, + toolFormat, + voice, + onClearConversation, + onExportConversation, +}) => ( +
+
+ {isRenaming ? ( + setTitleDraft(e.target.value)} + onBlur={commitRename} + onKeyDown={(e) => { + if (e.key === 'Enter') commitRename(); + else if (e.key === 'Escape') cancelRenaming(); + }} + /> + ) : ( +

{title || 'New Chat'}

+ )} + + + + {isThreadRunning ? 'Responding…' : 'Idle'} + + 0} + toolFormat={toolFormat} + /> +
+
+ + {voice?.isSupported && ( + + )} + + +
+
+); diff --git a/src/components/ChatMessagesPanel/ChatMessagesPanel.tsx b/src/components/ChatMessagesPanel/ChatMessagesPanel.tsx index c5466076..61322ab9 100644 --- a/src/components/ChatMessagesPanel/ChatMessagesPanel.tsx +++ b/src/components/ChatMessagesPanel/ChatMessagesPanel.tsx @@ -3,22 +3,14 @@ import 'highlight.js/styles/github-dark.css'; import { appLogger } from '../../services/platform'; import { ThreadPrimitive, - ComposerPrimitive, useThreadRuntime, useThread, - useComposerRuntime, } from '@assistant-ui/react'; import type { ThreadMessageLike } from '@assistant-ui/react'; -import { AlertTriangle, Download, Mic, MicOff, Pencil, RotateCcw, Sparkles } from 'lucide-react'; -import { Button } from '../ui/Button'; import { getMessages, deleteMessage, saveMessage, updateMessage } from '../../services/clients/chat'; import type { ConversationSummary } from '../../services/clients/chat'; import type { ToastType } from '../Toast'; import { ConfirmDeleteModal } from './ConfirmDeleteModal'; -import { ToolsPopover } from '../ToolsPopover'; -import { Icon } from '../ui/Icon'; -import { Input } from '../ui/Input'; -import { Textarea } from '../ui/Textarea'; import { MessageActionsContext, AssistantMessageBubble, @@ -35,13 +27,13 @@ import { DeepResearchProvider } from './context/DeepResearchContext'; import { VoiceProvider, useVoiceContextValue } from './context/VoiceContext'; import type { ReasoningTimingTracker } from '../../hooks/useGglibRuntime/reasoningTiming'; import type { UseVoiceModeReturn } from '../../hooks/useVoiceMode'; -import { DeepResearchToggle } from '../DeepResearch'; import { useDeepResearch } from '../../hooks/useDeepResearch'; import type { ResearchState } from '../../hooks/useDeepResearch/types'; -import { cn } from '../../utils/cn'; import { DEFAULT_SYSTEM_PROMPT } from '../../hooks/useGglibRuntime'; -import { ToolSupportIndicator } from '../ToolSupportIndicator'; -import { getToolRegistry } from '../../services/tools'; +import { ChatHeader } from './ChatHeader'; +import { SystemPromptCard } from './SystemPromptCard'; +import { ChatAlerts } from './ChatAlerts'; +import { ChatComposer } from './ChatComposer'; interface ChatMessagesPanelProps { @@ -96,7 +88,6 @@ const ChatMessagesPanel: React.FC = ({ toolFormat, }) => { const threadRuntime = useThreadRuntime({ optional: true }); - const composerRuntime = useComposerRuntime({ optional: true }); const threadState = useThread({ optional: true }); const isThreadRunning = threadState?.isRunning ?? false; @@ -554,177 +545,47 @@ const ChatMessagesPanel: React.FC = ({ // ───────────────────────────────────────────────────────────────────────────── return (
- {/* Header */} -
-
- {isRenaming ? ( - setTitleDraft(e.target.value)} - onBlur={commitRename} - onKeyDown={(e) => { - if (e.key === 'Enter') commitRename(); - else if (e.key === 'Escape') cancelRenaming(); - }} - /> - ) : ( -

{activeConversation?.title || 'New Chat'}

- )} - - - - {isThreadRunning ? 'Responding…' : 'Idle'} - - 0} - toolFormat={toolFormat} - /> -
-
- - {voice?.isSupported && ( - - )} - - -
-
+ {/* Content */}
- {/* System prompt card */} -
-
-
-

System prompt

- {!isEditingPrompt && ( -

{promptPreview}

- )} -
-
- {isEditingPrompt ? ( - Editing… - ) : ( - - )} -
-
- {isEditingPrompt && ( - <> -