@gram-ai/elements 1.25.1 → 1.26.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.
Files changed (177) hide show
  1. package/dist/components/Chat/stories/Charts.stories.d.ts +37 -0
  2. package/dist/components/Chat/stories/GenerativeUI.stories.d.ts +17 -0
  3. package/dist/components/Chat/stories/MessageFeedback.stories.d.ts +1 -1
  4. package/dist/components/ui/button.d.ts +1 -1
  5. package/dist/components/ui/buttonVariants.d.ts +1 -1
  6. package/dist/components/ui/charts.stories.d.ts +43 -0
  7. package/dist/components/ui/generative-ui.stories.d.ts +53 -0
  8. package/dist/contexts/ChatIdContext.d.ts +11 -0
  9. package/dist/contexts/contexts.d.ts +1 -0
  10. package/dist/elements.cjs +1 -1
  11. package/dist/elements.css +1 -1
  12. package/dist/elements.js +7 -6
  13. package/dist/index-BJnv49-A.js +37057 -0
  14. package/dist/index-BJnv49-A.js.map +1 -0
  15. package/dist/index-BpJstUh1.cjs +280 -0
  16. package/dist/index-BpJstUh1.cjs.map +1 -0
  17. package/dist/index-CUitXazZ.js +30426 -0
  18. package/dist/index-CUitXazZ.js.map +1 -0
  19. package/dist/index-ChW-CSuu.cjs +147 -0
  20. package/dist/index-ChW-CSuu.cjs.map +1 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/plugins/chart/catalog.d.ts +123 -0
  23. package/dist/plugins/chart/index.d.ts +1 -1
  24. package/dist/plugins/chart/ui/area-chart.d.ts +16 -0
  25. package/dist/plugins/chart/ui/bar-chart.d.ts +16 -0
  26. package/dist/plugins/chart/ui/donut-chart.d.ts +17 -0
  27. package/dist/plugins/chart/ui/index.d.ts +7 -0
  28. package/dist/plugins/chart/ui/line-chart.d.ts +17 -0
  29. package/dist/plugins/chart/ui/pie-chart.d.ts +15 -0
  30. package/dist/plugins/chart/ui/radar-chart.d.ts +14 -0
  31. package/dist/plugins/chart/ui/scatter-chart.d.ts +18 -0
  32. package/dist/plugins/components/MacOSWindowFrame.d.ts +13 -0
  33. package/dist/plugins/components/PluginLoadingState.d.ts +1 -1
  34. package/dist/plugins/generative-ui/catalog.d.ts +293 -0
  35. package/dist/plugins/generative-ui/ui/accordion-wrapper.d.ts +18 -0
  36. package/dist/plugins/generative-ui/ui/accordion.d.ts +7 -0
  37. package/dist/plugins/generative-ui/ui/action-button.d.ts +10 -0
  38. package/dist/plugins/generative-ui/ui/alert-wrapper.d.ts +9 -0
  39. package/dist/plugins/generative-ui/ui/alert.d.ts +9 -0
  40. package/dist/plugins/generative-ui/ui/avatar-wrapper.d.ts +9 -0
  41. package/dist/plugins/generative-ui/ui/avatar.d.ts +11 -0
  42. package/dist/plugins/generative-ui/ui/badge.d.ts +12 -0
  43. package/dist/plugins/generative-ui/ui/button-wrapper.d.ts +15 -0
  44. package/dist/plugins/generative-ui/ui/button.d.ts +10 -0
  45. package/dist/plugins/generative-ui/ui/card-wrapper.d.ts +10 -0
  46. package/dist/plugins/generative-ui/ui/card.d.ts +9 -0
  47. package/dist/plugins/generative-ui/ui/checkbox-wrapper.d.ts +10 -0
  48. package/dist/plugins/generative-ui/ui/checkbox.d.ts +4 -0
  49. package/dist/plugins/generative-ui/ui/data-table.d.ts +10 -0
  50. package/dist/plugins/generative-ui/ui/dialog.d.ts +17 -0
  51. package/dist/plugins/generative-ui/ui/dropdown-menu.d.ts +25 -0
  52. package/dist/plugins/generative-ui/ui/grid.d.ts +6 -0
  53. package/dist/plugins/generative-ui/ui/index.d.ts +40 -0
  54. package/dist/plugins/generative-ui/ui/input-wrapper.d.ts +11 -0
  55. package/dist/plugins/generative-ui/ui/input.d.ts +3 -0
  56. package/dist/plugins/generative-ui/ui/label.d.ts +4 -0
  57. package/dist/plugins/generative-ui/ui/list.d.ts +6 -0
  58. package/dist/plugins/generative-ui/ui/metric.d.ts +7 -0
  59. package/dist/plugins/generative-ui/ui/pagination.d.ts +13 -0
  60. package/dist/plugins/generative-ui/ui/popover.d.ts +10 -0
  61. package/dist/plugins/generative-ui/ui/progress.d.ts +10 -0
  62. package/dist/plugins/generative-ui/ui/radio-group.d.ts +5 -0
  63. package/dist/plugins/generative-ui/ui/select-wrapper.d.ts +13 -0
  64. package/dist/plugins/generative-ui/ui/select.d.ts +15 -0
  65. package/dist/plugins/generative-ui/ui/separator.d.ts +4 -0
  66. package/dist/plugins/generative-ui/ui/skeleton-wrapper.d.ts +9 -0
  67. package/dist/plugins/generative-ui/ui/skeleton.d.ts +2 -0
  68. package/dist/plugins/generative-ui/ui/stack.d.ts +8 -0
  69. package/dist/plugins/generative-ui/ui/switch.d.ts +6 -0
  70. package/dist/plugins/generative-ui/ui/table.d.ts +10 -0
  71. package/dist/plugins/generative-ui/ui/tabs-wrapper.d.ts +21 -0
  72. package/dist/plugins/generative-ui/ui/tabs.d.ts +11 -0
  73. package/dist/plugins/generative-ui/ui/text.d.ts +7 -0
  74. package/dist/plugins/generative-ui/ui/textarea.d.ts +3 -0
  75. package/dist/plugins/generative-ui/ui/tooltip.d.ts +7 -0
  76. package/dist/plugins.cjs +1 -1
  77. package/dist/plugins.js +1 -1
  78. package/dist/{profiler-CijCgLrw.js → profiler-D4Tw5ecI.js} +2 -2
  79. package/dist/{profiler-CijCgLrw.js.map → profiler-D4Tw5ecI.js.map} +1 -1
  80. package/dist/{profiler-DAT0DL1W.cjs → profiler-DCWYDZ1F.cjs} +2 -2
  81. package/dist/{profiler-DAT0DL1W.cjs.map → profiler-DCWYDZ1F.cjs.map} +1 -1
  82. package/dist/{startRecording-DotsE8QT.cjs → startRecording-3sTskM3H.cjs} +2 -2
  83. package/dist/{startRecording-DotsE8QT.cjs.map → startRecording-3sTskM3H.cjs.map} +1 -1
  84. package/dist/{startRecording-gmhENmf0.js → startRecording-BHhcCWQE.js} +2 -2
  85. package/dist/{startRecording-gmhENmf0.js.map → startRecording-BHhcCWQE.js.map} +1 -1
  86. package/dist/types/index.d.ts +4 -4
  87. package/package.json +4 -1
  88. package/src/components/Chat/stories/Charts.stories.tsx +260 -0
  89. package/src/components/Chat/stories/ConnectionConfiguration.stories.tsx +6 -6
  90. package/src/components/Chat/stories/GenerativeUI.stories.tsx +113 -0
  91. package/src/components/Chat/stories/MessageFeedback.stories.tsx +6 -6
  92. package/src/components/Chat/stories/ToolApproval.stories.tsx +10 -10
  93. package/src/components/Chat/stories/Tools.stories.tsx +122 -104
  94. package/src/components/Chat/stories/Variants.stories.tsx +1 -1
  95. package/src/components/Replay.stories.tsx +1 -1
  96. package/src/components/Replay.tsx +18 -13
  97. package/src/components/ShadowRoot.tsx +5 -1
  98. package/src/components/assistant-ui/message-feedback.tsx +6 -7
  99. package/src/components/assistant-ui/thread.tsx +76 -11
  100. package/src/components/ui/charts.stories.tsx +246 -0
  101. package/src/components/ui/generative-ui.stories.tsx +557 -0
  102. package/src/components/ui/generative-ui.tsx +60 -360
  103. package/src/components/ui/tool-ui.stories.tsx +6 -3
  104. package/src/contexts/ChatIdContext.tsx +21 -0
  105. package/src/contexts/ElementsProvider.tsx +77 -37
  106. package/src/contexts/contexts.ts +2 -0
  107. package/src/hooks/useAuth.ts +18 -3
  108. package/src/hooks/useFollowOnSuggestions.ts +6 -1
  109. package/src/index.ts +1 -0
  110. package/src/plugins/chart/catalog.ts +141 -0
  111. package/src/plugins/chart/component.tsx +79 -125
  112. package/src/plugins/chart/index.ts +141 -89
  113. package/src/plugins/chart/ui/area-chart.tsx +133 -0
  114. package/src/plugins/chart/ui/bar-chart.tsx +137 -0
  115. package/src/plugins/chart/ui/donut-chart.tsx +167 -0
  116. package/src/plugins/chart/ui/index.ts +7 -0
  117. package/src/plugins/chart/ui/line-chart.tsx +135 -0
  118. package/src/plugins/chart/ui/pie-chart.tsx +148 -0
  119. package/src/plugins/chart/ui/radar-chart.tsx +105 -0
  120. package/src/plugins/chart/ui/scatter-chart.tsx +132 -0
  121. package/src/plugins/components/MacOSWindowFrame.tsx +55 -0
  122. package/src/plugins/components/PluginLoadingState.tsx +9 -13
  123. package/src/plugins/generative-ui/catalog.ts +277 -0
  124. package/src/plugins/generative-ui/component.tsx +112 -21
  125. package/src/plugins/generative-ui/index.ts +20 -141
  126. package/src/plugins/generative-ui/ui/accordion-wrapper.tsx +57 -0
  127. package/src/plugins/generative-ui/ui/accordion.tsx +66 -0
  128. package/src/plugins/generative-ui/ui/action-button.tsx +68 -0
  129. package/src/plugins/generative-ui/ui/alert-wrapper.tsx +26 -0
  130. package/src/plugins/generative-ui/ui/alert.tsx +66 -0
  131. package/src/plugins/generative-ui/ui/avatar-wrapper.tsx +22 -0
  132. package/src/plugins/generative-ui/ui/avatar.tsx +109 -0
  133. package/src/plugins/generative-ui/ui/badge.tsx +65 -0
  134. package/src/plugins/generative-ui/ui/button-wrapper.tsx +32 -0
  135. package/src/plugins/generative-ui/ui/button.tsx +65 -0
  136. package/src/plugins/generative-ui/ui/card-wrapper.tsx +36 -0
  137. package/src/plugins/generative-ui/ui/card.tsx +92 -0
  138. package/src/plugins/generative-ui/ui/checkbox-wrapper.tsx +39 -0
  139. package/src/plugins/generative-ui/ui/checkbox.tsx +32 -0
  140. package/src/plugins/generative-ui/ui/data-table.tsx +53 -0
  141. package/src/plugins/generative-ui/ui/dialog.tsx +158 -0
  142. package/src/plugins/generative-ui/ui/dropdown-menu.tsx +257 -0
  143. package/src/plugins/generative-ui/ui/grid.tsx +29 -0
  144. package/src/plugins/generative-ui/ui/index.ts +43 -0
  145. package/src/plugins/generative-ui/ui/input-wrapper.tsx +38 -0
  146. package/src/plugins/generative-ui/ui/input.tsx +21 -0
  147. package/src/plugins/generative-ui/ui/label.tsx +24 -0
  148. package/src/plugins/generative-ui/ui/list.tsx +34 -0
  149. package/src/plugins/generative-ui/ui/metric.tsx +53 -0
  150. package/src/plugins/generative-ui/ui/pagination.tsx +127 -0
  151. package/src/plugins/generative-ui/ui/popover.tsx +89 -0
  152. package/src/plugins/generative-ui/ui/progress.tsx +57 -0
  153. package/src/plugins/generative-ui/ui/radio-group.tsx +45 -0
  154. package/src/plugins/generative-ui/ui/select-wrapper.tsx +41 -0
  155. package/src/plugins/generative-ui/ui/select.tsx +190 -0
  156. package/src/plugins/generative-ui/ui/separator.tsx +28 -0
  157. package/src/plugins/generative-ui/ui/skeleton-wrapper.tsx +30 -0
  158. package/src/plugins/generative-ui/ui/skeleton.tsx +13 -0
  159. package/src/plugins/generative-ui/ui/stack.tsx +54 -0
  160. package/src/plugins/generative-ui/ui/switch.tsx +35 -0
  161. package/src/plugins/generative-ui/ui/table.tsx +116 -0
  162. package/src/plugins/generative-ui/ui/tabs-wrapper.tsx +51 -0
  163. package/src/plugins/generative-ui/ui/tabs.tsx +92 -0
  164. package/src/plugins/generative-ui/ui/text.tsx +33 -0
  165. package/src/plugins/generative-ui/ui/textarea.tsx +18 -0
  166. package/src/plugins/generative-ui/ui/tooltip.tsx +57 -0
  167. package/src/types/index.ts +4 -4
  168. package/dist/components/Chat/stories/Plugins.stories.d.ts +0 -12
  169. package/dist/index-C3UbmFRR.cjs +0 -178
  170. package/dist/index-C3UbmFRR.cjs.map +0 -1
  171. package/dist/index-CtyV0c-T.js +0 -27225
  172. package/dist/index-CtyV0c-T.js.map +0 -1
  173. package/dist/index-DxJwZ5Kc.js +0 -39975
  174. package/dist/index-DxJwZ5Kc.js.map +0 -1
  175. package/dist/index-iUSSoKFz.cjs +0 -251
  176. package/dist/index-iUSSoKFz.cjs.map +0 -1
  177. package/src/components/Chat/stories/Plugins.stories.tsx +0 -158
@@ -59,6 +59,7 @@ import {
59
59
  useConnectionStatusOptional,
60
60
  } from './ConnectionStatusContext'
61
61
  import { ToolExecutionProvider } from './ToolExecutionContext'
62
+ import { ChatIdContext } from './ChatIdContext'
62
63
 
63
64
  /**
64
65
  * Extracts executable tools from frontend tool definitions.
@@ -93,14 +94,25 @@ export interface ElementsProviderProps {
93
94
  config: ElementsConfig
94
95
  }
95
96
 
96
- const BASE_SYSTEM_PROMPT = `You are a helpful assistant that can answer questions and help with tasks.`
97
+ const BASE_SYSTEM_PROMPT = `You are a helpful assistant that can answer questions and help with tasks.
98
+
99
+ Tool Result Display:
100
+ Some tools have custom visual components that automatically render their results (you'll see a rich card/widget appear). For these, do not repeat the data - just add brief context or a follow-up question if needed.
101
+
102
+ For tools WITHOUT custom components, you should present the data clearly - either as plain text for simple results, or using the UI code block format for structured data like lists of items, categories, or dashboards.`
97
103
 
98
104
  function mergeInternalSystemPromptWith(
99
105
  userSystemPrompt: string | undefined,
100
- plugins: Plugin[]
106
+ plugins: Plugin[],
107
+ toolsWithCustomComponents: string[]
101
108
  ) {
109
+ const customToolsSection =
110
+ toolsWithCustomComponents.length > 0
111
+ ? `\n\nTools with custom visual components (DO NOT render UI widgets for these - they already display rich visuals):\n${toolsWithCustomComponents.map((t) => `- ${t}`).join('\n')}`
112
+ : ''
113
+
102
114
  return `
103
- ${BASE_SYSTEM_PROMPT}
115
+ ${BASE_SYSTEM_PROMPT}${customToolsSection}
104
116
 
105
117
  User-provided System Prompt:
106
118
  ${userSystemPrompt ?? 'None provided'}
@@ -158,9 +170,13 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
158
170
 
159
171
  const plugins = config.plugins ?? recommended
160
172
 
173
+ // Get list of tools that have custom components registered
174
+ const toolsWithCustomComponents = Object.keys(config.tools?.components ?? {})
175
+
161
176
  const systemPrompt = mergeInternalSystemPromptWith(
162
177
  config.systemPrompt,
163
- plugins
178
+ plugins,
179
+ toolsWithCustomComponents
164
180
  )
165
181
 
166
182
  // Initialize error tracking on mount
@@ -176,6 +192,9 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
176
192
  // When history is enabled, the thread adapter manages chat IDs instead
177
193
  const chatIdRef = useRef<string | null>(null)
178
194
 
195
+ // State to expose the current chat ID via context
196
+ const [currentChatId, setCurrentChatId] = useState<string | null>(null)
197
+
179
198
  const { data: mcpTools, mcpHeaders } = useMCPTools({
180
199
  auth,
181
200
  mcp: config.mcp,
@@ -288,6 +307,8 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
288
307
  // chat ID on subsequent tool call requests.
289
308
  if (chatId) {
290
309
  mcpHeaders['Gram-Chat-ID'] = chatId
310
+ // Update the context state so consumers can access the current chat ID
311
+ setCurrentChatId(chatId)
291
312
  }
292
313
 
293
314
  const context = runtimeRef.current?.thread.getModelContext()
@@ -467,6 +488,8 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
467
488
  localIdToUuidMap={localIdToUuidMapRef.current}
468
489
  currentRemoteIdRef={currentRemoteIdRef}
469
490
  executableTools={executableTools}
491
+ currentChatId={currentChatId}
492
+ setCurrentChatId={setCurrentChatId}
470
493
  >
471
494
  {children}
472
495
  </ElementsProviderWithHistory>
@@ -480,6 +503,7 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
480
503
  runtimeRef={runtimeRef}
481
504
  frontendTools={frontendTools}
482
505
  executableTools={executableTools}
506
+ currentChatId={currentChatId}
483
507
  >
484
508
  {children}
485
509
  </ElementsProviderWithoutHistory>
@@ -506,23 +530,28 @@ interface ElementsProviderWithHistoryProps {
506
530
  localIdToUuidMap: Map<string, string>
507
531
  currentRemoteIdRef: React.RefObject<string | null>
508
532
  executableTools: ExecutableToolSet
533
+ currentChatId: string | null
534
+ setCurrentChatId: (chatId: string | null) => void
509
535
  }
510
536
 
511
537
  /**
512
- * Component that syncs the current thread's remoteId to a ref.
538
+ * Component that syncs the current thread's remoteId to a ref and updates the chat ID context.
513
539
  * Must be rendered inside AssistantRuntimeProvider to access the state.
514
540
  */
515
541
  const ThreadIdSync = ({
516
542
  remoteIdRef,
543
+ onChatIdChange,
517
544
  }: {
518
545
  remoteIdRef: React.RefObject<string | null>
546
+ onChatIdChange: (chatId: string | null) => void
519
547
  }) => {
520
548
  const remoteId = useAssistantState(
521
549
  ({ threadListItem }) => threadListItem.remoteId ?? null
522
550
  )
523
551
  useEffect(() => {
524
552
  remoteIdRef.current = remoteId
525
- }, [remoteId, remoteIdRef])
553
+ onChatIdChange(remoteId)
554
+ }, [remoteId, remoteIdRef, onChatIdChange])
526
555
  return null
527
556
  }
528
557
 
@@ -537,6 +566,8 @@ const ElementsProviderWithHistory = ({
537
566
  localIdToUuidMap,
538
567
  currentRemoteIdRef,
539
568
  executableTools,
569
+ currentChatId,
570
+ setCurrentChatId,
540
571
  }: ElementsProviderWithHistoryProps) => {
541
572
  const threadListAdapter = useGramThreadListAdapter({
542
573
  apiUrl,
@@ -582,23 +613,28 @@ const ElementsProviderWithHistory = ({
582
613
 
583
614
  return (
584
615
  <AssistantRuntimeProvider runtime={runtime}>
585
- <ThreadIdSync remoteIdRef={currentRemoteIdRef} />
616
+ <ThreadIdSync
617
+ remoteIdRef={currentRemoteIdRef}
618
+ onChatIdChange={setCurrentChatId}
619
+ />
586
620
  <HistoryProvider>
587
- <ElementsContext.Provider value={contextValue}>
588
- <ToolExecutionProvider tools={executableTools}>
589
- <div
590
- className={cn(
591
- ROOT_SELECTOR,
592
- (contextValue?.config.variant === 'standalone' ||
593
- contextValue?.config.variant === 'sidecar') &&
594
- 'h-full'
595
- )}
596
- >
597
- {children}
598
- </div>
599
- <FrontendTools tools={frontendTools} />
600
- </ToolExecutionProvider>
601
- </ElementsContext.Provider>
621
+ <ChatIdContext.Provider value={{ chatId: currentChatId }}>
622
+ <ElementsContext.Provider value={contextValue}>
623
+ <ToolExecutionProvider tools={executableTools}>
624
+ <div
625
+ className={cn(
626
+ ROOT_SELECTOR,
627
+ (contextValue?.config.variant === 'standalone' ||
628
+ contextValue?.config.variant === 'sidecar') &&
629
+ 'h-full'
630
+ )}
631
+ >
632
+ {children}
633
+ </div>
634
+ <FrontendTools tools={frontendTools} />
635
+ </ToolExecutionProvider>
636
+ </ElementsContext.Provider>
637
+ </ChatIdContext.Provider>
602
638
  </HistoryProvider>
603
639
  </AssistantRuntimeProvider>
604
640
  )
@@ -613,6 +649,7 @@ interface ElementsProviderWithoutHistoryProps {
613
649
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
614
650
  frontendTools: Record<string, AssistantTool | FrontendTool<any, any>>
615
651
  executableTools: ExecutableToolSet
652
+ currentChatId: string | null
616
653
  }
617
654
 
618
655
  const ElementsProviderWithoutHistory = ({
@@ -622,6 +659,7 @@ const ElementsProviderWithoutHistory = ({
622
659
  runtimeRef,
623
660
  frontendTools,
624
661
  executableTools,
662
+ currentChatId,
625
663
  }: ElementsProviderWithoutHistoryProps) => {
626
664
  const runtime = useChatRuntime({ transport })
627
665
 
@@ -632,21 +670,23 @@ const ElementsProviderWithoutHistory = ({
632
670
 
633
671
  return (
634
672
  <AssistantRuntimeProvider runtime={runtime}>
635
- <ElementsContext.Provider value={contextValue}>
636
- <ToolExecutionProvider tools={executableTools}>
637
- <div
638
- className={cn(
639
- ROOT_SELECTOR,
640
- (contextValue?.config.variant === 'standalone' ||
641
- contextValue?.config.variant === 'sidecar') &&
642
- 'h-full'
643
- )}
644
- >
645
- {children}
646
- </div>
647
- <FrontendTools tools={frontendTools} />
648
- </ToolExecutionProvider>
649
- </ElementsContext.Provider>
673
+ <ChatIdContext.Provider value={{ chatId: currentChatId }}>
674
+ <ElementsContext.Provider value={contextValue}>
675
+ <ToolExecutionProvider tools={executableTools}>
676
+ <div
677
+ className={cn(
678
+ ROOT_SELECTOR,
679
+ (contextValue?.config.variant === 'standalone' ||
680
+ contextValue?.config.variant === 'sidecar') &&
681
+ 'h-full'
682
+ )}
683
+ >
684
+ {children}
685
+ </div>
686
+ <FrontendTools tools={frontendTools} />
687
+ </ToolExecutionProvider>
688
+ </ElementsContext.Provider>
689
+ </ChatIdContext.Provider>
650
690
  </AssistantRuntimeProvider>
651
691
  )
652
692
  }
@@ -8,3 +8,5 @@ export const ElementsContext = createContext<ElementsContextType | undefined>(
8
8
 
9
9
  export const ToolApprovalContext =
10
10
  createContext<ToolApprovalContextType | null>(null)
11
+
12
+ export { useChatId } from './ChatIdContext'
@@ -1,3 +1,4 @@
1
+ import { useReplayContext } from '@/contexts/ReplayContext'
1
2
  import { hasExplicitSessionAuth, isStaticSessionAuth } from '@/lib/auth'
2
3
  import { useMemo } from 'react'
3
4
  import { ApiConfig } from '../types'
@@ -40,24 +41,38 @@ export const useAuth = ({
40
41
  auth?: ApiConfig
41
42
  projectSlug: string
42
43
  }): Auth => {
44
+ const replayCtx = useReplayContext()
45
+ const isReplay = replayCtx?.isReplay ?? false
46
+
43
47
  const getSession = useMemo(() => {
48
+ // In replay mode, skip session fetching entirely
49
+ if (isReplay) {
50
+ return null
51
+ }
44
52
  if (isStaticSessionAuth(auth)) {
45
53
  return () => Promise.resolve(auth.sessionToken)
46
54
  }
47
55
  return !isStaticSessionAuth(auth) && hasExplicitSessionAuth(auth)
48
56
  ? auth.sessionFn
49
57
  : defaultGetSession
50
- }, [auth])
58
+ }, [auth, isReplay])
51
59
 
52
- // The session request is only neccessary if we are not using an API key auth
60
+ // The session request is only neccessary if we are not using static session auth
53
61
  // configuration. If a custom session fetcher is provided, we use it,
54
62
  // otherwise we fallback to the default session fetcher
55
63
  const session = useSession({
56
- // We want to check it's NOT API key auth, as the default auth scheme is session auth (if the user hasn't provided an explicit API config, we have a session auth config by default)
57
64
  getSession,
58
65
  projectSlug,
59
66
  })
60
67
 
68
+ // In replay mode, return immediately without waiting for session
69
+ if (isReplay) {
70
+ return {
71
+ headers: {},
72
+ isLoading: false,
73
+ }
74
+ }
75
+
61
76
  return !session
62
77
  ? {
63
78
  isLoading: true,
@@ -1,3 +1,4 @@
1
+ import { useReplayContext } from '@/contexts/ReplayContext'
1
2
  import { useAssistantState } from '@assistant-ui/react'
2
3
  import { useCallback, useEffect, useRef, useState } from 'react'
3
4
  import { useElements } from './useElements'
@@ -37,13 +38,17 @@ export function useFollowOnSuggestions(): {
37
38
  isLoading: boolean
38
39
  } {
39
40
  const { config } = useElements()
41
+ const replayCtx = useReplayContext()
42
+ const isReplay = replayCtx?.isReplay ?? false
43
+
40
44
  const auth = useAuth({
41
45
  auth: config.api,
42
46
  projectSlug: config.projectSlug,
43
47
  })
44
48
 
45
49
  // Check if follow-up suggestions are enabled (default: true)
46
- const isEnabled = config.thread?.followUpSuggestions !== false
50
+ // Disable in replay mode since we don't need AI-generated suggestions
51
+ const isEnabled = !isReplay && config.thread?.followUpSuggestions !== false
47
52
 
48
53
  const [suggestions, setSuggestions] = useState<FollowOnSuggestion[]>([])
49
54
  const [isLoading, setIsLoading] = useState(false)
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export { ElementsProvider } from './contexts/ElementsProvider'
10
10
  export { useElements as useGramElements } from './hooks/useElements'
11
11
  export { useElements } from './hooks/useElements'
12
12
  export { useThreadId } from './hooks/useThreadId'
13
+ export { useChatId } from './contexts/ChatIdContext'
13
14
 
14
15
  // Core Components
15
16
  export { Chat } from '@/components/Chat'
@@ -0,0 +1,141 @@
1
+ import { createCatalog } from '@json-render/core'
2
+ import { z } from 'zod'
3
+
4
+ /**
5
+ * Data point schema - common structure for all chart types
6
+ */
7
+ const dataPointSchema = z.object({
8
+ label: z.string(),
9
+ value: z.number(),
10
+ color: z.string().optional(),
11
+ })
12
+
13
+ /**
14
+ * Multi-series data point for line/area charts
15
+ */
16
+ const seriesDataPointSchema = z
17
+ .object({
18
+ label: z.string(),
19
+ })
20
+ .catchall(z.number())
21
+
22
+ /**
23
+ * Chart Catalog
24
+ *
25
+ * Defines all available chart components for LLM-generated visualizations.
26
+ * Uses Recharts under the hood for rendering.
27
+ */
28
+ export const chartCatalog = createCatalog({
29
+ name: 'chart',
30
+ components: {
31
+ BarChart: {
32
+ props: z.object({
33
+ title: z.string().optional(),
34
+ data: z.array(dataPointSchema),
35
+ layout: z.enum(['vertical', 'horizontal']).optional(),
36
+ showGrid: z.boolean().optional(),
37
+ showLegend: z.boolean().optional(),
38
+ className: z.string().optional(),
39
+ }),
40
+ description:
41
+ 'Bar chart for comparing categorical data. Use vertical for few categories, horizontal for many or long labels.',
42
+ },
43
+
44
+ LineChart: {
45
+ props: z.object({
46
+ title: z.string().optional(),
47
+ data: z.array(seriesDataPointSchema),
48
+ series: z.array(z.string()).optional(),
49
+ showGrid: z.boolean().optional(),
50
+ showLegend: z.boolean().optional(),
51
+ showDots: z.boolean().optional(),
52
+ curved: z.boolean().optional(),
53
+ className: z.string().optional(),
54
+ }),
55
+ description:
56
+ 'Line chart for showing trends over time or continuous data. Supports multiple series.',
57
+ },
58
+
59
+ AreaChart: {
60
+ props: z.object({
61
+ title: z.string().optional(),
62
+ data: z.array(seriesDataPointSchema),
63
+ series: z.array(z.string()).optional(),
64
+ stacked: z.boolean().optional(),
65
+ showGrid: z.boolean().optional(),
66
+ showLegend: z.boolean().optional(),
67
+ className: z.string().optional(),
68
+ }),
69
+ description:
70
+ 'Area chart for showing volume/magnitude over time. Use stacked for part-to-whole relationships.',
71
+ },
72
+
73
+ PieChart: {
74
+ props: z.object({
75
+ title: z.string().optional(),
76
+ data: z.array(dataPointSchema),
77
+ showLabels: z.boolean().optional(),
78
+ showLegend: z.boolean().optional(),
79
+ className: z.string().optional(),
80
+ }),
81
+ description:
82
+ 'Pie chart for showing proportions of a whole. Best for 2-6 categories.',
83
+ },
84
+
85
+ DonutChart: {
86
+ props: z.object({
87
+ title: z.string().optional(),
88
+ data: z.array(dataPointSchema),
89
+ showLabels: z.boolean().optional(),
90
+ showLegend: z.boolean().optional(),
91
+ innerLabel: z.string().optional(),
92
+ innerValue: z.union([z.string(), z.number()]).optional(),
93
+ className: z.string().optional(),
94
+ }),
95
+ description:
96
+ 'Donut chart (pie with center hole). Good for showing a key metric in the center.',
97
+ },
98
+
99
+ ScatterChart: {
100
+ props: z.object({
101
+ title: z.string().optional(),
102
+ data: z.array(
103
+ z.object({
104
+ x: z.number(),
105
+ y: z.number(),
106
+ label: z.string().optional(),
107
+ size: z.number().optional(),
108
+ color: z.string().optional(),
109
+ })
110
+ ),
111
+ xLabel: z.string().optional(),
112
+ yLabel: z.string().optional(),
113
+ showGrid: z.boolean().optional(),
114
+ className: z.string().optional(),
115
+ }),
116
+ description:
117
+ 'Scatter plot for showing correlation between two variables.',
118
+ },
119
+
120
+ RadarChart: {
121
+ props: z.object({
122
+ title: z.string().optional(),
123
+ data: z.array(dataPointSchema),
124
+ showLegend: z.boolean().optional(),
125
+ className: z.string().optional(),
126
+ }),
127
+ description:
128
+ 'Radar/spider chart for comparing multiple attributes. Best for 3-8 dimensions.',
129
+ },
130
+ },
131
+ })
132
+
133
+ export type ChartCatalogComponentProps = typeof chartCatalog extends {
134
+ components: infer C
135
+ }
136
+ ? {
137
+ [K in keyof C]: C[K] extends { props: infer P }
138
+ ? z.infer<P extends z.ZodType ? P : never>
139
+ : never
140
+ }
141
+ : never
@@ -1,149 +1,103 @@
1
1
  'use client'
2
2
 
3
3
  import { useDensity } from '@/hooks/useDensity'
4
- import { useRadius } from '@/hooks/useRadius'
5
4
  import { cn } from '@/lib/utils'
5
+ import { isJsonRenderTree, type JsonRenderNode } from '@/lib/generative-ui'
6
6
  import { SyntaxHighlighterProps } from '@assistant-ui/react-markdown'
7
7
  import { AlertCircleIcon } from 'lucide-react'
8
- import { FC, useEffect, useMemo, useRef, useState } from 'react'
9
- import { parse, View, Warn } from 'vega'
10
- import { expressionInterpreter } from 'vega-interpreter'
8
+ import { FC, useMemo } from 'react'
9
+ import { MacOSWindowFrame } from '../components/MacOSWindowFrame'
10
+ import { PluginLoadingState } from '../components/PluginLoadingState'
11
+
12
+ // Import all chart components
13
+ import {
14
+ BarChart,
15
+ LineChart,
16
+ AreaChart,
17
+ PieChart,
18
+ DonutChart,
19
+ ScatterChart,
20
+ RadarChart,
21
+ } from './ui'
22
+
23
+ const loadingMessages = [
24
+ 'Rendering chart...',
25
+ 'Visualizing data...',
26
+ 'Building chart...',
27
+ 'Processing data...',
28
+ ]
29
+
30
+ function getRandomLoadingMessage() {
31
+ return loadingMessages[Math.floor(Math.random() * loadingMessages.length)]
32
+ }
33
+
34
+ /**
35
+ * Chart components registry
36
+ */
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ const chartComponents: Record<string, FC<any>> = {
39
+ BarChart,
40
+ LineChart,
41
+ AreaChart,
42
+ PieChart,
43
+ DonutChart,
44
+ ScatterChart,
45
+ RadarChart,
46
+ }
47
+
48
+ /**
49
+ * Render a chart node from json-render tree
50
+ */
51
+ function renderChartNode(node: JsonRenderNode): React.ReactNode {
52
+ const Component = chartComponents[node.type]
53
+
54
+ if (!Component) {
55
+ return (
56
+ <div className="text-muted-foreground flex items-center gap-2 text-sm">
57
+ <AlertCircleIcon className="size-4" />
58
+ <span>Unknown chart type: {node.type}</span>
59
+ </div>
60
+ )
61
+ }
62
+
63
+ return <Component {...(node.props ?? {})} />
64
+ }
11
65
 
12
66
  export const ChartRenderer: FC<SyntaxHighlighterProps> = ({ code }) => {
13
- const containerRef = useRef<HTMLDivElement>(null)
14
- const viewRef = useRef<View | null>(null)
15
- const wrapperRef = useRef<HTMLDivElement>(null)
16
- const [error, setError] = useState<string | null>(null)
17
- const [chartReady, setChartReady] = useState(false)
18
- const [containerWidth, setContainerWidth] = useState(0)
19
- const r = useRadius()
20
67
  const d = useDensity()
21
68
 
22
- // Track container width so the Vega view can fill available space
23
- useEffect(() => {
24
- const el = wrapperRef.current
25
- if (!el) return
26
- const ro = new ResizeObserver(([entry]) => {
27
- setContainerWidth(entry.contentRect.width)
28
- })
29
- ro.observe(el)
30
- return () => ro.disconnect()
31
- }, [])
32
-
33
- // Parse and validate JSON in useMemo - only recomputes when code changes
34
- const parsedSpec = useMemo(() => {
69
+ // Parse JSON - returns null if invalid (still streaming)
70
+ const content = useMemo(() => {
35
71
  const trimmedCode = code.trim()
36
72
  if (!trimmedCode) return null
37
73
 
38
74
  try {
39
- const spec = JSON.parse(trimmedCode) as Record<string, unknown>
40
-
41
- // Validate that data array exists and has at least one record with values
42
- const dataArray = spec.data as Array<{ values?: unknown[] }> | undefined
43
- if (!dataArray?.length) return null
44
-
45
- const hasValidData = dataArray.some(
46
- (d) => Array.isArray(d.values) && d.values.length > 0
47
- )
48
- if (!hasValidData) return null
49
-
50
- return spec
75
+ const parsed = JSON.parse(trimmedCode)
76
+ // Validate it has a type field (basic json-render structure)
77
+ if (!isJsonRenderTree(parsed)) {
78
+ return null
79
+ }
80
+ return parsed
51
81
  } catch {
82
+ // JSON is incomplete (still streaming) - return null to show loading state
52
83
  return null
53
84
  }
54
85
  }, [code])
55
86
 
56
- // Only render when we have valid JSON
57
- const shouldRender = parsedSpec !== null
58
-
59
- // Build the spec with autosize and width derived from the container
60
- const sizedSpec = useMemo(() => {
61
- if (!parsedSpec || containerWidth === 0) return null
62
- // Padding used by the outer wrapper (p-lg ≈ 24px each side)
63
- const padding = 48
64
- const availableWidth = Math.max(containerWidth - padding, 100)
65
- return {
66
- ...parsedSpec,
67
- width: availableWidth,
68
- autosize: { type: 'fit' as const, contains: 'padding' as const },
69
- }
70
- }, [parsedSpec, containerWidth])
71
-
72
- useEffect(() => {
73
- if (!containerRef.current || !shouldRender || !sizedSpec) {
74
- return
75
- }
76
-
77
- setError(null)
78
- setChartReady(false)
79
-
80
- const runChart = async () => {
81
- try {
82
- // Clean up any existing view
83
- if (viewRef.current) {
84
- viewRef.current.finalize()
85
- viewRef.current = null
86
- }
87
-
88
- const chart = parse(sizedSpec, undefined, { ast: true })
89
- const view = new View(chart, {
90
- container: containerRef.current ?? undefined,
91
- renderer: 'svg',
92
- hover: true,
93
- logLevel: Warn,
94
- expr: expressionInterpreter,
95
- })
96
- viewRef.current = view
97
-
98
- await view.runAsync()
99
- setChartReady(true)
100
- } catch (err) {
101
- console.error('Failed to render chart:', err)
102
- setError(err instanceof Error ? err.message : 'Failed to render chart')
103
- }
104
- }
87
+ // Memoize the loading message so it doesn't change on every render
88
+ const loadingMessage = useMemo(() => getRandomLoadingMessage(), [])
105
89
 
106
- runChart()
107
-
108
- return () => {
109
- if (viewRef.current) {
110
- viewRef.current.finalize()
111
- viewRef.current = null
112
- }
113
- }
114
- }, [shouldRender, sizedSpec])
115
-
116
- const showLoading = !chartReady && !error
90
+ // Show loading shimmer while JSON is incomplete/streaming
91
+ if (!content) {
92
+ return <PluginLoadingState text={loadingMessage} />
93
+ }
117
94
 
95
+ // Render with macOS-style window frame
118
96
  return (
119
- <div
120
- ref={wrapperRef}
121
- className={cn(
122
- // the after:hidden is to prevent assistant-ui from showing its default code block loading indicator
123
- 'border-border relative min-h-[400px] w-full overflow-hidden border after:hidden',
124
- r('lg'),
125
- showLoading ? '' : d('p-lg')
126
- )}
127
- >
128
- {showLoading && (
129
- <div className="bg-muted absolute inset-0 z-10 flex items-center justify-center">
130
- <span className="shimmer text-muted-foreground text-sm">
131
- Rendering chart...
132
- </span>
133
- </div>
134
- )}
135
-
136
- {error && (
137
- <div className="bg-background absolute inset-0 z-10 flex items-center justify-center gap-2 text-rose-500">
138
- <AlertCircleIcon name="alert-circle" className="h-4 w-4" />
139
- {error}
140
- </div>
141
- )}
142
-
143
- <div
144
- ref={containerRef}
145
- className={error || showLoading ? 'invisible' : 'block'}
146
- />
147
- </div>
97
+ <MacOSWindowFrame>
98
+ <div className={cn('bg-card w-full', d('p-lg'))}>
99
+ {renderChartNode(content)}
100
+ </div>
101
+ </MacOSWindowFrame>
148
102
  )
149
103
  }