@v0-sdk/react 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -161,6 +161,30 @@ All components are optional JSX renderers that work with DOM environments. For h
161
161
  - `CodeProjectPart` - Code project file browser
162
162
  - `ContentPartRenderer` - Handles different content part types
163
163
 
164
+ ## Supported Task Types
165
+
166
+ The package automatically handles all v0 Platform API task types:
167
+
168
+ ### Explicitly Supported Tasks
169
+
170
+ - `task-thinking-v1` - AI reasoning and thought processes
171
+ - `task-search-web-v1` - Web search operations with results
172
+ - `task-search-repo-v1` - Repository/codebase search functionality
173
+ - `task-diagnostics-v1` - Code analysis and issue detection
174
+ - `task-read-file-v1` - File reading operations
175
+ - `task-coding-v1` - Code generation and editing tasks
176
+ - `task-generate-design-inspiration-v1` - Design inspiration generation
177
+ - `task-start-v1` - Task initialization (usually hidden)
178
+
179
+ ### Future-Proof Support
180
+
181
+ Any new task type following the `task-*-v1` pattern will be automatically supported with:
182
+
183
+ - Auto-generated readable titles
184
+ - Appropriate icon selection
185
+ - Proper task section rendering
186
+ - Graceful fallback handling
187
+
164
188
  ## Customization
165
189
 
166
190
  ### Custom Components
package/dist/index.cjs CHANGED
@@ -294,6 +294,12 @@ function getTypeIcon(type, title) {
294
294
  return 'folder';
295
295
  case 'task-diagnostics-v1':
296
296
  return 'settings';
297
+ case 'task-generate-design-inspiration-v1':
298
+ return 'wrench';
299
+ case 'task-read-file-v1':
300
+ return 'folder';
301
+ case 'task-coding-v1':
302
+ return 'wrench';
297
303
  default:
298
304
  return 'wrench';
299
305
  }
@@ -547,8 +553,23 @@ function useContentPart(part) {
547
553
  case 'task-start-v1':
548
554
  componentType = null; // Usually just indicates task start - can be hidden
549
555
  break;
556
+ case 'task-generate-design-inspiration-v1':
557
+ componentType = 'task';
558
+ title = metadata.taskNameComplete || metadata.taskNameActive || 'Generating Design Inspiration';
559
+ iconName = 'wrench';
560
+ break;
561
+ // Handle any other task-*-v1 patterns that might be added in the future
550
562
  default:
551
- componentType = 'unknown';
563
+ // Check if it's a task type we haven't explicitly handled yet
564
+ if (type && typeof type === 'string' && type.startsWith('task-') && type.endsWith('-v1')) {
565
+ componentType = 'task';
566
+ // Generate a readable title from the task type
567
+ const taskName = type.replace('task-', '').replace('-v1', '').split('-').map((word)=>word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
568
+ title = metadata.taskNameComplete || metadata.taskNameActive || taskName;
569
+ iconName = 'wrench'; // Default icon for unknown task types
570
+ } else {
571
+ componentType = 'unknown';
572
+ }
552
573
  break;
553
574
  }
554
575
  return {
package/dist/index.d.ts CHANGED
@@ -315,7 +315,7 @@ declare function useIcon(props: IconProps): IconData;
315
315
  */
316
316
  declare function Icon(props: IconProps): React$1.ReactElement<IconProps, string | React$1.JSXElementConstructor<any>> | React$1.DetailedReactHTMLElement<{
317
317
  className: string;
318
- 'data-icon': "search" | "chevron-right" | "chevron-down" | "folder" | "settings" | "file-text" | "brain" | "wrench";
318
+ 'data-icon': "search" | "brain" | "chevron-right" | "chevron-down" | "folder" | "settings" | "file-text" | "wrench";
319
319
  'aria-label': string;
320
320
  }, HTMLElement>;
321
321
  /**
@@ -1 +1 @@
1
- {"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 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}\n\n// Headless content part data\nexport interface ContentPartData {\n type: string\n parts: any[]\n metadata: Record<string, any>\n componentType: 'thinking' | 'task' | 'unknown' | null\n title?: string\n iconName?: IconProps['name']\n thinkingData?: {\n duration?: number\n thought?: string\n }\n}\n\n// Headless hook for content part\nexport function useContentPart(part: any): ContentPartData {\n if (!part) {\n return {\n type: '',\n parts: [],\n metadata: {},\n componentType: null,\n }\n }\n\n const { type, parts = [], ...metadata } = part\n\n let componentType: ContentPartData['componentType'] = 'unknown'\n let title: string | undefined\n let iconName: IconProps['name'] | undefined\n let thinkingData: ContentPartData['thinkingData']\n\n switch (type) {\n case 'task-thinking-v1':\n componentType = 'thinking'\n title = 'Thought'\n const thinkingPart = parts.find((p: any) => p.type === 'thinking-end')\n thinkingData = {\n duration: thinkingPart?.duration,\n thought: thinkingPart?.thought,\n }\n break\n\n case 'task-search-web-v1':\n componentType = 'task'\n title = metadata.taskNameComplete || metadata.taskNameActive\n iconName = 'search'\n break\n\n case 'task-search-repo-v1':\n componentType = 'task'\n title = metadata.taskNameComplete || metadata.taskNameActive\n iconName = 'folder'\n break\n\n case 'task-diagnostics-v1':\n componentType = 'task'\n title = metadata.taskNameComplete || metadata.taskNameActive\n iconName = 'settings'\n break\n\n case 'task-read-file-v1':\n componentType = 'task'\n title =\n metadata.taskNameComplete || metadata.taskNameActive || 'Reading file'\n iconName = 'folder'\n break\n\n case 'task-coding-v1':\n componentType = 'task'\n title = metadata.taskNameComplete || metadata.taskNameActive || 'Coding'\n iconName = 'wrench'\n break\n\n case 'task-start-v1':\n componentType = null // Usually just indicates task start - can be hidden\n break\n\n default:\n componentType = 'unknown'\n break\n }\n\n return {\n type,\n parts,\n metadata,\n componentType,\n title,\n iconName,\n thinkingData,\n }\n}\n\n/**\n * Content part renderer that handles different types of v0 API content parts\n *\n * For headless usage, use the useContentPart hook instead.\n */\nexport function ContentPartRenderer({\n part,\n iconRenderer,\n thinkingSectionRenderer,\n taskSectionRenderer,\n brainIcon,\n chevronRightIcon,\n chevronDownIcon,\n searchIcon,\n folderIcon,\n settingsIcon,\n wrenchIcon,\n}: ContentPartRendererProps) {\n const contentData = useContentPart(part)\n\n if (!contentData.componentType) {\n return null\n }\n\n if (contentData.componentType === 'thinking') {\n const ThinkingComponent = thinkingSectionRenderer || ThinkingSection\n const [collapsed, setCollapsed] = useState(true)\n\n return React.createElement(ThinkingComponent, {\n title: contentData.title,\n duration: contentData.thinkingData?.duration,\n thought: contentData.thinkingData?.thought,\n collapsed,\n onCollapse: () => setCollapsed(!collapsed),\n brainIcon,\n chevronRightIcon,\n chevronDownIcon,\n })\n }\n\n if (contentData.componentType === 'task') {\n const TaskComponent = taskSectionRenderer || TaskSection\n const [collapsed, setCollapsed] = useState(true)\n\n // Map icon names to icon components\n let taskIcon: React.ReactNode\n switch (contentData.iconName) {\n case 'search':\n taskIcon = searchIcon\n break\n case 'folder':\n taskIcon = folderIcon\n break\n case 'settings':\n taskIcon = settingsIcon\n break\n case 'wrench':\n taskIcon = wrenchIcon\n break\n default:\n taskIcon = undefined\n break\n }\n\n return React.createElement(TaskComponent, {\n title: contentData.title,\n type: contentData.type,\n parts: contentData.parts,\n collapsed,\n onCollapse: () => setCollapsed(!collapsed),\n taskIcon,\n chevronRightIcon,\n chevronDownIcon,\n })\n }\n\n if (contentData.componentType === 'unknown') {\n return React.createElement(\n 'div',\n { 'data-unknown-part-type': contentData.type },\n `Unknown part type: ${contentData.type}`,\n )\n }\n\n return null\n}\n","import React from 'react'\n\nexport interface MathPartProps {\n content: string\n inline?: boolean\n className?: string\n children?: React.ReactNode\n displayMode?: boolean\n}\n\n// Headless math data\nexport interface MathData {\n content: string\n inline: boolean\n displayMode: boolean\n processedContent: string\n}\n\n// Headless hook for math data\nexport function useMath(\n props: Omit<MathPartProps, 'className' | 'children'>,\n): MathData {\n return {\n content: props.content,\n inline: props.inline ?? false,\n displayMode: props.displayMode ?? !props.inline,\n processedContent: props.content, // Could be enhanced with math processing\n }\n}\n\n/**\n * Generic math renderer component\n * Renders plain math content by default - consumers should provide their own math rendering\n *\n * For headless usage, use the useMath hook instead.\n */\nexport function MathPart({\n content,\n inline = false,\n className = '',\n children,\n displayMode,\n}: MathPartProps) {\n // If children provided, use that (allows complete customization)\n if (children) {\n return React.createElement(React.Fragment, {}, children)\n }\n\n const mathData = useMath({ content, inline, displayMode })\n\n // Simple fallback - just render plain math content\n // Uses React.createElement for maximum compatibility across environments\n return React.createElement(\n mathData.inline ? 'span' : 'div',\n {\n className,\n 'data-math-inline': mathData.inline,\n 'data-math-display': mathData.displayMode,\n },\n mathData.processedContent,\n )\n}\n","import React from 'react'\n\nexport interface CodeBlockProps {\n language: string\n code: string\n className?: string\n children?: React.ReactNode\n filename?: string\n}\n\n// Headless code block data\nexport interface CodeBlockData {\n language: string\n code: string\n filename?: string\n lines: string[]\n lineCount: number\n}\n\n// Headless hook for code block data\nexport function useCodeBlock(\n props: Omit<CodeBlockProps, 'className' | 'children'>,\n): CodeBlockData {\n const lines = props.code.split('\\n')\n\n return {\n language: props.language,\n code: props.code,\n filename: props.filename,\n lines,\n lineCount: lines.length,\n }\n}\n\n/**\n * Generic code block component\n * Renders plain code by default - consumers should provide their own styling and highlighting\n *\n * For headless usage, use the useCodeBlock hook instead.\n */\nexport function CodeBlock({\n language,\n code,\n className = '',\n children,\n filename,\n}: CodeBlockProps) {\n // If children provided, use that (allows complete customization)\n if (children) {\n return React.createElement(React.Fragment, {}, children)\n }\n\n const codeBlockData = useCodeBlock({ language, code, filename })\n\n // Simple fallback - just render plain code\n // Uses React.createElement for maximum compatibility across environments\n return React.createElement(\n 'pre',\n {\n className,\n 'data-language': codeBlockData.language,\n ...(filename && { 'data-filename': filename }),\n },\n React.createElement('code', {}, codeBlockData.code),\n )\n}\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACA;;AC5KA;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;;ACxBA;AACP;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;;ACZA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC/EO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;AC3BO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;ACnCO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;ACtDO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;ACpCO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC9EO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;;ACxBO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;;;"}
1
+ {"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}\n\n// Headless content part data\nexport interface ContentPartData {\n type: string\n parts: any[]\n metadata: Record<string, any>\n componentType: 'thinking' | 'task' | 'unknown' | null\n title?: string\n iconName?: IconProps['name']\n thinkingData?: {\n duration?: number\n thought?: string\n }\n}\n\n// Headless hook for content part\nexport function useContentPart(part: any): ContentPartData {\n if (!part) {\n return {\n type: '',\n parts: [],\n metadata: {},\n componentType: null,\n }\n }\n\n const { type, parts = [], ...metadata } = part\n\n let componentType: ContentPartData['componentType'] = 'unknown'\n let title: string | undefined\n let iconName: IconProps['name'] | undefined\n let thinkingData: ContentPartData['thinkingData']\n\n switch (type) {\n case 'task-thinking-v1':\n componentType = 'thinking'\n title = 'Thought'\n const thinkingPart = parts.find((p: any) => p.type === 'thinking-end')\n thinkingData = {\n duration: thinkingPart?.duration,\n thought: thinkingPart?.thought,\n }\n break\n\n case 'task-search-web-v1':\n componentType = 'task'\n title = metadata.taskNameComplete || metadata.taskNameActive\n iconName = 'search'\n break\n\n case 'task-search-repo-v1':\n componentType = 'task'\n title = metadata.taskNameComplete || metadata.taskNameActive\n iconName = 'folder'\n break\n\n case 'task-diagnostics-v1':\n componentType = 'task'\n title = metadata.taskNameComplete || metadata.taskNameActive\n iconName = 'settings'\n break\n\n case 'task-read-file-v1':\n componentType = 'task'\n title =\n metadata.taskNameComplete || metadata.taskNameActive || 'Reading file'\n iconName = 'folder'\n break\n\n case 'task-coding-v1':\n componentType = 'task'\n title = metadata.taskNameComplete || metadata.taskNameActive || 'Coding'\n iconName = 'wrench'\n break\n\n case 'task-start-v1':\n componentType = null // Usually just indicates task start - can be hidden\n break\n\n case 'task-generate-design-inspiration-v1':\n componentType = 'task'\n title =\n metadata.taskNameComplete ||\n metadata.taskNameActive ||\n 'Generating Design Inspiration'\n iconName = 'wrench'\n break\n\n // Handle any other task-*-v1 patterns that might be added in the future\n default:\n // Check if it's a task type we haven't explicitly handled yet\n if (\n type &&\n typeof type === 'string' &&\n type.startsWith('task-') &&\n type.endsWith('-v1')\n ) {\n componentType = 'task'\n // Generate a readable title from the task type\n const taskName = type\n .replace('task-', '')\n .replace('-v1', '')\n .split('-')\n .map((word: string) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n title = metadata.taskNameComplete || metadata.taskNameActive || taskName\n iconName = 'wrench' // Default icon for unknown task types\n } else {\n componentType = 'unknown'\n }\n break\n }\n\n return {\n type,\n parts,\n metadata,\n componentType,\n title,\n iconName,\n thinkingData,\n }\n}\n\n/**\n * Content part renderer that handles different types of v0 API content parts\n *\n * For headless usage, use the useContentPart hook instead.\n */\nexport function ContentPartRenderer({\n part,\n iconRenderer,\n thinkingSectionRenderer,\n taskSectionRenderer,\n brainIcon,\n chevronRightIcon,\n chevronDownIcon,\n searchIcon,\n folderIcon,\n settingsIcon,\n wrenchIcon,\n}: ContentPartRendererProps) {\n const contentData = useContentPart(part)\n\n if (!contentData.componentType) {\n return null\n }\n\n if (contentData.componentType === 'thinking') {\n const ThinkingComponent = thinkingSectionRenderer || ThinkingSection\n const [collapsed, setCollapsed] = useState(true)\n\n return React.createElement(ThinkingComponent, {\n title: contentData.title,\n duration: contentData.thinkingData?.duration,\n thought: contentData.thinkingData?.thought,\n collapsed,\n onCollapse: () => setCollapsed(!collapsed),\n brainIcon,\n chevronRightIcon,\n chevronDownIcon,\n })\n }\n\n if (contentData.componentType === 'task') {\n const TaskComponent = taskSectionRenderer || TaskSection\n const [collapsed, setCollapsed] = useState(true)\n\n // Map icon names to icon components\n let taskIcon: React.ReactNode\n switch (contentData.iconName) {\n case 'search':\n taskIcon = searchIcon\n break\n case 'folder':\n taskIcon = folderIcon\n break\n case 'settings':\n taskIcon = settingsIcon\n break\n case 'wrench':\n taskIcon = wrenchIcon\n break\n default:\n taskIcon = undefined\n break\n }\n\n return React.createElement(TaskComponent, {\n title: contentData.title,\n type: contentData.type,\n parts: contentData.parts,\n collapsed,\n onCollapse: () => setCollapsed(!collapsed),\n taskIcon,\n chevronRightIcon,\n chevronDownIcon,\n })\n }\n\n if (contentData.componentType === 'unknown') {\n return React.createElement(\n 'div',\n { 'data-unknown-part-type': contentData.type },\n `Unknown part type: ${contentData.type}`,\n )\n }\n\n return null\n}\n","import React from 'react'\n\nexport interface MathPartProps {\n content: string\n inline?: boolean\n className?: string\n children?: React.ReactNode\n displayMode?: boolean\n}\n\n// Headless math data\nexport interface MathData {\n content: string\n inline: boolean\n displayMode: boolean\n processedContent: string\n}\n\n// Headless hook for math data\nexport function useMath(\n props: Omit<MathPartProps, 'className' | 'children'>,\n): MathData {\n return {\n content: props.content,\n inline: props.inline ?? false,\n displayMode: props.displayMode ?? !props.inline,\n processedContent: props.content, // Could be enhanced with math processing\n }\n}\n\n/**\n * Generic math renderer component\n * Renders plain math content by default - consumers should provide their own math rendering\n *\n * For headless usage, use the useMath hook instead.\n */\nexport function MathPart({\n content,\n inline = false,\n className = '',\n children,\n displayMode,\n}: MathPartProps) {\n // If children provided, use that (allows complete customization)\n if (children) {\n return React.createElement(React.Fragment, {}, children)\n }\n\n const mathData = useMath({ content, inline, displayMode })\n\n // Simple fallback - just render plain math content\n // Uses React.createElement for maximum compatibility across environments\n return React.createElement(\n mathData.inline ? 'span' : 'div',\n {\n className,\n 'data-math-inline': mathData.inline,\n 'data-math-display': mathData.displayMode,\n },\n mathData.processedContent,\n )\n}\n","import React from 'react'\n\nexport interface CodeBlockProps {\n language: string\n code: string\n className?: string\n children?: React.ReactNode\n filename?: string\n}\n\n// Headless code block data\nexport interface CodeBlockData {\n language: string\n code: string\n filename?: string\n lines: string[]\n lineCount: number\n}\n\n// Headless hook for code block data\nexport function useCodeBlock(\n props: Omit<CodeBlockProps, 'className' | 'children'>,\n): CodeBlockData {\n const lines = props.code.split('\\n')\n\n return {\n language: props.language,\n code: props.code,\n filename: props.filename,\n lines,\n lineCount: lines.length,\n }\n}\n\n/**\n * Generic code block component\n * Renders plain code by default - consumers should provide their own styling and highlighting\n *\n * For headless usage, use the useCodeBlock hook instead.\n */\nexport function CodeBlock({\n language,\n code,\n className = '',\n children,\n filename,\n}: CodeBlockProps) {\n // If children provided, use that (allows complete customization)\n if (children) {\n return React.createElement(React.Fragment, {}, children)\n }\n\n const codeBlockData = useCodeBlock({ language, code, filename })\n\n // Simple fallback - just render plain code\n // Uses React.createElement for maximum compatibility across environments\n return React.createElement(\n 'pre',\n {\n className,\n 'data-language': codeBlockData.language,\n ...(filename && { 'data-filename': filename }),\n },\n React.createElement('code', {}, codeBlockData.code),\n )\n}\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACA;;AC5KA;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;;ACxBA;AACP;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;;ACZA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC/EO;AACP;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;AC3BO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;ACnCO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;ACtDO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;;ACpCO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC9EO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;;ACxBO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;;;"}
package/dist/index.js CHANGED
@@ -269,6 +269,12 @@ function getTypeIcon(type, title) {
269
269
  return 'folder';
270
270
  case 'task-diagnostics-v1':
271
271
  return 'settings';
272
+ case 'task-generate-design-inspiration-v1':
273
+ return 'wrench';
274
+ case 'task-read-file-v1':
275
+ return 'folder';
276
+ case 'task-coding-v1':
277
+ return 'wrench';
272
278
  default:
273
279
  return 'wrench';
274
280
  }
@@ -522,8 +528,23 @@ function useContentPart(part) {
522
528
  case 'task-start-v1':
523
529
  componentType = null; // Usually just indicates task start - can be hidden
524
530
  break;
531
+ case 'task-generate-design-inspiration-v1':
532
+ componentType = 'task';
533
+ title = metadata.taskNameComplete || metadata.taskNameActive || 'Generating Design Inspiration';
534
+ iconName = 'wrench';
535
+ break;
536
+ // Handle any other task-*-v1 patterns that might be added in the future
525
537
  default:
526
- componentType = 'unknown';
538
+ // Check if it's a task type we haven't explicitly handled yet
539
+ if (type && typeof type === 'string' && type.startsWith('task-') && type.endsWith('-v1')) {
540
+ componentType = 'task';
541
+ // Generate a readable title from the task type
542
+ const taskName = type.replace('task-', '').replace('-v1', '').split('-').map((word)=>word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
543
+ title = metadata.taskNameComplete || metadata.taskNameActive || taskName;
544
+ iconName = 'wrench'; // Default icon for unknown task types
545
+ } else {
546
+ componentType = 'unknown';
547
+ }
527
548
  break;
528
549
  }
529
550
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v0-sdk/react",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Headless React components for rendering v0 Platform API content",
5
5
  "homepage": "https://v0.dev/docs/api",
6
6
  "repository": {
@@ -21,11 +21,6 @@
21
21
  "files": [
22
22
  "dist"
23
23
  ],
24
- "scripts": {
25
- "type-check": "tsc --noEmit",
26
- "build": "bunchee",
27
- "format": "prettier --write \"**/*.{ts,tsx,md}\""
28
- },
29
24
  "peerDependencies": {
30
25
  "react": "^18.0.0 || ^19.0.0"
31
26
  },
@@ -55,5 +50,12 @@
55
50
  },
56
51
  "dependencies": {
57
52
  "jsondiffpatch": "^0.7.3"
53
+ },
54
+ "scripts": {
55
+ "type-check": "tsc --noEmit",
56
+ "build": "bunchee",
57
+ "clean": "rm -rf dist *.tsbuildinfo",
58
+ "format": "prettier --write \"**/*.{ts,tsx,md}\"",
59
+ "lint": "echo 'No linting configured for react package'"
58
60
  }
59
- }
61
+ }