@gram-ai/elements 1.19.1 → 1.20.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/Variants.stories.d.ts +2 -0
- package/dist/elements.cjs +55 -53
- package/dist/elements.cjs.map +1 -1
- package/dist/elements.css +1 -1
- package/dist/elements.js +10083 -8852
- package/dist/elements.js.map +1 -1
- package/dist/hooks/useGramThreadListAdapter.d.ts +10 -0
- package/dist/index-B52U8PL6.cjs +99 -0
- package/dist/index-B52U8PL6.cjs.map +1 -0
- package/dist/{index-Cb5sxQuN.js → index-DaF9fGY-.js} +694 -1398
- package/dist/index-DaF9fGY-.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/lib/messageConverter.d.ts +45 -0
- package/dist/plugins.cjs +1 -1
- package/dist/plugins.js +1 -1
- package/dist/types/index.d.ts +39 -0
- package/package.json +1 -1
- package/src/components/Chat/stories/Variants.stories.tsx +49 -1
- package/src/components/assistant-ui/assistant-modal.tsx +18 -3
- package/src/components/assistant-ui/assistant-sidecar.tsx +18 -3
- package/src/components/assistant-ui/thread-list.tsx +52 -25
- package/src/contexts/ElementsProvider.tsx +150 -29
- package/src/hooks/useGramThreadListAdapter.tsx +302 -0
- package/src/index.ts +4 -0
- package/src/lib/messageConverter.ts +241 -0
- package/src/plugins/chart/component.tsx +15 -7
- package/src/plugins/chart/index.ts +83 -1
- package/src/types/index.ts +42 -0
- package/dist/index-Cb5sxQuN.js.map +0 -1
- package/dist/index-hrhDHFgW.cjs +0 -19
- package/dist/index-hrhDHFgW.cjs.map +0 -1
|
@@ -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
|
-
|
|
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
|
|
34
|
-
const shouldRender =
|
|
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
|
|
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
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
*/
|