@v0-sdk/react 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -382
- package/dist/index.cjs +742 -548
- package/dist/index.d.ts +219 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +734 -550
- 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
|
|
@@ -844,8 +979,8 @@ class StreamStateManager {
|
|
|
844
979
|
this.setComplete(true);
|
|
845
980
|
options.onComplete?.(currentContent);
|
|
846
981
|
return;
|
|
847
|
-
} else if (parsedData.object
|
|
848
|
-
// Handle
|
|
982
|
+
} else if (parsedData.object && parsedData.object.startsWith('chat')) {
|
|
983
|
+
// Handle chat metadata messages (chat, chat.title, chat.name, etc.)
|
|
849
984
|
options.onChatData?.(parsedData);
|
|
850
985
|
continue;
|
|
851
986
|
} else if (parsedData.delta) {
|
|
@@ -890,9 +1025,33 @@ class StreamStateManager {
|
|
|
890
1025
|
return state;
|
|
891
1026
|
}
|
|
892
1027
|
|
|
1028
|
+
// Headless hook for streaming message
|
|
1029
|
+
function useStreamingMessageData({ stream, messageId = 'unknown', role = 'assistant', components, renderers, onChunk, onComplete, onError, onChatData }) {
|
|
1030
|
+
const streamingState = useStreamingMessage(stream, {
|
|
1031
|
+
onChunk,
|
|
1032
|
+
onComplete,
|
|
1033
|
+
onError,
|
|
1034
|
+
onChatData
|
|
1035
|
+
});
|
|
1036
|
+
const messageData = streamingState.content.length > 0 ? useMessage({
|
|
1037
|
+
content: streamingState.content,
|
|
1038
|
+
messageId,
|
|
1039
|
+
role,
|
|
1040
|
+
streaming: streamingState.isStreaming,
|
|
1041
|
+
isLastMessage: true,
|
|
1042
|
+
components,
|
|
1043
|
+
renderers
|
|
1044
|
+
}) : null;
|
|
1045
|
+
return {
|
|
1046
|
+
...streamingState,
|
|
1047
|
+
messageData
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
893
1050
|
/**
|
|
894
1051
|
* Component for rendering streaming message content from v0 API
|
|
895
1052
|
*
|
|
1053
|
+
* For headless usage, use the useStreamingMessageData hook instead.
|
|
1054
|
+
*
|
|
896
1055
|
* @example
|
|
897
1056
|
* ```tsx
|
|
898
1057
|
* import { v0 } from 'v0-sdk'
|
|
@@ -925,73 +1084,98 @@ class StreamStateManager {
|
|
|
925
1084
|
* )
|
|
926
1085
|
* }
|
|
927
1086
|
* ```
|
|
928
|
-
*/ function StreamingMessage({ stream, showLoadingIndicator = true, loadingComponent, errorComponent, onChunk, onComplete, onError, onChatData, ...messageProps }) {
|
|
929
|
-
const
|
|
1087
|
+
*/ function StreamingMessage({ stream, showLoadingIndicator = true, loadingComponent, errorComponent, onChunk, onComplete, onError, onChatData, className, ...messageProps }) {
|
|
1088
|
+
const streamingData = useStreamingMessageData({
|
|
1089
|
+
stream,
|
|
930
1090
|
onChunk,
|
|
931
1091
|
onComplete,
|
|
932
1092
|
onError,
|
|
933
|
-
onChatData
|
|
1093
|
+
onChatData,
|
|
1094
|
+
...messageProps
|
|
934
1095
|
});
|
|
935
1096
|
// Handle error state
|
|
936
|
-
if (error) {
|
|
1097
|
+
if (streamingData.error) {
|
|
937
1098
|
if (errorComponent) {
|
|
938
|
-
return
|
|
939
|
-
children: errorComponent(error)
|
|
940
|
-
});
|
|
1099
|
+
return React.createElement(React.Fragment, {}, errorComponent(streamingData.error));
|
|
941
1100
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1101
|
+
// Fallback error component using React.createElement for compatibility
|
|
1102
|
+
return React.createElement('div', {
|
|
1103
|
+
className: 'text-red-500 p-4 border border-red-200 rounded',
|
|
1104
|
+
style: {
|
|
1105
|
+
color: 'red',
|
|
1106
|
+
padding: '1rem',
|
|
1107
|
+
border: '1px solid #fecaca',
|
|
1108
|
+
borderRadius: '0.375rem'
|
|
1109
|
+
}
|
|
1110
|
+
}, `Error: ${streamingData.error}`);
|
|
949
1111
|
}
|
|
950
1112
|
// Handle loading state
|
|
951
|
-
if (showLoadingIndicator && isStreaming && content.length === 0) {
|
|
1113
|
+
if (showLoadingIndicator && streamingData.isStreaming && streamingData.content.length === 0) {
|
|
952
1114
|
if (loadingComponent) {
|
|
953
|
-
return
|
|
954
|
-
children: loadingComponent
|
|
955
|
-
});
|
|
1115
|
+
return React.createElement(React.Fragment, {}, loadingComponent);
|
|
956
1116
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1117
|
+
// Fallback loading component using React.createElement for compatibility
|
|
1118
|
+
return React.createElement('div', {
|
|
1119
|
+
className: 'flex items-center space-x-2 text-gray-500',
|
|
1120
|
+
style: {
|
|
1121
|
+
display: 'flex',
|
|
1122
|
+
alignItems: 'center',
|
|
1123
|
+
gap: '0.5rem',
|
|
1124
|
+
color: '#6b7280'
|
|
1125
|
+
}
|
|
1126
|
+
}, React.createElement('div', {
|
|
1127
|
+
className: 'animate-spin h-4 w-4 border-2 border-gray-300 border-t-gray-600 rounded-full',
|
|
1128
|
+
style: {
|
|
1129
|
+
animation: 'spin 1s linear infinite',
|
|
1130
|
+
height: '1rem',
|
|
1131
|
+
width: '1rem',
|
|
1132
|
+
border: '2px solid #d1d5db',
|
|
1133
|
+
borderTopColor: '#4b5563',
|
|
1134
|
+
borderRadius: '50%'
|
|
1135
|
+
}
|
|
1136
|
+
}), React.createElement('span', {}, 'Loading...'));
|
|
968
1137
|
}
|
|
969
1138
|
// Render the message content
|
|
970
|
-
return
|
|
1139
|
+
return React.createElement(Message, {
|
|
971
1140
|
...messageProps,
|
|
972
|
-
content: content,
|
|
973
|
-
streaming: isStreaming,
|
|
974
|
-
isLastMessage: true
|
|
1141
|
+
content: streamingData.content,
|
|
1142
|
+
streaming: streamingData.isStreaming,
|
|
1143
|
+
isLastMessage: true,
|
|
1144
|
+
className
|
|
975
1145
|
});
|
|
976
1146
|
}
|
|
977
1147
|
|
|
1148
|
+
// Headless hook for math data
|
|
1149
|
+
function useMath(props) {
|
|
1150
|
+
return {
|
|
1151
|
+
content: props.content,
|
|
1152
|
+
inline: props.inline ?? false,
|
|
1153
|
+
displayMode: props.displayMode ?? !props.inline,
|
|
1154
|
+
processedContent: props.content
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
978
1157
|
/**
|
|
979
1158
|
* Generic math renderer component
|
|
980
1159
|
* Renders plain math content by default - consumers should provide their own math rendering
|
|
981
|
-
|
|
1160
|
+
*
|
|
1161
|
+
* For headless usage, use the useMath hook instead.
|
|
1162
|
+
*/ function MathPart({ content, inline = false, className = '', children, displayMode }) {
|
|
982
1163
|
// If children provided, use that (allows complete customization)
|
|
983
1164
|
if (children) {
|
|
984
|
-
return
|
|
985
|
-
children: children
|
|
986
|
-
});
|
|
1165
|
+
return React.createElement(React.Fragment, {}, children);
|
|
987
1166
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
"data-math-inline": inline,
|
|
993
|
-
children: content
|
|
1167
|
+
const mathData = useMath({
|
|
1168
|
+
content,
|
|
1169
|
+
inline,
|
|
1170
|
+
displayMode
|
|
994
1171
|
});
|
|
1172
|
+
// Simple fallback - just render plain math content
|
|
1173
|
+
// Uses React.createElement for maximum compatibility across environments
|
|
1174
|
+
return React.createElement(mathData.inline ? 'span' : 'div', {
|
|
1175
|
+
className,
|
|
1176
|
+
'data-math-inline': mathData.inline,
|
|
1177
|
+
'data-math-display': mathData.displayMode
|
|
1178
|
+
}, mathData.processedContent);
|
|
995
1179
|
}
|
|
996
1180
|
|
|
997
|
-
export { ContentPartRenderer as AssistantMessageContentPart, CodeBlock, CodeProjectPart as CodeProjectBlock, CodeProjectPart, ContentPartRenderer, Icon, MathPart, MathPart as MathRenderer, Message, Message as MessageContent, Message as MessageRenderer, StreamingMessage, TaskSection, ThinkingSection, Message as V0MessageRenderer, useStreamingMessage };
|
|
1181
|
+
export { ContentPartRenderer as AssistantMessageContentPart, CodeBlock, CodeProjectPart as CodeProjectBlock, CodeProjectPart, ContentPartRenderer, Icon, IconProvider, MathPart, MathPart as MathRenderer, Message, Message as MessageContent, Message as MessageRenderer, StreamingMessage, TaskSection, ThinkingSection, Message as V0MessageRenderer, useCodeBlock, useCodeProject, useContentPart, useIcon, useMath, useMessage, useStreamingMessage, useStreamingMessageData, useTaskSection, useThinkingSection };
|