@gram-ai/elements 1.22.4 → 1.23.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/dist/components/Chat/stories/Plugins.stories.d.ts +6 -0
- package/dist/components/Chat/stories/Tools.stories.d.ts +15 -0
- package/dist/components/assistant-ui/connection-status-indicator.d.ts +16 -0
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/components/ui/buttonVariants.d.ts +1 -1
- package/dist/components/ui/generative-ui.d.ts +13 -0
- package/dist/contexts/ConnectionStatusContext.d.ts +27 -0
- package/dist/contexts/ToolExecutionContext.d.ts +21 -0
- package/dist/elements.cjs +1 -1
- package/dist/elements.css +1 -1
- package/dist/elements.js +2 -2
- package/dist/index-CVYyyxfm.cjs +147 -0
- package/dist/index-CVYyyxfm.cjs.map +1 -0
- package/dist/index-Co05S1C8.cjs +251 -0
- package/dist/index-Co05S1C8.cjs.map +1 -0
- package/dist/{index-DqUXeR5-.js → index-D-QXb5EF.js} +10579 -10282
- package/dist/index-D-QXb5EF.js.map +1 -0
- package/dist/{index-BdXdd2ZM.js → index-vM3v0unX.js} +8442 -7926
- package/dist/index-vM3v0unX.js.map +1 -0
- package/dist/lib/generative-ui.d.ts +9 -0
- package/dist/lib/messageConverter.d.ts +13 -7
- package/dist/plugins/components/PluginLoadingState.d.ts +11 -0
- package/dist/plugins/components/index.d.ts +1 -0
- package/dist/plugins/generative-ui/component.d.ts +3 -0
- package/dist/plugins/generative-ui/index.d.ts +6 -0
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins.cjs +1 -1
- package/dist/plugins.js +3 -2
- package/dist/{profiler-Bc9bwSPu.js → profiler-D8-vgPGn.js} +2 -2
- package/dist/{profiler-Bc9bwSPu.js.map → profiler-D8-vgPGn.js.map} +1 -1
- package/dist/{profiler-BBSi_UtN.cjs → profiler-Dshm-O8k.cjs} +2 -2
- package/dist/{profiler-BBSi_UtN.cjs.map → profiler-Dshm-O8k.cjs.map} +1 -1
- package/dist/{startRecording-DsKSBkyg.cjs → startRecording-2p7-xVUh.cjs} +2 -2
- package/dist/{startRecording-DsKSBkyg.cjs.map → startRecording-2p7-xVUh.cjs.map} +1 -1
- package/dist/{startRecording-DC-9cgyZ.js → startRecording-DnWeZRhl.js} +2 -2
- package/dist/{startRecording-DC-9cgyZ.js.map → startRecording-DnWeZRhl.js.map} +1 -1
- package/package.json +8 -2
- package/src/components/Chat/stories/Plugins.stories.tsx +116 -0
- package/src/components/Chat/stories/Tools.stories.tsx +122 -0
- package/src/components/assistant-ui/connection-status-indicator.tsx +134 -0
- package/src/components/assistant-ui/thread.tsx +3 -1
- package/src/components/ui/generative-ui.tsx +437 -0
- package/src/contexts/ConnectionStatusContext.tsx +158 -0
- package/src/contexts/ElementsProvider.tsx +133 -25
- package/src/contexts/ToolExecutionContext.tsx +101 -0
- package/src/hooks/useGramThreadListAdapter.tsx +43 -46
- package/src/lib/generative-ui.ts +18 -0
- package/src/lib/messageConverter.ts +330 -57
- package/src/lib.d.ts +1 -0
- package/src/plugins/chart/component.tsx +8 -8
- package/src/plugins/components/PluginLoadingState.tsx +35 -0
- package/src/plugins/components/index.ts +1 -0
- package/src/plugins/generative-ui/component.tsx +56 -0
- package/src/plugins/generative-ui/index.ts +153 -0
- package/src/plugins/index.ts +3 -1
- package/dist/index-BdXdd2ZM.js.map +0 -1
- package/dist/index-C1TX1kmi.cjs +0 -145
- package/dist/index-C1TX1kmi.cjs.map +0 -1
- package/dist/index-CNVoovK7.cjs +0 -111
- package/dist/index-CNVoovK7.cjs.map +0 -1
- package/dist/index-DqUXeR5-.js.map +0 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Message format converter for Gram API <-> assistant-ui.
|
|
3
5
|
*
|
|
@@ -17,19 +19,22 @@ import type {
|
|
|
17
19
|
ThreadAssistantMessagePart,
|
|
18
20
|
TextMessagePart,
|
|
19
21
|
} from '@assistant-ui/react'
|
|
22
|
+
import type {
|
|
23
|
+
Message,
|
|
24
|
+
UserMessage,
|
|
25
|
+
AssistantMessage,
|
|
26
|
+
ToolResponseMessage,
|
|
27
|
+
} from '@openrouter/sdk/models'
|
|
28
|
+
import { UIMessage } from 'ai'
|
|
20
29
|
|
|
21
30
|
/**
|
|
22
31
|
* Represents a chat message from the Gram API.
|
|
23
32
|
* This mirrors the ChatMessage type from @gram/sdk without requiring the SDK dependency.
|
|
24
33
|
*/
|
|
25
|
-
export
|
|
34
|
+
export type GramChatMessage = Message & {
|
|
26
35
|
id: string
|
|
27
|
-
role: string
|
|
28
|
-
content?: string
|
|
29
36
|
model: string
|
|
30
|
-
|
|
31
|
-
toolCalls?: string
|
|
32
|
-
createdAt: Date | string
|
|
37
|
+
created_at: Date | string
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
/**
|
|
@@ -57,17 +62,6 @@ export interface GramChatOverview {
|
|
|
57
62
|
updatedAt: Date | string
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
/**
|
|
61
|
-
* Normalizes a role string to valid ThreadMessage roles.
|
|
62
|
-
*/
|
|
63
|
-
function normalizeRole(role: string): 'user' | 'assistant' | 'system' {
|
|
64
|
-
if (role === 'user') return 'user'
|
|
65
|
-
if (role === 'assistant') return 'assistant'
|
|
66
|
-
if (role === 'system') return 'system'
|
|
67
|
-
// Tool role messages should be handled differently, but for now treat as assistant
|
|
68
|
-
return 'assistant'
|
|
69
|
-
}
|
|
70
|
-
|
|
71
65
|
/**
|
|
72
66
|
* Parses a date that might be a string or Date object.
|
|
73
67
|
*/
|
|
@@ -79,21 +73,64 @@ function parseDate(date: Date | string): Date {
|
|
|
79
73
|
* Builds content parts for a user message.
|
|
80
74
|
*/
|
|
81
75
|
function buildUserContentParts(msg: GramChatMessage): ThreadUserMessagePart[] {
|
|
82
|
-
|
|
76
|
+
if (msg.role !== 'user') {
|
|
77
|
+
return []
|
|
78
|
+
}
|
|
83
79
|
|
|
84
|
-
if (msg.content) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
if (typeof msg.content === 'string' || !msg.content) {
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: msg.content ?? '',
|
|
85
|
+
},
|
|
86
|
+
]
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
const parts: ThreadUserMessagePart[] = []
|
|
90
|
+
|
|
91
|
+
for (const item of msg.content) {
|
|
92
|
+
switch (item.type) {
|
|
93
|
+
case 'text':
|
|
94
|
+
parts.push({
|
|
95
|
+
type: 'text',
|
|
96
|
+
text: item.text,
|
|
97
|
+
})
|
|
98
|
+
break
|
|
99
|
+
case 'image_url':
|
|
100
|
+
parts.push({
|
|
101
|
+
type: 'image',
|
|
102
|
+
image: (item as any).image_url?.url as FIXME<
|
|
103
|
+
string,
|
|
104
|
+
'Fixed by switching to Gram TS SDK.'
|
|
105
|
+
>,
|
|
106
|
+
})
|
|
107
|
+
break
|
|
108
|
+
case 'input_audio': {
|
|
109
|
+
const format = (item as any).input_audio?.format as FIXME<
|
|
110
|
+
string,
|
|
111
|
+
'Fixed by switching to Gram TS SDK.'
|
|
112
|
+
>
|
|
113
|
+
if (format === 'mp3' || format === 'wav') {
|
|
114
|
+
parts.push({
|
|
115
|
+
type: 'audio',
|
|
116
|
+
audio: {
|
|
117
|
+
data: (item as any).input_audio.data as FIXME<
|
|
118
|
+
string,
|
|
119
|
+
'Fixed by switching to Gram TS SDK.'
|
|
120
|
+
>,
|
|
121
|
+
format: format,
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
parts.push({
|
|
129
|
+
type: 'text',
|
|
130
|
+
text: '',
|
|
131
|
+
})
|
|
132
|
+
break
|
|
133
|
+
}
|
|
97
134
|
}
|
|
98
135
|
|
|
99
136
|
return parts
|
|
@@ -105,33 +142,43 @@ function buildUserContentParts(msg: GramChatMessage): ThreadUserMessagePart[] {
|
|
|
105
142
|
function buildAssistantContentParts(
|
|
106
143
|
msg: GramChatMessage
|
|
107
144
|
): ThreadAssistantMessagePart[] {
|
|
145
|
+
if (msg.role !== 'assistant') {
|
|
146
|
+
return []
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof msg.content === 'string' || !msg.content) {
|
|
150
|
+
return [
|
|
151
|
+
{
|
|
152
|
+
type: 'text',
|
|
153
|
+
text: msg.content ?? '',
|
|
154
|
+
},
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
|
|
108
158
|
const parts: ThreadAssistantMessagePart[] = []
|
|
109
159
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
160
|
+
const toolCallsJSON = (msg as any).tool_calls as FIXME<
|
|
161
|
+
string | undefined,
|
|
162
|
+
'Fixed by switching to Gram TS SDK.'
|
|
163
|
+
>
|
|
164
|
+
|
|
165
|
+
let toolCalls = tryParseJSON(toolCallsJSON || '[]')
|
|
166
|
+
if (!Array.isArray(toolCalls)) {
|
|
167
|
+
console.warn('Invalid tool_calls format, expected an array.')
|
|
168
|
+
toolCalls = []
|
|
115
169
|
}
|
|
116
170
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
argsText,
|
|
129
|
-
result: undefined,
|
|
130
|
-
} as ThreadAssistantMessagePart)
|
|
131
|
-
}
|
|
132
|
-
} catch {
|
|
133
|
-
// Ignore JSON parse errors for tool calls
|
|
134
|
-
}
|
|
171
|
+
for (const tc of toolCalls) {
|
|
172
|
+
const args = tc.function?.arguments ?? tc.args ?? {}
|
|
173
|
+
const argsText = typeof args === 'string' ? args : JSON.stringify(args)
|
|
174
|
+
parts.push({
|
|
175
|
+
type: 'tool-call',
|
|
176
|
+
toolCallId: tc.id ?? tc.toolCallId ?? '',
|
|
177
|
+
toolName: tc.function?.name ?? tc.toolName ?? '',
|
|
178
|
+
args: typeof args === 'string' ? JSON.parse(args) : args,
|
|
179
|
+
argsText,
|
|
180
|
+
result: undefined,
|
|
181
|
+
} as ThreadAssistantMessagePart)
|
|
135
182
|
}
|
|
136
183
|
|
|
137
184
|
// Return at least an empty text part if no content
|
|
@@ -145,14 +192,34 @@ function buildAssistantContentParts(
|
|
|
145
192
|
return parts
|
|
146
193
|
}
|
|
147
194
|
|
|
195
|
+
function buildSystemContentParts(msg: GramChatMessage): [TextMessagePart] {
|
|
196
|
+
if (msg.role !== 'system') {
|
|
197
|
+
return [{ type: 'text', text: '' }]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (typeof msg.content === 'string' || !msg.content) {
|
|
201
|
+
return [{ type: 'text', text: msg.content ?? '' }]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const text: string[] = []
|
|
205
|
+
|
|
206
|
+
for (const item of msg.content) {
|
|
207
|
+
if (item.type !== 'text') {
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
text.push(item.text)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return [{ type: 'text', text: text.join('\n') }]
|
|
214
|
+
}
|
|
215
|
+
|
|
148
216
|
/**
|
|
149
217
|
* Converts a single Gram ChatMessage to a ThreadMessage.
|
|
150
218
|
*/
|
|
151
219
|
function convertGramMessageToThreadMessage(
|
|
152
220
|
msg: GramChatMessage
|
|
153
221
|
): ThreadMessage {
|
|
154
|
-
const
|
|
155
|
-
const createdAt = parseDate(msg.createdAt)
|
|
222
|
+
const createdAt = parseDate(msg.created_at)
|
|
156
223
|
|
|
157
224
|
const baseMetadata = {
|
|
158
225
|
unstable_state: undefined,
|
|
@@ -163,7 +230,7 @@ function convertGramMessageToThreadMessage(
|
|
|
163
230
|
custom: {},
|
|
164
231
|
}
|
|
165
232
|
|
|
166
|
-
if (role === 'user') {
|
|
233
|
+
if (msg.role === 'user') {
|
|
167
234
|
return {
|
|
168
235
|
id: msg.id,
|
|
169
236
|
role: 'user',
|
|
@@ -174,12 +241,12 @@ function convertGramMessageToThreadMessage(
|
|
|
174
241
|
}
|
|
175
242
|
}
|
|
176
243
|
|
|
177
|
-
if (role === 'system') {
|
|
244
|
+
if (msg.role === 'system') {
|
|
178
245
|
return {
|
|
179
246
|
id: msg.id,
|
|
180
247
|
role: 'system',
|
|
181
248
|
createdAt,
|
|
182
|
-
content:
|
|
249
|
+
content: buildSystemContentParts(msg),
|
|
183
250
|
metadata: baseMetadata,
|
|
184
251
|
}
|
|
185
252
|
}
|
|
@@ -239,3 +306,209 @@ export function convertGramMessagesToExported(
|
|
|
239
306
|
headId: prevId,
|
|
240
307
|
}
|
|
241
308
|
}
|
|
309
|
+
|
|
310
|
+
export function convertGramMessagesToUIMessages(messages: GramChatMessage[]): {
|
|
311
|
+
headId: string | null
|
|
312
|
+
messages: { parentId: string | null; message: UIMessage }[]
|
|
313
|
+
} {
|
|
314
|
+
if (messages.length === 0) {
|
|
315
|
+
return { messages: [], headId: null }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const toolCallResults = new Map<string, ToolResponseMessage>()
|
|
319
|
+
for (const msg of messages) {
|
|
320
|
+
if (msg.role !== 'tool') {
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
const id = (msg as any).tool_call_id
|
|
324
|
+
if (typeof id !== 'string') {
|
|
325
|
+
continue
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
toolCallResults.set(id, msg as ToolResponseMessage)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const uiMessages: { parentId: string | null; message: UIMessage }[] = []
|
|
332
|
+
let prevId: string | null = null
|
|
333
|
+
|
|
334
|
+
for (const msg of messages) {
|
|
335
|
+
switch (msg.role) {
|
|
336
|
+
case 'developer':
|
|
337
|
+
case 'tool':
|
|
338
|
+
continue
|
|
339
|
+
case 'system': {
|
|
340
|
+
uiMessages.push({
|
|
341
|
+
parentId: prevId,
|
|
342
|
+
message: {
|
|
343
|
+
id: msg.id,
|
|
344
|
+
role: 'system',
|
|
345
|
+
parts: [
|
|
346
|
+
{
|
|
347
|
+
type: 'text',
|
|
348
|
+
text:
|
|
349
|
+
typeof msg.content === 'string'
|
|
350
|
+
? msg.content
|
|
351
|
+
: Array.isArray(msg.content)
|
|
352
|
+
? msg.content
|
|
353
|
+
.filter((item) => item.type === 'text')
|
|
354
|
+
.map((item) => item.text)
|
|
355
|
+
.join('\n')
|
|
356
|
+
: '',
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
},
|
|
360
|
+
})
|
|
361
|
+
break
|
|
362
|
+
}
|
|
363
|
+
case 'user': {
|
|
364
|
+
uiMessages.push({
|
|
365
|
+
parentId: prevId,
|
|
366
|
+
message: {
|
|
367
|
+
id: msg.id,
|
|
368
|
+
role: 'user',
|
|
369
|
+
parts: convertGramMessagePartsToUIMessageParts(
|
|
370
|
+
msg,
|
|
371
|
+
toolCallResults
|
|
372
|
+
),
|
|
373
|
+
},
|
|
374
|
+
})
|
|
375
|
+
break
|
|
376
|
+
}
|
|
377
|
+
case 'assistant': {
|
|
378
|
+
const uiMessage = {
|
|
379
|
+
parentId: prevId,
|
|
380
|
+
message: {
|
|
381
|
+
id: msg.id,
|
|
382
|
+
role: 'assistant',
|
|
383
|
+
parts: convertGramMessagePartsToUIMessageParts(
|
|
384
|
+
msg,
|
|
385
|
+
toolCallResults
|
|
386
|
+
),
|
|
387
|
+
} satisfies UIMessage,
|
|
388
|
+
}
|
|
389
|
+
uiMessages.push(uiMessage)
|
|
390
|
+
|
|
391
|
+
break
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
prevId = msg.id
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
messages: uiMessages,
|
|
400
|
+
headId: prevId,
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function convertGramMessagePartsToUIMessageParts(
|
|
405
|
+
msg: UserMessage | AssistantMessage,
|
|
406
|
+
toolResults: Map<string, ToolResponseMessage>
|
|
407
|
+
): UIMessage['parts'] {
|
|
408
|
+
const uiparts: UIMessage['parts'] = []
|
|
409
|
+
|
|
410
|
+
if (typeof msg.content === 'string' && msg.content) {
|
|
411
|
+
uiparts.push({
|
|
412
|
+
type: 'text',
|
|
413
|
+
text: msg.content,
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const content = Array.isArray(msg.content) ? msg.content : []
|
|
418
|
+
for (const p of content) {
|
|
419
|
+
switch (p.type) {
|
|
420
|
+
case 'text': {
|
|
421
|
+
uiparts.push({
|
|
422
|
+
type: 'text',
|
|
423
|
+
text: p.text,
|
|
424
|
+
})
|
|
425
|
+
break
|
|
426
|
+
}
|
|
427
|
+
case 'image_url': {
|
|
428
|
+
const url = (p as any).image_url?.url as FIXME<
|
|
429
|
+
string | undefined,
|
|
430
|
+
'Fixed by switching to Gram TS SDK.'
|
|
431
|
+
>
|
|
432
|
+
if (!url) {
|
|
433
|
+
break
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
uiparts.push({
|
|
437
|
+
type: 'file',
|
|
438
|
+
url,
|
|
439
|
+
mediaType: mediaTypeFromURL(url),
|
|
440
|
+
})
|
|
441
|
+
break
|
|
442
|
+
}
|
|
443
|
+
case 'input_audio': {
|
|
444
|
+
const url = (p as any).input_audio?.data as FIXME<
|
|
445
|
+
string | undefined,
|
|
446
|
+
'Fixed by switching to Gram TS SDK.'
|
|
447
|
+
>
|
|
448
|
+
if (!url) {
|
|
449
|
+
break
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
uiparts.push({
|
|
453
|
+
type: 'file',
|
|
454
|
+
url,
|
|
455
|
+
mediaType: mediaTypeFromURL(url),
|
|
456
|
+
})
|
|
457
|
+
break
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (msg.role === 'assistant' && msg.reasoning) {
|
|
463
|
+
uiparts.push({
|
|
464
|
+
type: 'reasoning',
|
|
465
|
+
text: msg.reasoning,
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (msg.role === 'assistant' && (msg as any).tool_calls) {
|
|
470
|
+
const toolCallsJSON = (msg as any).tool_calls as FIXME<
|
|
471
|
+
string,
|
|
472
|
+
'Fixed by switching to Gram TS SDK.'
|
|
473
|
+
>
|
|
474
|
+
let toolCalls = tryParseJSON<AssistantMessage['toolCalls']>(
|
|
475
|
+
toolCallsJSON || '[]'
|
|
476
|
+
)
|
|
477
|
+
if (!Array.isArray(toolCalls)) {
|
|
478
|
+
console.warn('Invalid tool_calls format, expected an array.')
|
|
479
|
+
toolCalls = []
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (const tc of toolCalls) {
|
|
483
|
+
const content = toolResults.get(tc.id)?.content
|
|
484
|
+
uiparts.push({
|
|
485
|
+
type: 'dynamic-tool',
|
|
486
|
+
toolCallId: tc.id,
|
|
487
|
+
toolName: tc.function?.name ?? '',
|
|
488
|
+
state: 'output-available',
|
|
489
|
+
input: tc.function?.arguments ?? {},
|
|
490
|
+
output: typeof content === 'string' ? tryParseJSON(content) : '',
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return uiparts
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function mediaTypeFromURL(url: string): string {
|
|
499
|
+
const unspecified = 'unknown/unknown'
|
|
500
|
+
if (!url.startsWith('data:')) {
|
|
501
|
+
return unspecified
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const match = url.match(/^data:([^;]+);/)
|
|
505
|
+
return match?.[1] || unspecified
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function tryParseJSON<T = any>(str: string): T | null {
|
|
509
|
+
try {
|
|
510
|
+
return JSON.parse(str) as T
|
|
511
|
+
} catch {
|
|
512
|
+
return null
|
|
513
|
+
}
|
|
514
|
+
}
|
package/src/lib.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
type FIXME<T, S extends string> = (T & S) | T
|
|
@@ -8,6 +8,7 @@ import { AlertCircleIcon } from 'lucide-react'
|
|
|
8
8
|
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
|
9
9
|
import { parse, View, Warn } from 'vega'
|
|
10
10
|
import { expressionInterpreter } from 'vega-interpreter'
|
|
11
|
+
import { PluginLoadingState } from '../components/PluginLoadingState'
|
|
11
12
|
|
|
12
13
|
export const ChartRenderer: FC<SyntaxHighlighterProps> = ({ code }) => {
|
|
13
14
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
@@ -84,21 +85,20 @@ export const ChartRenderer: FC<SyntaxHighlighterProps> = ({ code }) => {
|
|
|
84
85
|
}
|
|
85
86
|
}, [shouldRender, parsedSpec])
|
|
86
87
|
|
|
88
|
+
// Show loading state while JSON is incomplete/streaming
|
|
89
|
+
if (!shouldRender && !error) {
|
|
90
|
+
return <PluginLoadingState text="Rendering chart..." />
|
|
91
|
+
}
|
|
92
|
+
|
|
87
93
|
return (
|
|
88
94
|
<div
|
|
89
95
|
className={cn(
|
|
90
96
|
// the after:hidden is to prevent assistant-ui from showing its default code block loading indicator
|
|
91
|
-
'relative min-h-[400px] w-fit max-w-full min-w-[400px] overflow-auto border
|
|
97
|
+
'border-border relative min-h-[400px] w-fit max-w-full min-w-[400px] overflow-auto border after:hidden',
|
|
92
98
|
r('lg'),
|
|
93
99
|
d('p-lg')
|
|
94
100
|
)}
|
|
95
101
|
>
|
|
96
|
-
{!shouldRender && !error && (
|
|
97
|
-
<div className="shimmer text-muted-foreground bg-background/80 absolute inset-0 z-10 flex items-center justify-center">
|
|
98
|
-
Rendering chart...
|
|
99
|
-
</div>
|
|
100
|
-
)}
|
|
101
|
-
|
|
102
102
|
{error && (
|
|
103
103
|
<div className="bg-background absolute inset-0 z-10 flex items-center justify-center gap-2 text-rose-500">
|
|
104
104
|
<AlertCircleIcon name="alert-circle" className="h-4 w-4" />
|
|
@@ -106,7 +106,7 @@ export const ChartRenderer: FC<SyntaxHighlighterProps> = ({ code }) => {
|
|
|
106
106
|
</div>
|
|
107
107
|
)}
|
|
108
108
|
|
|
109
|
-
<div ref={containerRef} className={
|
|
109
|
+
<div ref={containerRef} className={error ? 'hidden' : 'block'} />
|
|
110
110
|
</div>
|
|
111
111
|
)
|
|
112
112
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useRadius } from '@/hooks/useRadius'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import { FC } from 'react'
|
|
6
|
+
|
|
7
|
+
interface PluginLoadingStateProps {
|
|
8
|
+
text: string
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Shared loading state component for plugins.
|
|
14
|
+
* Displays a shimmer effect with loading text.
|
|
15
|
+
*/
|
|
16
|
+
export const PluginLoadingState: FC<PluginLoadingStateProps> = ({
|
|
17
|
+
text,
|
|
18
|
+
className,
|
|
19
|
+
}) => {
|
|
20
|
+
const r = useRadius()
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={cn(
|
|
25
|
+
'border-border bg-card relative min-h-[400px] w-fit max-w-full min-w-[400px] overflow-hidden border after:hidden',
|
|
26
|
+
r('lg'),
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
<div className="shimmer text-muted-foreground absolute inset-0 flex items-center justify-center">
|
|
31
|
+
{text}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PluginLoadingState } from './PluginLoadingState'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { GenerativeUI } from '@/components/ui/generative-ui'
|
|
4
|
+
import { SyntaxHighlighterProps } from '@assistant-ui/react-markdown'
|
|
5
|
+
import { FC, useMemo } from 'react'
|
|
6
|
+
import { PluginLoadingState } from '../components/PluginLoadingState'
|
|
7
|
+
|
|
8
|
+
const loadingMessages = [
|
|
9
|
+
'Preparing your data...',
|
|
10
|
+
'Building your view...',
|
|
11
|
+
'Generating results...',
|
|
12
|
+
'Loading content...',
|
|
13
|
+
'Fetching information...',
|
|
14
|
+
'Processing your request...',
|
|
15
|
+
'Almost ready...',
|
|
16
|
+
'Setting things up...',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
function getRandomLoadingMessage() {
|
|
20
|
+
return loadingMessages[Math.floor(Math.random() * loadingMessages.length)]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const GenerativeUIRenderer: FC<SyntaxHighlighterProps> = ({ code }) => {
|
|
24
|
+
// Parse JSON - returns null if invalid (still streaming)
|
|
25
|
+
const content = useMemo(() => {
|
|
26
|
+
const trimmedCode = code.trim()
|
|
27
|
+
if (!trimmedCode) return null
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(trimmedCode)
|
|
31
|
+
// Validate it has a type field (basic json-render structure)
|
|
32
|
+
if (!parsed || typeof parsed !== 'object' || !('type' in parsed)) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
return parsed
|
|
36
|
+
} catch {
|
|
37
|
+
// JSON is incomplete (still streaming) - return null to show loading state
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}, [code])
|
|
41
|
+
|
|
42
|
+
// Memoize the loading message so it doesn't change on every render
|
|
43
|
+
const loadingMessage = useMemo(() => getRandomLoadingMessage(), [])
|
|
44
|
+
|
|
45
|
+
// Show loading shimmer while JSON is incomplete/streaming
|
|
46
|
+
if (!content) {
|
|
47
|
+
return <PluginLoadingState text={loadingMessage} />
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Render without outer border - the Card component inside provides the border
|
|
51
|
+
return (
|
|
52
|
+
<div className="overflow-hidden after:hidden">
|
|
53
|
+
<GenerativeUI content={content} />
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|