@gram-ai/elements 1.22.5 → 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-Dip7A_UI.cjs → index-CVYyyxfm.cjs} +43 -43
- 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-D_ZJq5T1.js → index-D-QXb5EF.js} +10930 -10799
- 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/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-CTZ-4zgJ.js → profiler-D8-vgPGn.js} +2 -2
- package/dist/{profiler-CTZ-4zgJ.js.map → profiler-D8-vgPGn.js.map} +1 -1
- package/dist/{profiler-Cucmmy3i.cjs → profiler-Dshm-O8k.cjs} +2 -2
- package/dist/{profiler-Cucmmy3i.cjs.map → profiler-Dshm-O8k.cjs.map} +1 -1
- package/dist/{startRecording-4CwQzWU_.cjs → startRecording-2p7-xVUh.cjs} +2 -2
- package/dist/{startRecording-4CwQzWU_.cjs.map → startRecording-2p7-xVUh.cjs.map} +1 -1
- package/dist/{startRecording-DnSD-PJG.js → startRecording-DnWeZRhl.js} +2 -2
- package/dist/{startRecording-DnSD-PJG.js.map → startRecording-DnWeZRhl.js.map} +1 -1
- package/package.json +7 -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/lib/generative-ui.ts +18 -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-CNVoovK7.cjs +0 -111
- package/dist/index-CNVoovK7.cjs.map +0 -1
- package/dist/index-D_ZJq5T1.js.map +0 -1
- package/dist/index-Dip7A_UI.cjs.map +0 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useState,
|
|
7
|
+
useCallback,
|
|
8
|
+
useRef,
|
|
9
|
+
useEffect,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from 'react'
|
|
12
|
+
|
|
13
|
+
export type ConnectionState = 'connected' | 'reconnecting' | 'disconnected'
|
|
14
|
+
|
|
15
|
+
interface ConnectionStatusContextValue {
|
|
16
|
+
/** Current connection state */
|
|
17
|
+
state: ConnectionState
|
|
18
|
+
/** Number of reconnection attempts */
|
|
19
|
+
retryCount: number
|
|
20
|
+
/** Whether the browser reports being online */
|
|
21
|
+
isOnline: boolean
|
|
22
|
+
/** Mark connection as failed - will trigger reconnecting state */
|
|
23
|
+
markDisconnected: () => void
|
|
24
|
+
/** Mark connection as restored */
|
|
25
|
+
markConnected: () => void
|
|
26
|
+
/** Reset the connection state */
|
|
27
|
+
reset: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ConnectionStatusContext =
|
|
31
|
+
createContext<ConnectionStatusContextValue | null>(null)
|
|
32
|
+
|
|
33
|
+
interface ConnectionStatusProviderProps {
|
|
34
|
+
children: ReactNode
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const ConnectionStatusProvider = ({
|
|
38
|
+
children,
|
|
39
|
+
}: ConnectionStatusProviderProps) => {
|
|
40
|
+
const [state, setState] = useState<ConnectionState>('connected')
|
|
41
|
+
const [retryCount, setRetryCount] = useState(0)
|
|
42
|
+
const [isOnline, setIsOnline] = useState(
|
|
43
|
+
typeof navigator !== 'undefined' ? navigator.onLine : true
|
|
44
|
+
)
|
|
45
|
+
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
46
|
+
|
|
47
|
+
// Monitor browser online/offline status
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (typeof window === 'undefined') return
|
|
50
|
+
|
|
51
|
+
const handleOnline = () => {
|
|
52
|
+
setIsOnline(true)
|
|
53
|
+
|
|
54
|
+
// Clear any existing timeout
|
|
55
|
+
if (reconnectTimeoutRef.current) {
|
|
56
|
+
clearTimeout(reconnectTimeoutRef.current)
|
|
57
|
+
reconnectTimeoutRef.current = null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Show "Reconnecting..." briefly, then mark as connected
|
|
61
|
+
setState('reconnecting')
|
|
62
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
63
|
+
setState('connected')
|
|
64
|
+
setRetryCount(0)
|
|
65
|
+
}, 1500) // Show reconnecting for 1.5s before clearing
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const handleOffline = () => {
|
|
69
|
+
setIsOnline(false)
|
|
70
|
+
// Immediately mark as disconnected when browser goes offline
|
|
71
|
+
setState('disconnected')
|
|
72
|
+
if (reconnectTimeoutRef.current) {
|
|
73
|
+
clearTimeout(reconnectTimeoutRef.current)
|
|
74
|
+
reconnectTimeoutRef.current = null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
window.addEventListener('online', handleOnline)
|
|
79
|
+
window.addEventListener('offline', handleOffline)
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
window.removeEventListener('online', handleOnline)
|
|
83
|
+
window.removeEventListener('offline', handleOffline)
|
|
84
|
+
// Clear any pending timeout to prevent memory leak
|
|
85
|
+
if (reconnectTimeoutRef.current) {
|
|
86
|
+
clearTimeout(reconnectTimeoutRef.current)
|
|
87
|
+
reconnectTimeoutRef.current = null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, [])
|
|
91
|
+
|
|
92
|
+
const markDisconnected = useCallback(() => {
|
|
93
|
+
setState('reconnecting')
|
|
94
|
+
setRetryCount((prev) => prev + 1)
|
|
95
|
+
|
|
96
|
+
// After 10 seconds of reconnecting, mark as fully disconnected
|
|
97
|
+
if (reconnectTimeoutRef.current) {
|
|
98
|
+
clearTimeout(reconnectTimeoutRef.current)
|
|
99
|
+
}
|
|
100
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
101
|
+
setState((current) =>
|
|
102
|
+
current === 'reconnecting' ? 'disconnected' : current
|
|
103
|
+
)
|
|
104
|
+
}, 10000)
|
|
105
|
+
}, [])
|
|
106
|
+
|
|
107
|
+
const markConnected = useCallback(() => {
|
|
108
|
+
if (reconnectTimeoutRef.current) {
|
|
109
|
+
clearTimeout(reconnectTimeoutRef.current)
|
|
110
|
+
reconnectTimeoutRef.current = null
|
|
111
|
+
}
|
|
112
|
+
setState('connected')
|
|
113
|
+
setRetryCount(0)
|
|
114
|
+
}, [])
|
|
115
|
+
|
|
116
|
+
const reset = useCallback(() => {
|
|
117
|
+
if (reconnectTimeoutRef.current) {
|
|
118
|
+
clearTimeout(reconnectTimeoutRef.current)
|
|
119
|
+
reconnectTimeoutRef.current = null
|
|
120
|
+
}
|
|
121
|
+
setState('connected')
|
|
122
|
+
setRetryCount(0)
|
|
123
|
+
}, [])
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<ConnectionStatusContext.Provider
|
|
127
|
+
value={{
|
|
128
|
+
state,
|
|
129
|
+
retryCount,
|
|
130
|
+
isOnline,
|
|
131
|
+
markDisconnected,
|
|
132
|
+
markConnected,
|
|
133
|
+
reset,
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{children}
|
|
137
|
+
</ConnectionStatusContext.Provider>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const useConnectionStatus = () => {
|
|
142
|
+
const context = useContext(ConnectionStatusContext)
|
|
143
|
+
if (!context) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
'useConnectionStatus must be used within a ConnectionStatusProvider'
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
return context
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Hook that returns connection status helpers for use in sendMessages.
|
|
153
|
+
* Returns null if not within a ConnectionStatusProvider (for backwards compatibility).
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
export const useConnectionStatusOptional = () => {
|
|
157
|
+
return useContext(ConnectionStatusContext)
|
|
158
|
+
}
|
|
@@ -54,6 +54,39 @@ import {
|
|
|
54
54
|
import { useAuth } from '../hooks/useAuth'
|
|
55
55
|
import { ElementsContext } from './contexts'
|
|
56
56
|
import { ToolApprovalProvider } from './ToolApprovalContext'
|
|
57
|
+
import {
|
|
58
|
+
ConnectionStatusProvider,
|
|
59
|
+
useConnectionStatusOptional,
|
|
60
|
+
} from './ConnectionStatusContext'
|
|
61
|
+
import { ToolExecutionProvider } from './ToolExecutionContext'
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extracts executable tools from frontend tool definitions.
|
|
65
|
+
* Frontend tools created via defineFrontendTool have an unstable_tool property
|
|
66
|
+
* that contains the tool definition with execute function.
|
|
67
|
+
*/
|
|
68
|
+
function extractExecutableTools(
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
frontendTools: Record<string, FrontendTool<any, any>> | undefined
|
|
71
|
+
): Record<
|
|
72
|
+
string,
|
|
73
|
+
{ execute?: (args: unknown, options?: unknown) => Promise<unknown> }
|
|
74
|
+
> {
|
|
75
|
+
if (!frontendTools) return {}
|
|
76
|
+
|
|
77
|
+
return Object.fromEntries(
|
|
78
|
+
Object.entries(frontendTools).map(([name, tool]) => {
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
|
+
const toolDef = (tool as any).unstable_tool
|
|
81
|
+
return [
|
|
82
|
+
name,
|
|
83
|
+
{
|
|
84
|
+
execute: toolDef?.execute,
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
}
|
|
57
90
|
|
|
58
91
|
export interface ElementsProviderProps {
|
|
59
92
|
children: ReactNode
|
|
@@ -159,6 +192,9 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
159
192
|
whitelistTool: toolApproval.whitelistTool,
|
|
160
193
|
})
|
|
161
194
|
|
|
195
|
+
// Connection status for tracking network failures
|
|
196
|
+
const connectionStatus = useConnectionStatusOptional()
|
|
197
|
+
|
|
162
198
|
approvalHelpersRef.current = {
|
|
163
199
|
requestApproval: toolApproval.requestApproval,
|
|
164
200
|
isToolApproved: toolApproval.isToolApproved,
|
|
@@ -312,13 +348,47 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
312
348
|
onError: ({ error }) => {
|
|
313
349
|
console.error('Stream error in onError callback:', error)
|
|
314
350
|
trackError(error, { source: 'streaming' })
|
|
351
|
+
|
|
352
|
+
// Check if this is a network/connection error
|
|
353
|
+
const isNetworkError =
|
|
354
|
+
error instanceof TypeError ||
|
|
355
|
+
(error instanceof Error &&
|
|
356
|
+
(error.message.includes('fetch') ||
|
|
357
|
+
error.message.includes('network') ||
|
|
358
|
+
error.message.includes('Failed to fetch') ||
|
|
359
|
+
error.message.includes('NetworkError') ||
|
|
360
|
+
error.message.includes('ECONNREFUSED') ||
|
|
361
|
+
error.message.includes('ETIMEDOUT')))
|
|
362
|
+
|
|
363
|
+
if (isNetworkError) {
|
|
364
|
+
connectionStatus?.markDisconnected()
|
|
365
|
+
}
|
|
315
366
|
},
|
|
316
367
|
})
|
|
317
368
|
|
|
369
|
+
// Mark as connected when stream starts successfully
|
|
370
|
+
connectionStatus?.markConnected()
|
|
371
|
+
|
|
318
372
|
return result.toUIMessageStream()
|
|
319
373
|
} catch (error) {
|
|
320
374
|
console.error('Error creating stream:', error)
|
|
321
375
|
trackError(error, { source: 'stream-creation' })
|
|
376
|
+
|
|
377
|
+
// Check if this is a network/connection error
|
|
378
|
+
const isNetworkError =
|
|
379
|
+
error instanceof TypeError ||
|
|
380
|
+
(error instanceof Error &&
|
|
381
|
+
(error.message.includes('fetch') ||
|
|
382
|
+
error.message.includes('network') ||
|
|
383
|
+
error.message.includes('Failed to fetch') ||
|
|
384
|
+
error.message.includes('NetworkError') ||
|
|
385
|
+
error.message.includes('ECONNREFUSED') ||
|
|
386
|
+
error.message.includes('ETIMEDOUT')))
|
|
387
|
+
|
|
388
|
+
if (isNetworkError) {
|
|
389
|
+
connectionStatus?.markDisconnected()
|
|
390
|
+
}
|
|
391
|
+
|
|
322
392
|
throw error
|
|
323
393
|
}
|
|
324
394
|
},
|
|
@@ -336,6 +406,7 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
336
406
|
apiUrl,
|
|
337
407
|
auth.headers,
|
|
338
408
|
auth.isLoading,
|
|
409
|
+
connectionStatus,
|
|
339
410
|
]
|
|
340
411
|
)
|
|
341
412
|
|
|
@@ -359,6 +430,24 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
359
430
|
|
|
360
431
|
const frontendTools = config.tools?.frontendTools ?? {}
|
|
361
432
|
|
|
433
|
+
// Create combined executable tools for direct tool execution (ActionButton)
|
|
434
|
+
// Uses a simplified type that focuses on the execute function
|
|
435
|
+
type ExecutableToolSet = Record<
|
|
436
|
+
string,
|
|
437
|
+
| { execute?: (args: unknown, options?: unknown) => Promise<unknown> }
|
|
438
|
+
| undefined
|
|
439
|
+
>
|
|
440
|
+
const executableTools = useMemo<ExecutableToolSet>(() => {
|
|
441
|
+
const extractedFrontendTools = extractExecutableTools(
|
|
442
|
+
config.tools?.frontendTools
|
|
443
|
+
)
|
|
444
|
+
// MCP tools and extracted frontend tools both have execute functions
|
|
445
|
+
return {
|
|
446
|
+
...mcpTools,
|
|
447
|
+
...extractedFrontendTools,
|
|
448
|
+
} as ExecutableToolSet
|
|
449
|
+
}, [mcpTools, config.tools?.frontendTools])
|
|
450
|
+
|
|
362
451
|
// Render the appropriate runtime provider based on history config.
|
|
363
452
|
// We use separate components to avoid conditional hook calls.
|
|
364
453
|
if (historyEnabled && !auth.isLoading) {
|
|
@@ -372,6 +461,7 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
372
461
|
frontendTools={frontendTools}
|
|
373
462
|
localIdToUuidMap={localIdToUuidMapRef.current}
|
|
374
463
|
currentRemoteIdRef={currentRemoteIdRef}
|
|
464
|
+
executableTools={executableTools}
|
|
375
465
|
>
|
|
376
466
|
{children}
|
|
377
467
|
</ElementsProviderWithHistory>
|
|
@@ -384,12 +474,20 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
384
474
|
contextValue={contextValue}
|
|
385
475
|
runtimeRef={runtimeRef}
|
|
386
476
|
frontendTools={frontendTools}
|
|
477
|
+
executableTools={executableTools}
|
|
387
478
|
>
|
|
388
479
|
{children}
|
|
389
480
|
</ElementsProviderWithoutHistory>
|
|
390
481
|
)
|
|
391
482
|
}
|
|
392
483
|
|
|
484
|
+
// Shared type for executable tools
|
|
485
|
+
type ExecutableToolSet = Record<
|
|
486
|
+
string,
|
|
487
|
+
| { execute?: (args: unknown, options?: unknown) => Promise<unknown> }
|
|
488
|
+
| undefined
|
|
489
|
+
>
|
|
490
|
+
|
|
393
491
|
// Separate component for history-enabled mode to avoid conditional hook calls
|
|
394
492
|
interface ElementsProviderWithHistoryProps {
|
|
395
493
|
children: ReactNode
|
|
@@ -402,6 +500,7 @@ interface ElementsProviderWithHistoryProps {
|
|
|
402
500
|
frontendTools: Record<string, AssistantTool | FrontendTool<any, any>>
|
|
403
501
|
localIdToUuidMap: Map<string, string>
|
|
404
502
|
currentRemoteIdRef: React.RefObject<string | null>
|
|
503
|
+
executableTools: ExecutableToolSet
|
|
405
504
|
}
|
|
406
505
|
|
|
407
506
|
/**
|
|
@@ -432,6 +531,7 @@ const ElementsProviderWithHistory = ({
|
|
|
432
531
|
frontendTools,
|
|
433
532
|
localIdToUuidMap,
|
|
434
533
|
currentRemoteIdRef,
|
|
534
|
+
executableTools,
|
|
435
535
|
}: ElementsProviderWithHistoryProps) => {
|
|
436
536
|
const threadListAdapter = useGramThreadListAdapter({
|
|
437
537
|
apiUrl,
|
|
@@ -480,17 +580,19 @@ const ElementsProviderWithHistory = ({
|
|
|
480
580
|
<ThreadIdSync remoteIdRef={currentRemoteIdRef} />
|
|
481
581
|
<HistoryProvider>
|
|
482
582
|
<ElementsContext.Provider value={contextValue}>
|
|
483
|
-
<
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
contextValue?.config.variant === '
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
583
|
+
<ToolExecutionProvider tools={executableTools}>
|
|
584
|
+
<div
|
|
585
|
+
className={cn(
|
|
586
|
+
ROOT_SELECTOR,
|
|
587
|
+
(contextValue?.config.variant === 'standalone' ||
|
|
588
|
+
contextValue?.config.variant === 'sidecar') &&
|
|
589
|
+
'h-full'
|
|
590
|
+
)}
|
|
591
|
+
>
|
|
592
|
+
{children}
|
|
593
|
+
</div>
|
|
594
|
+
<FrontendTools tools={frontendTools} />
|
|
595
|
+
</ToolExecutionProvider>
|
|
494
596
|
</ElementsContext.Provider>
|
|
495
597
|
</HistoryProvider>
|
|
496
598
|
</AssistantRuntimeProvider>
|
|
@@ -505,6 +607,7 @@ interface ElementsProviderWithoutHistoryProps {
|
|
|
505
607
|
runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>
|
|
506
608
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
507
609
|
frontendTools: Record<string, AssistantTool | FrontendTool<any, any>>
|
|
610
|
+
executableTools: ExecutableToolSet
|
|
508
611
|
}
|
|
509
612
|
|
|
510
613
|
const ElementsProviderWithoutHistory = ({
|
|
@@ -513,6 +616,7 @@ const ElementsProviderWithoutHistory = ({
|
|
|
513
616
|
contextValue,
|
|
514
617
|
runtimeRef,
|
|
515
618
|
frontendTools,
|
|
619
|
+
executableTools,
|
|
516
620
|
}: ElementsProviderWithoutHistoryProps) => {
|
|
517
621
|
const runtime = useChatRuntime({ transport })
|
|
518
622
|
|
|
@@ -524,17 +628,19 @@ const ElementsProviderWithoutHistory = ({
|
|
|
524
628
|
return (
|
|
525
629
|
<AssistantRuntimeProvider runtime={runtime}>
|
|
526
630
|
<ElementsContext.Provider value={contextValue}>
|
|
527
|
-
<
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
contextValue?.config.variant === '
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
631
|
+
<ToolExecutionProvider tools={executableTools}>
|
|
632
|
+
<div
|
|
633
|
+
className={cn(
|
|
634
|
+
ROOT_SELECTOR,
|
|
635
|
+
(contextValue?.config.variant === 'standalone' ||
|
|
636
|
+
contextValue?.config.variant === 'sidecar') &&
|
|
637
|
+
'h-full'
|
|
638
|
+
)}
|
|
639
|
+
>
|
|
640
|
+
{children}
|
|
641
|
+
</div>
|
|
642
|
+
<FrontendTools tools={frontendTools} />
|
|
643
|
+
</ToolExecutionProvider>
|
|
538
644
|
</ElementsContext.Provider>
|
|
539
645
|
</AssistantRuntimeProvider>
|
|
540
646
|
)
|
|
@@ -545,9 +651,11 @@ const queryClient = new QueryClient()
|
|
|
545
651
|
export const ElementsProvider = (props: ElementsProviderProps) => {
|
|
546
652
|
return (
|
|
547
653
|
<QueryClientProvider client={queryClient}>
|
|
548
|
-
<
|
|
549
|
-
<
|
|
550
|
-
|
|
654
|
+
<ConnectionStatusProvider>
|
|
655
|
+
<ToolApprovalProvider>
|
|
656
|
+
<ElementsProviderInner {...props} />
|
|
657
|
+
</ToolApprovalProvider>
|
|
658
|
+
</ConnectionStatusProvider>
|
|
551
659
|
</QueryClientProvider>
|
|
552
660
|
)
|
|
553
661
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useCallback, ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
// Define a minimal tool type for direct execution
|
|
6
|
+
// This avoids strict AI SDK type requirements while still being type-safe
|
|
7
|
+
interface ExecutableTool {
|
|
8
|
+
execute?: (args: unknown, options?: unknown) => Promise<unknown>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type ExecutableToolSet = Record<string, ExecutableTool | undefined>
|
|
12
|
+
|
|
13
|
+
export interface ToolExecutionResult {
|
|
14
|
+
success: boolean
|
|
15
|
+
result?: unknown
|
|
16
|
+
error?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ToolExecutionContextValue {
|
|
20
|
+
executeTool: (
|
|
21
|
+
toolName: string,
|
|
22
|
+
args: Record<string, unknown>
|
|
23
|
+
) => Promise<ToolExecutionResult>
|
|
24
|
+
isToolAvailable: (toolName: string) => boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ToolExecutionContext = createContext<ToolExecutionContextValue | null>(
|
|
28
|
+
null
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
interface ToolExecutionProviderProps {
|
|
32
|
+
children: ReactNode
|
|
33
|
+
tools: ExecutableToolSet | undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ToolExecutionProvider({
|
|
37
|
+
children,
|
|
38
|
+
tools,
|
|
39
|
+
}: ToolExecutionProviderProps) {
|
|
40
|
+
const executeTool = useCallback(
|
|
41
|
+
async (
|
|
42
|
+
toolName: string,
|
|
43
|
+
args: Record<string, unknown>
|
|
44
|
+
): Promise<ToolExecutionResult> => {
|
|
45
|
+
if (!tools) {
|
|
46
|
+
return { success: false, error: 'Tools not available' }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const tool = tools[toolName]
|
|
50
|
+
if (!tool) {
|
|
51
|
+
return { success: false, error: `Tool "${toolName}" not found` }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!tool.execute) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: `Tool "${toolName}" has no execute function`,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Generate a unique toolCallId for this execution
|
|
63
|
+
const toolCallId = `action-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
64
|
+
const result = await tool.execute(args, { toolCallId, messages: [] })
|
|
65
|
+
return { success: true, result }
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const errorMessage =
|
|
68
|
+
err instanceof Error ? err.message : 'Unknown error'
|
|
69
|
+
return { success: false, error: errorMessage }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[tools]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const isToolAvailable = useCallback(
|
|
76
|
+
(toolName: string): boolean => {
|
|
77
|
+
return !!tools?.[toolName]?.execute
|
|
78
|
+
},
|
|
79
|
+
[tools]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<ToolExecutionContext.Provider value={{ executeTool, isToolAvailable }}>
|
|
84
|
+
{children}
|
|
85
|
+
</ToolExecutionContext.Provider>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function useToolExecution(): ToolExecutionContextValue {
|
|
90
|
+
const context = useContext(ToolExecutionContext)
|
|
91
|
+
if (!context) {
|
|
92
|
+
return {
|
|
93
|
+
executeTool: async (): Promise<ToolExecutionResult> => ({
|
|
94
|
+
success: false,
|
|
95
|
+
error: 'ToolExecutionProvider not found',
|
|
96
|
+
}),
|
|
97
|
+
isToolAvailable: () => false,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return context
|
|
101
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Type for json-render tree structure
|
|
2
|
+
export interface JsonRenderNode {
|
|
3
|
+
type: string
|
|
4
|
+
props?: Record<string, unknown>
|
|
5
|
+
children?: JsonRenderNode[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if content is a json-render compatible tree structure
|
|
10
|
+
*/
|
|
11
|
+
export function isJsonRenderTree(content: unknown): content is JsonRenderNode {
|
|
12
|
+
return (
|
|
13
|
+
typeof content === 'object' &&
|
|
14
|
+
content !== null &&
|
|
15
|
+
'type' in content &&
|
|
16
|
+
typeof (content as JsonRenderNode).type === 'string'
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -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
|
+
}
|