@gram-ai/elements 1.25.0 → 1.25.2
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/MessageFeedback.stories.d.ts +1 -1
- package/dist/components/Replay.stories.d.ts +16 -0
- package/dist/contexts/ChatIdContext.d.ts +11 -0
- package/dist/contexts/contexts.d.ts +1 -0
- package/dist/elements.cjs +1 -1
- package/dist/elements.css +1 -1
- package/dist/elements.js +7 -6
- package/dist/{index-iUSSoKFz.cjs → index-B8nSCdu4.cjs} +11 -11
- package/dist/index-B8nSCdu4.cjs.map +1 -0
- package/dist/{index-wBHCO1r-.cjs → index-CAtaLV1E.cjs} +64 -55
- package/dist/index-CAtaLV1E.cjs.map +1 -0
- package/dist/{index-CtyV0c-T.js → index-CJrwma08.js} +3737 -3730
- package/dist/index-CJrwma08.js.map +1 -0
- package/dist/{index-DDb23655.js → index-DLWQ91ow.js} +8494 -8418
- package/dist/index-DLWQ91ow.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/lib/messageConverter.d.ts +1 -1
- package/dist/lib/messageConverter.test.d.ts +1 -0
- package/dist/plugins.cjs +1 -1
- package/dist/plugins.js +1 -1
- package/dist/{profiler-CGIJBY8c.js → profiler-BaG0scxd.js} +2 -2
- package/dist/{profiler-CGIJBY8c.js.map → profiler-BaG0scxd.js.map} +1 -1
- package/dist/{profiler-CLtQEzfv.cjs → profiler-CuqENACf.cjs} +2 -2
- package/dist/{profiler-CLtQEzfv.cjs.map → profiler-CuqENACf.cjs.map} +1 -1
- package/dist/{startRecording-x0G7lOpP.js → startRecording-86bHmd-l.js} +2 -2
- package/dist/{startRecording-x0G7lOpP.js.map → startRecording-86bHmd-l.js.map} +1 -1
- package/dist/{startRecording-DXZPNn9e.cjs → startRecording-BiLmoqZa.cjs} +2 -2
- package/dist/{startRecording-DXZPNn9e.cjs.map → startRecording-BiLmoqZa.cjs.map} +1 -1
- package/dist/types/index.d.ts +4 -4
- package/package.json +1 -1
- package/src/components/Chat/stories/MessageFeedback.stories.tsx +6 -6
- package/src/components/Chat/stories/ToolApproval.stories.tsx +10 -10
- package/src/components/Chat/stories/Tools.stories.tsx +122 -104
- package/src/components/Chat/stories/Variants.stories.tsx +1 -1
- package/src/components/Replay.stories.tsx +230 -0
- package/src/components/ShadowRoot.tsx +5 -1
- package/src/components/assistant-ui/message-feedback.tsx +6 -7
- package/src/components/assistant-ui/thread.tsx +76 -11
- package/src/contexts/ChatIdContext.tsx +21 -0
- package/src/contexts/ElementsProvider.tsx +77 -37
- package/src/contexts/contexts.ts +2 -0
- package/src/hooks/useAuth.ts +1 -2
- package/src/index.ts +1 -0
- package/src/lib/messageConverter.test.ts +242 -0
- package/src/lib/messageConverter.ts +22 -10
- package/src/types/index.ts +4 -4
- package/dist/index-CtyV0c-T.js.map +0 -1
- package/dist/index-DDb23655.js.map +0 -1
- package/dist/index-iUSSoKFz.cjs.map +0 -1
- package/dist/index-wBHCO1r-.cjs.map +0 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
convertGramMessagesToExported,
|
|
5
|
+
convertGramMessagesToUIMessages,
|
|
6
|
+
convertGramMessagePartsToUIMessageParts,
|
|
7
|
+
type GramChatMessage,
|
|
8
|
+
} from './messageConverter'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper to create a minimal GramChatMessage for testing.
|
|
12
|
+
*/
|
|
13
|
+
function makeMsg(
|
|
14
|
+
overrides: Partial<GramChatMessage> & { role: string }
|
|
15
|
+
): GramChatMessage {
|
|
16
|
+
return {
|
|
17
|
+
id: crypto.randomUUID(),
|
|
18
|
+
model: 'test-model',
|
|
19
|
+
created_at: new Date().toISOString(),
|
|
20
|
+
...overrides,
|
|
21
|
+
} as GramChatMessage
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeToolCallsJSON(
|
|
25
|
+
calls: { id: string; name: string; args?: string }[]
|
|
26
|
+
): string {
|
|
27
|
+
return JSON.stringify(
|
|
28
|
+
calls.map((c) => ({
|
|
29
|
+
id: c.id,
|
|
30
|
+
type: 'function',
|
|
31
|
+
function: { name: c.name, arguments: c.args ?? '{}' },
|
|
32
|
+
}))
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('convertGramMessagePartsToUIMessageParts', () => {
|
|
37
|
+
it('includes tool calls for a single assistant message', () => {
|
|
38
|
+
const msg = makeMsg({
|
|
39
|
+
role: 'assistant',
|
|
40
|
+
content: 'Let me search.',
|
|
41
|
+
tool_calls: makeToolCallsJSON([{ id: 'tc_1', name: 'search_deals' }]),
|
|
42
|
+
} as Partial<GramChatMessage> & { role: string })
|
|
43
|
+
|
|
44
|
+
const parts = convertGramMessagePartsToUIMessageParts(msg as any, new Map())
|
|
45
|
+
|
|
46
|
+
const toolParts = parts.filter((p) => p.type === 'dynamic-tool')
|
|
47
|
+
expect(toolParts).toHaveLength(1)
|
|
48
|
+
expect(toolParts[0]).toMatchObject({
|
|
49
|
+
type: 'dynamic-tool',
|
|
50
|
+
toolCallId: 'tc_1',
|
|
51
|
+
toolName: 'search_deals',
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('deduplicates tool calls when seenToolCallIds is provided', () => {
|
|
56
|
+
const seen = new Set(['tc_1'])
|
|
57
|
+
const msg = makeMsg({
|
|
58
|
+
role: 'assistant',
|
|
59
|
+
content: 'Trying again.',
|
|
60
|
+
tool_calls: makeToolCallsJSON([
|
|
61
|
+
{ id: 'tc_1', name: 'search_deals' },
|
|
62
|
+
{ id: 'tc_2', name: 'search_deals' },
|
|
63
|
+
]),
|
|
64
|
+
} as Partial<GramChatMessage> & { role: string })
|
|
65
|
+
|
|
66
|
+
const parts = convertGramMessagePartsToUIMessageParts(
|
|
67
|
+
msg as any,
|
|
68
|
+
new Map(),
|
|
69
|
+
seen
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const toolParts = parts.filter((p) => p.type === 'dynamic-tool')
|
|
73
|
+
expect(toolParts).toHaveLength(1)
|
|
74
|
+
expect(toolParts[0]).toMatchObject({ toolCallId: 'tc_2' })
|
|
75
|
+
expect(seen.has('tc_2')).toBe(true)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('convertGramMessagesToUIMessages - tool call deduplication', () => {
|
|
80
|
+
/**
|
|
81
|
+
* Simulates the server behavior where each assistant message in a multi-step
|
|
82
|
+
* tool use flow accumulates ALL tool calls from the turn, not just its own.
|
|
83
|
+
*
|
|
84
|
+
* Given 4 sequential tool calls, the server stores:
|
|
85
|
+
* message 1: tool_calls = [tc_1]
|
|
86
|
+
* message 2: tool_calls = [tc_1, tc_2]
|
|
87
|
+
* message 3: tool_calls = [tc_1, tc_2, tc_3]
|
|
88
|
+
* message 4: tool_calls = [tc_1, tc_2, tc_3, tc_4]
|
|
89
|
+
*
|
|
90
|
+
* Without dedup, each message renders all its tool calls → every group shows
|
|
91
|
+
* the accumulated count. With dedup, each message only renders the new ones.
|
|
92
|
+
*/
|
|
93
|
+
it('deduplicates accumulated tool calls across assistant messages', () => {
|
|
94
|
+
const messages: GramChatMessage[] = [
|
|
95
|
+
makeMsg({
|
|
96
|
+
role: 'user',
|
|
97
|
+
content: 'Search for deals',
|
|
98
|
+
}),
|
|
99
|
+
makeMsg({
|
|
100
|
+
role: 'assistant',
|
|
101
|
+
content: "I'll search for deals.",
|
|
102
|
+
tool_calls: makeToolCallsJSON([
|
|
103
|
+
{ id: 'tc_1', name: 'hubspot_search_deals' },
|
|
104
|
+
]),
|
|
105
|
+
} as Partial<GramChatMessage> & { role: string }),
|
|
106
|
+
makeMsg({
|
|
107
|
+
role: 'tool',
|
|
108
|
+
content: '{"error": "invalid filter"}',
|
|
109
|
+
tool_call_id: 'tc_1',
|
|
110
|
+
} as Partial<GramChatMessage> & { role: string }),
|
|
111
|
+
makeMsg({
|
|
112
|
+
role: 'assistant',
|
|
113
|
+
content: 'Let me try differently.',
|
|
114
|
+
tool_calls: makeToolCallsJSON([
|
|
115
|
+
{ id: 'tc_1', name: 'hubspot_search_deals' },
|
|
116
|
+
{ id: 'tc_2', name: 'hubspot_search_deals' },
|
|
117
|
+
]),
|
|
118
|
+
} as Partial<GramChatMessage> & { role: string }),
|
|
119
|
+
makeMsg({
|
|
120
|
+
role: 'tool',
|
|
121
|
+
content: '{"error": "empty filters"}',
|
|
122
|
+
tool_call_id: 'tc_2',
|
|
123
|
+
} as Partial<GramChatMessage> & { role: string }),
|
|
124
|
+
makeMsg({
|
|
125
|
+
role: 'assistant',
|
|
126
|
+
content: 'Let me try with proper filters.',
|
|
127
|
+
tool_calls: makeToolCallsJSON([
|
|
128
|
+
{ id: 'tc_1', name: 'hubspot_search_deals' },
|
|
129
|
+
{ id: 'tc_2', name: 'hubspot_search_deals' },
|
|
130
|
+
{ id: 'tc_3', name: 'hubspot_search_deals' },
|
|
131
|
+
]),
|
|
132
|
+
} as Partial<GramChatMessage> & { role: string }),
|
|
133
|
+
makeMsg({
|
|
134
|
+
role: 'tool',
|
|
135
|
+
content: '{"deals": []}',
|
|
136
|
+
tool_call_id: 'tc_3',
|
|
137
|
+
} as Partial<GramChatMessage> & { role: string }),
|
|
138
|
+
makeMsg({
|
|
139
|
+
role: 'assistant',
|
|
140
|
+
content: 'Here are the results.',
|
|
141
|
+
}),
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
const result = convertGramMessagesToUIMessages(messages)
|
|
145
|
+
const assistantMessages = result.messages.filter(
|
|
146
|
+
(m) => m.message.role === 'assistant'
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
// Each assistant message should only have its OWN tool call, not all accumulated ones
|
|
150
|
+
const firstAssistant = assistantMessages[0]!.message.parts.filter(
|
|
151
|
+
(p) => p.type === 'dynamic-tool'
|
|
152
|
+
)
|
|
153
|
+
expect(firstAssistant).toHaveLength(1)
|
|
154
|
+
expect(firstAssistant[0]).toMatchObject({ toolCallId: 'tc_1' })
|
|
155
|
+
|
|
156
|
+
const secondAssistant = assistantMessages[1]!.message.parts.filter(
|
|
157
|
+
(p) => p.type === 'dynamic-tool'
|
|
158
|
+
)
|
|
159
|
+
expect(secondAssistant).toHaveLength(1)
|
|
160
|
+
expect(secondAssistant[0]).toMatchObject({ toolCallId: 'tc_2' })
|
|
161
|
+
|
|
162
|
+
const thirdAssistant = assistantMessages[2]!.message.parts.filter(
|
|
163
|
+
(p) => p.type === 'dynamic-tool'
|
|
164
|
+
)
|
|
165
|
+
expect(thirdAssistant).toHaveLength(1)
|
|
166
|
+
expect(thirdAssistant[0]).toMatchObject({ toolCallId: 'tc_3' })
|
|
167
|
+
|
|
168
|
+
// Final assistant message has no tool calls
|
|
169
|
+
const fourthAssistant = assistantMessages[3]!.message.parts.filter(
|
|
170
|
+
(p) => p.type === 'dynamic-tool'
|
|
171
|
+
)
|
|
172
|
+
expect(fourthAssistant).toHaveLength(0)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('resets dedup tracking after a user message', () => {
|
|
176
|
+
const messages: GramChatMessage[] = [
|
|
177
|
+
makeMsg({ role: 'user', content: 'First question' }),
|
|
178
|
+
makeMsg({
|
|
179
|
+
role: 'assistant',
|
|
180
|
+
content: 'Searching.',
|
|
181
|
+
tool_calls: makeToolCallsJSON([{ id: 'tc_1', name: 'search' }]),
|
|
182
|
+
} as Partial<GramChatMessage> & { role: string }),
|
|
183
|
+
makeMsg({
|
|
184
|
+
role: 'tool',
|
|
185
|
+
content: '{}',
|
|
186
|
+
tool_call_id: 'tc_1',
|
|
187
|
+
} as Partial<GramChatMessage> & { role: string }),
|
|
188
|
+
makeMsg({ role: 'user', content: 'Second question' }),
|
|
189
|
+
// New turn — tc_1 reused as ID (different conversation turn)
|
|
190
|
+
makeMsg({
|
|
191
|
+
role: 'assistant',
|
|
192
|
+
content: 'Searching again.',
|
|
193
|
+
tool_calls: makeToolCallsJSON([{ id: 'tc_1', name: 'search' }]),
|
|
194
|
+
} as Partial<GramChatMessage> & { role: string }),
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
const result = convertGramMessagesToUIMessages(messages)
|
|
198
|
+
const assistantMessages = result.messages.filter(
|
|
199
|
+
(m) => m.message.role === 'assistant'
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Both assistant messages should have tc_1 since the user message resets tracking
|
|
203
|
+
const first = assistantMessages[0]!.message.parts.filter(
|
|
204
|
+
(p) => p.type === 'dynamic-tool'
|
|
205
|
+
)
|
|
206
|
+
expect(first).toHaveLength(1)
|
|
207
|
+
|
|
208
|
+
const second = assistantMessages[1]!.message.parts.filter(
|
|
209
|
+
(p) => p.type === 'dynamic-tool'
|
|
210
|
+
)
|
|
211
|
+
expect(second).toHaveLength(1)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('convertGramMessagesToExported - string content with tool calls', () => {
|
|
216
|
+
it('includes tool calls when assistant message has string content', () => {
|
|
217
|
+
const messages: GramChatMessage[] = [
|
|
218
|
+
makeMsg({ role: 'user', content: 'Search for deals' }),
|
|
219
|
+
makeMsg({
|
|
220
|
+
role: 'assistant',
|
|
221
|
+
content: 'Let me search.',
|
|
222
|
+
tool_calls: makeToolCallsJSON([
|
|
223
|
+
{ id: 'tc_1', name: 'hubspot_search_deals' },
|
|
224
|
+
]),
|
|
225
|
+
} as Partial<GramChatMessage> & { role: string }),
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
const result = convertGramMessagesToExported(messages)
|
|
229
|
+
const assistantEntry = result.messages.find(
|
|
230
|
+
(m) => m.message.role === 'assistant'
|
|
231
|
+
)!
|
|
232
|
+
const toolCallParts = (assistantEntry.message as any).content.filter(
|
|
233
|
+
(p: any) => p.type === 'tool-call'
|
|
234
|
+
)
|
|
235
|
+
expect(toolCallParts).toHaveLength(1)
|
|
236
|
+
expect(toolCallParts[0]).toMatchObject({
|
|
237
|
+
type: 'tool-call',
|
|
238
|
+
toolCallId: 'tc_1',
|
|
239
|
+
toolName: 'hubspot_search_deals',
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
})
|
|
@@ -146,17 +146,15 @@ function buildAssistantContentParts(
|
|
|
146
146
|
return []
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
const parts: ThreadAssistantMessagePart[] = []
|
|
150
|
+
|
|
149
151
|
if (typeof msg.content === 'string' || !msg.content) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
},
|
|
155
|
-
]
|
|
152
|
+
parts.push({
|
|
153
|
+
type: 'text',
|
|
154
|
+
text: msg.content ?? '',
|
|
155
|
+
})
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
const parts: ThreadAssistantMessagePart[] = []
|
|
159
|
-
|
|
160
158
|
const toolCallsJSON = (msg as any).tool_calls as FIXME<
|
|
161
159
|
string | undefined,
|
|
162
160
|
'Fixed by switching to Gram TS SDK.'
|
|
@@ -331,6 +329,11 @@ export function convertGramMessagesToUIMessages(messages: GramChatMessage[]): {
|
|
|
331
329
|
const uiMessages: { parentId: string | null; message: UIMessage }[] = []
|
|
332
330
|
let prevId: string | null = null
|
|
333
331
|
|
|
332
|
+
// Track tool call IDs across messages to deduplicate. The server accumulates
|
|
333
|
+
// all tool calls from a turn into each message, so without this, every
|
|
334
|
+
// assistant message in a multi-step tool use flow would show the full count.
|
|
335
|
+
const seenToolCallIds = new Set<string>()
|
|
336
|
+
|
|
334
337
|
for (const msg of messages) {
|
|
335
338
|
switch (msg.role) {
|
|
336
339
|
case 'developer':
|
|
@@ -361,6 +364,7 @@ export function convertGramMessagesToUIMessages(messages: GramChatMessage[]): {
|
|
|
361
364
|
break
|
|
362
365
|
}
|
|
363
366
|
case 'user': {
|
|
367
|
+
seenToolCallIds.clear()
|
|
364
368
|
uiMessages.push({
|
|
365
369
|
parentId: prevId,
|
|
366
370
|
message: {
|
|
@@ -382,7 +386,8 @@ export function convertGramMessagesToUIMessages(messages: GramChatMessage[]): {
|
|
|
382
386
|
role: 'assistant',
|
|
383
387
|
parts: convertGramMessagePartsToUIMessageParts(
|
|
384
388
|
msg,
|
|
385
|
-
toolCallResults
|
|
389
|
+
toolCallResults,
|
|
390
|
+
seenToolCallIds
|
|
386
391
|
),
|
|
387
392
|
} satisfies UIMessage,
|
|
388
393
|
}
|
|
@@ -403,7 +408,8 @@ export function convertGramMessagesToUIMessages(messages: GramChatMessage[]): {
|
|
|
403
408
|
|
|
404
409
|
export function convertGramMessagePartsToUIMessageParts(
|
|
405
410
|
msg: UserMessage | AssistantMessage,
|
|
406
|
-
toolResults: Map<string, ToolResponseMessage
|
|
411
|
+
toolResults: Map<string, ToolResponseMessage>,
|
|
412
|
+
seenToolCallIds?: Set<string>
|
|
407
413
|
): UIMessage['parts'] {
|
|
408
414
|
const uiparts: UIMessage['parts'] = []
|
|
409
415
|
|
|
@@ -480,6 +486,12 @@ export function convertGramMessagePartsToUIMessageParts(
|
|
|
480
486
|
}
|
|
481
487
|
|
|
482
488
|
for (const tc of toolCalls) {
|
|
489
|
+
// The server accumulates all tool calls from a turn into each message's
|
|
490
|
+
// tool_calls field. Deduplicate across messages so each tool call only
|
|
491
|
+
// appears in the first message that references it.
|
|
492
|
+
if (seenToolCallIds?.has(tc.id)) continue
|
|
493
|
+
seenToolCallIds?.add(tc.id)
|
|
494
|
+
|
|
483
495
|
const content = toolResults.get(tc.id)?.content
|
|
484
496
|
uiparts.push({
|
|
485
497
|
type: 'dynamic-tool',
|
package/src/types/index.ts
CHANGED
|
@@ -315,7 +315,7 @@ export interface ElementsConfig {
|
|
|
315
315
|
* @example
|
|
316
316
|
* const config: ElementsConfig = {
|
|
317
317
|
* thread: {
|
|
318
|
-
*
|
|
318
|
+
* showFeedback: true,
|
|
319
319
|
* followUpSuggestions: true,
|
|
320
320
|
* },
|
|
321
321
|
* }
|
|
@@ -853,7 +853,7 @@ export interface SidecarConfig extends ExpandableConfig {
|
|
|
853
853
|
* @example
|
|
854
854
|
* const config: ElementsConfig = {
|
|
855
855
|
* thread: {
|
|
856
|
-
*
|
|
856
|
+
* showFeedback: true,
|
|
857
857
|
* followUpSuggestions: true,
|
|
858
858
|
* },
|
|
859
859
|
* }
|
|
@@ -862,9 +862,9 @@ export interface ThreadConfig {
|
|
|
862
862
|
/**
|
|
863
863
|
* Whether to show feedback buttons (like/dislike) after assistant messages.
|
|
864
864
|
* When enabled, users can mark conversations as resolved or provide feedback.
|
|
865
|
-
* @default
|
|
865
|
+
* @default true
|
|
866
866
|
*/
|
|
867
|
-
|
|
867
|
+
showFeedback?: boolean
|
|
868
868
|
|
|
869
869
|
/**
|
|
870
870
|
* Whether to show AI-generated follow-up question suggestions after each assistant response.
|