@v0-sdk/react 0.2.1 → 0.3.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/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,291 @@ 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';
276
272
  default:
277
- return iconRenderer ? /*#__PURE__*/ React.createElement(iconRenderer, {
278
- name: 'wrench'
279
- }) : /*#__PURE__*/ jsx(Icon, {
280
- name: "wrench"
281
- });
273
+ return 'wrench';
282
274
  }
283
275
  }
284
- function renderTaskPart(part, index, iconRenderer) {
276
+ function processTaskPart(part, index) {
277
+ const baseData = {
278
+ type: part.type,
279
+ status: part.status,
280
+ content: null
281
+ };
285
282
  if (part.type === 'search-web') {
286
283
  if (part.status === 'searching') {
287
- return /*#__PURE__*/ jsx("div", {
288
- children: `Searching "${part.query}"`
289
- }, index);
284
+ return {
285
+ ...baseData,
286
+ isSearching: true,
287
+ query: part.query,
288
+ content: `Searching "${part.query}"`
289
+ };
290
290
  }
291
291
  if (part.status === 'analyzing') {
292
- return /*#__PURE__*/ jsx("div", {
293
- children: `Analyzing ${part.count} results...`
294
- }, index);
292
+ return {
293
+ ...baseData,
294
+ isAnalyzing: true,
295
+ count: part.count,
296
+ content: `Analyzing ${part.count} results...`
297
+ };
295
298
  }
296
299
  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);
300
+ return {
301
+ ...baseData,
302
+ isComplete: true,
303
+ answer: part.answer,
304
+ sources: part.sources,
305
+ content: part.answer
306
+ };
312
307
  }
313
308
  }
314
309
  if (part.type === 'search-repo') {
315
310
  if (part.status === 'searching') {
316
- return /*#__PURE__*/ jsx("div", {
317
- children: `Searching "${part.query}"`
318
- }, index);
311
+ return {
312
+ ...baseData,
313
+ isSearching: true,
314
+ query: part.query,
315
+ content: `Searching "${part.query}"`
316
+ };
319
317
  }
320
318
  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);
319
+ return {
320
+ ...baseData,
321
+ files: part.files,
322
+ content: 'Reading files'
323
+ };
339
324
  }
340
325
  }
341
326
  if (part.type === 'diagnostics') {
342
327
  if (part.status === 'checking') {
343
- return /*#__PURE__*/ jsx("div", {
344
- children: "Checking for issues..."
345
- }, index);
328
+ return {
329
+ ...baseData,
330
+ content: 'Checking for issues...'
331
+ };
346
332
  }
347
333
  if (part.status === 'complete' && part.issues === 0) {
348
- return /*#__PURE__*/ jsx("div", {
349
- children: "✅ No issues found"
350
- }, index);
334
+ return {
335
+ ...baseData,
336
+ isComplete: true,
337
+ issues: part.issues,
338
+ content: '✅ No issues found'
339
+ };
351
340
  }
352
341
  }
353
- return /*#__PURE__*/ jsx("div", {
354
- children: JSON.stringify(part)
355
- }, index);
342
+ return {
343
+ ...baseData,
344
+ content: JSON.stringify(part)
345
+ };
356
346
  }
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 }) {
347
+ function renderTaskPartContent(partData, index, iconRenderer) {
348
+ if (partData.type === 'search-web' && partData.isComplete && partData.sources) {
349
+ return React.createElement('div', {
350
+ key: index
351
+ }, React.createElement('p', {}, partData.content), partData.sources.length > 0 ? React.createElement('div', {}, partData.sources.map((source, sourceIndex)=>React.createElement('a', {
352
+ key: sourceIndex,
353
+ href: source.url,
354
+ target: '_blank',
355
+ rel: 'noopener noreferrer'
356
+ }, source.title))) : null);
357
+ }
358
+ if (partData.type === 'search-repo' && partData.files) {
359
+ return React.createElement('div', {
360
+ key: index
361
+ }, React.createElement('span', {}, partData.content), partData.files.map((file, fileIndex)=>React.createElement('span', {
362
+ key: fileIndex
363
+ }, iconRenderer ? React.createElement(iconRenderer, {
364
+ name: 'file-text'
365
+ }) : React.createElement(Icon, {
366
+ name: 'file-text'
367
+ }), ' ', file)));
368
+ }
369
+ return React.createElement('div', {
370
+ key: index
371
+ }, partData.content);
372
+ }
373
+ // Headless hook for task section
374
+ function useTaskSection({ title, type, parts = [], collapsed: initialCollapsed = true, onCollapse }) {
361
375
  const [internalCollapsed, setInternalCollapsed] = useState(initialCollapsed);
362
376
  const collapsed = onCollapse ? initialCollapsed : internalCollapsed;
363
377
  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
378
  // Count meaningful parts (parts that would render something)
371
379
  const meaningfulParts = parts.filter((part)=>{
372
380
  // Check if the part would render meaningful content
@@ -380,174 +388,216 @@ function renderTaskPart(part, index, iconRenderer) {
380
388
  if (part.type === 'finished-web-search' && part.answer) return true;
381
389
  if (part.type === 'diagnostics-passed') return true;
382
390
  if (part.type === 'fetching-diagnostics') return true;
383
- // Add more meaningful part types as needed
384
391
  return false;
385
392
  });
393
+ const processedParts = parts.map(processTaskPart);
394
+ return {
395
+ data: {
396
+ title: title || 'Task',
397
+ type,
398
+ parts,
399
+ collapsed,
400
+ meaningfulParts,
401
+ shouldShowCollapsible: meaningfulParts.length > 1,
402
+ iconName: getTypeIcon(type, title)
403
+ },
404
+ collapsed,
405
+ handleCollapse,
406
+ processedParts
407
+ };
408
+ }
409
+ /**
410
+ * Generic task section component
411
+ * Renders a collapsible task section with basic structure - consumers provide styling
412
+ *
413
+ * For headless usage, use the useTaskSection hook instead.
414
+ */ function TaskSection({ title, type, parts = [], collapsed: initialCollapsed = true, onCollapse, className, children, iconRenderer, taskIcon, chevronRightIcon, chevronDownIcon }) {
415
+ const { data, collapsed, handleCollapse, processedParts } = useTaskSection({
416
+ title,
417
+ type,
418
+ parts,
419
+ collapsed: initialCollapsed,
420
+ onCollapse
421
+ });
422
+ // If children provided, use that (allows complete customization)
423
+ if (children) {
424
+ return React.createElement(React.Fragment, {}, children);
425
+ }
386
426
  // 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
- });
427
+ if (!data.shouldShowCollapsible && data.meaningfulParts.length === 1) {
428
+ const partData = processTaskPart(data.meaningfulParts[0]);
429
+ return React.createElement('div', {
430
+ className,
431
+ 'data-component': 'task-section-inline'
432
+ }, React.createElement('div', {
433
+ 'data-part': true
434
+ }, renderTaskPartContent(partData, 0, iconRenderer)));
396
435
  }
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
- });
436
+ // Uses React.createElement for maximum compatibility across environments
437
+ return React.createElement('div', {
438
+ className,
439
+ 'data-component': 'task-section'
440
+ }, React.createElement('button', {
441
+ onClick: handleCollapse,
442
+ 'data-expanded': !collapsed,
443
+ 'data-button': true
444
+ }, React.createElement('div', {
445
+ 'data-icon-container': true
446
+ }, React.createElement('div', {
447
+ 'data-task-icon': true
448
+ }, taskIcon || (iconRenderer ? React.createElement(iconRenderer, {
449
+ name: data.iconName
450
+ }) : React.createElement(Icon, {
451
+ name: data.iconName
452
+ }))), collapsed ? chevronRightIcon || (iconRenderer ? React.createElement(iconRenderer, {
453
+ name: 'chevron-right'
454
+ }) : React.createElement(Icon, {
455
+ name: 'chevron-right'
456
+ })) : chevronDownIcon || (iconRenderer ? React.createElement(iconRenderer, {
457
+ name: 'chevron-down'
458
+ }) : React.createElement(Icon, {
459
+ name: 'chevron-down'
460
+ }))), React.createElement('span', {
461
+ 'data-title': true
462
+ }, data.title)), !collapsed ? React.createElement('div', {
463
+ 'data-content': true
464
+ }, React.createElement('div', {
465
+ 'data-parts-container': true
466
+ }, processedParts.map((partData, index)=>React.createElement('div', {
467
+ key: index,
468
+ 'data-part': true
469
+ }, renderTaskPartContent(partData, index, iconRenderer))))) : null);
442
470
  }
443
471
 
444
- function ContentPartRenderer({ part, iconRenderer, thinkingSectionRenderer, taskSectionRenderer, brainIcon, chevronRightIcon, chevronDownIcon, searchIcon, folderIcon, settingsIcon, wrenchIcon }) {
445
- if (!part) return null;
472
+ // Headless hook for content part
473
+ function useContentPart(part) {
474
+ if (!part) {
475
+ return {
476
+ type: '',
477
+ parts: [],
478
+ metadata: {},
479
+ componentType: null
480
+ };
481
+ }
446
482
  const { type, parts = [], ...metadata } = part;
483
+ let componentType = 'unknown';
484
+ let title;
485
+ let iconName;
486
+ let thinkingData;
447
487
  switch(type){
448
488
  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
- }
489
+ componentType = 'thinking';
490
+ title = 'Thought';
491
+ const thinkingPart = parts.find((p)=>p.type === 'thinking-end');
492
+ thinkingData = {
493
+ duration: thinkingPart?.duration,
494
+ thought: thinkingPart?.thought
495
+ };
496
+ break;
464
497
  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
- }
498
+ componentType = 'task';
499
+ title = metadata.taskNameComplete || metadata.taskNameActive;
500
+ iconName = 'search';
501
+ break;
479
502
  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
- }
503
+ componentType = 'task';
504
+ title = metadata.taskNameComplete || metadata.taskNameActive;
505
+ iconName = 'folder';
506
+ break;
494
507
  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
- }
508
+ componentType = 'task';
509
+ title = metadata.taskNameComplete || metadata.taskNameActive;
510
+ iconName = 'settings';
511
+ break;
509
512
  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
- }
513
+ componentType = 'task';
514
+ title = metadata.taskNameComplete || metadata.taskNameActive || 'Reading file';
515
+ iconName = 'folder';
516
+ break;
524
517
  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
- }
518
+ componentType = 'task';
519
+ title = metadata.taskNameComplete || metadata.taskNameActive || 'Coding';
520
+ iconName = 'wrench';
521
+ break;
539
522
  case 'task-start-v1':
540
- // Usually just indicates task start - can be hidden or show as status
541
- return null;
523
+ componentType = null; // Usually just indicates task start - can be hidden
524
+ break;
542
525
  default:
543
- return /*#__PURE__*/ jsxs("div", {
544
- "data-unknown-part-type": type,
545
- children: [
546
- "Unknown part type: ",
547
- type
548
- ]
549
- });
526
+ componentType = 'unknown';
527
+ break;
528
+ }
529
+ return {
530
+ type,
531
+ parts,
532
+ metadata,
533
+ componentType,
534
+ title,
535
+ iconName,
536
+ thinkingData
537
+ };
538
+ }
539
+ /**
540
+ * Content part renderer that handles different types of v0 API content parts
541
+ *
542
+ * For headless usage, use the useContentPart hook instead.
543
+ */ function ContentPartRenderer({ part, iconRenderer, thinkingSectionRenderer, taskSectionRenderer, brainIcon, chevronRightIcon, chevronDownIcon, searchIcon, folderIcon, settingsIcon, wrenchIcon }) {
544
+ const contentData = useContentPart(part);
545
+ if (!contentData.componentType) {
546
+ return null;
547
+ }
548
+ if (contentData.componentType === 'thinking') {
549
+ const ThinkingComponent = thinkingSectionRenderer || ThinkingSection;
550
+ const [collapsed, setCollapsed] = useState(true);
551
+ return React.createElement(ThinkingComponent, {
552
+ title: contentData.title,
553
+ duration: contentData.thinkingData?.duration,
554
+ thought: contentData.thinkingData?.thought,
555
+ collapsed,
556
+ onCollapse: ()=>setCollapsed(!collapsed),
557
+ brainIcon,
558
+ chevronRightIcon,
559
+ chevronDownIcon
560
+ });
550
561
  }
562
+ if (contentData.componentType === 'task') {
563
+ const TaskComponent = taskSectionRenderer || TaskSection;
564
+ const [collapsed, setCollapsed] = useState(true);
565
+ // Map icon names to icon components
566
+ let taskIcon;
567
+ switch(contentData.iconName){
568
+ case 'search':
569
+ taskIcon = searchIcon;
570
+ break;
571
+ case 'folder':
572
+ taskIcon = folderIcon;
573
+ break;
574
+ case 'settings':
575
+ taskIcon = settingsIcon;
576
+ break;
577
+ case 'wrench':
578
+ taskIcon = wrenchIcon;
579
+ break;
580
+ default:
581
+ taskIcon = undefined;
582
+ break;
583
+ }
584
+ return React.createElement(TaskComponent, {
585
+ title: contentData.title,
586
+ type: contentData.type,
587
+ parts: contentData.parts,
588
+ collapsed,
589
+ onCollapse: ()=>setCollapsed(!collapsed),
590
+ taskIcon,
591
+ chevronRightIcon,
592
+ chevronDownIcon
593
+ });
594
+ }
595
+ if (contentData.componentType === 'unknown') {
596
+ return React.createElement('div', {
597
+ 'data-unknown-part-type': contentData.type
598
+ }, `Unknown part type: ${contentData.type}`);
599
+ }
600
+ return null;
551
601
  }
552
602
 
553
603
  // Utility function to merge class names
@@ -555,11 +605,17 @@ function cn(...classes) {
555
605
  return classes.filter(Boolean).join(' ');
556
606
  }
557
607
 
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 }) {
608
+ // Headless hook for processing message content
609
+ function useMessage({ content, messageId = 'unknown', role = 'assistant', streaming = false, isLastMessage = false, components, renderers }) {
560
610
  if (!Array.isArray(content)) {
561
611
  console.warn('MessageContent: content must be an array (MessageBinaryFormat)');
562
- return null;
612
+ return {
613
+ elements: [],
614
+ messageId,
615
+ role,
616
+ streaming,
617
+ isLastMessage
618
+ };
563
619
  }
564
620
  // Merge components and renderers (backward compatibility)
565
621
  const mergedComponents = {
@@ -583,10 +639,7 @@ function MessageImpl({ content, messageId = 'unknown', role: _role = 'assistant'
583
639
  const key = `${messageId}-${index}`;
584
640
  // Markdown data (type 0) - this is the main content
585
641
  if (type === 0) {
586
- return /*#__PURE__*/ jsx(Elements, {
587
- data: data,
588
- components: mergedComponents
589
- }, key);
642
+ return processElements(data, key, mergedComponents);
590
643
  }
591
644
  // Metadata (type 1) - extract context but don't render
592
645
  if (type === 1) {
@@ -596,33 +649,40 @@ function MessageImpl({ content, messageId = 'unknown', role: _role = 'assistant'
596
649
  }
597
650
  // Other types - v0 doesn't handle these in the main renderer
598
651
  return null;
599
- });
600
- return /*#__PURE__*/ jsx("div", {
601
- className: className,
602
- children: elements
603
- });
652
+ }).filter(Boolean);
653
+ return {
654
+ elements,
655
+ messageId,
656
+ role,
657
+ streaming,
658
+ isLastMessage
659
+ };
604
660
  }
605
- // This component handles the markdown data array (equivalent to v0's Elements component)
606
- function Elements({ data, components }) {
661
+ // Process elements into headless data structure
662
+ function processElements(data, keyPrefix, components) {
607
663
  // Handle case where data might not be an array due to streaming/patching
608
664
  if (!Array.isArray(data)) {
609
665
  return null;
610
666
  }
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
- });
667
+ const children = data.map((item, index)=>{
668
+ const key = `${keyPrefix}-${index}`;
669
+ return processElement(item, key, components);
670
+ }).filter(Boolean);
671
+ return {
672
+ type: 'component',
673
+ key: keyPrefix,
674
+ data: 'elements',
675
+ children
676
+ };
619
677
  }
620
- // Render individual elements (equivalent to v0's element rendering logic)
621
- function renderElement(element, key, components) {
678
+ // Process individual elements into headless data structure
679
+ function processElement(element, key, components) {
622
680
  if (typeof element === 'string') {
623
- return /*#__PURE__*/ jsx("span", {
624
- children: element
625
- }, key);
681
+ return {
682
+ type: 'text',
683
+ key,
684
+ data: element
685
+ };
626
686
  }
627
687
  if (!Array.isArray(element)) {
628
688
  return null;
@@ -633,69 +693,144 @@ function renderElement(element, key, components) {
633
693
  }
634
694
  // Handle special v0 Platform API elements
635
695
  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);
696
+ return {
697
+ type: 'content-part',
698
+ key,
699
+ data: {
700
+ part: props.part,
701
+ iconRenderer: components?.Icon,
702
+ thinkingSectionRenderer: components?.ThinkingSection,
703
+ taskSectionRenderer: components?.TaskSection
704
+ }
705
+ };
642
706
  }
643
707
  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);
708
+ return {
709
+ type: 'code-project',
710
+ key,
711
+ data: {
712
+ language: props.lang,
713
+ code: children[0],
714
+ iconRenderer: components?.Icon,
715
+ customRenderer: components?.CodeProjectPart
716
+ }
717
+ };
651
718
  }
652
719
  if (tagName === 'text') {
653
- return /*#__PURE__*/ jsx("span", {
654
- children: children[0] || ''
655
- }, key);
720
+ return {
721
+ type: 'text',
722
+ key,
723
+ data: children[0] || ''
724
+ };
656
725
  }
657
- // Render children
658
- const renderedChildren = children.map((child, childIndex)=>{
726
+ // Process children
727
+ const processedChildren = children.map((child, childIndex)=>{
659
728
  const childKey = `${key}-child-${childIndex}`;
660
- return renderElement(child, childKey, components);
729
+ return processElement(child, childKey, components);
661
730
  }).filter(Boolean);
662
731
  // Handle standard HTML elements
663
- const className = props?.className;
664
732
  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';
733
+ return {
734
+ type: 'html',
735
+ key,
736
+ data: {
737
+ tagName,
738
+ props,
739
+ componentOrConfig
740
+ },
741
+ children: processedChildren
742
+ };
743
+ }
744
+ // Default JSX renderer for backward compatibility
745
+ function MessageRenderer({ messageData, className }) {
746
+ const renderElement = (element)=>{
747
+ switch(element.type){
748
+ case 'text':
749
+ return React.createElement('span', {
750
+ key: element.key
751
+ }, element.data);
752
+ case 'content-part':
753
+ return React.createElement(ContentPartRenderer, {
754
+ key: element.key,
755
+ part: element.data.part,
756
+ iconRenderer: element.data.iconRenderer,
757
+ thinkingSectionRenderer: element.data.thinkingSectionRenderer,
758
+ taskSectionRenderer: element.data.taskSectionRenderer
759
+ });
760
+ case 'code-project':
761
+ const CustomCodeProjectPart = element.data.customRenderer;
762
+ const CodeProjectComponent = CustomCodeProjectPart || CodeProjectPart;
763
+ return React.createElement(CodeProjectComponent, {
764
+ key: element.key,
765
+ language: element.data.language,
766
+ code: element.data.code,
767
+ iconRenderer: element.data.iconRenderer
768
+ });
769
+ case 'html':
770
+ const { tagName, props, componentOrConfig } = element.data;
771
+ const renderedChildren = element.children?.map(renderElement);
772
+ if (typeof componentOrConfig === 'function') {
773
+ const Component = componentOrConfig;
774
+ return React.createElement(Component, {
775
+ key: element.key,
776
+ ...props,
777
+ className: props?.className
778
+ }, renderedChildren);
779
+ } else if (componentOrConfig && typeof componentOrConfig === 'object') {
780
+ const mergedClassName = cn(props?.className, componentOrConfig.className);
781
+ return React.createElement(tagName, {
782
+ key: element.key,
783
+ ...props,
784
+ className: mergedClassName
785
+ }, renderedChildren);
786
+ } else {
787
+ // Default HTML element rendering
788
+ const elementProps = {
789
+ key: element.key,
790
+ ...props
791
+ };
792
+ if (props?.className) {
793
+ elementProps.className = props.className;
794
+ }
795
+ // Special handling for links
796
+ if (tagName === 'a') {
797
+ elementProps.target = '_blank';
798
+ elementProps.rel = 'noopener noreferrer';
799
+ }
800
+ return React.createElement(tagName, elementProps, renderedChildren);
801
+ }
802
+ case 'component':
803
+ return React.createElement(React.Fragment, {
804
+ key: element.key
805
+ }, element.children?.map(renderElement));
806
+ default:
807
+ return null;
692
808
  }
693
- return /*#__PURE__*/ React.createElement(tagName, elementProps, renderedChildren);
694
- }
809
+ };
810
+ return React.createElement('div', {
811
+ className
812
+ }, messageData.elements.map(renderElement));
813
+ }
814
+ // Simplified renderer that matches v0's exact approach (backward compatibility)
815
+ function MessageImpl({ content, messageId = 'unknown', role = 'assistant', streaming = false, isLastMessage = false, className, components, renderers }) {
816
+ const messageData = useMessage({
817
+ content,
818
+ messageId,
819
+ role,
820
+ streaming,
821
+ isLastMessage,
822
+ components,
823
+ renderers
824
+ });
825
+ return React.createElement(MessageRenderer, {
826
+ messageData,
827
+ className
828
+ });
695
829
  }
696
830
  /**
697
831
  * Main component for rendering v0 Platform API message content
698
- */ const Message = /*#__PURE__*/ React.memo(MessageImpl);
832
+ * This is a backward-compatible JSX renderer. For headless usage, use the useMessage hook.
833
+ */ const Message = React.memo(MessageImpl);
699
834
 
700
835
  const jdf = jsondiffpatch.create({});
701
836
  // Exact copy of the patch function from v0/chat/lib/diffpatch.ts
@@ -844,8 +979,8 @@ class StreamStateManager {
844
979
  this.setComplete(true);
845
980
  options.onComplete?.(currentContent);
846
981
  return;
847
- } else if (parsedData.object === 'chat' && parsedData.id) {
848
- // Handle the initial chat data message
982
+ } else if (parsedData.object && parsedData.object.startsWith('chat')) {
983
+ // Handle chat metadata messages (chat, chat.title, chat.name, etc.)
849
984
  options.onChatData?.(parsedData);
850
985
  continue;
851
986
  } else if (parsedData.delta) {
@@ -890,9 +1025,33 @@ class StreamStateManager {
890
1025
  return state;
891
1026
  }
892
1027
 
1028
+ // Headless hook for streaming message
1029
+ function useStreamingMessageData({ stream, messageId = 'unknown', role = 'assistant', components, renderers, onChunk, onComplete, onError, onChatData }) {
1030
+ const streamingState = useStreamingMessage(stream, {
1031
+ onChunk,
1032
+ onComplete,
1033
+ onError,
1034
+ onChatData
1035
+ });
1036
+ const messageData = streamingState.content.length > 0 ? useMessage({
1037
+ content: streamingState.content,
1038
+ messageId,
1039
+ role,
1040
+ streaming: streamingState.isStreaming,
1041
+ isLastMessage: true,
1042
+ components,
1043
+ renderers
1044
+ }) : null;
1045
+ return {
1046
+ ...streamingState,
1047
+ messageData
1048
+ };
1049
+ }
893
1050
  /**
894
1051
  * Component for rendering streaming message content from v0 API
895
1052
  *
1053
+ * For headless usage, use the useStreamingMessageData hook instead.
1054
+ *
896
1055
  * @example
897
1056
  * ```tsx
898
1057
  * import { v0 } from 'v0-sdk'
@@ -925,73 +1084,98 @@ class StreamStateManager {
925
1084
  * )
926
1085
  * }
927
1086
  * ```
928
- */ function StreamingMessage({ stream, showLoadingIndicator = true, loadingComponent, errorComponent, onChunk, onComplete, onError, onChatData, ...messageProps }) {
929
- const { content, isStreaming, error, isComplete } = useStreamingMessage(stream, {
1087
+ */ function StreamingMessage({ stream, showLoadingIndicator = true, loadingComponent, errorComponent, onChunk, onComplete, onError, onChatData, className, ...messageProps }) {
1088
+ const streamingData = useStreamingMessageData({
1089
+ stream,
930
1090
  onChunk,
931
1091
  onComplete,
932
1092
  onError,
933
- onChatData
1093
+ onChatData,
1094
+ ...messageProps
934
1095
  });
935
1096
  // Handle error state
936
- if (error) {
1097
+ if (streamingData.error) {
937
1098
  if (errorComponent) {
938
- return /*#__PURE__*/ jsx(Fragment, {
939
- children: errorComponent(error)
940
- });
1099
+ return React.createElement(React.Fragment, {}, errorComponent(streamingData.error));
941
1100
  }
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
- });
1101
+ // Fallback error component using React.createElement for compatibility
1102
+ return React.createElement('div', {
1103
+ className: 'text-red-500 p-4 border border-red-200 rounded',
1104
+ style: {
1105
+ color: 'red',
1106
+ padding: '1rem',
1107
+ border: '1px solid #fecaca',
1108
+ borderRadius: '0.375rem'
1109
+ }
1110
+ }, `Error: ${streamingData.error}`);
949
1111
  }
950
1112
  // Handle loading state
951
- if (showLoadingIndicator && isStreaming && content.length === 0) {
1113
+ if (showLoadingIndicator && streamingData.isStreaming && streamingData.content.length === 0) {
952
1114
  if (loadingComponent) {
953
- return /*#__PURE__*/ jsx(Fragment, {
954
- children: loadingComponent
955
- });
1115
+ return React.createElement(React.Fragment, {}, loadingComponent);
956
1116
  }
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
- });
1117
+ // Fallback loading component using React.createElement for compatibility
1118
+ return React.createElement('div', {
1119
+ className: 'flex items-center space-x-2 text-gray-500',
1120
+ style: {
1121
+ display: 'flex',
1122
+ alignItems: 'center',
1123
+ gap: '0.5rem',
1124
+ color: '#6b7280'
1125
+ }
1126
+ }, React.createElement('div', {
1127
+ className: 'animate-spin h-4 w-4 border-2 border-gray-300 border-t-gray-600 rounded-full',
1128
+ style: {
1129
+ animation: 'spin 1s linear infinite',
1130
+ height: '1rem',
1131
+ width: '1rem',
1132
+ border: '2px solid #d1d5db',
1133
+ borderTopColor: '#4b5563',
1134
+ borderRadius: '50%'
1135
+ }
1136
+ }), React.createElement('span', {}, 'Loading...'));
968
1137
  }
969
1138
  // Render the message content
970
- return /*#__PURE__*/ jsx(Message, {
1139
+ return React.createElement(Message, {
971
1140
  ...messageProps,
972
- content: content,
973
- streaming: isStreaming,
974
- isLastMessage: true
1141
+ content: streamingData.content,
1142
+ streaming: streamingData.isStreaming,
1143
+ isLastMessage: true,
1144
+ className
975
1145
  });
976
1146
  }
977
1147
 
1148
+ // Headless hook for math data
1149
+ function useMath(props) {
1150
+ return {
1151
+ content: props.content,
1152
+ inline: props.inline ?? false,
1153
+ displayMode: props.displayMode ?? !props.inline,
1154
+ processedContent: props.content
1155
+ };
1156
+ }
978
1157
  /**
979
1158
  * Generic math renderer component
980
1159
  * Renders plain math content by default - consumers should provide their own math rendering
981
- */ function MathPart({ content, inline = false, className = '', children }) {
1160
+ *
1161
+ * For headless usage, use the useMath hook instead.
1162
+ */ function MathPart({ content, inline = false, className = '', children, displayMode }) {
982
1163
  // If children provided, use that (allows complete customization)
983
1164
  if (children) {
984
- return /*#__PURE__*/ jsx(Fragment, {
985
- children: children
986
- });
1165
+ return React.createElement(React.Fragment, {}, children);
987
1166
  }
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
1167
+ const mathData = useMath({
1168
+ content,
1169
+ inline,
1170
+ displayMode
994
1171
  });
1172
+ // Simple fallback - just render plain math content
1173
+ // Uses React.createElement for maximum compatibility across environments
1174
+ return React.createElement(mathData.inline ? 'span' : 'div', {
1175
+ className,
1176
+ 'data-math-inline': mathData.inline,
1177
+ 'data-math-display': mathData.displayMode
1178
+ }, mathData.processedContent);
995
1179
  }
996
1180
 
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 };
1181
+ 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 };