diff --git a/src/components/AI/AIMessage.stories.tsx b/src/components/AI/AIMessage.stories.tsx index 9eef4a02..33f86489 100644 --- a/src/components/AI/AIMessage.stories.tsx +++ b/src/components/AI/AIMessage.stories.tsx @@ -165,11 +165,31 @@ export const StreamingMessage: Story = { }, }; -/** A collapsible reasoning ("thinking") block ahead of the final answer. */ -export const ThinkingBlock: Story = { +/** Thinking pill while the model is actively reasoning — pulsing dot, "Thinking" label. */ +export const ThinkingActive: Story = { render: () => { const message: AIMessage = { - id: '5', + id: '5a', + role: 'assistant', + content: [ + { + type: 'thinking', + text: 'The user wants to add a new patient. I should use the create_patient tool with the provided information. Validating date format and required fields...', + collapsed: false, + }, + ], + timestamp: new Date(), + status: 'streaming', + }; + return ; + }, +}; + +/** Thinking pill after the model finished — "Thought" label, no dot, collapsed by default. */ +export const ThinkingComplete: Story = { + render: () => { + const message: AIMessage = { + id: '5b', role: 'assistant', content: [ { @@ -189,6 +209,30 @@ export const ThinkingBlock: Story = { }, }; +/** Thinking pill expanded — user clicked to reveal reasoning content. */ +export const ThinkingExpanded: Story = { + render: () => { + const message: AIMessage = { + id: '5c', + role: 'assistant', + content: [ + { + type: 'thinking', + text: 'The user wants to add a new patient. I should use the create_patient tool with the provided information. I need to validate the date format and ensure all required fields are present.', + collapsed: false, + }, + { + type: 'text', + text: "I'll create a new patient record for John Smith.", + }, + ], + timestamp: new Date(), + status: 'complete', + }; + return ; + }, +}; + /** * Demonstrates the `renderTextContent` extension point. This trivial demo turns * `**bold**` into ``; in a real app you would plug in a full Markdown diff --git a/src/components/AI/AIMessage.tsx b/src/components/AI/AIMessage.tsx index 0c97844e..1bb89352 100644 --- a/src/components/AI/AIMessage.tsx +++ b/src/components/AI/AIMessage.tsx @@ -14,7 +14,8 @@ import type { MCPResourceLink, } from './types'; import { MCPToolCallDisplay } from './MCPToolCall'; -import { SparklesIcon, ChevronIcon } from './icons'; +import { SparklesIcon } from './icons'; +import { CollapsiblePill } from './CollapsiblePill'; import { AudioPlayer } from '../AudioPlayer'; // ============================================================================ @@ -161,6 +162,59 @@ function AITypingIndicator({ className }: { className?: string }) { ); } +// ============================================================================ +// Thinking Block +// ============================================================================ + +function ThinkingBlock({ + text, + streaming, + defaultCollapsed = false, +}: { + text: string; + streaming: boolean; + defaultCollapsed?: boolean; +}) { + const startedAt = React.useRef(Date.now()); + const [elapsed, setElapsed] = React.useState(null); + const prevStreaming = React.useRef(streaming); + + React.useEffect(() => { + if (prevStreaming.current && !streaming) { + setElapsed(Math.round((Date.now() - startedAt.current) / 1000)); + } + prevStreaming.current = streaming; + }, [streaming]); + + const label = streaming + ? 'Thinking' + : elapsed !== null && elapsed > 0 + ? `Thought for ${elapsed}s` + : 'Thought'; + + const dot = streaming ? ( +