@gram-ai/elements 1.21.2 → 1.21.3

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 (45) hide show
  1. package/dist/components/Chat/stories/StyleIsolation.stories.d.ts +6 -0
  2. package/dist/components/Chat/stories/Theme.stories.d.ts +8 -0
  3. package/dist/components/ChatHistory.d.ts +5 -0
  4. package/dist/components/ShadowRoot.d.ts +8 -0
  5. package/dist/elements.cjs +1 -1
  6. package/dist/elements.css +1 -1
  7. package/dist/elements.js +1 -1
  8. package/dist/embedded.d.ts +0 -0
  9. package/dist/hooks/useGramThreadListAdapter.d.ts +7 -0
  10. package/dist/hooks/useMCPTools.d.ts +2 -1
  11. package/dist/index-CKBUBkLd.js +37757 -0
  12. package/dist/index-CKBUBkLd.js.map +1 -0
  13. package/dist/index-D5cSC22A.cjs +144 -0
  14. package/dist/index-D5cSC22A.cjs.map +1 -0
  15. package/dist/index.d.ts +1 -1
  16. package/dist/{profiler-DPH9zydw.cjs → profiler-BY6U7KRN.cjs} +2 -2
  17. package/dist/{profiler-DPH9zydw.cjs.map → profiler-BY6U7KRN.cjs.map} +1 -1
  18. package/dist/{profiler-D_HNXmyv.js → profiler-CRozVGVC.js} +2 -2
  19. package/dist/{profiler-D_HNXmyv.js.map → profiler-CRozVGVC.js.map} +1 -1
  20. package/dist/{startRecording-B97e4Mlu.cjs → startRecording-Bi82O_dZ.cjs} +2 -2
  21. package/dist/{startRecording-B97e4Mlu.cjs.map → startRecording-Bi82O_dZ.cjs.map} +1 -1
  22. package/dist/{startRecording-DOMzQAAr.js → startRecording-DQXktVbB.js} +2 -2
  23. package/dist/{startRecording-DOMzQAAr.js.map → startRecording-DQXktVbB.js.map} +1 -1
  24. package/dist/types/index.d.ts +6 -0
  25. package/package.json +13 -4
  26. package/src/components/Chat/index.tsx +4 -13
  27. package/src/components/Chat/stories/StyleIsolation.stories.tsx +41 -0
  28. package/src/components/Chat/stories/Theme.stories.tsx +74 -0
  29. package/src/components/Chat/stories/ToolApproval.stories.tsx +0 -1
  30. package/src/components/ChatHistory.tsx +16 -0
  31. package/src/components/ShadowRoot.tsx +90 -0
  32. package/src/components/assistant-ui/markdown-text.tsx +0 -2
  33. package/src/components/assistant-ui/thread-list.tsx +1 -3
  34. package/src/contexts/ElementsProvider.tsx +65 -9
  35. package/src/embedded.ts +3 -0
  36. package/src/global.css +45 -0
  37. package/src/hooks/useGramThreadListAdapter.tsx +90 -9
  38. package/src/hooks/useMCPTools.ts +4 -1
  39. package/src/index.ts +1 -1
  40. package/src/types/index.ts +7 -0
  41. package/src/vite-env.d.ts +5 -0
  42. package/dist/index-BVvrv2G3.cjs +0 -169
  43. package/dist/index-BVvrv2G3.cjs.map +0 -1
  44. package/dist/index-OU3wjArm.js +0 -54670
  45. package/dist/index-OU3wjArm.js.map +0 -1
@@ -46,7 +46,11 @@ import {
46
46
  import { useAuth } from '../hooks/useAuth'
47
47
  import { ElementsContext } from './contexts'
48
48
  import { ToolApprovalProvider } from './ToolApprovalContext'
49
- import { useGramThreadListAdapter } from '@/hooks/useGramThreadListAdapter'
49
+ import {
50
+ isLocalThreadId,
51
+ useGramThreadListAdapter,
52
+ } from '@/hooks/useGramThreadListAdapter'
53
+ import { ROOT_SELECTOR } from '@/constants/tailwind'
50
54
 
51
55
  export interface ElementsProviderProps {
52
56
  children: ReactNode
@@ -139,6 +143,7 @@ const ElementsProviderWithApproval = ({
139
143
  auth,
140
144
  mcp: config.mcp,
141
145
  environment: config.environment ?? {},
146
+ gramEnvironment: config.gramEnvironment,
142
147
  })
143
148
 
144
149
  // Store approval helpers in ref so they can be used in async contexts
@@ -187,6 +192,9 @@ const ElementsProviderWithApproval = ({
187
192
  // When history is enabled, the thread adapter manages chat IDs instead
188
193
  const chatIdRef = useRef<string | null>(null)
189
194
 
195
+ // Map to share local thread IDs to UUIDs between adapter and transport (for history mode)
196
+ const localIdToUuidMapRef = useRef(new Map<string, string>())
197
+
190
198
  // Create chat transport configuration
191
199
  const transport = useMemo<ChatTransport<UIMessage>>(
192
200
  () => ({
@@ -197,9 +205,47 @@ const ElementsProviderWithApproval = ({
197
205
  throw new Error('Session is loading')
198
206
  }
199
207
 
200
- // Generate chat ID on first message if not already set
201
- if (!chatIdRef.current) {
202
- chatIdRef.current = crypto.randomUUID()
208
+ // Get chat ID - try runtime's thread remoteId first (history mode),
209
+ // fall back to generated ID (non-history mode)
210
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
211
+ const runtimeAny = runtimeRef.current as any
212
+
213
+ // Try multiple paths to get thread info
214
+ const threadListItemState = runtimeAny?.threadListItem?.getState?.()
215
+ const threadsState = runtimeAny?.threads?.getState?.()
216
+
217
+ // Get the thread ID - try different sources
218
+ const threadRemoteId = threadListItemState?.remoteId as
219
+ | string
220
+ | undefined
221
+ const localThreadId = (threadListItemState?.id ??
222
+ threadsState?.mainThreadId ??
223
+ threadsState?.threadIds?.[0]) as string | undefined
224
+
225
+ let chatId = threadRemoteId
226
+
227
+ if (isLocalThreadId(chatId) || (!chatId && localThreadId)) {
228
+ const lookupKey = chatId ?? localThreadId
229
+ if (lookupKey) {
230
+ // For local thread IDs, check if we already have a UUID mapping
231
+ const existingUuid = localIdToUuidMapRef.current.get(lookupKey)
232
+ if (existingUuid) {
233
+ chatId = existingUuid
234
+ } else {
235
+ // Generate a new UUID and store the mapping
236
+ const newUuid = crypto.randomUUID()
237
+ localIdToUuidMapRef.current.set(lookupKey, newUuid)
238
+ chatId = newUuid
239
+ }
240
+ }
241
+ }
242
+
243
+ if (!chatId) {
244
+ // Non-history mode fallback - use stable chatIdRef
245
+ if (!chatIdRef.current) {
246
+ chatIdRef.current = crypto.randomUUID()
247
+ }
248
+ chatId = chatIdRef.current
203
249
  }
204
250
 
205
251
  const context = runtimeRef.current?.thread.getModelContext()
@@ -207,10 +253,13 @@ const ElementsProviderWithApproval = ({
207
253
  getEnabledTools(context?.tools ?? {})
208
254
  )
209
255
 
210
- // Include Gram-Chat-ID header for chat persistence
256
+ // Include Gram-Chat-ID header for chat persistence and Gram-Environment for environment selection
211
257
  const headersWithChatId = {
212
258
  ...auth.headers,
213
- 'Gram-Chat-ID': chatIdRef.current,
259
+ 'Gram-Chat-ID': chatId,
260
+ ...(config.gramEnvironment && {
261
+ 'Gram-Environment': config.gramEnvironment,
262
+ }),
214
263
  }
215
264
 
216
265
  // Create OpenRouter model (only needed when not using custom model)
@@ -317,6 +366,7 @@ const ElementsProviderWithApproval = ({
317
366
  contextValue={contextValue}
318
367
  runtimeRef={runtimeRef}
319
368
  frontendTools={frontendTools}
369
+ localIdToUuidMap={localIdToUuidMapRef.current}
320
370
  >
321
371
  {children}
322
372
  </ElementsProviderWithHistory>
@@ -344,6 +394,7 @@ interface ElementsProviderWithHistoryProps {
344
394
  contextValue: React.ContextType<typeof ElementsContext>
345
395
  runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>
346
396
  frontendTools: Record<string, AssistantTool>
397
+ localIdToUuidMap: Map<string, string>
347
398
  }
348
399
 
349
400
  const ElementsProviderWithHistory = ({
@@ -354,8 +405,13 @@ const ElementsProviderWithHistory = ({
354
405
  contextValue,
355
406
  runtimeRef,
356
407
  frontendTools,
408
+ localIdToUuidMap,
357
409
  }: ElementsProviderWithHistoryProps) => {
358
- const threadListAdapter = useGramThreadListAdapter({ apiUrl, headers })
410
+ const threadListAdapter = useGramThreadListAdapter({
411
+ apiUrl,
412
+ headers,
413
+ localIdToUuidMap,
414
+ })
359
415
 
360
416
  // Hook factory for creating the base chat runtime
361
417
  const useChatRuntimeHook = useCallback(() => {
@@ -381,7 +437,7 @@ const ElementsProviderWithHistory = ({
381
437
  <AssistantRuntimeProvider runtime={runtime}>
382
438
  <HistoryProvider>
383
439
  <ElementsContext.Provider value={contextValue}>
384
- {children}
440
+ <div className={`${ROOT_SELECTOR} h-full`}>{children}</div>
385
441
  <FrontendTools tools={frontendTools} />
386
442
  </ElementsContext.Provider>
387
443
  </HistoryProvider>
@@ -415,7 +471,7 @@ const ElementsProviderWithoutHistory = ({
415
471
  return (
416
472
  <AssistantRuntimeProvider runtime={runtime}>
417
473
  <ElementsContext.Provider value={contextValue}>
418
- {children}
474
+ <div className={`${ROOT_SELECTOR} h-full`}>{children}</div>
419
475
  <FrontendTools tools={frontendTools} />
420
476
  </ElementsContext.Provider>
421
477
  </AssistantRuntimeProvider>
@@ -0,0 +1,3 @@
1
+ // Side-effect import to include embedded CSS in build
2
+ // Use this CSS for embedding elements in apps that already have Tailwind CSS
3
+ import './global-embedded.css'
package/src/global.css CHANGED
@@ -247,3 +247,48 @@
247
247
  [data-radius='pill'] .gram-elements {
248
248
  --radius: 9999px;
249
249
  }
250
+
251
+ /* assistant-ui loading dot styles (from @assistant-ui/react-markdown/styles/dot.css) */
252
+ @keyframes aui-pulse {
253
+ 50% {
254
+ opacity: 0.5;
255
+ }
256
+ }
257
+
258
+ .gram-elements :where(.aui-md[data-status='running']):empty::after,
259
+ .gram-elements
260
+ :where(.aui-md[data-status='running'])
261
+ > :where(:not(ol):not(ul):not(pre)):last-child::after,
262
+ .gram-elements :where(.aui-md[data-status='running']) > pre:last-child code::after,
263
+ .gram-elements
264
+ :where(.aui-md[data-status='running'])
265
+ > :where(:is(ol, ul):last-child)
266
+ > :where(li:last-child:not(:has(* > li)))::after,
267
+ .gram-elements
268
+ :where(.aui-md[data-status='running'])
269
+ > :where(:is(ol, ul):last-child)
270
+ > :where(li:last-child)
271
+ > :where(:is(ol, ul):last-child)
272
+ > :where(li:last-child:not(:has(* > li)))::after,
273
+ .gram-elements
274
+ :where(.aui-md[data-status='running'])
275
+ > :where(:is(ol, ul):last-child)
276
+ > :where(li:last-child)
277
+ > :where(:is(ol, ul):last-child)
278
+ > :where(li:last-child)
279
+ > :where(:is(ol, ul):last-child)
280
+ > :where(li:last-child)::after {
281
+ animation: aui-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
282
+ font-family:
283
+ ui-sans-serif,
284
+ system-ui,
285
+ sans-serif,
286
+ 'Apple Color Emoji',
287
+ 'Segoe UI Emoji',
288
+ 'Segoe UI Symbol',
289
+ 'Noto Color Emoji';
290
+ --aui-content: '\25cf';
291
+ content: var(--aui-content);
292
+ margin-left: 0.25rem;
293
+ margin-right: 0.25rem;
294
+ }
@@ -6,7 +6,7 @@ import {
6
6
  useAssistantApi,
7
7
  type AssistantApi,
8
8
  } from '@assistant-ui/react'
9
- import type { AssistantStream } from 'assistant-stream'
9
+ import { createAssistantStream, type AssistantStream } from 'assistant-stream'
10
10
  import {
11
11
  GramChatOverview,
12
12
  GramChat,
@@ -21,9 +21,26 @@ import {
21
21
  type PropsWithChildren,
22
22
  } from 'react'
23
23
 
24
+ /**
25
+ * Prefix used by assistant-ui for local thread IDs that haven't been persisted yet.
26
+ * This is an internal implementation detail of assistant-ui's RemoteThreadListThreadListRuntimeCore.
27
+ * If the library changes this prefix, we only need to update it here.
28
+ */
29
+ const LOCAL_THREAD_ID_PREFIX = '__LOCALID_'
30
+
31
+ /**
32
+ * Checks if a thread ID is a local (unpersisted) thread ID.
33
+ * Local IDs are generated by assistant-ui before the thread is initialized with a remote ID.
34
+ */
35
+ export function isLocalThreadId(threadId: string | undefined): boolean {
36
+ return !!threadId?.startsWith(LOCAL_THREAD_ID_PREFIX)
37
+ }
38
+
24
39
  export interface ThreadListAdapterOptions {
25
40
  apiUrl: string
26
41
  headers: Record<string, string>
42
+ /** Map to translate local thread IDs to UUIDs (shared with transport) */
43
+ localIdToUuidMap?: Map<string, string>
27
44
  }
28
45
 
29
46
  interface ListChatsResponse {
@@ -227,6 +244,26 @@ export function useGramThreadListAdapter(
227
244
  },
228
245
 
229
246
  async initialize(threadId: string) {
247
+ // For new threads (local IDs), check if sendMessages already created a UUID
248
+ if (isLocalThreadId(threadId)) {
249
+ // Check if transport already generated a UUID for this local ID
250
+ const existingUuid =
251
+ optionsRef.current.localIdToUuidMap?.get(threadId)
252
+ if (existingUuid) {
253
+ return {
254
+ remoteId: existingUuid,
255
+ externalId: existingUuid,
256
+ }
257
+ }
258
+ // Otherwise generate a new one and store it
259
+ const uuid = crypto.randomUUID()
260
+ optionsRef.current.localIdToUuidMap?.set(threadId, uuid)
261
+ return {
262
+ remoteId: uuid,
263
+ externalId: uuid,
264
+ }
265
+ }
266
+ // For existing threads, use the ID as-is
230
267
  return {
231
268
  remoteId: threadId,
232
269
  externalId: threadId,
@@ -250,21 +287,65 @@ export function useGramThreadListAdapter(
250
287
  },
251
288
 
252
289
  async generateTitle(
253
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
254
- _remoteId: string,
290
+ remoteId: string,
255
291
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
256
292
  _messages: readonly ThreadMessage[]
257
293
  ): Promise<AssistantStream> {
258
- // Return an empty stream that immediately completes
259
- // Server generates titles automatically, so we just provide a placeholder
260
- return new ReadableStream({
261
- start(controller) {
294
+ // Skip if this is a local ID that hasn't been persisted yet
295
+ if (!remoteId || isLocalThreadId(remoteId)) {
296
+ return createAssistantStream((controller) => {
262
297
  controller.close()
263
- },
264
- }) as AssistantStream
298
+ })
299
+ }
300
+
301
+ // Call the server to generate and persist the title
302
+ try {
303
+ const response = await fetch(
304
+ `${optionsRef.current.apiUrl}/rpc/chat.generateTitle`,
305
+ {
306
+ method: 'POST',
307
+ headers: {
308
+ ...optionsRef.current.headers,
309
+ 'Content-Type': 'application/json',
310
+ },
311
+ body: JSON.stringify({ id: remoteId }),
312
+ }
313
+ )
314
+
315
+ if (response.ok) {
316
+ const result = (await response.json()) as { title: string }
317
+ const title = result.title || 'New Chat'
318
+
319
+ // Return a stream that emits the title as text
320
+ return createAssistantStream((controller) => {
321
+ controller.appendText(title)
322
+ controller.close()
323
+ })
324
+ }
325
+
326
+ // 404 is expected for new chats that haven't been persisted yet
327
+ if (response.status !== 404) {
328
+ console.error('Error generating title:', response.status)
329
+ }
330
+ } catch (error) {
331
+ console.error('Error generating title:', error)
332
+ }
333
+
334
+ // Fallback: return empty stream
335
+ return createAssistantStream((controller) => {
336
+ controller.close()
337
+ })
265
338
  },
266
339
 
267
340
  async fetch(threadId: string) {
341
+ // Skip if this is a local ID that hasn't been persisted yet
342
+ if (!threadId || isLocalThreadId(threadId)) {
343
+ return {
344
+ remoteId: threadId,
345
+ status: 'regular' as const,
346
+ }
347
+ }
348
+
268
349
  try {
269
350
  const response = await fetch(
270
351
  `${optionsRef.current.apiUrl}/rpc/chat.load?id=${encodeURIComponent(threadId)}`,
@@ -11,17 +11,19 @@ export function useMCPTools({
11
11
  auth,
12
12
  mcp,
13
13
  environment,
14
+ gramEnvironment,
14
15
  }: {
15
16
  auth: Auth
16
17
  mcp: string | undefined
17
18
  environment: Record<string, unknown>
19
+ gramEnvironment?: string
18
20
  }): UseQueryResult<MCPToolsResult, Error> {
19
21
  const authQueryKey = Object.entries(auth.headers ?? {}).map(
20
22
  (k, v) => `${k}:${v}`
21
23
  )
22
24
 
23
25
  const queryResult = useQuery({
24
- queryKey: ['mcpTools', mcp, ...authQueryKey],
26
+ queryKey: ['mcpTools', mcp, gramEnvironment, ...authQueryKey],
25
27
  queryFn: async () => {
26
28
  assert(!auth.isLoading, 'No auth found')
27
29
  assert(mcp, 'No MCP URL found')
@@ -34,6 +36,7 @@ export function useMCPTools({
34
36
  headers: {
35
37
  ...transformEnvironmentToHeaders(environment ?? {}),
36
38
  ...auth.headers,
39
+ ...(gramEnvironment && { 'Gram-Environment': gramEnvironment }),
37
40
  },
38
41
  },
39
42
  })
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@ export { useElements } from './hooks/useElements'
9
9
 
10
10
  // Core Components
11
11
  export { Chat } from '@/components/Chat'
12
- export { ThreadList as ChatHistory } from '@/components/assistant-ui/thread-list'
12
+ export { ChatHistory } from '@/components/ChatHistory'
13
13
 
14
14
  // Frontend Tools
15
15
  export { defineFrontendTool } from './lib/tools'
@@ -111,6 +111,13 @@ export interface ElementsConfig {
111
111
  */
112
112
  environment?: Record<string, unknown>
113
113
 
114
+ /**
115
+ * The environment slug to use for resolving secrets.
116
+ * When specified, this is sent as the Gram-Environment header to select
117
+ * which environment's secrets to use for tool execution.
118
+ */
119
+ gramEnvironment?: string
120
+
114
121
  /**
115
122
  * The layout variant for the chat interface.
116
123
  *
package/src/vite-env.d.ts CHANGED
@@ -13,3 +13,8 @@ interface ImportMetaEnv {
13
13
  interface ImportMeta {
14
14
  readonly env: ImportMetaEnv
15
15
  }
16
+
17
+ declare module '*.css?inline' {
18
+ const content: string
19
+ export default content
20
+ }