@v0-sdk/react 0.1.1 → 0.2.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/LICENSE +13 -0
- package/README.md +61 -11
- package/dist/index.cjs +446 -244
- package/dist/index.d.ts +67 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +429 -248
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
2
|
-
import React, { useContext, createContext, useState } from 'react';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Generic math renderer component
|
|
6
|
-
* Renders plain math content by default - consumers should provide their own math rendering
|
|
7
|
-
*/ function MathPart({ content, inline = false, className = '', children }) {
|
|
8
|
-
// If children provided, use that (allows complete customization)
|
|
9
|
-
if (children) {
|
|
10
|
-
return /*#__PURE__*/ jsx(Fragment, {
|
|
11
|
-
children: children
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
// Simple fallback - just render plain math content
|
|
15
|
-
const Element = inline ? 'span' : 'div';
|
|
16
|
-
return /*#__PURE__*/ jsx(Element, {
|
|
17
|
-
className: className,
|
|
18
|
-
"data-math-inline": inline,
|
|
19
|
-
children: content
|
|
20
|
-
});
|
|
21
|
-
}
|
|
2
|
+
import React, { useContext, createContext, useState, useRef, useSyncExternalStore } from 'react';
|
|
3
|
+
import * as jsondiffpatch from 'jsondiffpatch';
|
|
22
4
|
|
|
23
5
|
/**
|
|
24
6
|
* Generic code block component
|
|
@@ -524,6 +506,36 @@ function ContentPartRenderer({ part, iconRenderer, thinkingSectionRenderer, task
|
|
|
524
506
|
chevronDownIcon: chevronDownIcon
|
|
525
507
|
});
|
|
526
508
|
}
|
|
509
|
+
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
|
+
}
|
|
524
|
+
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
|
+
}
|
|
527
539
|
case 'task-start-v1':
|
|
528
540
|
// Usually just indicates task start - can be hidden or show as status
|
|
529
541
|
return null;
|
|
@@ -543,33 +555,8 @@ function cn(...classes) {
|
|
|
543
555
|
return classes.filter(Boolean).join(' ');
|
|
544
556
|
}
|
|
545
557
|
|
|
546
|
-
//
|
|
547
|
-
function
|
|
548
|
-
if (typeof componentOrConfig === 'function') {
|
|
549
|
-
const Component = componentOrConfig;
|
|
550
|
-
return /*#__PURE__*/ jsx(Component, {
|
|
551
|
-
...props,
|
|
552
|
-
className: className,
|
|
553
|
-
children: renderChildren(children, key, components)
|
|
554
|
-
}, key);
|
|
555
|
-
} else if (componentOrConfig && typeof componentOrConfig === 'object') {
|
|
556
|
-
const mergedClassName = cn(className, componentOrConfig.className);
|
|
557
|
-
return /*#__PURE__*/ React.createElement(tagName, {
|
|
558
|
-
key,
|
|
559
|
-
...props,
|
|
560
|
-
className: mergedClassName
|
|
561
|
-
}, renderChildren(children, key, components));
|
|
562
|
-
} else {
|
|
563
|
-
return /*#__PURE__*/ React.createElement(tagName, {
|
|
564
|
-
key,
|
|
565
|
-
...props,
|
|
566
|
-
className
|
|
567
|
-
}, renderChildren(children, key, components));
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
/**
|
|
571
|
-
* Core renderer component for v0 Platform API message content
|
|
572
|
-
*/ function MessageImpl({ content, messageId = 'unknown', role: _role = 'assistant', streaming: _streaming = false, isLastMessage: _isLastMessage = false, className, components, renderers }) {
|
|
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 }) {
|
|
573
560
|
if (!Array.isArray(content)) {
|
|
574
561
|
console.warn('MessageContent: content must be an array (MessageBinaryFormat)');
|
|
575
562
|
return null;
|
|
@@ -591,239 +578,433 @@ function renderHtmlElement(tagName, key, props, children, className, componentOr
|
|
|
591
578
|
Icon: renderers.Icon
|
|
592
579
|
}
|
|
593
580
|
};
|
|
594
|
-
|
|
581
|
+
// Process content exactly like v0's Renderer component
|
|
582
|
+
const elements = content.map(([type, data], index)=>{
|
|
595
583
|
const key = `${messageId}-${index}`;
|
|
596
|
-
// Markdown
|
|
584
|
+
// Markdown data (type 0) - this is the main content
|
|
597
585
|
if (type === 0) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
return /*#__PURE__*/ jsx("div", {
|
|
603
|
-
children: markdownData.map((item, mdIndex)=>{
|
|
604
|
-
const mdKey = `${key}-md-${mdIndex}`;
|
|
605
|
-
return renderMarkdownElement(item, mdKey, mergedComponents);
|
|
606
|
-
})
|
|
586
|
+
return /*#__PURE__*/ jsx(Elements, {
|
|
587
|
+
data: data,
|
|
588
|
+
components: mergedComponents
|
|
607
589
|
}, key);
|
|
608
590
|
}
|
|
609
|
-
//
|
|
591
|
+
// Metadata (type 1) - extract context but don't render
|
|
610
592
|
if (type === 1) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
return
|
|
614
|
-
language: language || 'text',
|
|
615
|
-
code: code || ''
|
|
616
|
-
}, key);
|
|
617
|
-
}
|
|
618
|
-
// Math (type 2 for inline, type 3 for block)
|
|
619
|
-
if (type === 2 || type === 3) {
|
|
620
|
-
const mathContent = data[0] || '';
|
|
621
|
-
const MathPartComponent = mergedComponents?.MathPart || MathPart;
|
|
622
|
-
return /*#__PURE__*/ jsx(MathPartComponent, {
|
|
623
|
-
content: mathContent,
|
|
624
|
-
inline: type === 2
|
|
625
|
-
}, key);
|
|
593
|
+
// In the future, we could extract sources/context here like v0 does
|
|
594
|
+
// For now, just return null like v0's renderer
|
|
595
|
+
return null;
|
|
626
596
|
}
|
|
627
|
-
//
|
|
628
|
-
return
|
|
629
|
-
children: [
|
|
630
|
-
"[Unknown content type: ",
|
|
631
|
-
type,
|
|
632
|
-
"]"
|
|
633
|
-
]
|
|
634
|
-
}, key);
|
|
597
|
+
// Other types - v0 doesn't handle these in the main renderer
|
|
598
|
+
return null;
|
|
635
599
|
});
|
|
636
600
|
return /*#__PURE__*/ jsx("div", {
|
|
637
601
|
className: className,
|
|
638
602
|
children: elements
|
|
639
603
|
});
|
|
640
604
|
}
|
|
641
|
-
|
|
642
|
-
|
|
605
|
+
// This component handles the markdown data array (equivalent to v0's Elements component)
|
|
606
|
+
function Elements({ data, components }) {
|
|
607
|
+
// Handle case where data might not be an array due to streaming/patching
|
|
608
|
+
if (!Array.isArray(data)) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
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
|
+
});
|
|
619
|
+
}
|
|
620
|
+
// Render individual elements (equivalent to v0's element rendering logic)
|
|
621
|
+
function renderElement(element, key, components) {
|
|
622
|
+
if (typeof element === 'string') {
|
|
643
623
|
return /*#__PURE__*/ jsx("span", {
|
|
644
|
-
children:
|
|
624
|
+
children: element
|
|
645
625
|
}, key);
|
|
646
626
|
}
|
|
647
|
-
if (Array.isArray(
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
627
|
+
if (!Array.isArray(element)) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
const [tagName, props, ...children] = element;
|
|
631
|
+
if (!tagName) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
// Handle special v0 Platform API elements
|
|
635
|
+
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);
|
|
642
|
+
}
|
|
643
|
+
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);
|
|
651
|
+
}
|
|
652
|
+
if (tagName === 'text') {
|
|
653
|
+
return /*#__PURE__*/ jsx("span", {
|
|
654
|
+
children: children[0] || ''
|
|
655
|
+
}, key);
|
|
656
|
+
}
|
|
657
|
+
// Render children
|
|
658
|
+
const renderedChildren = children.map((child, childIndex)=>{
|
|
659
|
+
const childKey = `${key}-child-${childIndex}`;
|
|
660
|
+
return renderElement(child, childKey, components);
|
|
661
|
+
}).filter(Boolean);
|
|
662
|
+
// Handle standard HTML elements
|
|
663
|
+
const className = props?.className;
|
|
664
|
+
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;
|
|
657
687
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
language: props.lang,
|
|
663
|
-
code: children[0],
|
|
664
|
-
iconRenderer: components?.Icon
|
|
665
|
-
}, key);
|
|
688
|
+
// Special handling for links
|
|
689
|
+
if (tagName === 'a') {
|
|
690
|
+
elementProps.target = '_blank';
|
|
691
|
+
elementProps.rel = 'noopener noreferrer';
|
|
666
692
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
693
|
+
return /*#__PURE__*/ React.createElement(tagName, elementProps, renderedChildren);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Main component for rendering v0 Platform API message content
|
|
698
|
+
*/ const Message = /*#__PURE__*/ React.memo(MessageImpl);
|
|
699
|
+
|
|
700
|
+
const jdf = jsondiffpatch.create({});
|
|
701
|
+
// Exact copy of the patch function from v0/chat/lib/diffpatch.ts
|
|
702
|
+
function patch(original, delta) {
|
|
703
|
+
const newObj = jdf.clone(original);
|
|
704
|
+
// Check for our customized delta
|
|
705
|
+
if (Array.isArray(delta) && delta[1] === 9 && delta[2] === 9) {
|
|
706
|
+
// Get the path to the modified element
|
|
707
|
+
const indexes = delta[0].slice(0, -1);
|
|
708
|
+
// Get the string to be appended
|
|
709
|
+
const value = delta[0].slice(-1);
|
|
710
|
+
let obj = newObj;
|
|
711
|
+
for (const index of indexes){
|
|
712
|
+
if (typeof obj[index] === 'string') {
|
|
713
|
+
obj[index] += value;
|
|
714
|
+
return newObj;
|
|
715
|
+
}
|
|
716
|
+
obj = obj[index];
|
|
671
717
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
return
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
718
|
+
}
|
|
719
|
+
// If not custom delta, apply standard jsondiffpatch-ing
|
|
720
|
+
jdf.patch(newObj, delta);
|
|
721
|
+
return newObj;
|
|
722
|
+
}
|
|
723
|
+
// Stream state manager - isolated from React lifecycle
|
|
724
|
+
class StreamStateManager {
|
|
725
|
+
constructor(){
|
|
726
|
+
this.content = [];
|
|
727
|
+
this.isStreaming = false;
|
|
728
|
+
this.isComplete = false;
|
|
729
|
+
this.callbacks = new Set();
|
|
730
|
+
this.processedStreams = new WeakSet();
|
|
731
|
+
this.cachedState = null;
|
|
732
|
+
this.subscribe = (callback)=>{
|
|
733
|
+
this.callbacks.add(callback);
|
|
734
|
+
return ()=>{
|
|
735
|
+
this.callbacks.delete(callback);
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
this.notifySubscribers = ()=>{
|
|
739
|
+
// Invalidate cached state when state changes
|
|
740
|
+
this.cachedState = null;
|
|
741
|
+
this.callbacks.forEach((callback)=>callback());
|
|
742
|
+
};
|
|
743
|
+
this.getState = ()=>{
|
|
744
|
+
// Return cached state to prevent infinite re-renders
|
|
745
|
+
if (this.cachedState === null) {
|
|
746
|
+
this.cachedState = {
|
|
747
|
+
content: this.content,
|
|
748
|
+
isStreaming: this.isStreaming,
|
|
749
|
+
error: this.error,
|
|
750
|
+
isComplete: this.isComplete
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
return this.cachedState;
|
|
754
|
+
};
|
|
755
|
+
this.processStream = async (stream, options = {})=>{
|
|
756
|
+
// Prevent processing the same stream multiple times
|
|
757
|
+
if (this.processedStreams.has(stream)) {
|
|
758
|
+
console.log('Stream already processed, skipping');
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
// Handle locked streams gracefully
|
|
762
|
+
if (stream.locked) {
|
|
763
|
+
console.warn('Stream is locked, cannot process');
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
this.processedStreams.add(stream);
|
|
767
|
+
this.reset();
|
|
768
|
+
this.setStreaming(true);
|
|
769
|
+
try {
|
|
770
|
+
await this.readStream(stream, options);
|
|
771
|
+
} catch (err) {
|
|
772
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown streaming error';
|
|
773
|
+
this.setError(errorMessage);
|
|
774
|
+
options.onError?.(errorMessage);
|
|
775
|
+
} finally{
|
|
776
|
+
this.setStreaming(false);
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
this.reset = ()=>{
|
|
780
|
+
this.content = [];
|
|
781
|
+
this.isStreaming = false;
|
|
782
|
+
this.error = undefined;
|
|
783
|
+
this.isComplete = false;
|
|
784
|
+
this.notifySubscribers();
|
|
785
|
+
};
|
|
786
|
+
this.setStreaming = (streaming)=>{
|
|
787
|
+
this.isStreaming = streaming;
|
|
788
|
+
this.notifySubscribers();
|
|
789
|
+
};
|
|
790
|
+
this.setError = (error)=>{
|
|
791
|
+
this.error = error;
|
|
792
|
+
this.notifySubscribers();
|
|
793
|
+
};
|
|
794
|
+
this.setComplete = (complete)=>{
|
|
795
|
+
this.isComplete = complete;
|
|
796
|
+
this.notifySubscribers();
|
|
797
|
+
};
|
|
798
|
+
this.updateContent = (newContent)=>{
|
|
799
|
+
this.content = [
|
|
800
|
+
...newContent
|
|
801
|
+
];
|
|
802
|
+
this.notifySubscribers();
|
|
803
|
+
};
|
|
804
|
+
this.readStream = async (stream, options)=>{
|
|
805
|
+
const reader = stream.getReader();
|
|
806
|
+
const decoder = new TextDecoder();
|
|
807
|
+
let buffer = '';
|
|
808
|
+
let currentContent = [];
|
|
809
|
+
try {
|
|
810
|
+
while(true){
|
|
811
|
+
const { done, value } = await reader.read();
|
|
812
|
+
if (done) {
|
|
813
|
+
console.log('Stream reading completed');
|
|
814
|
+
break;
|
|
757
815
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
816
|
+
const chunk = decoder.decode(value, {
|
|
817
|
+
stream: true
|
|
818
|
+
});
|
|
819
|
+
console.log('Received raw chunk:', chunk);
|
|
820
|
+
buffer += chunk;
|
|
821
|
+
const lines = buffer.split('\n');
|
|
822
|
+
buffer = lines.pop() || '';
|
|
823
|
+
for (const line of lines){
|
|
824
|
+
if (line.trim() === '') {
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
console.log('Processing line:', line);
|
|
828
|
+
// Handle SSE format (data: ...)
|
|
829
|
+
let jsonData;
|
|
830
|
+
if (line.startsWith('data: ')) {
|
|
831
|
+
jsonData = line.slice(6); // Remove "data: " prefix
|
|
832
|
+
if (jsonData === '[DONE]') {
|
|
833
|
+
console.log('Stream marked as done via SSE');
|
|
834
|
+
this.setComplete(true);
|
|
835
|
+
options.onComplete?.(currentContent);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
} else {
|
|
839
|
+
// Handle raw JSON lines (fallback)
|
|
840
|
+
jsonData = line;
|
|
841
|
+
}
|
|
842
|
+
try {
|
|
843
|
+
// Parse the JSON data
|
|
844
|
+
const parsedData = JSON.parse(jsonData);
|
|
845
|
+
console.log('Parsed data:', JSON.stringify(parsedData, null, 2));
|
|
846
|
+
// Handle v0 streaming format
|
|
847
|
+
if (parsedData.type === 'connected') {
|
|
848
|
+
console.log('Stream connected');
|
|
849
|
+
continue;
|
|
850
|
+
} else if (parsedData.type === 'done') {
|
|
851
|
+
console.log('Stream marked as done');
|
|
852
|
+
this.setComplete(true);
|
|
853
|
+
options.onComplete?.(currentContent);
|
|
854
|
+
return;
|
|
855
|
+
} else if (parsedData.object === 'chat' && parsedData.id) {
|
|
856
|
+
// Handle the initial chat data message
|
|
857
|
+
console.log('Received chat data:', parsedData.id);
|
|
858
|
+
options.onChatData?.(parsedData);
|
|
859
|
+
continue;
|
|
860
|
+
} else if (parsedData.delta) {
|
|
861
|
+
// Apply the delta using jsondiffpatch
|
|
862
|
+
console.log('Applying delta to content:', JSON.stringify(currentContent, null, 2));
|
|
863
|
+
console.log('Delta:', JSON.stringify(parsedData.delta, null, 2));
|
|
864
|
+
const patchedContent = patch(currentContent, parsedData.delta);
|
|
865
|
+
currentContent = Array.isArray(patchedContent) ? patchedContent : [];
|
|
866
|
+
console.log('Patched content result:', JSON.stringify(currentContent, null, 2));
|
|
867
|
+
this.updateContent(currentContent);
|
|
868
|
+
options.onChunk?.(currentContent);
|
|
869
|
+
}
|
|
870
|
+
} catch (e) {
|
|
871
|
+
console.warn('Failed to parse streaming data:', line, e);
|
|
872
|
+
}
|
|
781
873
|
}
|
|
782
874
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
children: renderChildren(children, key, components)
|
|
790
|
-
}, key);
|
|
791
|
-
}
|
|
875
|
+
this.setComplete(true);
|
|
876
|
+
options.onComplete?.(currentContent);
|
|
877
|
+
} finally{
|
|
878
|
+
reader.releaseLock();
|
|
879
|
+
}
|
|
880
|
+
};
|
|
792
881
|
}
|
|
793
|
-
return null;
|
|
794
882
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
883
|
+
/**
|
|
884
|
+
* Hook for handling streaming message content from v0 API using useSyncExternalStore
|
|
885
|
+
*/ function useStreamingMessage(stream, options = {}) {
|
|
886
|
+
// Create a stable stream manager instance
|
|
887
|
+
const managerRef = useRef(null);
|
|
888
|
+
if (!managerRef.current) {
|
|
889
|
+
managerRef.current = new StreamStateManager();
|
|
890
|
+
}
|
|
891
|
+
const manager = managerRef.current;
|
|
892
|
+
// Subscribe to state changes using useSyncExternalStore
|
|
893
|
+
const state = useSyncExternalStore(manager.subscribe, manager.getState, manager.getState);
|
|
894
|
+
// Process stream when it changes
|
|
895
|
+
const lastStreamRef = useRef(null);
|
|
896
|
+
if (stream !== lastStreamRef.current) {
|
|
897
|
+
lastStreamRef.current = stream;
|
|
898
|
+
if (stream) {
|
|
899
|
+
console.log('New stream detected, starting processing');
|
|
900
|
+
manager.processStream(stream, options);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return state;
|
|
800
904
|
}
|
|
905
|
+
|
|
801
906
|
/**
|
|
802
|
-
*
|
|
907
|
+
* Component for rendering streaming message content from v0 API
|
|
803
908
|
*
|
|
804
909
|
* @example
|
|
805
910
|
* ```tsx
|
|
806
|
-
* import {
|
|
911
|
+
* import { v0 } from 'v0-sdk'
|
|
912
|
+
* import { StreamingMessage } from '@v0-sdk/react'
|
|
913
|
+
*
|
|
914
|
+
* function ChatDemo() {
|
|
915
|
+
* const [stream, setStream] = useState<ReadableStream<Uint8Array> | null>(null)
|
|
807
916
|
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
917
|
+
* const handleSubmit = async () => {
|
|
918
|
+
* const response = await v0.chats.create({
|
|
919
|
+
* message: 'Create a button component',
|
|
920
|
+
* responseMode: 'experimental_stream'
|
|
921
|
+
* })
|
|
922
|
+
* setStream(response)
|
|
923
|
+
* }
|
|
810
924
|
*
|
|
811
925
|
* return (
|
|
812
|
-
* <
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
* }
|
|
823
|
-
*
|
|
926
|
+
* <div>
|
|
927
|
+
* <button onClick={handleSubmit}>Send Message</button>
|
|
928
|
+
* {stream && (
|
|
929
|
+
* <StreamingMessage
|
|
930
|
+
* stream={stream}
|
|
931
|
+
* messageId="demo-message"
|
|
932
|
+
* role="assistant"
|
|
933
|
+
* onComplete={(content) => console.log('Stream complete:', content)}
|
|
934
|
+
* onChatData={(chatData) => console.log('Chat created:', chatData.id)}
|
|
935
|
+
* />
|
|
936
|
+
* )}
|
|
937
|
+
* </div>
|
|
824
938
|
* )
|
|
825
939
|
* }
|
|
826
940
|
* ```
|
|
827
|
-
*/
|
|
941
|
+
*/ function StreamingMessage({ stream, showLoadingIndicator = true, loadingComponent, errorComponent, onChunk, onComplete, onError, onChatData, ...messageProps }) {
|
|
942
|
+
const { content, isStreaming, error, isComplete } = useStreamingMessage(stream, {
|
|
943
|
+
onChunk,
|
|
944
|
+
onComplete,
|
|
945
|
+
onError,
|
|
946
|
+
onChatData
|
|
947
|
+
});
|
|
948
|
+
// Handle error state
|
|
949
|
+
if (error) {
|
|
950
|
+
if (errorComponent) {
|
|
951
|
+
return /*#__PURE__*/ jsx(Fragment, {
|
|
952
|
+
children: errorComponent(error)
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
956
|
+
className: "text-red-500 p-4 border border-red-200 rounded",
|
|
957
|
+
children: [
|
|
958
|
+
"Error: ",
|
|
959
|
+
error
|
|
960
|
+
]
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
// Handle loading state
|
|
964
|
+
if (showLoadingIndicator && isStreaming && content.length === 0) {
|
|
965
|
+
if (loadingComponent) {
|
|
966
|
+
return /*#__PURE__*/ jsx(Fragment, {
|
|
967
|
+
children: loadingComponent
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
971
|
+
className: "flex items-center space-x-2 text-gray-500",
|
|
972
|
+
children: [
|
|
973
|
+
/*#__PURE__*/ jsx("div", {
|
|
974
|
+
className: "animate-spin h-4 w-4 border-2 border-gray-300 border-t-gray-600 rounded-full"
|
|
975
|
+
}),
|
|
976
|
+
/*#__PURE__*/ jsx("span", {
|
|
977
|
+
children: "Loading..."
|
|
978
|
+
})
|
|
979
|
+
]
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
// Render the message content
|
|
983
|
+
return /*#__PURE__*/ jsx(Message, {
|
|
984
|
+
...messageProps,
|
|
985
|
+
content: content,
|
|
986
|
+
streaming: isStreaming,
|
|
987
|
+
isLastMessage: true
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Generic math renderer component
|
|
993
|
+
* Renders plain math content by default - consumers should provide their own math rendering
|
|
994
|
+
*/ function MathPart({ content, inline = false, className = '', children }) {
|
|
995
|
+
// If children provided, use that (allows complete customization)
|
|
996
|
+
if (children) {
|
|
997
|
+
return /*#__PURE__*/ jsx(Fragment, {
|
|
998
|
+
children: children
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
// Simple fallback - just render plain math content
|
|
1002
|
+
const Element = inline ? 'span' : 'div';
|
|
1003
|
+
return /*#__PURE__*/ jsx(Element, {
|
|
1004
|
+
className: className,
|
|
1005
|
+
"data-math-inline": inline,
|
|
1006
|
+
children: content
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
828
1009
|
|
|
829
|
-
export { ContentPartRenderer as AssistantMessageContentPart, CodeBlock, CodeProjectPart as CodeProjectBlock, CodeProjectPart, ContentPartRenderer, Icon, MathPart, MathPart as MathRenderer, Message, Message as MessageContent, Message as MessageRenderer, TaskSection, ThinkingSection, Message as V0MessageRenderer };
|
|
1010
|
+
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 };
|