@v0-sdk/react 0.2.0 → 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/README.md +180 -382
- package/dist/index.cjs +744 -563
- package/dist/index.d.ts +221 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +736 -565
- package/package.json +3 -5
package/dist/index.js
CHANGED
|
@@ -1,49 +1,80 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
|
12
|
-
children: children
|
|
13
|
-
});
|
|
23
|
+
return React.createElement(React.Fragment, {}, children);
|
|
14
24
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
65
|
+
// This uses minimal DOM-specific attributes for maximum compatibility
|
|
66
|
+
return React.createElement('span', {
|
|
41
67
|
className: props.className,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
70
|
-
children: children
|
|
71
|
-
});
|
|
142
|
+
return React.createElement(React.Fragment, {}, children);
|
|
72
143
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
184
|
-
children: children
|
|
185
|
-
});
|
|
220
|
+
return React.createElement(React.Fragment, {}, children);
|
|
186
221
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
256
|
+
function getTypeIcon(type, title) {
|
|
241
257
|
// Check title content for specific cases
|
|
242
258
|
if (title?.includes('No issues found')) {
|
|
243
|
-
return
|
|
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
|
|
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
|
|
260
|
-
name: 'search'
|
|
261
|
-
}) : /*#__PURE__*/ jsx(Icon, {
|
|
262
|
-
name: "search"
|
|
263
|
-
});
|
|
267
|
+
return 'search';
|
|
264
268
|
case 'task-search-repo-v1':
|
|
265
|
-
return
|
|
266
|
-
name: 'folder'
|
|
267
|
-
}) : /*#__PURE__*/ jsx(Icon, {
|
|
268
|
-
name: "folder"
|
|
269
|
-
});
|
|
269
|
+
return 'folder';
|
|
270
270
|
case 'task-diagnostics-v1':
|
|
271
|
-
return
|
|
272
|
-
name: 'settings'
|
|
273
|
-
}) : /*#__PURE__*/ jsx(Icon, {
|
|
274
|
-
name: "settings"
|
|
275
|
-
});
|
|
271
|
+
return 'settings';
|
|
276
272
|
default:
|
|
277
|
-
return
|
|
278
|
-
name: 'wrench'
|
|
279
|
-
}) : /*#__PURE__*/ jsx(Icon, {
|
|
280
|
-
name: "wrench"
|
|
281
|
-
});
|
|
273
|
+
return 'wrench';
|
|
282
274
|
}
|
|
283
275
|
}
|
|
284
|
-
function
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
344
|
-
|
|
345
|
-
|
|
328
|
+
return {
|
|
329
|
+
...baseData,
|
|
330
|
+
content: 'Checking for issues...'
|
|
331
|
+
};
|
|
346
332
|
}
|
|
347
333
|
if (part.status === 'complete' && part.issues === 0) {
|
|
348
|
-
return
|
|
349
|
-
|
|
350
|
-
|
|
334
|
+
return {
|
|
335
|
+
...baseData,
|
|
336
|
+
isComplete: true,
|
|
337
|
+
issues: part.issues,
|
|
338
|
+
content: '✅ No issues found'
|
|
339
|
+
};
|
|
351
340
|
}
|
|
352
341
|
}
|
|
353
|
-
return
|
|
354
|
-
|
|
355
|
-
|
|
342
|
+
return {
|
|
343
|
+
...baseData,
|
|
344
|
+
content: JSON.stringify(part)
|
|
345
|
+
};
|
|
356
346
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
445
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
|
541
|
-
|
|
523
|
+
componentType = null; // Usually just indicates task start - can be hidden
|
|
524
|
+
break;
|
|
542
525
|
default:
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
//
|
|
559
|
-
function
|
|
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
|
|
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
|
|
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
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
652
|
+
}).filter(Boolean);
|
|
653
|
+
return {
|
|
654
|
+
elements,
|
|
655
|
+
messageId,
|
|
656
|
+
role,
|
|
657
|
+
streaming,
|
|
658
|
+
isLastMessage
|
|
659
|
+
};
|
|
604
660
|
}
|
|
605
|
-
//
|
|
606
|
-
function
|
|
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
|
|
612
|
-
const key =
|
|
613
|
-
return
|
|
614
|
-
}).filter(Boolean)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
//
|
|
621
|
-
function
|
|
678
|
+
// Process individual elements into headless data structure
|
|
679
|
+
function processElement(element, key, components) {
|
|
622
680
|
if (typeof element === 'string') {
|
|
623
|
-
return
|
|
624
|
-
|
|
625
|
-
|
|
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
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
|
654
|
-
|
|
655
|
-
|
|
720
|
+
return {
|
|
721
|
+
type: 'text',
|
|
722
|
+
key,
|
|
723
|
+
data: children[0] || ''
|
|
724
|
+
};
|
|
656
725
|
}
|
|
657
|
-
//
|
|
658
|
-
const
|
|
726
|
+
// Process children
|
|
727
|
+
const processedChildren = children.map((child, childIndex)=>{
|
|
659
728
|
const childKey = `${key}-child-${childIndex}`;
|
|
660
|
-
return
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -755,7 +890,6 @@ class StreamStateManager {
|
|
|
755
890
|
this.processStream = async (stream, options = {})=>{
|
|
756
891
|
// Prevent processing the same stream multiple times
|
|
757
892
|
if (this.processedStreams.has(stream)) {
|
|
758
|
-
console.log('Stream already processed, skipping');
|
|
759
893
|
return;
|
|
760
894
|
}
|
|
761
895
|
// Handle locked streams gracefully
|
|
@@ -810,13 +944,11 @@ class StreamStateManager {
|
|
|
810
944
|
while(true){
|
|
811
945
|
const { done, value } = await reader.read();
|
|
812
946
|
if (done) {
|
|
813
|
-
console.log('Stream reading completed');
|
|
814
947
|
break;
|
|
815
948
|
}
|
|
816
949
|
const chunk = decoder.decode(value, {
|
|
817
950
|
stream: true
|
|
818
951
|
});
|
|
819
|
-
console.log('Received raw chunk:', chunk);
|
|
820
952
|
buffer += chunk;
|
|
821
953
|
const lines = buffer.split('\n');
|
|
822
954
|
buffer = lines.pop() || '';
|
|
@@ -824,13 +956,11 @@ class StreamStateManager {
|
|
|
824
956
|
if (line.trim() === '') {
|
|
825
957
|
continue;
|
|
826
958
|
}
|
|
827
|
-
console.log('Processing line:', line);
|
|
828
959
|
// Handle SSE format (data: ...)
|
|
829
960
|
let jsonData;
|
|
830
961
|
if (line.startsWith('data: ')) {
|
|
831
962
|
jsonData = line.slice(6); // Remove "data: " prefix
|
|
832
963
|
if (jsonData === '[DONE]') {
|
|
833
|
-
console.log('Stream marked as done via SSE');
|
|
834
964
|
this.setComplete(true);
|
|
835
965
|
options.onComplete?.(currentContent);
|
|
836
966
|
return;
|
|
@@ -842,28 +972,21 @@ class StreamStateManager {
|
|
|
842
972
|
try {
|
|
843
973
|
// Parse the JSON data
|
|
844
974
|
const parsedData = JSON.parse(jsonData);
|
|
845
|
-
console.log('Parsed data:', JSON.stringify(parsedData, null, 2));
|
|
846
975
|
// Handle v0 streaming format
|
|
847
976
|
if (parsedData.type === 'connected') {
|
|
848
|
-
console.log('Stream connected');
|
|
849
977
|
continue;
|
|
850
978
|
} else if (parsedData.type === 'done') {
|
|
851
|
-
console.log('Stream marked as done');
|
|
852
979
|
this.setComplete(true);
|
|
853
980
|
options.onComplete?.(currentContent);
|
|
854
981
|
return;
|
|
855
|
-
} else if (parsedData.object
|
|
856
|
-
// Handle
|
|
857
|
-
console.log('Received chat data:', parsedData.id);
|
|
982
|
+
} else if (parsedData.object && parsedData.object.startsWith('chat')) {
|
|
983
|
+
// Handle chat metadata messages (chat, chat.title, chat.name, etc.)
|
|
858
984
|
options.onChatData?.(parsedData);
|
|
859
985
|
continue;
|
|
860
986
|
} else if (parsedData.delta) {
|
|
861
987
|
// 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
988
|
const patchedContent = patch(currentContent, parsedData.delta);
|
|
865
989
|
currentContent = Array.isArray(patchedContent) ? patchedContent : [];
|
|
866
|
-
console.log('Patched content result:', JSON.stringify(currentContent, null, 2));
|
|
867
990
|
this.updateContent(currentContent);
|
|
868
991
|
options.onChunk?.(currentContent);
|
|
869
992
|
}
|
|
@@ -896,16 +1019,39 @@ class StreamStateManager {
|
|
|
896
1019
|
if (stream !== lastStreamRef.current) {
|
|
897
1020
|
lastStreamRef.current = stream;
|
|
898
1021
|
if (stream) {
|
|
899
|
-
console.log('New stream detected, starting processing');
|
|
900
1022
|
manager.processStream(stream, options);
|
|
901
1023
|
}
|
|
902
1024
|
}
|
|
903
1025
|
return state;
|
|
904
1026
|
}
|
|
905
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
|
+
}
|
|
906
1050
|
/**
|
|
907
1051
|
* Component for rendering streaming message content from v0 API
|
|
908
1052
|
*
|
|
1053
|
+
* For headless usage, use the useStreamingMessageData hook instead.
|
|
1054
|
+
*
|
|
909
1055
|
* @example
|
|
910
1056
|
* ```tsx
|
|
911
1057
|
* import { v0 } from 'v0-sdk'
|
|
@@ -930,81 +1076,106 @@ class StreamStateManager {
|
|
|
930
1076
|
* stream={stream}
|
|
931
1077
|
* messageId="demo-message"
|
|
932
1078
|
* role="assistant"
|
|
933
|
-
* onComplete={(content) =>
|
|
934
|
-
* onChatData={(chatData) =>
|
|
1079
|
+
* onComplete={(content) => handleCompletion(content)}
|
|
1080
|
+
* onChatData={(chatData) => handleChatData(chatData)}
|
|
935
1081
|
* />
|
|
936
1082
|
* )}
|
|
937
1083
|
* </div>
|
|
938
1084
|
* )
|
|
939
1085
|
* }
|
|
940
1086
|
* ```
|
|
941
|
-
*/ function StreamingMessage({ stream, showLoadingIndicator = true, loadingComponent, errorComponent, onChunk, onComplete, onError, onChatData, ...messageProps }) {
|
|
942
|
-
const
|
|
1087
|
+
*/ function StreamingMessage({ stream, showLoadingIndicator = true, loadingComponent, errorComponent, onChunk, onComplete, onError, onChatData, className, ...messageProps }) {
|
|
1088
|
+
const streamingData = useStreamingMessageData({
|
|
1089
|
+
stream,
|
|
943
1090
|
onChunk,
|
|
944
1091
|
onComplete,
|
|
945
1092
|
onError,
|
|
946
|
-
onChatData
|
|
1093
|
+
onChatData,
|
|
1094
|
+
...messageProps
|
|
947
1095
|
});
|
|
948
1096
|
// Handle error state
|
|
949
|
-
if (error) {
|
|
1097
|
+
if (streamingData.error) {
|
|
950
1098
|
if (errorComponent) {
|
|
951
|
-
return
|
|
952
|
-
children: errorComponent(error)
|
|
953
|
-
});
|
|
1099
|
+
return React.createElement(React.Fragment, {}, errorComponent(streamingData.error));
|
|
954
1100
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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}`);
|
|
962
1111
|
}
|
|
963
1112
|
// Handle loading state
|
|
964
|
-
if (showLoadingIndicator && isStreaming && content.length === 0) {
|
|
1113
|
+
if (showLoadingIndicator && streamingData.isStreaming && streamingData.content.length === 0) {
|
|
965
1114
|
if (loadingComponent) {
|
|
966
|
-
return
|
|
967
|
-
children: loadingComponent
|
|
968
|
-
});
|
|
1115
|
+
return React.createElement(React.Fragment, {}, loadingComponent);
|
|
969
1116
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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...'));
|
|
981
1137
|
}
|
|
982
1138
|
// Render the message content
|
|
983
|
-
return
|
|
1139
|
+
return React.createElement(Message, {
|
|
984
1140
|
...messageProps,
|
|
985
|
-
content: content,
|
|
986
|
-
streaming: isStreaming,
|
|
987
|
-
isLastMessage: true
|
|
1141
|
+
content: streamingData.content,
|
|
1142
|
+
streaming: streamingData.isStreaming,
|
|
1143
|
+
isLastMessage: true,
|
|
1144
|
+
className
|
|
988
1145
|
});
|
|
989
1146
|
}
|
|
990
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
|
+
}
|
|
991
1157
|
/**
|
|
992
1158
|
* Generic math renderer component
|
|
993
1159
|
* Renders plain math content by default - consumers should provide their own math rendering
|
|
994
|
-
|
|
1160
|
+
*
|
|
1161
|
+
* For headless usage, use the useMath hook instead.
|
|
1162
|
+
*/ function MathPart({ content, inline = false, className = '', children, displayMode }) {
|
|
995
1163
|
// If children provided, use that (allows complete customization)
|
|
996
1164
|
if (children) {
|
|
997
|
-
return
|
|
998
|
-
children: children
|
|
999
|
-
});
|
|
1165
|
+
return React.createElement(React.Fragment, {}, children);
|
|
1000
1166
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
"data-math-inline": inline,
|
|
1006
|
-
children: content
|
|
1167
|
+
const mathData = useMath({
|
|
1168
|
+
content,
|
|
1169
|
+
inline,
|
|
1170
|
+
displayMode
|
|
1007
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);
|
|
1008
1179
|
}
|
|
1009
1180
|
|
|
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 };
|
|
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 };
|