@gram-ai/elements 1.19.1 → 1.20.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.
Files changed (38) hide show
  1. package/dist/components/Chat/index.d.ts +5 -1
  2. package/dist/components/Chat/stories/Variants.stories.d.ts +4 -1
  3. package/dist/components/assistant-ui/assistant-modal.d.ts +5 -1
  4. package/dist/components/assistant-ui/assistant-sidecar.d.ts +5 -1
  5. package/dist/components/assistant-ui/thread-list.d.ts +5 -1
  6. package/dist/components/assistant-ui/thread.d.ts +5 -1
  7. package/dist/elements.cjs +55 -53
  8. package/dist/elements.cjs.map +1 -1
  9. package/dist/elements.css +1 -1
  10. package/dist/elements.js +10119 -8884
  11. package/dist/elements.js.map +1 -1
  12. package/dist/hooks/useGramThreadListAdapter.d.ts +10 -0
  13. package/dist/index-B52U8PL6.cjs +99 -0
  14. package/dist/index-B52U8PL6.cjs.map +1 -0
  15. package/dist/{index-Cb5sxQuN.js → index-DaF9fGY-.js} +694 -1398
  16. package/dist/index-DaF9fGY-.js.map +1 -0
  17. package/dist/index.d.ts +4 -1
  18. package/dist/lib/messageConverter.d.ts +45 -0
  19. package/dist/plugins.cjs +1 -1
  20. package/dist/plugins.js +1 -1
  21. package/dist/types/index.d.ts +39 -0
  22. package/package.json +4 -2
  23. package/src/components/Chat/index.tsx +8 -4
  24. package/src/components/Chat/stories/Variants.stories.tsx +77 -2
  25. package/src/components/assistant-ui/assistant-modal.tsx +25 -5
  26. package/src/components/assistant-ui/assistant-sidecar.tsx +25 -5
  27. package/src/components/assistant-ui/thread-list.tsx +58 -26
  28. package/src/components/assistant-ui/thread.tsx +7 -2
  29. package/src/contexts/ElementsProvider.tsx +150 -29
  30. package/src/hooks/useGramThreadListAdapter.tsx +302 -0
  31. package/src/index.ts +4 -0
  32. package/src/lib/messageConverter.ts +241 -0
  33. package/src/plugins/chart/component.tsx +15 -7
  34. package/src/plugins/chart/index.ts +83 -1
  35. package/src/types/index.ts +42 -0
  36. package/dist/index-Cb5sxQuN.js.map +0 -1
  37. package/dist/index-hrhDHFgW.cjs +0 -19
  38. package/dist/index-hrhDHFgW.cjs.map +0 -1
package/src/index.ts CHANGED
@@ -3,10 +3,13 @@ import './global.css'
3
3
 
4
4
  // Context Providers
5
5
  export { ElementsProvider as GramElementsProvider } from './contexts/ElementsProvider'
6
+ export { ElementsProvider } from './contexts/ElementsProvider'
6
7
  export { useElements as useGramElements } from './hooks/useElements'
8
+ export { useElements } from './hooks/useElements'
7
9
 
8
10
  // Core Components
9
11
  export { Chat } from '@/components/Chat'
12
+ export { ThreadList as ChatHistory } from '@/components/assistant-ui/thread-list'
10
13
 
11
14
  // Frontend Tools
12
15
  export { defineFrontendTool } from './lib/tools'
@@ -25,6 +28,7 @@ export type {
25
28
  Dimensions,
26
29
  ElementsConfig,
27
30
  GetSessionFn,
31
+ HistoryConfig,
28
32
  ModalConfig,
29
33
  ModalTriggerPosition,
30
34
  Model,
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Message format converter for Gram API <-> assistant-ui.
3
+ *
4
+ * The Gram API returns chat messages in its own schema (GramChatMessage),
5
+ * while assistant-ui expects messages in its internal ThreadMessage format.
6
+ * This module bridges that gap by converting between the two formats.
7
+ *
8
+ * Main export: `convertGramMessagesToExported` - converts an array of Gram
9
+ * messages into an ExportedMessageRepository with parent-child relationships
10
+ * for conversation threading.
11
+ */
12
+
13
+ import type {
14
+ ExportedMessageRepository,
15
+ ThreadMessage,
16
+ ThreadUserMessagePart,
17
+ ThreadAssistantMessagePart,
18
+ TextMessagePart,
19
+ } from '@assistant-ui/react'
20
+
21
+ /**
22
+ * Represents a chat message from the Gram API.
23
+ * This mirrors the ChatMessage type from @gram/sdk without requiring the SDK dependency.
24
+ */
25
+ export interface GramChatMessage {
26
+ id: string
27
+ role: string
28
+ content?: string
29
+ model: string
30
+ toolCallId?: string
31
+ toolCalls?: string
32
+ createdAt: Date | string
33
+ }
34
+
35
+ /**
36
+ * Represents a chat from the Gram API.
37
+ */
38
+ export interface GramChat {
39
+ id: string
40
+ title: string
41
+ userId: string
42
+ numMessages: number
43
+ messages: GramChatMessage[]
44
+ createdAt: Date | string
45
+ updatedAt: Date | string
46
+ }
47
+
48
+ /**
49
+ * Represents a chat overview from the Gram API (without full messages).
50
+ */
51
+ export interface GramChatOverview {
52
+ id: string
53
+ title: string
54
+ userId: string
55
+ numMessages: number
56
+ createdAt: Date | string
57
+ updatedAt: Date | string
58
+ }
59
+
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
+ /**
72
+ * Parses a date that might be a string or Date object.
73
+ */
74
+ function parseDate(date: Date | string): Date {
75
+ return typeof date === 'string' ? new Date(date) : date
76
+ }
77
+
78
+ /**
79
+ * Builds content parts for a user message.
80
+ */
81
+ function buildUserContentParts(msg: GramChatMessage): ThreadUserMessagePart[] {
82
+ const parts: ThreadUserMessagePart[] = []
83
+
84
+ if (msg.content) {
85
+ parts.push({
86
+ type: 'text',
87
+ text: msg.content,
88
+ } as TextMessagePart)
89
+ }
90
+
91
+ // Return at least an empty text part if no content
92
+ if (parts.length === 0) {
93
+ parts.push({
94
+ type: 'text',
95
+ text: '',
96
+ } as TextMessagePart)
97
+ }
98
+
99
+ return parts
100
+ }
101
+
102
+ /**
103
+ * Builds content parts for an assistant message, including tool calls.
104
+ */
105
+ function buildAssistantContentParts(
106
+ msg: GramChatMessage
107
+ ): ThreadAssistantMessagePart[] {
108
+ const parts: ThreadAssistantMessagePart[] = []
109
+
110
+ if (msg.content) {
111
+ parts.push({
112
+ type: 'text',
113
+ text: msg.content,
114
+ } as TextMessagePart)
115
+ }
116
+
117
+ if (msg.toolCalls) {
118
+ try {
119
+ const toolCalls = JSON.parse(msg.toolCalls)
120
+ for (const tc of toolCalls) {
121
+ const args = tc.function?.arguments ?? tc.args ?? {}
122
+ const argsText = typeof args === 'string' ? args : JSON.stringify(args)
123
+ parts.push({
124
+ type: 'tool-call',
125
+ toolCallId: tc.id ?? tc.toolCallId ?? '',
126
+ toolName: tc.function?.name ?? tc.toolName ?? '',
127
+ args: typeof args === 'string' ? JSON.parse(args) : args,
128
+ argsText,
129
+ result: undefined,
130
+ } as ThreadAssistantMessagePart)
131
+ }
132
+ } catch {
133
+ // Ignore JSON parse errors for tool calls
134
+ }
135
+ }
136
+
137
+ // Return at least an empty text part if no content
138
+ if (parts.length === 0) {
139
+ parts.push({
140
+ type: 'text',
141
+ text: '',
142
+ } as TextMessagePart)
143
+ }
144
+
145
+ return parts
146
+ }
147
+
148
+ /**
149
+ * Converts a single Gram ChatMessage to a ThreadMessage.
150
+ */
151
+ function convertGramMessageToThreadMessage(
152
+ msg: GramChatMessage
153
+ ): ThreadMessage {
154
+ const role = normalizeRole(msg.role)
155
+ const createdAt = parseDate(msg.createdAt)
156
+
157
+ const baseMetadata = {
158
+ unstable_state: undefined,
159
+ unstable_annotations: undefined,
160
+ unstable_data: undefined,
161
+ steps: undefined,
162
+ submittedFeedback: undefined,
163
+ custom: {},
164
+ }
165
+
166
+ if (role === 'user') {
167
+ return {
168
+ id: msg.id,
169
+ role: 'user',
170
+ createdAt,
171
+ content: buildUserContentParts(msg),
172
+ attachments: [],
173
+ metadata: baseMetadata,
174
+ }
175
+ }
176
+
177
+ if (role === 'system') {
178
+ return {
179
+ id: msg.id,
180
+ role: 'system',
181
+ createdAt,
182
+ content: [{ type: 'text', text: msg.content ?? '' }],
183
+ metadata: baseMetadata,
184
+ }
185
+ }
186
+
187
+ // Assistant message
188
+ return {
189
+ id: msg.id,
190
+ role: 'assistant',
191
+ createdAt,
192
+ content: buildAssistantContentParts(msg),
193
+ status: { type: 'complete', reason: 'stop' },
194
+ metadata: {
195
+ unstable_state: null,
196
+ unstable_annotations: [],
197
+ unstable_data: [],
198
+ steps: [],
199
+ submittedFeedback: undefined,
200
+ custom: {},
201
+ },
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Converts an array of Gram ChatMessages to an ExportedMessageRepository.
207
+ * Creates parent-child relationships based on message order.
208
+ *
209
+ * Note: System messages are filtered out because assistant-ui's
210
+ * `fromThreadMessageLike` doesn't support them in the exported format.
211
+ */
212
+ export function convertGramMessagesToExported(
213
+ messages: GramChatMessage[]
214
+ ): ExportedMessageRepository {
215
+ if (messages.length === 0) {
216
+ return { messages: [], headId: null }
217
+ }
218
+
219
+ const exportedMessages: ExportedMessageRepository['messages'] = []
220
+ let prevId: string | null = null
221
+
222
+ for (const msg of messages) {
223
+ // Skip system messages - they're not supported in the exported message format
224
+ if (msg.role === 'system') {
225
+ continue
226
+ }
227
+
228
+ const threadMessage = convertGramMessageToThreadMessage(msg)
229
+ exportedMessages.push({
230
+ message: threadMessage,
231
+ parentId: prevId,
232
+ runConfig: undefined,
233
+ })
234
+ prevId = msg.id
235
+ }
236
+
237
+ return {
238
+ messages: exportedMessages,
239
+ headId: prevId,
240
+ }
241
+ }
@@ -3,18 +3,15 @@
3
3
  import { useDensity } from '@/hooks/useDensity'
4
4
  import { useRadius } from '@/hooks/useRadius'
5
5
  import { cn } from '@/lib/utils'
6
- import { useAssistantState } from '@assistant-ui/react'
7
6
  import { SyntaxHighlighterProps } from '@assistant-ui/react-markdown'
8
7
  import { AlertCircleIcon } from 'lucide-react'
9
8
  import { FC, useEffect, useMemo, useRef, useState } from 'react'
10
9
  import { parse, View, Warn } from 'vega'
11
10
 
12
11
  export const ChartRenderer: FC<SyntaxHighlighterProps> = ({ code }) => {
13
- const message = useAssistantState(({ message }) => message)
14
12
  const containerRef = useRef<HTMLDivElement>(null)
15
13
  const viewRef = useRef<View | null>(null)
16
14
  const [error, setError] = useState<string | null>(null)
17
- const messageIsComplete = message.status?.type === 'complete'
18
15
  const r = useRadius()
19
16
  const d = useDensity()
20
17
 
@@ -24,14 +21,25 @@ export const ChartRenderer: FC<SyntaxHighlighterProps> = ({ code }) => {
24
21
  if (!trimmedCode) return null
25
22
 
26
23
  try {
27
- return JSON.parse(trimmedCode) as Record<string, unknown>
24
+ const spec = JSON.parse(trimmedCode) as Record<string, unknown>
25
+
26
+ // Validate that data array exists and has at least one record with values
27
+ const dataArray = spec.data as Array<{ values?: unknown[] }> | undefined
28
+ if (!dataArray?.length) return null
29
+
30
+ const hasValidData = dataArray.some(
31
+ (d) => Array.isArray(d.values) && d.values.length > 0
32
+ )
33
+ if (!hasValidData) return null
34
+
35
+ return spec
28
36
  } catch {
29
37
  return null
30
38
  }
31
39
  }, [code])
32
40
 
33
- // Only render when we have valid JSON AND message is complete
34
- const shouldRender = messageIsComplete && parsedSpec !== null
41
+ // Only render when we have valid JSON
42
+ const shouldRender = parsedSpec !== null
35
43
 
36
44
  useEffect(() => {
37
45
  if (!containerRef.current || !shouldRender) {
@@ -78,7 +86,7 @@ export const ChartRenderer: FC<SyntaxHighlighterProps> = ({ code }) => {
78
86
  <div
79
87
  className={cn(
80
88
  // the after:hidden is to prevent assistant-ui from showing its default code block loading indicator
81
- 'relative flex min-h-[400px] w-fit max-w-full min-w-[400px] items-center justify-center border p-6 after:hidden',
89
+ 'relative min-h-[400px] w-fit max-w-full min-w-[400px] overflow-auto border p-6 after:hidden',
82
90
  r('lg'),
83
91
  d('p-lg')
84
92
  )}
@@ -21,7 +21,89 @@ CONTENT GUIDELINES:
21
21
  - Do not describe visual properties or technical implementation details
22
22
  - Do not mention "Vega" or other technical terms - this is user-facing
23
23
 
24
- The Vega spec will be parsed with JSON.parse() - if it fails, the chart will error. Ensure strict JSON validity.`,
24
+ The Vega spec will be parsed with JSON.parse() - if it fails, the chart will error. Ensure strict JSON validity.
25
+
26
+ REQUIRED STRUCTURE:
27
+ Every spec needs: "$schema", "width", "height", "data", "scales", "marks". Include "padding" (5 or object) and "axes" for readability.
28
+ Data format:
29
+ {"name": "table", "values": [{"category": "A", "amount": 28}]}
30
+ SCALES - Choose the right type:
31
+ - "band": categorical x-axis (bar charts) - domain from data field, range: "width", padding: 0.1
32
+ - "linear": numerical axes - domain from data field, range: "width"/"height", nice: true
33
+ - "time"/"utc": temporal data
34
+ - "ordinal": for colors use range: {"scheme": "category10"} or range: ["#1f77b4", "#ff7f0e", "#2ca02c"]
35
+ MARKS - Common types:
36
+ - "rect": bar charts (requires x, width, y, y2)
37
+ - "line": time series (requires x, y)
38
+ - "area": filled areas (requires x, y, y2)
39
+ - "symbol": scatter plots (requires x, y)
40
+ CHART PATTERNS:
41
+ Bar: band scale (x) + linear scale (y) + rect marks. Set y2: {"scale": "yscale", "value": 0}
42
+ Line: linear/point scale (x) + linear scale (y) + line mark. Add "interpolate": "monotone"
43
+ Scatter: linear scales (both) + symbol marks
44
+ Area: like line but use area mark with y2: {"scale": "yscale", "value": 0}
45
+ Stacked: add transform [{"type": "stack", "groupby": ["x"], "field": "y"}], use y0/y1 fields
46
+ CRITICAL RULES:
47
+ 1. Data must contain at least one record with valid (non-null) values for ALL fields used in scales
48
+ 2. ONLY reference fields that actually exist in your data - never use datum.meta, datum.id, or any field not in your values array
49
+ 3. Always include y2 for rect/area marks (or bars/areas have zero height)
50
+ 4. Use "band" for categories, not "linear"
51
+ 5. For position scales use "range": "width" or "height". For color scales NEVER use "range": "category10" - use "range": {"scheme": "category10"} or an array
52
+ 6. Match scale/data names exactly between definition and usage
53
+ 7. Include "from": {"data": "dataName"} on marks
54
+ 8. Add padding to prevent label cutoff
55
+ EXAMPLE: COMPLETE BAR CHART
56
+ {
57
+ "$schema": "https://vega.github.io/schema/vega/v5.json",
58
+ "width": 500,
59
+ "height": 300,
60
+ "padding": 5,
61
+ "data": [
62
+ {
63
+ "name": "table",
64
+ "values": [
65
+ {"category": "A", "amount": 28},
66
+ {"category": "B", "amount": 55},
67
+ {"category": "C", "amount": 43}
68
+ ]
69
+ }
70
+ ],
71
+ "scales": [
72
+ {
73
+ "name": "xscale",
74
+ "type": "band",
75
+ "domain": {"data": "table", "field": "category"},
76
+ "range": "width",
77
+ "padding": 0.1
78
+ },
79
+ {
80
+ "name": "yscale",
81
+ "type": "linear",
82
+ "domain": {"data": "table", "field": "amount"},
83
+ "range": "height",
84
+ "nice": true
85
+ }
86
+ ],
87
+ "axes": [
88
+ {"scale": "xscale", "orient": "bottom"},
89
+ {"scale": "yscale", "orient": "left", "title": "Amount"}
90
+ ],
91
+ "marks": [
92
+ {
93
+ "type": "rect",
94
+ "from": {"data": "table"},
95
+ "encode": {
96
+ "enter": {
97
+ "x": {"scale": "xscale", "field": "category"},
98
+ "width": {"scale": "xscale", "band": 1},
99
+ "y": {"scale": "yscale", "field": "amount"},
100
+ "y2": {"scale": "yscale", "value": 0},
101
+ "fill": {"value": "steelblue"}
102
+ }
103
+ }
104
+ }
105
+ ]
106
+ }`,
25
107
  Component: ChartRenderer,
26
108
  Header: undefined,
27
109
  }
@@ -261,6 +261,20 @@ export interface ElementsConfig {
261
261
  */
262
262
  tools?: ToolsConfig
263
263
 
264
+ /**
265
+ * Configuration for chat history and thread persistence.
266
+ * When enabled, conversations are saved and the thread list is shown.
267
+ *
268
+ * @example
269
+ * const config: ElementsConfig = {
270
+ * history: {
271
+ * enabled: true,
272
+ * showThreadList: true,
273
+ * },
274
+ * }
275
+ */
276
+ history?: HistoryConfig
277
+
264
278
  /**
265
279
  * The API configuration to use for the Elements library.
266
280
  *
@@ -736,6 +750,34 @@ export interface SidecarConfig extends ExpandableConfig {
736
750
  title?: string
737
751
  }
738
752
 
753
+ /**
754
+ * Configuration for chat history persistence.
755
+ * When enabled, threads are persisted and can be restored from the thread list.
756
+ *
757
+ * @example
758
+ * const config: ElementsConfig = {
759
+ * history: {
760
+ * enabled: true,
761
+ * showThreadList: true,
762
+ * },
763
+ * }
764
+ */
765
+ export interface HistoryConfig {
766
+ /**
767
+ * Whether to enable chat history persistence.
768
+ * When true, threads will be saved and can be loaded from the thread list.
769
+ * @default false
770
+ */
771
+ enabled: boolean
772
+
773
+ /**
774
+ * Whether to show the thread list sidebar/panel.
775
+ * Only applies when history is enabled.
776
+ * @default true when history.enabled is true
777
+ */
778
+ showThreadList?: boolean
779
+ }
780
+
739
781
  /**
740
782
  * @internal
741
783
  */