From cada229f4a7e2300e9958555ed68ca069ef04a1c Mon Sep 17 00:00:00 2001 From: Aditya Damerla <75409196+aditya-damerla128@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:05:20 -0400 Subject: [PATCH 01/12] feat: add expandable pills and tool workflow styling from Ozwell widget Addresses #290. Brings the Ozwell chat widget's pill patterns into the UI package as themeable components: - PillSelect: collapsible option selector (the reasoning-mode capsule -> segmented control). Any number of options, fully themeable via tokens. - ThinkingBlock: reasoning pill that shows "Thinking" with a pulsing dot while streaming, then "Thought for Xs" when done. Click to expand/collapse. - MCPToolCall: status pill header (green success / red error kept the same) with friendly tool names (underscores stripped, tense-shifted), result and resource links in a capped-width box, and toggleable raw params with a smooth expand/collapse animation. - CollapsiblePill: shared primitive behind the thinking + tool pills. --- src/components/AI/AIMessage.stories.tsx | 50 +++- src/components/AI/AIMessage.tsx | 105 +++++---- src/components/AI/CollapsiblePill.tsx | 74 ++++++ src/components/AI/MCPToolCall.tsx | 217 ++++++++---------- src/components/AI/index.ts | 3 + .../PillSelect/PillSelect.stories.tsx | 113 +++++++++ src/components/PillSelect/PillSelect.tsx | 108 +++++++++ src/components/PillSelect/index.ts | 5 + src/index.ts | 1 + 9 files changed, 514 insertions(+), 162 deletions(-) create mode 100644 src/components/AI/CollapsiblePill.tsx create mode 100644 src/components/PillSelect/PillSelect.stories.tsx create mode 100644 src/components/PillSelect/PillSelect.tsx create mode 100644 src/components/PillSelect/index.ts 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..de00ce7c 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,61 @@ 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 ? ( +