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