@v0-sdk/react 0.5.1 → 3.0.0-canary.9ae7b76

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 DELETED
@@ -1,1203 +0,0 @@
1
- import React, { createContext, useContext, useState, useRef, useSyncExternalStore } from 'react';
2
- import * as jsondiffpatch from 'jsondiffpatch';
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
- }
15
- /**
16
- * Generic code block component
17
- * Renders plain code by default - consumers should provide their own styling and highlighting
18
- *
19
- * For headless usage, use the useCodeBlock hook instead.
20
- */ function CodeBlock({ language, code, className = '', children, filename }) {
21
- // If children provided, use that (allows complete customization)
22
- if (children) {
23
- return React.createElement(React.Fragment, {}, children);
24
- }
25
- const codeBlockData = useCodeBlock({
26
- language,
27
- code,
28
- filename
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));
39
- }
40
-
41
- // Context for providing custom icon implementation
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
- }
51
- /**
52
- * Generic icon component that can be customized by consumers.
53
- * By default, renders a simple fallback. Consumers should provide
54
- * their own icon implementation via context or props.
55
- *
56
- * For headless usage, use the useIcon hook instead.
57
- */ function Icon(props) {
58
- const CustomIcon = useContext(IconContext);
59
- // Use custom icon implementation if provided via context
60
- if (CustomIcon) {
61
- return React.createElement(CustomIcon, props);
62
- }
63
- const iconData = useIcon(props);
64
- // Fallback implementation - consumers should override this
65
- // This uses minimal DOM-specific attributes for maximum compatibility
66
- return React.createElement('span', {
67
- className: props.className,
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);
78
- }
79
- function getIconFallback(name) {
80
- const iconMap = {
81
- 'chevron-right': '▶',
82
- 'chevron-down': '▼',
83
- search: '🔍',
84
- folder: '📁',
85
- settings: '⚙️',
86
- 'file-text': '📄',
87
- brain: '🧠',
88
- wrench: '🔧'
89
- };
90
- return iconMap[name] || '•';
91
- }
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
- }
127
- /**
128
- * Generic code project block component
129
- * Renders a collapsible code project with basic structure - consumers provide styling
130
- *
131
- * For headless usage, use the useCodeProject hook instead.
132
- */ function CodeProjectPart({ title, filename, code, language = 'typescript', collapsed: initialCollapsed = true, className, children, iconRenderer }) {
133
- const { data, collapsed, toggleCollapsed } = useCodeProject({
134
- title,
135
- filename,
136
- code,
137
- language,
138
- collapsed: initialCollapsed
139
- });
140
- // If children provided, use that (allows complete customization)
141
- if (children) {
142
- return React.createElement(React.Fragment, {}, children);
143
- }
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);
183
- }
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
- }
205
- /**
206
- * Generic thinking section component
207
- * Renders a collapsible section with basic structure - consumers provide styling
208
- *
209
- * For headless usage, use the useThinkingSection hook instead.
210
- */ function ThinkingSection({ title, duration, thought, collapsed: initialCollapsed = true, onCollapse, className, children, iconRenderer, brainIcon, chevronRightIcon, chevronDownIcon }) {
211
- const { data, collapsed, handleCollapse } = useThinkingSection({
212
- title,
213
- duration,
214
- thought,
215
- collapsed: initialCollapsed,
216
- onCollapse
217
- });
218
- // If children provided, use that (allows complete customization)
219
- if (children) {
220
- return React.createElement(React.Fragment, {}, children);
221
- }
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);
254
- }
255
-
256
- function getTypeIcon(type, title) {
257
- // Check title content for specific cases
258
- if (title?.includes('No issues found')) {
259
- return 'wrench';
260
- }
261
- if (title?.includes('Analyzed codebase')) {
262
- return 'search';
263
- }
264
- // Fallback to type-based icons
265
- switch(type){
266
- case 'task-search-web-v1':
267
- return 'search';
268
- case 'task-search-repo-v1':
269
- return 'folder';
270
- case 'task-diagnostics-v1':
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';
278
- default:
279
- return 'wrench';
280
- }
281
- }
282
- function processTaskPart(part, index) {
283
- const baseData = {
284
- type: part.type,
285
- status: part.status,
286
- content: null
287
- };
288
- if (part.type === 'search-web') {
289
- if (part.status === 'searching') {
290
- return {
291
- ...baseData,
292
- isSearching: true,
293
- query: part.query,
294
- content: `Searching "${part.query}"`
295
- };
296
- }
297
- if (part.status === 'analyzing') {
298
- return {
299
- ...baseData,
300
- isAnalyzing: true,
301
- count: part.count,
302
- content: `Analyzing ${part.count} results...`
303
- };
304
- }
305
- if (part.status === 'complete' && part.answer) {
306
- return {
307
- ...baseData,
308
- isComplete: true,
309
- answer: part.answer,
310
- sources: part.sources,
311
- content: part.answer
312
- };
313
- }
314
- }
315
- if (part.type === 'search-repo') {
316
- if (part.status === 'searching') {
317
- return {
318
- ...baseData,
319
- isSearching: true,
320
- query: part.query,
321
- content: `Searching "${part.query}"`
322
- };
323
- }
324
- if (part.status === 'reading' && part.files) {
325
- return {
326
- ...baseData,
327
- files: part.files,
328
- content: 'Reading files'
329
- };
330
- }
331
- }
332
- if (part.type === 'diagnostics') {
333
- if (part.status === 'checking') {
334
- return {
335
- ...baseData,
336
- content: 'Checking for issues...'
337
- };
338
- }
339
- if (part.status === 'complete' && part.issues === 0) {
340
- return {
341
- ...baseData,
342
- isComplete: true,
343
- issues: part.issues,
344
- content: '✅ No issues found'
345
- };
346
- }
347
- }
348
- return {
349
- ...baseData,
350
- content: JSON.stringify(part)
351
- };
352
- }
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 }) {
381
- const [internalCollapsed, setInternalCollapsed] = useState(initialCollapsed);
382
- const collapsed = onCollapse ? initialCollapsed : internalCollapsed;
383
- const handleCollapse = onCollapse || (()=>setInternalCollapsed(!internalCollapsed));
384
- // Count meaningful parts (parts that would render something)
385
- const meaningfulParts = parts.filter((part)=>{
386
- // Check if the part would render meaningful content
387
- if (part.type === 'search-web') {
388
- return part.status === 'searching' || part.status === 'analyzing' || part.status === 'complete' && part.answer;
389
- }
390
- if (part.type === 'starting-repo-search' && part.query) return true;
391
- if (part.type === 'select-files' && part.filePaths?.length > 0) return true;
392
- if (part.type === 'starting-web-search' && part.query) return true;
393
- if (part.type === 'got-results' && part.count) return true;
394
- if (part.type === 'finished-web-search' && part.answer) return true;
395
- if (part.type === 'diagnostics-passed') return true;
396
- if (part.type === 'fetching-diagnostics') return true;
397
- return false;
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
- }
432
- // If there's only one meaningful part, show just the content without the collapsible wrapper
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)));
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);
476
- }
477
-
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
- }
488
- const { type, parts = [], ...metadata } = part;
489
- let componentType = 'unknown';
490
- let title;
491
- let iconName;
492
- let thinkingData;
493
- switch(type){
494
- case 'task-thinking-v1':
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;
503
- case 'task-search-web-v1':
504
- componentType = 'task';
505
- title = metadata.taskNameComplete || metadata.taskNameActive;
506
- iconName = 'search';
507
- break;
508
- case 'task-search-repo-v1':
509
- componentType = 'task';
510
- title = metadata.taskNameComplete || metadata.taskNameActive;
511
- iconName = 'folder';
512
- break;
513
- case 'task-diagnostics-v1':
514
- componentType = 'task';
515
- title = metadata.taskNameComplete || metadata.taskNameActive;
516
- iconName = 'settings';
517
- break;
518
- case 'task-read-file-v1':
519
- componentType = 'task';
520
- title = metadata.taskNameComplete || metadata.taskNameActive || 'Reading file';
521
- iconName = 'folder';
522
- break;
523
- case 'task-coding-v1':
524
- componentType = 'task';
525
- title = metadata.taskNameComplete || metadata.taskNameActive || 'Coding';
526
- iconName = 'wrench';
527
- break;
528
- case 'task-start-v1':
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
537
- default:
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;
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;
622
- }
623
-
624
- // Utility function to merge class names
625
- function cn(...classes) {
626
- return classes.filter(Boolean).join(' ');
627
- }
628
-
629
- // Headless hook for processing message content
630
- function useMessage({ content, messageId = 'unknown', role = 'assistant', streaming = false, isLastMessage = false, components, renderers }) {
631
- if (!Array.isArray(content)) {
632
- console.warn('MessageContent: content must be an array (MessageBinaryFormat)');
633
- return {
634
- elements: [],
635
- messageId,
636
- role,
637
- streaming,
638
- isLastMessage
639
- };
640
- }
641
- // Merge components and renderers (backward compatibility)
642
- const mergedComponents = {
643
- ...components,
644
- // Map legacy renderers to new component names
645
- ...renderers?.CodeBlock && {
646
- CodeBlock: renderers.CodeBlock
647
- },
648
- ...renderers?.MathRenderer && {
649
- MathPart: renderers.MathRenderer
650
- },
651
- ...renderers?.MathPart && {
652
- MathPart: renderers.MathPart
653
- },
654
- ...renderers?.Icon && {
655
- Icon: renderers.Icon
656
- }
657
- };
658
- // Process content exactly like v0's Renderer component
659
- const elements = content.map(([type, data], index)=>{
660
- const key = `${messageId}-${index}`;
661
- // Markdown data (type 0) - this is the main content
662
- if (type === 0) {
663
- return processElements(data, key, mergedComponents);
664
- }
665
- // Metadata (type 1) - extract context but don't render
666
- if (type === 1) {
667
- // In the future, we could extract sources/context here like v0 does
668
- // For now, just return null like v0's renderer
669
- return null;
670
- }
671
- // Other types - v0 doesn't handle these in the main renderer
672
- return null;
673
- }).filter(Boolean);
674
- return {
675
- elements,
676
- messageId,
677
- role,
678
- streaming,
679
- isLastMessage
680
- };
681
- }
682
- // Process elements into headless data structure
683
- function processElements(data, keyPrefix, components) {
684
- // Handle case where data might not be an array due to streaming/patching
685
- if (!Array.isArray(data)) {
686
- return null;
687
- }
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
- };
698
- }
699
- // Process individual elements into headless data structure
700
- function processElement(element, key, components) {
701
- if (typeof element === 'string') {
702
- return {
703
- type: 'text',
704
- key,
705
- data: element
706
- };
707
- }
708
- if (!Array.isArray(element)) {
709
- return null;
710
- }
711
- const [tagName, props, ...children] = element;
712
- if (!tagName) {
713
- return null;
714
- }
715
- // Handle special v0 Platform API elements
716
- if (tagName === 'AssistantMessageContentPart') {
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
- };
727
- }
728
- if (tagName === 'Codeblock') {
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
- };
739
- }
740
- if (tagName === 'text') {
741
- return {
742
- type: 'text',
743
- key,
744
- data: children[0] || ''
745
- };
746
- }
747
- // Process children
748
- const processedChildren = children.map((child, childIndex)=>{
749
- const childKey = `${key}-child-${childIndex}`;
750
- return processElement(child, childKey, components);
751
- }).filter(Boolean);
752
- // Handle standard HTML elements
753
- const componentOrConfig = components?.[tagName];
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
- const elementProps = {
803
- key: element.key,
804
- ...props,
805
- className: mergedClassName
806
- };
807
- return renderedChildren?.length ? React.createElement(tagName, elementProps, renderedChildren) : React.createElement(tagName, elementProps);
808
- } else {
809
- // Default HTML element rendering
810
- const elementProps = {
811
- key: element.key,
812
- ...props
813
- };
814
- if (props?.className) {
815
- elementProps.className = props.className;
816
- }
817
- // Special handling for links
818
- if (tagName === 'a') {
819
- elementProps.target = '_blank';
820
- elementProps.rel = 'noopener noreferrer';
821
- }
822
- return renderedChildren?.length ? React.createElement(tagName, elementProps, renderedChildren) : React.createElement(tagName, elementProps);
823
- }
824
- case 'component':
825
- return React.createElement(React.Fragment, {
826
- key: element.key
827
- }, element.children?.map(renderElement));
828
- default:
829
- return null;
830
- }
831
- };
832
- return React.createElement('div', {
833
- className
834
- }, messageData.elements.map(renderElement));
835
- }
836
- // Simplified renderer that matches v0's exact approach (backward compatibility)
837
- function MessageImpl({ content, messageId = 'unknown', role = 'assistant', streaming = false, isLastMessage = false, className, components, renderers }) {
838
- const messageData = useMessage({
839
- content,
840
- messageId,
841
- role,
842
- streaming,
843
- isLastMessage,
844
- components,
845
- renderers
846
- });
847
- return React.createElement(MessageRenderer, {
848
- messageData,
849
- className
850
- });
851
- }
852
- /**
853
- * Main component for rendering v0 Platform API message content
854
- * This is a backward-compatible JSX renderer. For headless usage, use the useMessage hook.
855
- */ const Message = React.memo(MessageImpl);
856
-
857
- const jdf = jsondiffpatch.create({});
858
- // Exact copy of the patch function from v0/chat/lib/diffpatch.ts
859
- function patch(original, delta) {
860
- const newObj = jdf.clone(original);
861
- // Check for our customized delta
862
- if (Array.isArray(delta) && delta[1] === 9 && delta[2] === 9) {
863
- // Get the path to the modified element
864
- const indexes = delta[0].slice(0, -1);
865
- // Get the string to be appended
866
- const value = delta[0].slice(-1);
867
- let obj = newObj;
868
- for (const index of indexes){
869
- if (typeof obj[index] === 'string') {
870
- obj[index] += value;
871
- return newObj;
872
- }
873
- obj = obj[index];
874
- }
875
- }
876
- // If not custom delta, apply standard jsondiffpatch-ing
877
- jdf.patch(newObj, delta);
878
- return newObj;
879
- }
880
- // Stream state manager - isolated from React lifecycle
881
- class StreamStateManager {
882
- constructor(){
883
- this.content = [];
884
- this.isStreaming = false;
885
- this.isComplete = false;
886
- this.callbacks = new Set();
887
- this.processedStreams = new WeakSet();
888
- this.cachedState = null;
889
- this.subscribe = (callback)=>{
890
- this.callbacks.add(callback);
891
- return ()=>{
892
- this.callbacks.delete(callback);
893
- };
894
- };
895
- this.notifySubscribers = ()=>{
896
- // Invalidate cached state when state changes
897
- this.cachedState = null;
898
- this.callbacks.forEach((callback)=>callback());
899
- };
900
- this.getState = ()=>{
901
- // Return cached state to prevent infinite re-renders
902
- if (this.cachedState === null) {
903
- this.cachedState = {
904
- content: this.content,
905
- isStreaming: this.isStreaming,
906
- error: this.error,
907
- isComplete: this.isComplete
908
- };
909
- }
910
- return this.cachedState;
911
- };
912
- this.processStream = async (stream, options = {})=>{
913
- // Prevent processing the same stream multiple times
914
- if (this.processedStreams.has(stream)) {
915
- return;
916
- }
917
- // Handle locked streams gracefully
918
- if (stream.locked) {
919
- console.warn('Stream is locked, cannot process');
920
- return;
921
- }
922
- this.processedStreams.add(stream);
923
- this.reset();
924
- this.setStreaming(true);
925
- try {
926
- await this.readStream(stream, options);
927
- } catch (err) {
928
- const errorMessage = err instanceof Error ? err.message : 'Unknown streaming error';
929
- this.setError(errorMessage);
930
- options.onError?.(errorMessage);
931
- } finally{
932
- this.setStreaming(false);
933
- }
934
- };
935
- this.reset = ()=>{
936
- this.content = [];
937
- this.isStreaming = false;
938
- this.error = undefined;
939
- this.isComplete = false;
940
- this.notifySubscribers();
941
- };
942
- this.setStreaming = (streaming)=>{
943
- this.isStreaming = streaming;
944
- this.notifySubscribers();
945
- };
946
- this.setError = (error)=>{
947
- this.error = error;
948
- this.notifySubscribers();
949
- };
950
- this.setComplete = (complete)=>{
951
- this.isComplete = complete;
952
- this.notifySubscribers();
953
- };
954
- this.updateContent = (newContent)=>{
955
- this.content = [
956
- ...newContent
957
- ];
958
- this.notifySubscribers();
959
- };
960
- this.readStream = async (stream, options)=>{
961
- const reader = stream.getReader();
962
- const decoder = new TextDecoder();
963
- let buffer = '';
964
- let currentContent = [];
965
- try {
966
- while(true){
967
- const { done, value } = await reader.read();
968
- if (done) {
969
- break;
970
- }
971
- const chunk = decoder.decode(value, {
972
- stream: true
973
- });
974
- buffer += chunk;
975
- const lines = buffer.split('\n');
976
- buffer = lines.pop() || '';
977
- for (const line of lines){
978
- if (line.trim() === '') {
979
- continue;
980
- }
981
- // Handle SSE format (data: ...)
982
- let jsonData;
983
- if (line.startsWith('data: ')) {
984
- jsonData = line.slice(6); // Remove "data: " prefix
985
- if (jsonData === '[DONE]') {
986
- this.setComplete(true);
987
- options.onComplete?.(currentContent);
988
- return;
989
- }
990
- } else {
991
- // Handle raw JSON lines (fallback)
992
- jsonData = line;
993
- }
994
- try {
995
- // Parse the JSON data
996
- const parsedData = JSON.parse(jsonData);
997
- // Handle v0 streaming format
998
- if (parsedData.type === 'connected') {
999
- continue;
1000
- } else if (parsedData.type === 'done') {
1001
- this.setComplete(true);
1002
- options.onComplete?.(currentContent);
1003
- return;
1004
- } else if (parsedData.object && parsedData.object.startsWith('chat')) {
1005
- // Handle chat metadata messages (chat, chat.title, chat.name, etc.)
1006
- options.onChatData?.(parsedData);
1007
- continue;
1008
- } else if (parsedData.delta) {
1009
- // Apply the delta using jsondiffpatch
1010
- const patchedContent = patch(currentContent, parsedData.delta);
1011
- currentContent = Array.isArray(patchedContent) ? patchedContent : [];
1012
- this.updateContent(currentContent);
1013
- options.onChunk?.(currentContent);
1014
- }
1015
- } catch (e) {
1016
- console.warn('Failed to parse streaming data:', line, e);
1017
- }
1018
- }
1019
- }
1020
- this.setComplete(true);
1021
- options.onComplete?.(currentContent);
1022
- } finally{
1023
- reader.releaseLock();
1024
- }
1025
- };
1026
- }
1027
- }
1028
- /**
1029
- * Hook for handling streaming message content from v0 API using useSyncExternalStore
1030
- */ function useStreamingMessage(stream, options = {}) {
1031
- // Create a stable stream manager instance
1032
- const managerRef = useRef(null);
1033
- if (!managerRef.current) {
1034
- managerRef.current = new StreamStateManager();
1035
- }
1036
- const manager = managerRef.current;
1037
- // Subscribe to state changes using useSyncExternalStore
1038
- const state = useSyncExternalStore(manager.subscribe, manager.getState, manager.getState);
1039
- // Process stream when it changes
1040
- const lastStreamRef = useRef(null);
1041
- if (stream !== lastStreamRef.current) {
1042
- lastStreamRef.current = stream;
1043
- if (stream) {
1044
- manager.processStream(stream, options);
1045
- }
1046
- }
1047
- return state;
1048
- }
1049
-
1050
- // Headless hook for streaming message
1051
- function useStreamingMessageData({ stream, messageId = 'unknown', role = 'assistant', components, renderers, onChunk, onComplete, onError, onChatData }) {
1052
- const streamingState = useStreamingMessage(stream, {
1053
- onChunk,
1054
- onComplete,
1055
- onError,
1056
- onChatData
1057
- });
1058
- const messageData = streamingState.content.length > 0 ? useMessage({
1059
- content: streamingState.content,
1060
- messageId,
1061
- role,
1062
- streaming: streamingState.isStreaming,
1063
- isLastMessage: true,
1064
- components,
1065
- renderers
1066
- }) : null;
1067
- return {
1068
- ...streamingState,
1069
- messageData
1070
- };
1071
- }
1072
- /**
1073
- * Component for rendering streaming message content from v0 API
1074
- *
1075
- * For headless usage, use the useStreamingMessageData hook instead.
1076
- *
1077
- * @example
1078
- * ```tsx
1079
- * import { v0 } from 'v0-sdk'
1080
- * import { StreamingMessage } from '@v0-sdk/react'
1081
- *
1082
- * function ChatDemo() {
1083
- * const [stream, setStream] = useState<ReadableStream<Uint8Array> | null>(null)
1084
- *
1085
- * const handleSubmit = async () => {
1086
- * const response = await v0.chats.create({
1087
- * message: 'Create a button component',
1088
- * responseMode: 'experimental_stream'
1089
- * })
1090
- * setStream(response)
1091
- * }
1092
- *
1093
- * return (
1094
- * <div>
1095
- * <button onClick={handleSubmit}>Send Message</button>
1096
- * {stream && (
1097
- * <StreamingMessage
1098
- * stream={stream}
1099
- * messageId="demo-message"
1100
- * role="assistant"
1101
- * onComplete={(content) => handleCompletion(content)}
1102
- * onChatData={(chatData) => handleChatData(chatData)}
1103
- * />
1104
- * )}
1105
- * </div>
1106
- * )
1107
- * }
1108
- * ```
1109
- */ function StreamingMessage({ stream, showLoadingIndicator = true, loadingComponent, errorComponent, onChunk, onComplete, onError, onChatData, className, ...messageProps }) {
1110
- const streamingData = useStreamingMessageData({
1111
- stream,
1112
- onChunk,
1113
- onComplete,
1114
- onError,
1115
- onChatData,
1116
- ...messageProps
1117
- });
1118
- // Handle error state
1119
- if (streamingData.error) {
1120
- if (errorComponent) {
1121
- return React.createElement(React.Fragment, {}, errorComponent(streamingData.error));
1122
- }
1123
- // Fallback error component using React.createElement for compatibility
1124
- return React.createElement('div', {
1125
- className: 'text-red-500 p-4 border border-red-200 rounded',
1126
- style: {
1127
- color: 'red',
1128
- padding: '1rem',
1129
- border: '1px solid #fecaca',
1130
- borderRadius: '0.375rem'
1131
- }
1132
- }, `Error: ${streamingData.error}`);
1133
- }
1134
- // Handle loading state
1135
- if (showLoadingIndicator && streamingData.isStreaming && streamingData.content.length === 0) {
1136
- if (loadingComponent) {
1137
- return React.createElement(React.Fragment, {}, loadingComponent);
1138
- }
1139
- // Fallback loading component using React.createElement for compatibility
1140
- return React.createElement('div', {
1141
- className: 'flex items-center space-x-2 text-gray-500',
1142
- style: {
1143
- display: 'flex',
1144
- alignItems: 'center',
1145
- gap: '0.5rem',
1146
- color: '#6b7280'
1147
- }
1148
- }, React.createElement('div', {
1149
- className: 'animate-spin h-4 w-4 border-2 border-gray-300 border-t-gray-600 rounded-full',
1150
- style: {
1151
- animation: 'spin 1s linear infinite',
1152
- height: '1rem',
1153
- width: '1rem',
1154
- border: '2px solid #d1d5db',
1155
- borderTopColor: '#4b5563',
1156
- borderRadius: '50%'
1157
- }
1158
- }), React.createElement('span', {}, 'Loading...'));
1159
- }
1160
- // Render the message content
1161
- return React.createElement(Message, {
1162
- ...messageProps,
1163
- content: streamingData.content,
1164
- streaming: streamingData.isStreaming,
1165
- isLastMessage: true,
1166
- className
1167
- });
1168
- }
1169
-
1170
- // Headless hook for math data
1171
- function useMath(props) {
1172
- return {
1173
- content: props.content,
1174
- inline: props.inline ?? false,
1175
- displayMode: props.displayMode ?? !props.inline,
1176
- processedContent: props.content
1177
- };
1178
- }
1179
- /**
1180
- * Generic math renderer component
1181
- * Renders plain math content by default - consumers should provide their own math rendering
1182
- *
1183
- * For headless usage, use the useMath hook instead.
1184
- */ function MathPart({ content, inline = false, className = '', children, displayMode }) {
1185
- // If children provided, use that (allows complete customization)
1186
- if (children) {
1187
- return React.createElement(React.Fragment, {}, children);
1188
- }
1189
- const mathData = useMath({
1190
- content,
1191
- inline,
1192
- displayMode
1193
- });
1194
- // Simple fallback - just render plain math content
1195
- // Uses React.createElement for maximum compatibility across environments
1196
- return React.createElement(mathData.inline ? 'span' : 'div', {
1197
- className,
1198
- 'data-math-inline': mathData.inline,
1199
- 'data-math-display': mathData.displayMode
1200
- }, mathData.processedContent);
1201
- }
1202
-
1203
- 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 };