UNPKG

@v0-sdk/react

Version:

Headless React components for rendering v0 Platform API content

1 lines 61.7 kB
{"version":3,"file":"index.d.ts","sources":["../src/types.ts","../src/components/message.ts","../src/hooks/use-streaming-message.ts","../src/components/streaming-message.ts","../src/components/icon.ts","../src/components/thinking-section.ts","../src/components/task-section.ts","../src/components/code-project-part.ts","../src/components/content-part-renderer.ts","../src/components/math-part.ts","../src/components/code-block.ts"],"sourcesContent":["/**\n * Binary format for message content as returned by the v0 Platform API\n * Each row is a tuple where the first element is the type and the rest are data\n */\nexport type MessageBinaryFormat = [number, ...any[]][]\n\n/**\n * Individual row in the message binary format\n */\nexport type MessageBinaryFormatRow = MessageBinaryFormat[number]\n\n/**\n * Props for the Message component\n */\nexport interface MessageProps {\n /**\n * The parsed content from the v0 Platform API\n * This should be the JSON.parsed value of the 'content' field from API responses\n */\n content: MessageBinaryFormat\n\n /**\n * Optional message ID for tracking purposes\n */\n messageId?: string\n\n /**\n * Role of the message sender\n */\n role?: 'user' | 'assistant' | 'system' | 'tool'\n\n /**\n * Whether the message is currently being streamed\n */\n streaming?: boolean\n\n /**\n * Whether this is the last message in the conversation\n */\n isLastMessage?: boolean\n\n /**\n * Custom className for styling the root container\n */\n className?: string\n\n /**\n * Custom component renderers (react-markdown style)\n * Override specific components by name\n * Can be either a React component or an object with className for simple styling\n */\n components?: {\n // Content components\n CodeBlock?: React.ComponentType<{\n language: string\n code: string\n className?: string\n }>\n MathPart?: React.ComponentType<{\n content: string\n inline?: boolean\n className?: string\n }>\n CodeProjectPart?: React.ComponentType<{\n title?: string\n filename?: string\n code?: string\n language?: string\n collapsed?: boolean\n className?: string\n }>\n ThinkingSection?: React.ComponentType<{\n title?: string\n duration?: number\n thought?: string\n collapsed?: boolean\n onCollapse?: () => void\n className?: string\n children?: React.ReactNode\n brainIcon?: React.ReactNode\n chevronRightIcon?: React.ReactNode\n chevronDownIcon?: React.ReactNode\n }>\n TaskSection?: React.ComponentType<{\n title?: string\n type?: string\n parts?: any[]\n collapsed?: boolean\n onCollapse?: () => void\n className?: string\n children?: React.ReactNode\n taskIcon?: React.ReactNode\n chevronRightIcon?: React.ReactNode\n chevronDownIcon?: React.ReactNode\n }>\n Icon?: React.ComponentType<{\n name:\n | 'chevron-right'\n | 'chevron-down'\n | 'search'\n | 'folder'\n | 'settings'\n | 'file-text'\n | 'brain'\n | 'wrench'\n className?: string\n }>\n\n // HTML elements (react-markdown style)\n // Can be either a React component or an object with className\n p?:\n | React.ComponentType<React.HTMLAttributes<HTMLParagraphElement>>\n | { className?: string }\n h1?:\n | React.ComponentType<React.HTMLAttributes<HTMLHeadingElement>>\n | { className?: string }\n h2?:\n | React.ComponentType<React.HTMLAttributes<HTMLHeadingElement>>\n | { className?: string }\n h3?:\n | React.ComponentType<React.HTMLAttributes<HTMLHeadingElement>>\n | { className?: string }\n h4?:\n | React.ComponentType<React.HTMLAttributes<HTMLHeadingElement>>\n | { className?: string }\n h5?:\n | React.ComponentType<React.HTMLAttributes<HTMLHeadingElement>>\n | { className?: string }\n h6?:\n | React.ComponentType<React.HTMLAttributes<HTMLHeadingElement>>\n | { className?: string }\n ul?:\n | React.ComponentType<React.HTMLAttributes<HTMLUListElement>>\n | { className?: string }\n ol?:\n | React.ComponentType<React.HTMLAttributes<HTMLOListElement>>\n | { className?: string }\n li?:\n | React.ComponentType<React.HTMLAttributes<HTMLLIElement>>\n | { className?: string }\n blockquote?:\n | React.ComponentType<React.HTMLAttributes<HTMLQuoteElement>>\n | { className?: string }\n code?:\n | React.ComponentType<React.HTMLAttributes<HTMLElement>>\n | { className?: string }\n pre?:\n | React.ComponentType<React.HTMLAttributes<HTMLPreElement>>\n | { className?: string }\n strong?:\n | React.ComponentType<React.HTMLAttributes<HTMLElement>>\n | { className?: string }\n em?:\n | React.ComponentType<React.HTMLAttributes<HTMLElement>>\n | { className?: string }\n a?:\n | React.ComponentType<React.AnchorHTMLAttributes<HTMLAnchorElement>>\n | { className?: string }\n hr?:\n | React.ComponentType<React.HTMLAttributes<HTMLHRElement>>\n | { className?: string }\n div?:\n | React.ComponentType<React.HTMLAttributes<HTMLDivElement>>\n | { className?: string }\n span?:\n | React.ComponentType<React.HTMLAttributes<HTMLSpanElement>>\n | { className?: string }\n }\n\n /**\n * @deprecated Use `components` instead. Will be removed in next major version.\n */\n renderers?: {\n CodeBlock?: React.ComponentType<{\n language: string\n code: string\n className?: string\n }>\n MathRenderer?: React.ComponentType<{\n content: string\n inline?: boolean\n className?: string\n }>\n MathPart?: React.ComponentType<{\n content: string\n inline?: boolean\n className?: string\n }>\n Icon?: React.ComponentType<{\n name:\n | 'chevron-right'\n | 'chevron-down'\n | 'search'\n | 'folder'\n | 'settings'\n | 'file-text'\n | 'brain'\n | 'wrench'\n className?: string\n }>\n }\n}\n\n// Backward compatibility exports\nexport type MessageRendererProps = MessageProps\nexport type V0MessageRendererProps = MessageProps\n// Note: MessageStyles/MessageRendererStyles/V0MessageRendererStyles removed as styles prop is no longer supported\n","import React from 'react'\nimport { MessageProps } from '../types'\nimport { MathPart } from './math-part'\nimport { CodeBlock } from './code-block'\nimport { CodeProjectPart } from './code-project-part'\nimport { ContentPartRenderer } from './content-part-renderer'\nimport { cn } from '../utils/cn'\n\n// Headless message data structure\nexport interface MessageData {\n elements: MessageElement[]\n messageId: string\n role: string\n streaming: boolean\n isLastMessage: boolean\n}\n\nexport interface MessageElement {\n type: 'text' | 'html' | 'component' | 'content-part' | 'code-project'\n key: string\n data: any\n props?: Record<string, any>\n children?: MessageElement[]\n}\n\n// Headless hook for processing message content\nexport function useMessage({\n content,\n messageId = 'unknown',\n role = 'assistant',\n streaming = false,\n isLastMessage = false,\n components,\n renderers, // deprecated\n}: Omit<MessageProps, 'className'>): MessageData {\n if (!Array.isArray(content)) {\n console.warn(\n 'MessageContent: content must be an array (MessageBinaryFormat)',\n )\n return {\n elements: [],\n messageId,\n role,\n streaming,\n isLastMessage,\n }\n }\n\n // Merge components and renderers (backward compatibility)\n const mergedComponents = {\n ...components,\n // Map legacy renderers to new component names\n ...(renderers?.CodeBlock && { CodeBlock: renderers.CodeBlock }),\n ...(renderers?.MathRenderer && { MathPart: renderers.MathRenderer }),\n ...(renderers?.MathPart && { MathPart: renderers.MathPart }),\n ...(renderers?.Icon && { Icon: renderers.Icon }),\n }\n\n // Process content exactly like v0's Renderer component\n const elements = content\n .map(([type, data], index) => {\n const key = `${messageId}-${index}`\n\n // Markdown data (type 0) - this is the main content\n if (type === 0) {\n return processElements(data, key, mergedComponents)\n }\n\n // Metadata (type 1) - extract context but don't render\n if (type === 1) {\n // In the future, we could extract sources/context here like v0 does\n // For now, just return null like v0's renderer\n return null\n }\n\n // Other types - v0 doesn't handle these in the main renderer\n return null\n })\n .filter(Boolean) as MessageElement[]\n\n return {\n elements,\n messageId,\n role,\n streaming,\n isLastMessage,\n }\n}\n\n// Process elements into headless data structure\nfunction processElements(\n data: any,\n keyPrefix: string,\n components?: MessageProps['components'],\n): MessageElement | null {\n // Handle case where data might not be an array due to streaming/patching\n if (!Array.isArray(data)) {\n return null\n }\n\n const children = data\n .map((item, index) => {\n const key = `${keyPrefix}-${index}`\n return processElement(item, key, components)\n })\n .filter(Boolean) as MessageElement[]\n\n return {\n type: 'component',\n key: keyPrefix,\n data: 'elements',\n children,\n }\n}\n\n// Process individual elements into headless data structure\nfunction processElement(\n element: any,\n key: string,\n components?: MessageProps['components'],\n): MessageElement | null {\n if (typeof element === 'string') {\n return {\n type: 'text',\n key,\n data: element,\n }\n }\n\n if (!Array.isArray(element)) {\n return null\n }\n\n const [tagName, props, ...children] = element\n\n if (!tagName) {\n return null\n }\n\n // Handle special v0 Platform API elements\n if (tagName === 'AssistantMessageContentPart') {\n return {\n type: 'content-part',\n key,\n data: {\n part: props.part,\n iconRenderer: components?.Icon,\n thinkingSectionRenderer: components?.ThinkingSection,\n taskSectionRenderer: components?.TaskSection,\n },\n }\n }\n\n if (tagName === 'Codeblock') {\n return {\n type: 'code-project',\n key,\n data: {\n language: props.lang,\n code: children[0],\n iconRenderer: components?.Icon,\n customRenderer: components?.CodeProjectPart,\n },\n }\n }\n\n if (tagName === 'text') {\n return {\n type: 'text',\n key,\n data: children[0] || '',\n }\n }\n\n // Process children\n const processedChildren = children\n .map((child, childIndex) => {\n const childKey = `${key}-child-${childIndex}`\n return processElement(child, childKey, components)\n })\n .filter(Boolean) as MessageElement[]\n\n // Handle standard HTML elements\n const componentOrConfig = components?.[tagName as keyof typeof components]\n\n return {\n type: 'html',\n key,\n data: {\n tagName,\n props,\n componentOrConfig,\n },\n children: processedChildren,\n }\n}\n\n// Default JSX renderer for backward compatibility\nfunction MessageRenderer({\n messageData,\n className,\n}: {\n messageData: MessageData\n className?: string\n}) {\n const renderElement = (element: MessageElement): React.ReactNode => {\n switch (element.type) {\n case 'text':\n return React.createElement('span', { key: element.key }, element.data)\n\n case 'content-part':\n return React.createElement(ContentPartRenderer, {\n key: element.key,\n part: element.data.part,\n iconRenderer: element.data.iconRenderer,\n thinkingSectionRenderer: element.data.thinkingSectionRenderer,\n taskSectionRenderer: element.data.taskSectionRenderer,\n })\n\n case 'code-project':\n const CustomCodeProjectPart = element.data.customRenderer\n const CodeProjectComponent = CustomCodeProjectPart || CodeProjectPart\n return React.createElement(CodeProjectComponent, {\n key: element.key,\n language: element.data.language,\n code: element.data.code,\n iconRenderer: element.data.iconRenderer,\n })\n\n case 'html':\n const { tagName, props, componentOrConfig } = element.data\n const renderedChildren = element.children?.map(renderElement)\n\n if (typeof componentOrConfig === 'function') {\n const Component = componentOrConfig\n return React.createElement(\n Component,\n {\n key: element.key,\n ...props,\n className: props?.className,\n },\n renderedChildren,\n )\n } else if (componentOrConfig && typeof componentOrConfig === 'object') {\n const mergedClassName = cn(\n props?.className,\n componentOrConfig.className,\n )\n return React.createElement(\n tagName,\n { key: element.key, ...props, className: mergedClassName },\n renderedChildren,\n )\n } else {\n // Default HTML element rendering\n const elementProps: Record<string, any> = {\n key: element.key,\n ...props,\n }\n if (props?.className) {\n elementProps.className = props.className\n }\n\n // Special handling for links\n if (tagName === 'a') {\n elementProps.target = '_blank'\n elementProps.rel = 'noopener noreferrer'\n }\n\n return React.createElement(tagName, elementProps, renderedChildren)\n }\n\n case 'component':\n return React.createElement(\n React.Fragment,\n { key: element.key },\n element.children?.map(renderElement),\n )\n\n default:\n return null\n }\n }\n\n return React.createElement(\n 'div',\n { className },\n messageData.elements.map(renderElement),\n )\n}\n\n// Simplified renderer that matches v0's exact approach (backward compatibility)\nfunction MessageImpl({\n content,\n messageId = 'unknown',\n role = 'assistant',\n streaming = false,\n isLastMessage = false,\n className,\n components,\n renderers, // deprecated\n}: MessageProps) {\n const messageData = useMessage({\n content,\n messageId,\n role,\n streaming,\n isLastMessage,\n components,\n renderers,\n })\n\n return React.createElement(MessageRenderer, { messageData, className })\n}\n\n/**\n * Main component for rendering v0 Platform API message content\n * This is a backward-compatible JSX renderer. For headless usage, use the useMessage hook.\n */\nexport const Message = React.memo(MessageImpl)\n","import { useRef, useSyncExternalStore } from 'react'\nimport { MessageBinaryFormat } from '../types'\nimport * as jsondiffpatch from 'jsondiffpatch'\n\nconst jdf = jsondiffpatch.create({})\n\n// Exact copy of the patch function from v0/chat/lib/diffpatch.ts\nfunction patch(original: any, delta: any) {\n const newObj = jdf.clone(original)\n\n // Check for our customized delta\n if (Array.isArray(delta) && delta[1] === 9 && delta[2] === 9) {\n // Get the path to the modified element\n const indexes = delta[0].slice(0, -1)\n // Get the string to be appended\n const value = delta[0].slice(-1)\n let obj = newObj as any\n for (const index of indexes) {\n if (typeof obj[index] === 'string') {\n obj[index] += value\n return newObj\n }\n obj = obj[index]\n }\n }\n\n // If not custom delta, apply standard jsondiffpatch-ing\n jdf.patch(newObj, delta)\n return newObj\n}\n\nexport interface StreamingMessageState {\n content: MessageBinaryFormat\n isStreaming: boolean\n error?: string\n isComplete: boolean\n}\n\nexport interface UseStreamingMessageOptions {\n onChunk?: (chunk: MessageBinaryFormat) => void\n onComplete?: (finalContent: MessageBinaryFormat) => void\n onError?: (error: string) => void\n onChatData?: (chatData: any) => void\n}\n\n// Stream state manager - isolated from React lifecycle\nclass StreamStateManager {\n private content: MessageBinaryFormat = []\n private isStreaming: boolean = false\n private error?: string\n private isComplete: boolean = false\n private callbacks = new Set<() => void>()\n private processedStreams = new WeakSet<ReadableStream<Uint8Array>>()\n private cachedState: StreamingMessageState | null = null\n\n subscribe = (callback: () => void) => {\n this.callbacks.add(callback)\n return () => {\n this.callbacks.delete(callback)\n }\n }\n\n private notifySubscribers = () => {\n // Invalidate cached state when state changes\n this.cachedState = null\n this.callbacks.forEach((callback) => callback())\n }\n\n getState = (): StreamingMessageState => {\n // Return cached state to prevent infinite re-renders\n if (this.cachedState === null) {\n this.cachedState = {\n content: this.content,\n isStreaming: this.isStreaming,\n error: this.error,\n isComplete: this.isComplete,\n }\n }\n return this.cachedState\n }\n\n processStream = async (\n stream: ReadableStream<Uint8Array>,\n options: UseStreamingMessageOptions = {},\n ): Promise<void> => {\n // Prevent processing the same stream multiple times\n if (this.processedStreams.has(stream)) {\n return\n }\n\n // Handle locked streams gracefully\n if (stream.locked) {\n console.warn('Stream is locked, cannot process')\n return\n }\n\n this.processedStreams.add(stream)\n this.reset()\n this.setStreaming(true)\n\n try {\n await this.readStream(stream, options)\n } catch (err) {\n const errorMessage =\n err instanceof Error ? err.message : 'Unknown streaming error'\n this.setError(errorMessage)\n options.onError?.(errorMessage)\n } finally {\n this.setStreaming(false)\n }\n }\n\n private reset = () => {\n this.content = []\n this.isStreaming = false\n this.error = undefined\n this.isComplete = false\n this.notifySubscribers()\n }\n\n private setStreaming = (streaming: boolean) => {\n this.isStreaming = streaming\n this.notifySubscribers()\n }\n\n private setError = (error: string) => {\n this.error = error\n this.notifySubscribers()\n }\n\n private setComplete = (complete: boolean) => {\n this.isComplete = complete\n this.notifySubscribers()\n }\n\n private updateContent = (newContent: MessageBinaryFormat) => {\n this.content = [...newContent]\n this.notifySubscribers()\n }\n\n private readStream = async (\n stream: ReadableStream<Uint8Array>,\n options: UseStreamingMessageOptions,\n ): Promise<void> => {\n const reader = stream.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n let currentContent: MessageBinaryFormat = []\n\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) {\n break\n }\n\n const chunk = decoder.decode(value, { stream: true })\n buffer += chunk\n const lines = buffer.split('\\n')\n buffer = lines.pop() || ''\n\n for (const line of lines) {\n if (line.trim() === '') {\n continue\n }\n\n // Handle SSE format (data: ...)\n let jsonData: string\n if (line.startsWith('data: ')) {\n jsonData = line.slice(6) // Remove \"data: \" prefix\n if (jsonData === '[DONE]') {\n this.setComplete(true)\n options.onComplete?.(currentContent)\n return\n }\n } else {\n // Handle raw JSON lines (fallback)\n jsonData = line\n }\n\n try {\n // Parse the JSON data\n const parsedData = JSON.parse(jsonData)\n\n // Handle v0 streaming format\n if (parsedData.type === 'connected') {\n continue\n } else if (parsedData.type === 'done') {\n this.setComplete(true)\n options.onComplete?.(currentContent)\n return\n } else if (\n parsedData.object &&\n parsedData.object.startsWith('chat')\n ) {\n // Handle chat metadata messages (chat, chat.title, chat.name, etc.)\n options.onChatData?.(parsedData)\n continue\n } else if (parsedData.delta) {\n // Apply the delta using jsondiffpatch\n const patchedContent = patch(currentContent, parsedData.delta)\n currentContent = Array.isArray(patchedContent)\n ? (patchedContent as MessageBinaryFormat)\n : []\n\n this.updateContent(currentContent)\n options.onChunk?.(currentContent)\n }\n } catch (e) {\n console.warn('Failed to parse streaming data:', line, e)\n }\n }\n }\n\n this.setComplete(true)\n options.onComplete?.(currentContent)\n } finally {\n reader.releaseLock()\n }\n }\n}\n\n/**\n * Hook for handling streaming message content from v0 API using useSyncExternalStore\n */\nexport function useStreamingMessage(\n stream: ReadableStream<Uint8Array> | null,\n options: UseStreamingMessageOptions = {},\n): StreamingMessageState {\n // Create a stable stream manager instance\n const managerRef = useRef<StreamStateManager | null>(null)\n if (!managerRef.current) {\n managerRef.current = new StreamStateManager()\n }\n\n const manager = managerRef.current\n\n // Subscribe to state changes using useSyncExternalStore\n const state = useSyncExternalStore(\n manager.subscribe,\n manager.getState,\n manager.getState,\n )\n\n // Process stream when it changes\n const lastStreamRef = useRef<ReadableStream<Uint8Array> | null>(null)\n\n if (stream !== lastStreamRef.current) {\n lastStreamRef.current = stream\n if (stream) {\n manager.processStream(stream, options)\n }\n }\n\n return state\n}\n","import React from 'react'\nimport { Message, useMessage, MessageData } from './message'\nimport {\n useStreamingMessage,\n UseStreamingMessageOptions,\n StreamingMessageState,\n} from '../hooks/use-streaming-message'\nimport { MessageProps } from '../types'\n\nexport interface StreamingMessageProps\n extends Omit<MessageProps, 'content' | 'streaming' | 'isLastMessage'>,\n UseStreamingMessageOptions {\n /**\n * The streaming response from v0.chats.create() with responseMode: 'experimental_stream'\n */\n stream: ReadableStream<Uint8Array> | null\n\n /**\n * Show a loading indicator while no content has been received yet\n */\n showLoadingIndicator?: boolean\n\n /**\n * Custom loading component\n */\n loadingComponent?: React.ReactNode\n\n /**\n * Custom error component\n */\n errorComponent?: (error: string) => React.ReactNode\n}\n\n// Headless streaming message data\nexport interface StreamingMessageData extends StreamingMessageState {\n messageData: MessageData | null\n}\n\n// Headless hook for streaming message\nexport function useStreamingMessageData({\n stream,\n messageId = 'unknown',\n role = 'assistant',\n components,\n renderers,\n onChunk,\n onComplete,\n onError,\n onChatData,\n}: Omit<\n StreamingMessageProps,\n 'className' | 'showLoadingIndicator' | 'loadingComponent' | 'errorComponent'\n>): StreamingMessageData {\n const streamingState = useStreamingMessage(stream, {\n onChunk,\n onComplete,\n onError,\n onChatData,\n })\n\n const messageData =\n streamingState.content.length > 0\n ? useMessage({\n content: streamingState.content,\n messageId,\n role,\n streaming: streamingState.isStreaming,\n isLastMessage: true,\n components,\n renderers,\n })\n : null\n\n return {\n ...streamingState,\n messageData,\n }\n}\n\n/**\n * Component for rendering streaming message content from v0 API\n *\n * For headless usage, use the useStreamingMessageData hook instead.\n *\n * @example\n * ```tsx\n * import { v0 } from 'v0-sdk'\n * import { StreamingMessage } from '@v0-sdk/react'\n *\n * function ChatDemo() {\n * const [stream, setStream] = useState<ReadableStream<Uint8Array> | null>(null)\n *\n * const handleSubmit = async () => {\n * const response = await v0.chats.create({\n * message: 'Create a button component',\n * responseMode: 'experimental_stream'\n * })\n * setStream(response)\n * }\n *\n * return (\n * <div>\n * <button onClick={handleSubmit}>Send Message</button>\n * {stream && (\n * <StreamingMessage\n * stream={stream}\n * messageId=\"demo-message\"\n * role=\"assistant\"\n * onComplete={(content) => handleCompletion(content)}\n * onChatData={(chatData) => handleChatData(chatData)}\n * />\n * )}\n * </div>\n * )\n * }\n * ```\n */\nexport function StreamingMessage({\n stream,\n showLoadingIndicator = true,\n loadingComponent,\n errorComponent,\n onChunk,\n onComplete,\n onError,\n onChatData,\n className,\n ...messageProps\n}: StreamingMessageProps) {\n const streamingData = useStreamingMessageData({\n stream,\n onChunk,\n onComplete,\n onError,\n onChatData,\n ...messageProps,\n })\n\n // Handle error state\n if (streamingData.error) {\n if (errorComponent) {\n return React.createElement(\n React.Fragment,\n {},\n errorComponent(streamingData.error),\n )\n }\n // Fallback error component using React.createElement for compatibility\n return React.createElement(\n 'div',\n {\n className: 'text-red-500 p-4 border border-red-200 rounded',\n style: {\n color: 'red',\n padding: '1rem',\n border: '1px solid #fecaca',\n borderRadius: '0.375rem',\n },\n },\n `Error: ${streamingData.error}`,\n )\n }\n\n // Handle loading state\n if (\n showLoadingIndicator &&\n streamingData.isStreaming &&\n streamingData.content.length === 0\n ) {\n if (loadingComponent) {\n return React.createElement(React.Fragment, {}, loadingComponent)\n }\n // Fallback loading component using React.createElement for compatibility\n return React.createElement(\n 'div',\n {\n className: 'flex items-center space-x-2 text-gray-500',\n style: {\n display: 'flex',\n alignItems: 'center',\n gap: '0.5rem',\n color: '#6b7280',\n },\n },\n React.createElement('div', {\n className:\n 'animate-spin h-4 w-4 border-2 border-gray-300 border-t-gray-600 rounded-full',\n style: {\n animation: 'spin 1s linear infinite',\n height: '1rem',\n width: '1rem',\n border: '2px solid #d1d5db',\n borderTopColor: '#4b5563',\n borderRadius: '50%',\n },\n }),\n React.createElement('span', {}, 'Loading...'),\n )\n }\n\n // Render the message content\n return React.createElement(Message, {\n ...messageProps,\n content: streamingData.content,\n streaming: streamingData.isStreaming,\n isLastMessage: true,\n className,\n })\n}\n","import React, { createContext, useContext } from 'react'\n\n// Context for providing custom icon implementation\nconst IconContext = createContext<React.ComponentType<IconProps> | null>(null)\n\nexport interface IconProps {\n name:\n | 'chevron-right'\n | 'chevron-down'\n | 'search'\n | 'folder'\n | 'settings'\n | 'file-text'\n | 'brain'\n | 'wrench'\n className?: string\n}\n\n// Headless icon data\nexport interface IconData {\n name: IconProps['name']\n fallback: string\n ariaLabel: string\n}\n\n// Headless hook for icon data\nexport function useIcon(props: IconProps): IconData {\n return {\n name: props.name,\n fallback: getIconFallback(props.name),\n ariaLabel: props.name.replace('-', ' '),\n }\n}\n\n/**\n * Generic icon component that can be customized by consumers.\n * By default, renders a simple fallback. Consumers should provide\n * their own icon implementation via context or props.\n *\n * For headless usage, use the useIcon hook instead.\n */\nexport function Icon(props: IconProps) {\n const CustomIcon = useContext(IconContext)\n\n // Use custom icon implementation if provided via context\n if (CustomIcon) {\n return React.createElement(CustomIcon, props)\n }\n\n const iconData = useIcon(props)\n\n // Fallback implementation - consumers should override this\n // This uses minimal DOM-specific attributes for maximum compatibility\n return React.createElement(\n 'span',\n {\n className: props.className,\n 'data-icon': iconData.name,\n 'aria-label': iconData.ariaLabel,\n // Note: suppressHydrationWarning removed for React Native compatibility\n },\n iconData.fallback,\n )\n}\n\n/**\n * Provider for custom icon implementation\n */\nexport function IconProvider({\n children,\n component,\n}: {\n children: React.ReactNode\n component: React.ComponentType<IconProps>\n}) {\n return React.createElement(\n IconContext.Provider,\n { value: component },\n children,\n )\n}\n\nfunction getIconFallback(name: string): string {\n const iconMap: Record<string, string> = {\n 'chevron-right': '▶',\n 'chevron-down': '▼',\n search: '🔍',\n folder: '📁',\n settings: '⚙️',\n 'file-text': '📄',\n brain: '🧠',\n wrench: '🔧',\n }\n return iconMap[name] || '•'\n}\n","import React, { useState } from 'react'\nimport { Icon, IconProps } from './icon'\n\nexport interface ThinkingSectionProps {\n title?: string\n duration?: number\n thought?: string\n collapsed?: boolean\n onCollapse?: () => void\n className?: string\n children?: React.ReactNode\n iconRenderer?: React.ComponentType<IconProps>\n brainIcon?: React.ReactNode\n chevronRightIcon?: React.ReactNode\n chevronDownIcon?: React.ReactNode\n}\n\n// Headless thinking section data\nexport interface ThinkingSectionData {\n title: string\n duration?: number\n thought?: string\n collapsed: boolean\n paragraphs: string[]\n formattedDuration?: string\n}\n\n// Headless hook for thinking section\nexport function useThinkingSection({\n title,\n duration,\n thought,\n collapsed: initialCollapsed = true,\n onCollapse,\n}: Omit<\n ThinkingSectionProps,\n | 'className'\n | 'children'\n | 'iconRenderer'\n | 'brainIcon'\n | 'chevronRightIcon'\n | 'chevronDownIcon'\n>): {\n data: ThinkingSectionData\n collapsed: boolean\n handleCollapse: () => void\n} {\n const [internalCollapsed, setInternalCollapsed] = useState(initialCollapsed)\n const collapsed = onCollapse ? initialCollapsed : internalCollapsed\n const handleCollapse =\n onCollapse || (() => setInternalCollapsed(!internalCollapsed))\n\n const paragraphs = thought ? thought.split('\\n\\n') : []\n const formattedDuration = duration ? `${Math.round(duration)}s` : undefined\n\n return {\n data: {\n title: title || 'Thinking',\n duration,\n thought,\n collapsed,\n paragraphs,\n formattedDuration,\n },\n collapsed,\n handleCollapse,\n }\n}\n\n/**\n * Generic thinking section component\n * Renders a collapsible section with basic structure - consumers provide styling\n *\n * For headless usage, use the useThinkingSection hook instead.\n */\nexport function ThinkingSection({\n title,\n duration,\n thought,\n collapsed: initialCollapsed = true,\n onCollapse,\n className,\n children,\n iconRenderer,\n brainIcon,\n chevronRightIcon,\n chevronDownIcon,\n}: ThinkingSectionProps) {\n const { data, collapsed, handleCollapse } = useThinkingSection({\n title,\n duration,\n thought,\n collapsed: initialCollapsed,\n onCollapse,\n })\n\n // If children provided, use that (allows complete customization)\n if (children) {\n return React.createElement(React.Fragment, {}, children)\n }\n\n // Uses React.createElement for maximum compatibility across environments\n return React.createElement(\n 'div',\n {\n className,\n 'data-component': 'thinking-section',\n },\n React.createElement(\n 'button',\n {\n onClick: handleCollapse,\n 'data-expanded': !collapsed,\n 'data-button': true,\n },\n React.createElement(\n 'div',\n { 'data-icon-container': true },\n collapsed\n ? React.createElement(\n React.Fragment,\n {},\n brainIcon ||\n (iconRenderer\n ? React.createElement(iconRenderer, { name: 'brain' })\n : React.createElement(Icon, { name: 'brain' })),\n chevronRightIcon ||\n (iconRenderer\n ? React.createElement(iconRenderer, { name: 'chevron-right' })\n : React.createElement(Icon, { name: 'chevron-right' })),\n )\n : chevronDownIcon ||\n (iconRenderer\n ? React.createElement(iconRenderer, { name: 'chevron-down' })\n : React.createElement(Icon, { name: 'chevron-down' })),\n ),\n React.createElement(\n 'span',\n { 'data-title': true },\n data.title +\n (data.formattedDuration ? ` for ${data.formattedDuration}` : ''),\n ),\n ),\n !collapsed && data.thought\n ? React.createElement(\n 'div',\n { 'data-content': true },\n React.createElement(\n 'div',\n { 'data-thought-container': true },\n data.paragraphs.map((paragraph, index) =>\n React.createElement(\n 'div',\n { key: index, 'data-paragraph': true },\n paragraph,\n ),\n ),\n ),\n )\n : null,\n )\n}\n","import React, { useState } from 'react'\nimport { Icon, IconProps } from './icon'\n\nexport interface TaskSectionProps {\n title?: string\n type?: string\n parts?: any[]\n collapsed?: boolean\n onCollapse?: () => void\n className?: string\n children?: React.ReactNode\n iconRenderer?: React.ComponentType<IconProps>\n taskIcon?: React.ReactNode\n chevronRightIcon?: React.ReactNode\n chevronDownIcon?: React.ReactNode\n}\n\n// Headless task section data\nexport interface TaskSectionData {\n title: string\n type?: string\n parts: any[]\n collapsed: boolean\n meaningfulParts: any[]\n shouldShowCollapsible: boolean\n iconName: IconProps['name']\n}\n\n// Headless task part data\nexport interface TaskPartData {\n type: string\n status?: string\n content: React.ReactNode\n isSearching?: boolean\n isAnalyzing?: boolean\n isComplete?: boolean\n query?: string\n count?: number\n answer?: string\n sources?: Array<{ url: string; title: string }>\n files?: string[]\n issues?: number\n}\n\nfunction getTypeIcon(type?: string, title?: string): IconProps['name'] {\n // Check title content for specific cases\n if (title?.includes('No issues found')) {\n return 'wrench'\n }\n if (title?.includes('Analyzed codebase')) {\n return 'search'\n }\n\n // Fallback to type-based icons\n switch (type) {\n case 'task-search-web-v1':\n return 'search'\n case 'task-search-repo-v1':\n return 'folder'\n case 'task-diagnostics-v1':\n return 'settings'\n case 'task-generate-design-inspiration-v1':\n return 'wrench'\n case 'task-read-file-v1':\n return 'folder'\n case 'task-coding-v1':\n return 'wrench'\n default:\n return 'wrench'\n }\n}\n\nfunction processTaskPart(part: any, index: number): TaskPartData {\n const baseData: TaskPartData = {\n type: part.type,\n status: part.status,\n content: null,\n }\n\n if (part.type === 'search-web') {\n if (part.status === 'searching') {\n return {\n ...baseData,\n isSearching: true,\n query: part.query,\n content: `Searching \"${part.query}\"`,\n }\n }\n if (part.status === 'analyzing') {\n return {\n ...baseData,\n isAnalyzing: true,\n count: part.count,\n content: `Analyzing ${part.count} results...`,\n }\n }\n if (part.status === 'complete' && part.answer) {\n return {\n ...baseData,\n isComplete: true,\n answer: part.answer,\n sources: part.sources,\n content: part.answer,\n }\n }\n }\n\n if (part.type === 'search-repo') {\n if (part.status === 'searching') {\n return {\n ...baseData,\n isSearching: true,\n query: part.query,\n content: `Searching \"${part.query}\"`,\n }\n }\n if (part.status === 'reading' && part.files) {\n return {\n ...baseData,\n files: part.files,\n content: 'Reading files',\n }\n }\n }\n\n if (part.type === 'diagnostics') {\n if (part.status === 'checking') {\n return {\n ...baseData,\n content: 'Checking for issues...',\n }\n }\n if (part.status === 'complete' && part.issues === 0) {\n return {\n ...baseData,\n isComplete: true,\n issues: part.issues,\n content: '✅ No issues found',\n }\n }\n }\n\n return {\n ...baseData,\n content: JSON.stringify(part),\n }\n}\n\nfunction renderTaskPartContent(\n partData: TaskPartData,\n index: number,\n iconRenderer?: React.ComponentType<IconProps>,\n): React.ReactNode {\n if (\n partData.type === 'search-web' &&\n partData.isComplete &&\n partData.sources\n ) {\n return React.createElement(\n 'div',\n { key: index },\n React.createElement('p', {}, partData.content),\n partData.sources.length > 0\n ? React.createElement(\n 'div',\n {},\n partData.sources.map((source, sourceIndex) =>\n React.createElement(\n 'a',\n {\n key: sourceIndex,\n href: source.url,\n target: '_blank',\n rel: 'noopener noreferrer',\n },\n source.title,\n ),\n ),\n )\n : null,\n )\n }\n\n if (partData.type === 'search-repo' && partData.files) {\n return React.createElement(\n 'div',\n { key: index },\n React.createElement('span', {}, partData.content),\n partData.files.map((file, fileIndex) =>\n React.createElement(\n 'span',\n { key: fileIndex },\n iconRenderer\n ? React.createElement(iconRenderer, { name: 'file-text' })\n : React.createElement(Icon, { name: 'file-text' }),\n ' ',\n file,\n ),\n ),\n )\n }\n\n return React.createElement('div', { key: index }, partData.content)\n}\n\n// Headless hook for task section\nexport function useTaskSection({\n title,\n type,\n parts = [],\n collapsed: initialCollapsed = true,\n onCollapse,\n}: Omit<\n TaskSectionProps,\n | 'className'\n | 'children'\n | 'iconRenderer'\n | 'taskIcon'\n | 'chevronRightIcon'\n | 'chevronDownIcon'\n>): {\n data: TaskSectionData\n collapsed: boolean\n handleCollapse: () => void\n processedParts: TaskPartData[]\n} {\n const [internalCollapsed, setInternalCollapsed] = useState(initialCollapsed)\n const collapsed = onCollapse ? initialCollapsed : internalCollapsed\n const handleCollapse =\n onCollapse || (() => setInternalCollapsed(!internalCollapsed))\n\n // Count meaningful parts (parts that would render something)\n const meaningfulParts = parts.filter((part) => {\n // Check if the part would render meaningful content\n if (part.type === 'search-web') {\n return (\n part.status === 'searching' ||\n part.status === 'analyzing' ||\n (part.status === 'complete' && part.answer)\n )\n }\n if (part.type === 'starting-repo-search' && part.query) return true\n if (part.type === 'select-files' && part.filePaths?.length > 0) return true\n if (part.type === 'starting-web-search' && part.query) return true\n if (part.type === 'got-results' && part.count) return true\n if (part.type === 'finished-web-search' && part.answer) return true\n if (part.type === 'diagnostics-passed') return true\n if (part.type === 'fetching-diagnostics') return true\n return false\n })\n\n const processedParts = parts.map(processTaskPart)\n\n return {\n data: {\n title: title || 'Task',\n type,\n parts,\n collapsed,\n meaningfulParts,\n shouldShowCollapsible: meaningfulParts.length > 1,\n iconName: getTypeIcon(type, title),\n },\n collapsed,\n handleCollapse,\n processedParts,\n }\n}\n\n/**\n * Generic task section component\n * Renders a collapsible task section with basic structure - consumers provide styling\n *\n * For headless usage, use the useTaskSection hook instead.\n */\nexport function TaskSection({\n title,\n type,\n parts = [],\n collapsed: initialCollapsed = true,\n onCollapse,\n className,\n children,\n iconRenderer,\n taskIcon,\n chevronRightIcon,\n chevronDownIcon,\n}: TaskSectionProps) {\n const { data, collapsed, handleCollapse, processedParts } = useTaskSection({\n title,\n type,\n parts,\n collapsed: initialCollapsed,\n onCollapse,\n })\n\n // If children provided, use that (allows complete customization)\n if (children) {\n return React.createElement(React.Fragment, {}, children)\n }\n\n // If there's only one meaningful part, show just the content without the collapsible wrapper\n if (!data.shouldShowCollapsible && data.meaningfulParts.length === 1) {\n const partData = processTaskPart(data.meaningfulParts[0], 0)\n return React.createElement(\n 'div',\n {\n className,\n 'data-component': 'task-section-inline',\n },\n React.createElement(\n 'div',\n { 'data-part': true },\n renderTaskPartContent(partData, 0, iconRenderer),\n ),\n )\n }\n\n // Uses React.createElement for maximum compatibility across environments\n return React.createElement(\n 'div',\n {\n className,\n 'data-component': 'task-section',\n },\n React.createElement(\n 'button',\n {\n onClick: handleCollapse,\n 'data-expanded': !collapsed,\n 'data-button': true,\n },\n React.createElement(\n 'div',\n { 'data-icon-container': true },\n React.createElement(\n 'div',\n { 'data-task-icon': true },\n taskIcon ||\n (iconRenderer\n ? React.createElement(iconRenderer, { name: data.iconName })\n : React.createElement(Icon, { name: data.iconName })),\n ),\n collapsed\n ? chevronRightIcon ||\n (iconRenderer\n ? React.createElement(iconRenderer, { name: 'chevron-right' })\n : React.createElement(Icon, { name: 'chevron-right' }))\n : chevronDownIcon ||\n (iconRenderer\n ? React.createElement(iconRenderer, { name: 'chevron-down' })\n : React.createElement(Icon, { name: 'chevron-down' })),\n ),\n React.createElement('span', { 'data-title': true }, data.title),\n ),\n !collapsed\n ? React.createElement(\n 'div',\n { 'data-content': true },\n React.createElement(\n 'div',\n { 'data-parts-container': true },\n processedParts.map((partData, index) =>\n React.createElement(\n 'div',\n { key: index, 'data-part': true },\n renderTaskPartContent(partData, index, iconRenderer),\n ),\n ),\n ),\n )\n : null,\n )\n}\n","import React, { useState } from 'react'\nimport { CodeBlock } from './code-block'\nimport { Icon, IconProps } from './icon'\n\nexport interface CodeProjectPartProps {\n title?: string\n filename?: string\n code?: string\n language?: string\n collapsed?: boolean\n className?: string\n children?: React.ReactNode\n iconRenderer?: React.ComponentType<IconProps>\n}\n\n// Headless code project data\nexport interface CodeProjectData {\n title: string\n filename?: string\n code?: string\n language: string\n collapsed: boolean\n files: Array<{\n name: string\n path: string\n active: boolean\n }>\n}\n\n// Headless hook for code project\nexport function useCodeProject({\n title,\n filename,\n code,\n language = 'typescript',\n collapsed: initialCollapsed = true,\n}: Omit<CodeProjectPartProps, 'className' | 'children' | 'iconRenderer'>): {\n data: CodeProjectData\n collapsed: boolean\n toggleCollapsed: () => void\n} {\n const [collapsed, setCollapsed] = useState(initialCollapsed)\n\n // Mock file structure - in a real implementation this could be dynamic\n const files = [\n {\n name: filename || 'page.tsx',\n path: 'app/page.tsx',\n active: true,\n },\n {\n name: 'layout.tsx',\n path: 'app/layout.tsx',\n active: false,\n },\n {\n name: 'globals.css',\n path: 'app/globals.css',\n active: false,\n },\n ]\n\n return {\n data: {\n title: title || 'Code Project',\n filename,\n code,\n language,\n collapsed,\n files,\n },\n collapsed,\n toggleCollapsed: () => setCollapsed(!collapsed),\n }\n}\n\n/**\n * Generic code project block component\n * Renders a collapsible code project with basic structure - consumers provide styling\n *\n * For headless usage, use the useCodeProject hook instead.\n */\nexport function CodeProjectPart({\n title,\n filename,\n code,\n language = 'typescript',\n collapsed: initialCollapsed = true,\n className,\n children,\n iconRenderer,\n}: CodeProjectPartProps) {\n const { data, collapsed, toggleCollapsed } = useCodeProject({\n title,\n filename,\n code,\n language,\n collapsed: initialCollapsed,\n })\n\n // If children provided, use that (allows complete customization)\n if (children) {\n return React.createElement(React.Fragment, {}, children)\n }\n\n // Uses React.createElement for maximum compatibility across environments\n return React.createElement(\n 'div',\n {\n className,\n 'data-component': 'code-project-block',\n },\n React.createElement(\n 'button',\n {\n onClick: toggleCollapsed,\n 'data-expanded': !collapsed,\n },\n React.createElement(\n 'div',\n { 'data-header': true },\n iconRenderer\n ? React.createElement(iconRenderer, { name: 'folder' })\n : React.createElement(Icon, { name: 'folder' }),\n React.createElement('span', { 'data-title': true }, data.title),\n ),\n React.createElement('span', { 'data-version': true }, 'v1'),\n ),\n !collapsed\n ? React.createElement(\n 'div',\n { 'data-content': true },\n React.createElement(\n 'div',\n { 'data-file-list': true },\n data.files.map((file, index) =>\n React.createElement(\n 'div',\n {\n key: index,\n 'data-file': true,\n ...(file.active && { 'data-active': true }),\n },\n iconRenderer\n ? React.createElement(iconRenderer, { name: 'file-text' })\n : React.createElement(Icon, { name: 'file-text' }),\n React.createElement(\n 'span',\n { 'data-filename': true },\n file.name,\n ),\n React.createElement(\n 'span',\n { 'data-filepath': true },\n file.path,\n ),\n ),\n ),\n ),\n data.code\n ? React.createElement(CodeBlock, {\n language: data.language,\n code: data.code,\n })\n : null,\n )\n : null,\n )\n}\n","import React, { useState } from 'react'\nimport { ThinkingSection } from './thinking-section'\nimport { TaskSection } from './task-section'\nimport { IconProps } from './icon'\n\nexport interface ContentPartRendererProps {\n part: any\n iconRenderer?: React.ComponentType<IconProps>\n thinkingSectionRenderer?: React.ComponentType<{\n title?: string\n duration?: number\n thought?: string\n collapsed?: boolean\n onCollapse?: () => void\n className?: string\n children?: React.ReactNode\n brainIcon?: React.ReactNode\n chevronRightIcon?: React.ReactNode\n chevronDownIcon?: React.ReactNode\n }>\n taskSectionRenderer?: React.ComponentType<{\n title?: string\n type?: string\n parts?: any[]\n collapsed?: boolean\n onCollapse?: () => void\n className?: string\n children?: React.ReactNode\n taskIcon?: React.ReactNode\n chevronRightIcon?: React.ReactNode\n chevronDownIcon?: React.ReactNode\n }>\n // Individual icon props for direct icon usage\n brainIcon?: React.ReactNode\n chevronRightIcon?: React.ReactNode\n chevronDownIcon?: React.ReactNode\n searchIcon?: React.ReactNode\n folderIcon?: React.ReactNode\n settingsIcon?: React.ReactNode\n wrenchIcon?: React.ReactNode\n}\