@v0-sdk/react 0.2.1 → 0.3.1

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/dist/index.js CHANGED
@@ -1,49 +1,80 @@
1
- import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
2
- import React, { useContext, createContext, useState, useRef, useSyncExternalStore } from 'react';
1
+ import React, { createContext, useContext, useState, useRef, useSyncExternalStore } from 'react';
3
2
  import * as jsondiffpatch from 'jsondiffpatch';
4
3
 
4
+ // Headless hook for code block data
5
+ function useCodeBlock(props) {
6
+ const lines = props.code.split('\n');
7
+ return {
8
+ language: props.language,
9
+ code: props.code,
10
+ filename: props.filename,
11
+ lines,
12
+ lineCount: lines.length
13
+ };
14
+ }
5
15
  /**
6
16
  * Generic code block component
7
17
  * Renders plain code by default - consumers should provide their own styling and highlighting
8
- */ function CodeBlock({ language, code, className = '', children }) {
18
+ *
19
+ * For headless usage, use the useCodeBlock hook instead.
20
+ */ function CodeBlock({ language, code, className = '', children, filename }) {
9
21
  // If children provided, use that (allows complete customization)
10
22
  if (children) {
11
- return /*#__PURE__*/ jsx(Fragment, {
12
- children: children
13
- });
23
+ return React.createElement(React.Fragment, {}, children);
14
24
  }
15
- // Simple fallback - just render plain code
16
- return /*#__PURE__*/ jsx("pre", {
17
- className: className,
18
- "data-language": language,
19
- children: /*#__PURE__*/ jsx("code", {
20
- children: code
21
- })
25
+ const codeBlockData = useCodeBlock({
26
+ language,
27
+ code,
28
+ filename
22
29
  });
30
+ // Simple fallback - just render plain code
31
+ // Uses React.createElement for maximum compatibility across environments
32
+ return React.createElement('pre', {
33
+ className,
34
+ 'data-language': codeBlockData.language,
35
+ ...filename && {
36
+ 'data-filename': filename
37
+ }
38
+ }, React.createElement('code', {}, codeBlockData.code));
23
39
  }
24
40
 
25
41
  // Context for providing custom icon implementation
26
- const IconContext = /*#__PURE__*/ createContext(null);
42
+ const IconContext = createContext(null);
43
+ // Headless hook for icon data
44
+ function useIcon(props) {
45
+ return {
46
+ name: props.name,
47
+ fallback: getIconFallback(props.name),
48
+ ariaLabel: props.name.replace('-', ' ')
49
+ };
50
+ }
27
51
  /**
28
52
  * Generic icon component that can be customized by consumers.
29
53
  * By default, renders a simple fallback. Consumers should provide
30
54
  * their own icon implementation via context or props.
55
+ *
56
+ * For headless usage, use the useIcon hook instead.
31
57
  */ function Icon(props) {
32
58
  const CustomIcon = useContext(IconContext);
33
59
  // Use custom icon implementation if provided via context
34
60
  if (CustomIcon) {
35
- return /*#__PURE__*/ jsx(CustomIcon, {
36
- ...props
37
- });
61
+ return React.createElement(CustomIcon, props);
38
62
  }
63
+ const iconData = useIcon(props);
39
64
  // Fallback implementation - consumers should override this
40
- return /*#__PURE__*/ jsx("span", {
65
+ // This uses minimal DOM-specific attributes for maximum compatibility
66
+ return React.createElement('span', {
41
67
  className: props.className,
42
- "data-icon": props.name,
43
- suppressHydrationWarning: true,
44
- "aria-label": props.name.replace('-', ' '),
45
- children: getIconFallback(props.name)
46
- });
68
+ 'data-icon': iconData.name,
69
+ 'aria-label': iconData.ariaLabel
70
+ }, iconData.fallback);
71
+ }
72
+ /**
73
+ * Provider for custom icon implementation
74
+ */ function IconProvider({ children, component }) {
75
+ return React.createElement(IconContext.Provider, {
76
+ value: component
77
+ }, children);
47
78
  }
48
79
  function getIconFallback(name) {
49
80
  const iconMap = {
@@ -59,314 +90,297 @@ function getIconFallback(name) {
59
90
  return iconMap[name] || '•';
60
91
  }
61
92
 
93
+ // Headless hook for code project
94
+ function useCodeProject({ title, filename, code, language = 'typescript', collapsed: initialCollapsed = true }) {
95
+ const [collapsed, setCollapsed] = useState(initialCollapsed);
96
+ // Mock file structure - in a real implementation this could be dynamic
97
+ const files = [
98
+ {
99
+ name: filename || 'page.tsx',
100
+ path: 'app/page.tsx',
101
+ active: true
102
+ },
103
+ {
104
+ name: 'layout.tsx',
105
+ path: 'app/layout.tsx',
106
+ active: false
107
+ },
108
+ {
109
+ name: 'globals.css',
110
+ path: 'app/globals.css',
111
+ active: false
112
+ }
113
+ ];
114
+ return {
115
+ data: {
116
+ title: title || 'Code Project',
117
+ filename,
118
+ code,
119
+ language,
120
+ collapsed,
121
+ files
122
+ },
123
+ collapsed,
124
+ toggleCollapsed: ()=>setCollapsed(!collapsed)
125
+ };
126
+ }
62
127
  /**
63
128
  * Generic code project block component
64
129
  * Renders a collapsible code project with basic structure - consumers provide styling
130
+ *
131
+ * For headless usage, use the useCodeProject hook instead.
65
132
  */ function CodeProjectPart({ title, filename, code, language = 'typescript', collapsed: initialCollapsed = true, className, children, iconRenderer }) {
66
- const [collapsed, setCollapsed] = useState(initialCollapsed);
133
+ const { data, collapsed, toggleCollapsed } = useCodeProject({
134
+ title,
135
+ filename,
136
+ code,
137
+ language,
138
+ collapsed: initialCollapsed
139
+ });
67
140
  // If children provided, use that (allows complete customization)
68
141
  if (children) {
69
- return /*#__PURE__*/ jsx(Fragment, {
70
- children: children
71
- });
142
+ return React.createElement(React.Fragment, {}, children);
72
143
  }
73
- return /*#__PURE__*/ jsxs("div", {
74
- className: className,
75
- "data-component": "code-project-block",
76
- children: [
77
- /*#__PURE__*/ jsxs("button", {
78
- onClick: ()=>setCollapsed(!collapsed),
79
- "data-expanded": !collapsed,
80
- children: [
81
- /*#__PURE__*/ jsxs("div", {
82
- "data-header": true,
83
- children: [
84
- iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
85
- name: 'folder'
86
- }) : /*#__PURE__*/ jsx(Icon, {
87
- name: "folder"
88
- }),
89
- /*#__PURE__*/ jsx("span", {
90
- "data-title": true,
91
- children: title || 'Code Project'
92
- })
93
- ]
94
- }),
95
- /*#__PURE__*/ jsx("span", {
96
- "data-version": true,
97
- children: "v1"
98
- })
99
- ]
100
- }),
101
- !collapsed && /*#__PURE__*/ jsxs("div", {
102
- "data-content": true,
103
- children: [
104
- /*#__PURE__*/ jsxs("div", {
105
- "data-file-list": true,
106
- children: [
107
- /*#__PURE__*/ jsxs("div", {
108
- "data-file": true,
109
- "data-active": true,
110
- children: [
111
- iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
112
- name: 'file-text'
113
- }) : /*#__PURE__*/ jsx(Icon, {
114
- name: "file-text"
115
- }),
116
- /*#__PURE__*/ jsx("span", {
117
- "data-filename": true,
118
- children: filename
119
- }),
120
- /*#__PURE__*/ jsx("span", {
121
- "data-filepath": true,
122
- children: "app/page.tsx"
123
- })
124
- ]
125
- }),
126
- /*#__PURE__*/ jsxs("div", {
127
- "data-file": true,
128
- children: [
129
- iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
130
- name: 'file-text'
131
- }) : /*#__PURE__*/ jsx(Icon, {
132
- name: "file-text"
133
- }),
134
- /*#__PURE__*/ jsx("span", {
135
- "data-filename": true,
136
- children: "layout.tsx"
137
- }),
138
- /*#__PURE__*/ jsx("span", {
139
- "data-filepath": true,
140
- children: "app/layout.tsx"
141
- })
142
- ]
143
- }),
144
- /*#__PURE__*/ jsxs("div", {
145
- "data-file": true,
146
- children: [
147
- iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
148
- name: 'file-text'
149
- }) : /*#__PURE__*/ jsx(Icon, {
150
- name: "file-text"
151
- }),
152
- /*#__PURE__*/ jsx("span", {
153
- "data-filename": true,
154
- children: "globals.css"
155
- }),
156
- /*#__PURE__*/ jsx("span", {
157
- "data-filepath": true,
158
- children: "app/globals.css"
159
- })
160
- ]
161
- })
162
- ]
163
- }),
164
- code && /*#__PURE__*/ jsx(CodeBlock, {
165
- language: language,
166
- code: code
167
- })
168
- ]
169
- })
170
- ]
171
- });
144
+ // Uses React.createElement for maximum compatibility across environments
145
+ return React.createElement('div', {
146
+ className,
147
+ 'data-component': 'code-project-block'
148
+ }, React.createElement('button', {
149
+ onClick: toggleCollapsed,
150
+ 'data-expanded': !collapsed
151
+ }, React.createElement('div', {
152
+ 'data-header': true
153
+ }, iconRenderer ? React.createElement(iconRenderer, {
154
+ name: 'folder'
155
+ }) : React.createElement(Icon, {
156
+ name: 'folder'
157
+ }), React.createElement('span', {
158
+ 'data-title': true
159
+ }, data.title)), React.createElement('span', {
160
+ 'data-version': true
161
+ }, 'v1')), !collapsed ? React.createElement('div', {
162
+ 'data-content': true
163
+ }, React.createElement('div', {
164
+ 'data-file-list': true
165
+ }, data.files.map((file, index)=>React.createElement('div', {
166
+ key: index,
167
+ 'data-file': true,
168
+ ...file.active && {
169
+ 'data-active': true
170
+ }
171
+ }, iconRenderer ? React.createElement(iconRenderer, {
172
+ name: 'file-text'
173
+ }) : React.createElement(Icon, {
174
+ name: 'file-text'
175
+ }), React.createElement('span', {
176
+ 'data-filename': true
177
+ }, file.name), React.createElement('span', {
178
+ 'data-filepath': true
179
+ }, file.path)))), data.code ? React.createElement(CodeBlock, {
180
+ language: data.language,
181
+ code: data.code
182
+ }) : null) : null);
172
183
  }
173
184
 
185
+ // Headless hook for thinking section
186
+ function useThinkingSection({ title, duration, thought, collapsed: initialCollapsed = true, onCollapse }) {
187
+ const [internalCollapsed, setInternalCollapsed] = useState(initialCollapsed);
188
+ const collapsed = onCollapse ? initialCollapsed : internalCollapsed;
189
+ const handleCollapse = onCollapse || (()=>setInternalCollapsed(!internalCollapsed));
190
+ const paragraphs = thought ? thought.split('\n\n') : [];
191
+ const formattedDuration = duration ? `${Math.round(duration)}s` : undefined;
192
+ return {
193
+ data: {
194
+ title: title || 'Thinking',
195
+ duration,
196
+ thought,
197
+ collapsed,
198
+ paragraphs,
199
+ formattedDuration
200
+ },
201
+ collapsed,
202
+ handleCollapse
203
+ };
204
+ }
174
205
  /**
175
206
  * Generic thinking section component
176
207
  * Renders a collapsible section with basic structure - consumers provide styling
208
+ *
209
+ * For headless usage, use the useThinkingSection hook instead.
177
210
  */ function ThinkingSection({ title, duration, thought, collapsed: initialCollapsed = true, onCollapse, className, children, iconRenderer, brainIcon, chevronRightIcon, chevronDownIcon }) {
178
- const [internalCollapsed, setInternalCollapsed] = useState(initialCollapsed);
179
- const collapsed = onCollapse ? initialCollapsed : internalCollapsed;
180
- const handleCollapse = onCollapse || (()=>setInternalCollapsed(!internalCollapsed));
211
+ const { data, collapsed, handleCollapse } = useThinkingSection({
212
+ title,
213
+ duration,
214
+ thought,
215
+ collapsed: initialCollapsed,
216
+ onCollapse
217
+ });
181
218
  // If children provided, use that (allows complete customization)
182
219
  if (children) {
183
- return /*#__PURE__*/ jsx(Fragment, {
184
- children: children
185
- });
220
+ return React.createElement(React.Fragment, {}, children);
186
221
  }
187
- return /*#__PURE__*/ jsxs("div", {
188
- className: className,
189
- "data-component": "thinking-section",
190
- children: [
191
- /*#__PURE__*/ jsxs("button", {
192
- onClick: handleCollapse,
193
- "data-expanded": !collapsed,
194
- "data-button": true,
195
- children: [
196
- /*#__PURE__*/ jsx("div", {
197
- "data-icon-container": true,
198
- children: collapsed ? /*#__PURE__*/ jsxs(Fragment, {
199
- children: [
200
- brainIcon || (iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
201
- name: 'brain'
202
- }) : /*#__PURE__*/ jsx(Icon, {
203
- name: "brain"
204
- })),
205
- chevronRightIcon || (iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
206
- name: 'chevron-right'
207
- }) : /*#__PURE__*/ jsx(Icon, {
208
- name: "chevron-right"
209
- }))
210
- ]
211
- }) : chevronDownIcon || (iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
212
- name: 'chevron-down'
213
- }) : /*#__PURE__*/ jsx(Icon, {
214
- name: "chevron-down"
215
- }))
216
- }),
217
- /*#__PURE__*/ jsxs("span", {
218
- "data-title": true,
219
- children: [
220
- title || 'Thinking',
221
- duration && ` for ${Math.round(duration)}s`
222
- ]
223
- })
224
- ]
225
- }),
226
- !collapsed && thought && /*#__PURE__*/ jsx("div", {
227
- "data-content": true,
228
- children: /*#__PURE__*/ jsx("div", {
229
- "data-thought-container": true,
230
- children: thought.split('\n\n').map((paragraph, index)=>/*#__PURE__*/ jsx("div", {
231
- "data-paragraph": true,
232
- children: paragraph
233
- }, index))
234
- })
235
- })
236
- ]
237
- });
222
+ // Uses React.createElement for maximum compatibility across environments
223
+ return React.createElement('div', {
224
+ className,
225
+ 'data-component': 'thinking-section'
226
+ }, React.createElement('button', {
227
+ onClick: handleCollapse,
228
+ 'data-expanded': !collapsed,
229
+ 'data-button': true
230
+ }, React.createElement('div', {
231
+ 'data-icon-container': true
232
+ }, collapsed ? React.createElement(React.Fragment, {}, brainIcon || (iconRenderer ? React.createElement(iconRenderer, {
233
+ name: 'brain'
234
+ }) : React.createElement(Icon, {
235
+ name: 'brain'
236
+ })), chevronRightIcon || (iconRenderer ? React.createElement(iconRenderer, {
237
+ name: 'chevron-right'
238
+ }) : React.createElement(Icon, {
239
+ name: 'chevron-right'
240
+ }))) : chevronDownIcon || (iconRenderer ? React.createElement(iconRenderer, {
241
+ name: 'chevron-down'
242
+ }) : React.createElement(Icon, {
243
+ name: 'chevron-down'
244
+ }))), React.createElement('span', {
245
+ 'data-title': true
246
+ }, data.title + (data.formattedDuration ? ` for ${data.formattedDuration}` : ''))), !collapsed && data.thought ? React.createElement('div', {
247
+ 'data-content': true
248
+ }, React.createElement('div', {
249
+ 'data-thought-container': true
250
+ }, data.paragraphs.map((paragraph, index)=>React.createElement('div', {
251
+ key: index,
252
+ 'data-paragraph': true
253
+ }, paragraph)))) : null);
238
254
  }
239
255
 
240
- function getTypeIcon(type, title, iconRenderer) {
256
+ function getTypeIcon(type, title) {
241
257
  // Check title content for specific cases
242
258
  if (title?.includes('No issues found')) {
243
- return iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
244
- name: 'wrench'
245
- }) : /*#__PURE__*/ jsx(Icon, {
246
- name: "wrench"
247
- });
259
+ return 'wrench';
248
260
  }
249
261
  if (title?.includes('Analyzed codebase')) {
250
- return iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
251
- name: 'search'
252
- }) : /*#__PURE__*/ jsx(Icon, {
253
- name: "search"
254
- });
262
+ return 'search';
255
263
  }
256
264
  // Fallback to type-based icons
257
265
  switch(type){
258
266
  case 'task-search-web-v1':
259
- return iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
260
- name: 'search'
261
- }) : /*#__PURE__*/ jsx(Icon, {
262
- name: "search"
263
- });
267
+ return 'search';
264
268
  case 'task-search-repo-v1':
265
- return iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
266
- name: 'folder'
267
- }) : /*#__PURE__*/ jsx(Icon, {
268
- name: "folder"
269
- });
269
+ return 'folder';
270
270
  case 'task-diagnostics-v1':
271
- return iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
272
- name: 'settings'
273
- }) : /*#__PURE__*/ jsx(Icon, {
274
- name: "settings"
275
- });
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';
276
278
  default:
277
- return iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
278
- name: 'wrench'
279
- }) : /*#__PURE__*/ jsx(Icon, {
280
- name: "wrench"
281
- });
279
+ return 'wrench';
282
280
  }
283
281
  }
284
- function renderTaskPart(part, index, iconRenderer) {
282
+ function processTaskPart(part, index) {
283
+ const baseData = {
284
+ type: part.type,
285
+ status: part.status,
286
+ content: null
287
+ };
285
288
  if (part.type === 'search-web') {
286
289
  if (part.status === 'searching') {
287
- return /*#__PURE__*/ jsx("div", {
288
- children: `Searching "${part.query}"`
289
- }, index);
290
+ return {
291
+ ...baseData,
292
+ isSearching: true,
293
+ query: part.query,
294
+ content: `Searching "${part.query}"`
295
+ };
290
296
  }
291
297
  if (part.status === 'analyzing') {
292
- return /*#__PURE__*/ jsx("div", {
293
- children: `Analyzing ${part.count} results...`
294
- }, index);
298
+ return {
299
+ ...baseData,
300
+ isAnalyzing: true,
301
+ count: part.count,
302
+ content: `Analyzing ${part.count} results...`
303
+ };
295
304
  }
296
305
  if (part.status === 'complete' && part.answer) {
297
- return /*#__PURE__*/ jsxs("div", {
298
- children: [
299
- /*#__PURE__*/ jsx("p", {
300
- children: part.answer
301
- }),
302
- part.sources && part.sources.length > 0 && /*#__PURE__*/ jsx("div", {
303
- children: part.sources.map((source, sourceIndex)=>/*#__PURE__*/ jsx("a", {
304
- href: source.url,
305
- target: "_blank",
306
- rel: "noopener noreferrer",
307
- children: source.title
308
- }, sourceIndex))
309
- })
310
- ]
311
- }, index);
306
+ return {
307
+ ...baseData,
308
+ isComplete: true,
309
+ answer: part.answer,
310
+ sources: part.sources,
311
+ content: part.answer
312
+ };
312
313
  }
313
314
  }
314
315
  if (part.type === 'search-repo') {
315
316
  if (part.status === 'searching') {
316
- return /*#__PURE__*/ jsx("div", {
317
- children: `Searching "${part.query}"`
318
- }, index);
317
+ return {
318
+ ...baseData,
319
+ isSearching: true,
320
+ query: part.query,
321
+ content: `Searching "${part.query}"`
322
+ };
319
323
  }
320
324
  if (part.status === 'reading' && part.files) {
321
- return /*#__PURE__*/ jsxs("div", {
322
- children: [
323
- /*#__PURE__*/ jsx("span", {
324
- children: "Reading files"
325
- }),
326
- part.files.map((file, fileIndex)=>/*#__PURE__*/ jsxs("span", {
327
- children: [
328
- iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
329
- name: 'file-text'
330
- }) : /*#__PURE__*/ jsx(Icon, {
331
- name: "file-text"
332
- }),
333
- ' ',
334
- file
335
- ]
336
- }, fileIndex))
337
- ]
338
- }, index);
325
+ return {
326
+ ...baseData,
327
+ files: part.files,
328
+ content: 'Reading files'
329
+ };
339
330
  }
340
331
  }
341
332
  if (part.type === 'diagnostics') {
342
333
  if (part.status === 'checking') {
343
- return /*#__PURE__*/ jsx("div", {
344
- children: "Checking for issues..."
345
- }, index);
334
+ return {
335
+ ...baseData,
336
+ content: 'Checking for issues...'
337
+ };
346
338
  }
347
339
  if (part.status === 'complete' && part.issues === 0) {
348
- return /*#__PURE__*/ jsx("div", {
349
- children: "✅ No issues found"
350
- }, index);
340
+ return {
341
+ ...baseData,
342
+ isComplete: true,
343
+ issues: part.issues,
344
+ content: '✅ No issues found'
345
+ };
351
346
  }
352
347
  }
353
- return /*#__PURE__*/ jsx("div", {
354
- children: JSON.stringify(part)
355
- }, index);
348
+ return {
349
+ ...baseData,
350
+ content: JSON.stringify(part)
351
+ };
356
352
  }
357
- /**
358
- * Generic task section component
359
- * Renders a collapsible task section with basic structure - consumers provide styling
360
- */ function TaskSection({ title, type, parts = [], collapsed: initialCollapsed = true, onCollapse, className, children, iconRenderer, taskIcon, chevronRightIcon, chevronDownIcon }) {
353
+ function renderTaskPartContent(partData, index, iconRenderer) {
354
+ if (partData.type === 'search-web' && partData.isComplete && partData.sources) {
355
+ return React.createElement('div', {
356
+ key: index
357
+ }, React.createElement('p', {}, partData.content), partData.sources.length > 0 ? React.createElement('div', {}, partData.sources.map((source, sourceIndex)=>React.createElement('a', {
358
+ key: sourceIndex,
359
+ href: source.url,
360
+ target: '_blank',
361
+ rel: 'noopener noreferrer'
362
+ }, source.title))) : null);
363
+ }
364
+ if (partData.type === 'search-repo' && partData.files) {
365
+ return React.createElement('div', {
366
+ key: index
367
+ }, React.createElement('span', {}, partData.content), partData.files.map((file, fileIndex)=>React.createElement('span', {
368
+ key: fileIndex
369
+ }, iconRenderer ? React.createElement(iconRenderer, {
370
+ name: 'file-text'
371
+ }) : React.createElement(Icon, {
372
+ name: 'file-text'
373
+ }), ' ', file)));
374
+ }
375
+ return React.createElement('div', {
376
+ key: index
377
+ }, partData.content);
378
+ }
379
+ // Headless hook for task section
380
+ function useTaskSection({ title, type, parts = [], collapsed: initialCollapsed = true, onCollapse }) {
361
381
  const [internalCollapsed, setInternalCollapsed] = useState(initialCollapsed);
362
382
  const collapsed = onCollapse ? initialCollapsed : internalCollapsed;
363
383
  const handleCollapse = onCollapse || (()=>setInternalCollapsed(!internalCollapsed));
364
- // If children provided, use that (allows complete customization)
365
- if (children) {
366
- return /*#__PURE__*/ jsx(Fragment, {
367
- children: children
368
- });
369
- }
370
384
  // Count meaningful parts (parts that would render something)
371
385
  const meaningfulParts = parts.filter((part)=>{
372
386
  // Check if the part would render meaningful content
@@ -380,174 +394,231 @@ function renderTaskPart(part, index, iconRenderer) {
380
394
  if (part.type === 'finished-web-search' && part.answer) return true;
381
395
  if (part.type === 'diagnostics-passed') return true;
382
396
  if (part.type === 'fetching-diagnostics') return true;
383
- // Add more meaningful part types as needed
384
397
  return false;
385
398
  });
399
+ const processedParts = parts.map(processTaskPart);
400
+ return {
401
+ data: {
402
+ title: title || 'Task',
403
+ type,
404
+ parts,
405
+ collapsed,
406
+ meaningfulParts,
407
+ shouldShowCollapsible: meaningfulParts.length > 1,
408
+ iconName: getTypeIcon(type, title)
409
+ },
410
+ collapsed,
411
+ handleCollapse,
412
+ processedParts
413
+ };
414
+ }
415
+ /**
416
+ * Generic task section component
417
+ * Renders a collapsible task section with basic structure - consumers provide styling
418
+ *
419
+ * For headless usage, use the useTaskSection hook instead.
420
+ */ function TaskSection({ title, type, parts = [], collapsed: initialCollapsed = true, onCollapse, className, children, iconRenderer, taskIcon, chevronRightIcon, chevronDownIcon }) {
421
+ const { data, collapsed, handleCollapse, processedParts } = useTaskSection({
422
+ title,
423
+ type,
424
+ parts,
425
+ collapsed: initialCollapsed,
426
+ onCollapse
427
+ });
428
+ // If children provided, use that (allows complete customization)
429
+ if (children) {
430
+ return React.createElement(React.Fragment, {}, children);
431
+ }
386
432
  // If there's only one meaningful part, show just the content without the collapsible wrapper
387
- if (meaningfulParts.length === 1) {
388
- return /*#__PURE__*/ jsx("div", {
389
- className: className,
390
- "data-component": "task-section-inline",
391
- children: /*#__PURE__*/ jsx("div", {
392
- "data-part": true,
393
- children: renderTaskPart(meaningfulParts[0], 0, iconRenderer)
394
- })
395
- });
433
+ if (!data.shouldShowCollapsible && data.meaningfulParts.length === 1) {
434
+ const partData = processTaskPart(data.meaningfulParts[0]);
435
+ return React.createElement('div', {
436
+ className,
437
+ 'data-component': 'task-section-inline'
438
+ }, React.createElement('div', {
439
+ 'data-part': true
440
+ }, renderTaskPartContent(partData, 0, iconRenderer)));
396
441
  }
397
- return /*#__PURE__*/ jsxs("div", {
398
- className: className,
399
- "data-component": "task-section",
400
- children: [
401
- /*#__PURE__*/ jsxs("button", {
402
- onClick: handleCollapse,
403
- "data-expanded": !collapsed,
404
- "data-button": true,
405
- children: [
406
- /*#__PURE__*/ jsxs("div", {
407
- "data-icon-container": true,
408
- children: [
409
- /*#__PURE__*/ jsx("div", {
410
- "data-task-icon": true,
411
- children: taskIcon || getTypeIcon(type, title, iconRenderer)
412
- }),
413
- collapsed ? chevronRightIcon || (iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
414
- name: 'chevron-right'
415
- }) : /*#__PURE__*/ jsx(Icon, {
416
- name: "chevron-right"
417
- })) : chevronDownIcon || (iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
418
- name: 'chevron-down'
419
- }) : /*#__PURE__*/ jsx(Icon, {
420
- name: "chevron-down"
421
- }))
422
- ]
423
- }),
424
- /*#__PURE__*/ jsx("span", {
425
- "data-title": true,
426
- children: title || 'Task'
427
- })
428
- ]
429
- }),
430
- !collapsed && /*#__PURE__*/ jsx("div", {
431
- "data-content": true,
432
- children: /*#__PURE__*/ jsx("div", {
433
- "data-parts-container": true,
434
- children: parts.map((part, index)=>/*#__PURE__*/ jsx("div", {
435
- "data-part": true,
436
- children: renderTaskPart(part, index, iconRenderer)
437
- }, index))
438
- })
439
- })
440
- ]
441
- });
442
+ // Uses React.createElement for maximum compatibility across environments
443
+ return React.createElement('div', {
444
+ className,
445
+ 'data-component': 'task-section'
446
+ }, React.createElement('button', {
447
+ onClick: handleCollapse,
448
+ 'data-expanded': !collapsed,
449
+ 'data-button': true
450
+ }, React.createElement('div', {
451
+ 'data-icon-container': true
452
+ }, React.createElement('div', {
453
+ 'data-task-icon': true
454
+ }, taskIcon || (iconRenderer ? React.createElement(iconRenderer, {
455
+ name: data.iconName
456
+ }) : React.createElement(Icon, {
457
+ name: data.iconName
458
+ }))), collapsed ? chevronRightIcon || (iconRenderer ? React.createElement(iconRenderer, {
459
+ name: 'chevron-right'
460
+ }) : React.createElement(Icon, {
461
+ name: 'chevron-right'
462
+ })) : chevronDownIcon || (iconRenderer ? React.createElement(iconRenderer, {
463
+ name: 'chevron-down'
464
+ }) : React.createElement(Icon, {
465
+ name: 'chevron-down'
466
+ }))), React.createElement('span', {
467
+ 'data-title': true
468
+ }, data.title)), !collapsed ? React.createElement('div', {
469
+ 'data-content': true
470
+ }, React.createElement('div', {
471
+ 'data-parts-container': true
472
+ }, processedParts.map((partData, index)=>React.createElement('div', {
473
+ key: index,
474
+ 'data-part': true
475
+ }, renderTaskPartContent(partData, index, iconRenderer))))) : null);
442
476
  }
443
477
 
444
- function ContentPartRenderer({ part, iconRenderer, thinkingSectionRenderer, taskSectionRenderer, brainIcon, chevronRightIcon, chevronDownIcon, searchIcon, folderIcon, settingsIcon, wrenchIcon }) {
445
- if (!part) return null;
478
+ // Headless hook for content part
479
+ function useContentPart(part) {
480
+ if (!part) {
481
+ return {
482
+ type: '',
483
+ parts: [],
484
+ metadata: {},
485
+ componentType: null
486
+ };
487
+ }
446
488
  const { type, parts = [], ...metadata } = part;
489
+ let componentType = 'unknown';
490
+ let title;
491
+ let iconName;
492
+ let thinkingData;
447
493
  switch(type){
448
494
  case 'task-thinking-v1':
449
- {
450
- const thinkingPart = parts.find((p)=>p.type === 'thinking-end');
451
- const ThinkingComponent = thinkingSectionRenderer || ThinkingSection;
452
- const [collapsed, setCollapsed] = useState(true);
453
- return /*#__PURE__*/ jsx(ThinkingComponent, {
454
- title: "Thought",
455
- duration: thinkingPart?.duration,
456
- thought: thinkingPart?.thought,
457
- collapsed: collapsed,
458
- onCollapse: ()=>setCollapsed(!collapsed),
459
- brainIcon: brainIcon,
460
- chevronRightIcon: chevronRightIcon,
461
- chevronDownIcon: chevronDownIcon
462
- });
463
- }
495
+ componentType = 'thinking';
496
+ title = 'Thought';
497
+ const thinkingPart = parts.find((p)=>p.type === 'thinking-end');
498
+ thinkingData = {
499
+ duration: thinkingPart?.duration,
500
+ thought: thinkingPart?.thought
501
+ };
502
+ break;
464
503
  case 'task-search-web-v1':
465
- {
466
- const TaskComponent = taskSectionRenderer || TaskSection;
467
- const [collapsed, setCollapsed] = useState(true);
468
- return /*#__PURE__*/ jsx(TaskComponent, {
469
- title: metadata.taskNameComplete || metadata.taskNameActive,
470
- type: type,
471
- parts: parts,
472
- collapsed: collapsed,
473
- onCollapse: ()=>setCollapsed(!collapsed),
474
- taskIcon: searchIcon,
475
- chevronRightIcon: chevronRightIcon,
476
- chevronDownIcon: chevronDownIcon
477
- });
478
- }
504
+ componentType = 'task';
505
+ title = metadata.taskNameComplete || metadata.taskNameActive;
506
+ iconName = 'search';
507
+ break;
479
508
  case 'task-search-repo-v1':
480
- {
481
- const TaskComponent = taskSectionRenderer || TaskSection;
482
- const [collapsed, setCollapsed] = useState(true);
483
- return /*#__PURE__*/ jsx(TaskComponent, {
484
- title: metadata.taskNameComplete || metadata.taskNameActive,
485
- type: type,
486
- parts: parts,
487
- collapsed: collapsed,
488
- onCollapse: ()=>setCollapsed(!collapsed),
489
- taskIcon: folderIcon,
490
- chevronRightIcon: chevronRightIcon,
491
- chevronDownIcon: chevronDownIcon
492
- });
493
- }
509
+ componentType = 'task';
510
+ title = metadata.taskNameComplete || metadata.taskNameActive;
511
+ iconName = 'folder';
512
+ break;
494
513
  case 'task-diagnostics-v1':
495
- {
496
- const TaskComponent = taskSectionRenderer || TaskSection;
497
- const [collapsed, setCollapsed] = useState(true);
498
- return /*#__PURE__*/ jsx(TaskComponent, {
499
- title: metadata.taskNameComplete || metadata.taskNameActive,
500
- type: type,
501
- parts: parts,
502
- collapsed: collapsed,
503
- onCollapse: ()=>setCollapsed(!collapsed),
504
- taskIcon: settingsIcon,
505
- chevronRightIcon: chevronRightIcon,
506
- chevronDownIcon: chevronDownIcon
507
- });
508
- }
514
+ componentType = 'task';
515
+ title = metadata.taskNameComplete || metadata.taskNameActive;
516
+ iconName = 'settings';
517
+ break;
509
518
  case 'task-read-file-v1':
510
- {
511
- const TaskComponent = taskSectionRenderer || TaskSection;
512
- const [collapsed, setCollapsed] = useState(true);
513
- return /*#__PURE__*/ jsx(TaskComponent, {
514
- title: metadata.taskNameComplete || metadata.taskNameActive || 'Reading file',
515
- type: type,
516
- parts: parts,
517
- collapsed: collapsed,
518
- onCollapse: ()=>setCollapsed(!collapsed),
519
- taskIcon: folderIcon,
520
- chevronRightIcon: chevronRightIcon,
521
- chevronDownIcon: chevronDownIcon
522
- });
523
- }
519
+ componentType = 'task';
520
+ title = metadata.taskNameComplete || metadata.taskNameActive || 'Reading file';
521
+ iconName = 'folder';
522
+ break;
524
523
  case 'task-coding-v1':
525
- {
526
- const TaskComponent = taskSectionRenderer || TaskSection;
527
- const [collapsed, setCollapsed] = useState(true);
528
- return /*#__PURE__*/ jsx(TaskComponent, {
529
- title: metadata.taskNameComplete || metadata.taskNameActive || 'Coding',
530
- type: type,
531
- parts: parts,
532
- collapsed: collapsed,
533
- onCollapse: ()=>setCollapsed(!collapsed),
534
- taskIcon: wrenchIcon,
535
- chevronRightIcon: chevronRightIcon,
536
- chevronDownIcon: chevronDownIcon
537
- });
538
- }
524
+ componentType = 'task';
525
+ title = metadata.taskNameComplete || metadata.taskNameActive || 'Coding';
526
+ iconName = 'wrench';
527
+ break;
539
528
  case 'task-start-v1':
540
- // Usually just indicates task start - can be hidden or show as status
541
- return null;
529
+ componentType = null; // Usually just indicates task start - can be hidden
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
542
537
  default:
543
- return /*#__PURE__*/ jsxs("div", {
544
- "data-unknown-part-type": type,
545
- children: [
546
- "Unknown part type: ",
547
- type
548
- ]
549
- });
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
+ }
548
+ break;
550
549
  }
550
+ return {
551
+ type,
552
+ parts,
553
+ metadata,
554
+ componentType,
555
+ title,
556
+ iconName,
557
+ thinkingData
558
+ };
559
+ }
560
+ /**
561
+ * Content part renderer that handles different types of v0 API content parts
562
+ *
563
+ * For headless usage, use the useContentPart hook instead.
564
+ */ function ContentPartRenderer({ part, iconRenderer, thinkingSectionRenderer, taskSectionRenderer, brainIcon, chevronRightIcon, chevronDownIcon, searchIcon, folderIcon, settingsIcon, wrenchIcon }) {
565
+ const contentData = useContentPart(part);
566
+ if (!contentData.componentType) {
567
+ return null;
568
+ }
569
+ if (contentData.componentType === 'thinking') {
570
+ const ThinkingComponent = thinkingSectionRenderer || ThinkingSection;
571
+ const [collapsed, setCollapsed] = useState(true);
572
+ return React.createElement(ThinkingComponent, {
573
+ title: contentData.title,
574
+ duration: contentData.thinkingData?.duration,
575
+ thought: contentData.thinkingData?.thought,
576
+ collapsed,
577
+ onCollapse: ()=>setCollapsed(!collapsed),
578
+ brainIcon,
579
+ chevronRightIcon,
580
+ chevronDownIcon
581
+ });
582
+ }
583
+ if (contentData.componentType === 'task') {
584
+ const TaskComponent = taskSectionRenderer || TaskSection;
585
+ const [collapsed, setCollapsed] = useState(true);
586
+ // Map icon names to icon components
587
+ let taskIcon;
588
+ switch(contentData.iconName){
589
+ case 'search':
590
+ taskIcon = searchIcon;
591
+ break;
592
+ case 'folder':
593
+ taskIcon = folderIcon;
594
+ break;
595
+ case 'settings':
596
+ taskIcon = settingsIcon;
597
+ break;
598
+ case 'wrench':
599
+ taskIcon = wrenchIcon;
600
+ break;
601
+ default:
602
+ taskIcon = undefined;
603
+ break;
604
+ }
605
+ return React.createElement(TaskComponent, {
606
+ title: contentData.title,
607
+ type: contentData.type,
608
+ parts: contentData.parts,
609
+ collapsed,
610
+ onCollapse: ()=>setCollapsed(!collapsed),
611
+ taskIcon,
612
+ chevronRightIcon,
613
+ chevronDownIcon
614
+ });
615
+ }
616
+ if (contentData.componentType === 'unknown') {
617
+ return React.createElement('div', {
618
+ 'data-unknown-part-type': contentData.type
619
+ }, `Unknown part type: ${contentData.type}`);
620
+ }
621
+ return null;
551
622
  }
552
623
 
553
624
  // Utility function to merge class names
@@ -555,11 +626,17 @@ function cn(...classes) {
555
626
  return classes.filter(Boolean).join(' ');
556
627
  }
557
628
 
558
- // Simplified renderer that matches v0's exact approach
559
- function MessageImpl({ content, messageId = 'unknown', role: _role = 'assistant', streaming: _streaming = false, isLastMessage: _isLastMessage = false, className, components, renderers }) {
629
+ // Headless hook for processing message content
630
+ function useMessage({ content, messageId = 'unknown', role = 'assistant', streaming = false, isLastMessage = false, components, renderers }) {
560
631
  if (!Array.isArray(content)) {
561
632
  console.warn('MessageContent: content must be an array (MessageBinaryFormat)');
562
- return null;
633
+ return {
634
+ elements: [],
635
+ messageId,
636
+ role,
637
+ streaming,
638
+ isLastMessage
639
+ };
563
640
  }
564
641
  // Merge components and renderers (backward compatibility)
565
642
  const mergedComponents = {
@@ -583,10 +660,7 @@ function MessageImpl({ content, messageId = 'unknown', role: _role = 'assistant'
583
660
  const key = `${messageId}-${index}`;
584
661
  // Markdown data (type 0) - this is the main content
585
662
  if (type === 0) {
586
- return /*#__PURE__*/ jsx(Elements, {
587
- data: data,
588
- components: mergedComponents
589
- }, key);
663
+ return processElements(data, key, mergedComponents);
590
664
  }
591
665
  // Metadata (type 1) - extract context but don't render
592
666
  if (type === 1) {
@@ -596,33 +670,40 @@ function MessageImpl({ content, messageId = 'unknown', role: _role = 'assistant'
596
670
  }
597
671
  // Other types - v0 doesn't handle these in the main renderer
598
672
  return null;
599
- });
600
- return /*#__PURE__*/ jsx("div", {
601
- className: className,
602
- children: elements
603
- });
673
+ }).filter(Boolean);
674
+ return {
675
+ elements,
676
+ messageId,
677
+ role,
678
+ streaming,
679
+ isLastMessage
680
+ };
604
681
  }
605
- // This component handles the markdown data array (equivalent to v0's Elements component)
606
- function Elements({ data, components }) {
682
+ // Process elements into headless data structure
683
+ function processElements(data, keyPrefix, components) {
607
684
  // Handle case where data might not be an array due to streaming/patching
608
685
  if (!Array.isArray(data)) {
609
686
  return null;
610
687
  }
611
- const renderedElements = data.map((item, index)=>{
612
- const key = `element-${index}`;
613
- return renderElement(item, key, components);
614
- }).filter(Boolean) // Filter out null/undefined elements
615
- ;
616
- return /*#__PURE__*/ jsx(Fragment, {
617
- children: renderedElements
618
- });
688
+ const children = data.map((item, index)=>{
689
+ const key = `${keyPrefix}-${index}`;
690
+ return processElement(item, key, components);
691
+ }).filter(Boolean);
692
+ return {
693
+ type: 'component',
694
+ key: keyPrefix,
695
+ data: 'elements',
696
+ children
697
+ };
619
698
  }
620
- // Render individual elements (equivalent to v0's element rendering logic)
621
- function renderElement(element, key, components) {
699
+ // Process individual elements into headless data structure
700
+ function processElement(element, key, components) {
622
701
  if (typeof element === 'string') {
623
- return /*#__PURE__*/ jsx("span", {
624
- children: element
625
- }, key);
702
+ return {
703
+ type: 'text',
704
+ key,
705
+ data: element
706
+ };
626
707
  }
627
708
  if (!Array.isArray(element)) {
628
709
  return null;
@@ -633,69 +714,144 @@ function renderElement(element, key, components) {
633
714
  }
634
715
  // Handle special v0 Platform API elements
635
716
  if (tagName === 'AssistantMessageContentPart') {
636
- return /*#__PURE__*/ jsx(ContentPartRenderer, {
637
- part: props.part,
638
- iconRenderer: components?.Icon,
639
- thinkingSectionRenderer: components?.ThinkingSection,
640
- taskSectionRenderer: components?.TaskSection
641
- }, key);
717
+ return {
718
+ type: 'content-part',
719
+ key,
720
+ data: {
721
+ part: props.part,
722
+ iconRenderer: components?.Icon,
723
+ thinkingSectionRenderer: components?.ThinkingSection,
724
+ taskSectionRenderer: components?.TaskSection
725
+ }
726
+ };
642
727
  }
643
728
  if (tagName === 'Codeblock') {
644
- const CustomCodeProjectPart = components?.CodeProjectPart;
645
- const CodeProjectComponent = CustomCodeProjectPart || CodeProjectPart;
646
- return /*#__PURE__*/ jsx(CodeProjectComponent, {
647
- language: props.lang,
648
- code: children[0],
649
- iconRenderer: components?.Icon
650
- }, key);
729
+ return {
730
+ type: 'code-project',
731
+ key,
732
+ data: {
733
+ language: props.lang,
734
+ code: children[0],
735
+ iconRenderer: components?.Icon,
736
+ customRenderer: components?.CodeProjectPart
737
+ }
738
+ };
651
739
  }
652
740
  if (tagName === 'text') {
653
- return /*#__PURE__*/ jsx("span", {
654
- children: children[0] || ''
655
- }, key);
741
+ return {
742
+ type: 'text',
743
+ key,
744
+ data: children[0] || ''
745
+ };
656
746
  }
657
- // Render children
658
- const renderedChildren = children.map((child, childIndex)=>{
747
+ // Process children
748
+ const processedChildren = children.map((child, childIndex)=>{
659
749
  const childKey = `${key}-child-${childIndex}`;
660
- return renderElement(child, childKey, components);
750
+ return processElement(child, childKey, components);
661
751
  }).filter(Boolean);
662
752
  // Handle standard HTML elements
663
- const className = props?.className;
664
753
  const componentOrConfig = components?.[tagName];
665
- if (typeof componentOrConfig === 'function') {
666
- const Component = componentOrConfig;
667
- return /*#__PURE__*/ jsx(Component, {
668
- ...props,
669
- className: className,
670
- children: renderedChildren
671
- }, key);
672
- } else if (componentOrConfig && typeof componentOrConfig === 'object') {
673
- const mergedClassName = cn(className, componentOrConfig.className);
674
- return /*#__PURE__*/ React.createElement(tagName, {
675
- key,
676
- ...props,
677
- className: mergedClassName
678
- }, renderedChildren);
679
- } else {
680
- // Default HTML element rendering
681
- const elementProps = {
682
- key,
683
- ...props
684
- };
685
- if (className) {
686
- elementProps.className = className;
687
- }
688
- // Special handling for links
689
- if (tagName === 'a') {
690
- elementProps.target = '_blank';
691
- elementProps.rel = 'noopener noreferrer';
754
+ return {
755
+ type: 'html',
756
+ key,
757
+ data: {
758
+ tagName,
759
+ props,
760
+ componentOrConfig
761
+ },
762
+ children: processedChildren
763
+ };
764
+ }
765
+ // Default JSX renderer for backward compatibility
766
+ function MessageRenderer({ messageData, className }) {
767
+ const renderElement = (element)=>{
768
+ switch(element.type){
769
+ case 'text':
770
+ return React.createElement('span', {
771
+ key: element.key
772
+ }, element.data);
773
+ case 'content-part':
774
+ return React.createElement(ContentPartRenderer, {
775
+ key: element.key,
776
+ part: element.data.part,
777
+ iconRenderer: element.data.iconRenderer,
778
+ thinkingSectionRenderer: element.data.thinkingSectionRenderer,
779
+ taskSectionRenderer: element.data.taskSectionRenderer
780
+ });
781
+ case 'code-project':
782
+ const CustomCodeProjectPart = element.data.customRenderer;
783
+ const CodeProjectComponent = CustomCodeProjectPart || CodeProjectPart;
784
+ return React.createElement(CodeProjectComponent, {
785
+ key: element.key,
786
+ language: element.data.language,
787
+ code: element.data.code,
788
+ iconRenderer: element.data.iconRenderer
789
+ });
790
+ case 'html':
791
+ const { tagName, props, componentOrConfig } = element.data;
792
+ const renderedChildren = element.children?.map(renderElement);
793
+ if (typeof componentOrConfig === 'function') {
794
+ const Component = componentOrConfig;
795
+ return React.createElement(Component, {
796
+ key: element.key,
797
+ ...props,
798
+ className: props?.className
799
+ }, renderedChildren);
800
+ } else if (componentOrConfig && typeof componentOrConfig === 'object') {
801
+ const mergedClassName = cn(props?.className, componentOrConfig.className);
802
+ return React.createElement(tagName, {
803
+ key: element.key,
804
+ ...props,
805
+ className: mergedClassName
806
+ }, renderedChildren);
807
+ } else {
808
+ // Default HTML element rendering
809
+ const elementProps = {
810
+ key: element.key,
811
+ ...props
812
+ };
813
+ if (props?.className) {
814
+ elementProps.className = props.className;
815
+ }
816
+ // Special handling for links
817
+ if (tagName === 'a') {
818
+ elementProps.target = '_blank';
819
+ elementProps.rel = 'noopener noreferrer';
820
+ }
821
+ return React.createElement(tagName, elementProps, renderedChildren);
822
+ }
823
+ case 'component':
824
+ return React.createElement(React.Fragment, {
825
+ key: element.key
826
+ }, element.children?.map(renderElement));
827
+ default:
828
+ return null;
692
829
  }
693
- return /*#__PURE__*/ React.createElement(tagName, elementProps, renderedChildren);
694
- }
830
+ };
831
+ return React.createElement('div', {
832
+ className
833
+ }, messageData.elements.map(renderElement));
834
+ }
835
+ // Simplified renderer that matches v0's exact approach (backward compatibility)
836
+ function MessageImpl({ content, messageId = 'unknown', role = 'assistant', streaming = false, isLastMessage = false, className, components, renderers }) {
837
+ const messageData = useMessage({
838
+ content,
839
+ messageId,
840
+ role,
841
+ streaming,
842
+ isLastMessage,
843
+ components,
844
+ renderers
845
+ });
846
+ return React.createElement(MessageRenderer, {
847
+ messageData,
848
+ className
849
+ });
695
850
  }
696
851
  /**
697
852
  * Main component for rendering v0 Platform API message content
698
- */ const Message = /*#__PURE__*/ React.memo(MessageImpl);
853
+ * This is a backward-compatible JSX renderer. For headless usage, use the useMessage hook.
854
+ */ const Message = React.memo(MessageImpl);
699
855
 
700
856
  const jdf = jsondiffpatch.create({});
701
857
  // Exact copy of the patch function from v0/chat/lib/diffpatch.ts
@@ -844,8 +1000,8 @@ class StreamStateManager {
844
1000
  this.setComplete(true);
845
1001
  options.onComplete?.(currentContent);
846
1002
  return;
847
- } else if (parsedData.object === 'chat' && parsedData.id) {
848
- // Handle the initial chat data message
1003
+ } else if (parsedData.object && parsedData.object.startsWith('chat')) {
1004
+ // Handle chat metadata messages (chat, chat.title, chat.name, etc.)
849
1005
  options.onChatData?.(parsedData);
850
1006
  continue;
851
1007
  } else if (parsedData.delta) {
@@ -890,9 +1046,33 @@ class StreamStateManager {
890
1046
  return state;
891
1047
  }
892
1048
 
1049
+ // Headless hook for streaming message
1050
+ function useStreamingMessageData({ stream, messageId = 'unknown', role = 'assistant', components, renderers, onChunk, onComplete, onError, onChatData }) {
1051
+ const streamingState = useStreamingMessage(stream, {
1052
+ onChunk,
1053
+ onComplete,
1054
+ onError,
1055
+ onChatData
1056
+ });
1057
+ const messageData = streamingState.content.length > 0 ? useMessage({
1058
+ content: streamingState.content,
1059
+ messageId,
1060
+ role,
1061
+ streaming: streamingState.isStreaming,
1062
+ isLastMessage: true,
1063
+ components,
1064
+ renderers
1065
+ }) : null;
1066
+ return {
1067
+ ...streamingState,
1068
+ messageData
1069
+ };
1070
+ }
893
1071
  /**
894
1072
  * Component for rendering streaming message content from v0 API
895
1073
  *
1074
+ * For headless usage, use the useStreamingMessageData hook instead.
1075
+ *
896
1076
  * @example
897
1077
  * ```tsx
898
1078
  * import { v0 } from 'v0-sdk'
@@ -925,73 +1105,98 @@ class StreamStateManager {
925
1105
  * )
926
1106
  * }
927
1107
  * ```
928
- */ function StreamingMessage({ stream, showLoadingIndicator = true, loadingComponent, errorComponent, onChunk, onComplete, onError, onChatData, ...messageProps }) {
929
- const { content, isStreaming, error, isComplete } = useStreamingMessage(stream, {
1108
+ */ function StreamingMessage({ stream, showLoadingIndicator = true, loadingComponent, errorComponent, onChunk, onComplete, onError, onChatData, className, ...messageProps }) {
1109
+ const streamingData = useStreamingMessageData({
1110
+ stream,
930
1111
  onChunk,
931
1112
  onComplete,
932
1113
  onError,
933
- onChatData
1114
+ onChatData,
1115
+ ...messageProps
934
1116
  });
935
1117
  // Handle error state
936
- if (error) {
1118
+ if (streamingData.error) {
937
1119
  if (errorComponent) {
938
- return /*#__PURE__*/ jsx(Fragment, {
939
- children: errorComponent(error)
940
- });
1120
+ return React.createElement(React.Fragment, {}, errorComponent(streamingData.error));
941
1121
  }
942
- return /*#__PURE__*/ jsxs("div", {
943
- className: "text-red-500 p-4 border border-red-200 rounded",
944
- children: [
945
- "Error: ",
946
- error
947
- ]
948
- });
1122
+ // Fallback error component using React.createElement for compatibility
1123
+ return React.createElement('div', {
1124
+ className: 'text-red-500 p-4 border border-red-200 rounded',
1125
+ style: {
1126
+ color: 'red',
1127
+ padding: '1rem',
1128
+ border: '1px solid #fecaca',
1129
+ borderRadius: '0.375rem'
1130
+ }
1131
+ }, `Error: ${streamingData.error}`);
949
1132
  }
950
1133
  // Handle loading state
951
- if (showLoadingIndicator && isStreaming && content.length === 0) {
1134
+ if (showLoadingIndicator && streamingData.isStreaming && streamingData.content.length === 0) {
952
1135
  if (loadingComponent) {
953
- return /*#__PURE__*/ jsx(Fragment, {
954
- children: loadingComponent
955
- });
1136
+ return React.createElement(React.Fragment, {}, loadingComponent);
956
1137
  }
957
- return /*#__PURE__*/ jsxs("div", {
958
- className: "flex items-center space-x-2 text-gray-500",
959
- children: [
960
- /*#__PURE__*/ jsx("div", {
961
- className: "animate-spin h-4 w-4 border-2 border-gray-300 border-t-gray-600 rounded-full"
962
- }),
963
- /*#__PURE__*/ jsx("span", {
964
- children: "Loading..."
965
- })
966
- ]
967
- });
1138
+ // Fallback loading component using React.createElement for compatibility
1139
+ return React.createElement('div', {
1140
+ className: 'flex items-center space-x-2 text-gray-500',
1141
+ style: {
1142
+ display: 'flex',
1143
+ alignItems: 'center',
1144
+ gap: '0.5rem',
1145
+ color: '#6b7280'
1146
+ }
1147
+ }, React.createElement('div', {
1148
+ className: 'animate-spin h-4 w-4 border-2 border-gray-300 border-t-gray-600 rounded-full',
1149
+ style: {
1150
+ animation: 'spin 1s linear infinite',
1151
+ height: '1rem',
1152
+ width: '1rem',
1153
+ border: '2px solid #d1d5db',
1154
+ borderTopColor: '#4b5563',
1155
+ borderRadius: '50%'
1156
+ }
1157
+ }), React.createElement('span', {}, 'Loading...'));
968
1158
  }
969
1159
  // Render the message content
970
- return /*#__PURE__*/ jsx(Message, {
1160
+ return React.createElement(Message, {
971
1161
  ...messageProps,
972
- content: content,
973
- streaming: isStreaming,
974
- isLastMessage: true
1162
+ content: streamingData.content,
1163
+ streaming: streamingData.isStreaming,
1164
+ isLastMessage: true,
1165
+ className
975
1166
  });
976
1167
  }
977
1168
 
1169
+ // Headless hook for math data
1170
+ function useMath(props) {
1171
+ return {
1172
+ content: props.content,
1173
+ inline: props.inline ?? false,
1174
+ displayMode: props.displayMode ?? !props.inline,
1175
+ processedContent: props.content
1176
+ };
1177
+ }
978
1178
  /**
979
1179
  * Generic math renderer component
980
1180
  * Renders plain math content by default - consumers should provide their own math rendering
981
- */ function MathPart({ content, inline = false, className = '', children }) {
1181
+ *
1182
+ * For headless usage, use the useMath hook instead.
1183
+ */ function MathPart({ content, inline = false, className = '', children, displayMode }) {
982
1184
  // If children provided, use that (allows complete customization)
983
1185
  if (children) {
984
- return /*#__PURE__*/ jsx(Fragment, {
985
- children: children
986
- });
1186
+ return React.createElement(React.Fragment, {}, children);
987
1187
  }
988
- // Simple fallback - just render plain math content
989
- const Element = inline ? 'span' : 'div';
990
- return /*#__PURE__*/ jsx(Element, {
991
- className: className,
992
- "data-math-inline": inline,
993
- children: content
1188
+ const mathData = useMath({
1189
+ content,
1190
+ inline,
1191
+ displayMode
994
1192
  });
1193
+ // Simple fallback - just render plain math content
1194
+ // Uses React.createElement for maximum compatibility across environments
1195
+ return React.createElement(mathData.inline ? 'span' : 'div', {
1196
+ className,
1197
+ 'data-math-inline': mathData.inline,
1198
+ 'data-math-display': mathData.displayMode
1199
+ }, mathData.processedContent);
995
1200
  }
996
1201
 
997
- export { ContentPartRenderer as AssistantMessageContentPart, CodeBlock, CodeProjectPart as CodeProjectBlock, CodeProjectPart, ContentPartRenderer, Icon, MathPart, MathPart as MathRenderer, Message, Message as MessageContent, Message as MessageRenderer, StreamingMessage, TaskSection, ThinkingSection, Message as V0MessageRenderer, useStreamingMessage };
1202
+ export { ContentPartRenderer as AssistantMessageContentPart, CodeBlock, CodeProjectPart as CodeProjectBlock, CodeProjectPart, ContentPartRenderer, Icon, IconProvider, MathPart, MathPart as MathRenderer, Message, Message as MessageContent, Message as MessageRenderer, StreamingMessage, TaskSection, ThinkingSection, Message as V0MessageRenderer, useCodeBlock, useCodeProject, useContentPart, useIcon, useMath, useMessage, useStreamingMessage, useStreamingMessageData, useTaskSection, useThinkingSection };