@gram-ai/elements 1.20.1 → 1.21.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 (82) hide show
  1. package/bin/cli.js +14 -12
  2. package/dist/components/Chat/stories/ConnectionConfiguration.stories.d.ts +2 -2
  3. package/dist/components/Chat/stories/{ColorScheme.stories.d.ts → ErrorBoundary.stories.d.ts} +4 -4
  4. package/dist/components/Chat/stories/ToolApproval.stories.d.ts +2 -0
  5. package/dist/components/assistant-ui/error-boundary.d.ts +28 -0
  6. package/dist/components/ui/dialog.d.ts +1 -1
  7. package/dist/components/ui/tooltip.d.ts +3 -1
  8. package/dist/constants/tailwind.d.ts +1 -0
  9. package/dist/contexts/portal-container-context.d.ts +2 -0
  10. package/dist/contexts/portal-container.d.ts +7 -0
  11. package/dist/elements.cjs +1 -160
  12. package/dist/elements.cjs.map +1 -1
  13. package/dist/elements.css +1 -1
  14. package/dist/elements.js +11 -47174
  15. package/dist/elements.js.map +1 -1
  16. package/dist/hooks/usePortalContainer.d.ts +8 -0
  17. package/dist/hooks/useSession.d.ts +1 -2
  18. package/dist/index-B48xzOEm.cjs +169 -0
  19. package/dist/index-B48xzOEm.cjs.map +1 -0
  20. package/dist/{index-DaF9fGY-.js → index-BwdTXSZG.js} +4 -3
  21. package/dist/{index-DaF9fGY-.js.map → index-BwdTXSZG.js.map} +1 -1
  22. package/dist/index-C-iaUGd_.js +54687 -0
  23. package/dist/index-C-iaUGd_.js.map +1 -0
  24. package/dist/{index-B52U8PL6.cjs → index-D8g4LkEy.cjs} +3 -3
  25. package/dist/{index-B52U8PL6.cjs.map → index-D8g4LkEy.cjs.map} +1 -1
  26. package/dist/index.d.ts +3 -1
  27. package/dist/lib/auth.d.ts +2 -2
  28. package/dist/lib/errorTracking.config.d.ts +16 -0
  29. package/dist/lib/errorTracking.d.ts +24 -0
  30. package/dist/lib/tools.d.ts +3 -2
  31. package/dist/plugins.cjs +1 -1
  32. package/dist/plugins.js +1 -1
  33. package/dist/profiler-WPgSewiM.js +278 -0
  34. package/dist/profiler-WPgSewiM.js.map +1 -0
  35. package/dist/profiler-j7uDglf5.cjs +2 -0
  36. package/dist/profiler-j7uDglf5.cjs.map +1 -0
  37. package/dist/startRecording-Cahc4WH4.cjs +3 -0
  38. package/dist/startRecording-Cahc4WH4.cjs.map +1 -0
  39. package/dist/startRecording-DpwlHYPJ.js +1212 -0
  40. package/dist/startRecording-DpwlHYPJ.js.map +1 -0
  41. package/dist/types/index.d.ts +45 -15
  42. package/package.json +16 -2
  43. package/src/components/Chat/index.tsx +39 -3
  44. package/src/components/Chat/stories/Composer.stories.tsx +0 -7
  45. package/src/components/Chat/stories/ConnectionConfiguration.stories.tsx +7 -14
  46. package/src/components/Chat/stories/CustomComponents.stories.tsx +0 -7
  47. package/src/components/Chat/stories/Density.stories.tsx +0 -7
  48. package/src/components/Chat/stories/ErrorBoundary.stories.tsx +202 -0
  49. package/src/components/Chat/stories/FrontendTools.stories.tsx +0 -7
  50. package/src/components/Chat/stories/Model.stories.tsx +0 -7
  51. package/src/components/Chat/stories/Plugins.stories.tsx +0 -7
  52. package/src/components/Chat/stories/Radius.stories.tsx +0 -7
  53. package/src/components/Chat/stories/ToolApproval.stories.tsx +51 -7
  54. package/src/components/Chat/stories/Tools.stories.tsx +0 -7
  55. package/src/components/Chat/stories/Variants.stories.tsx +5 -2
  56. package/src/components/Chat/stories/Welcome.stories.tsx +0 -8
  57. package/src/components/assistant-ui/assistant-modal.tsx +4 -1
  58. package/src/components/assistant-ui/assistant-sidecar.tsx +5 -5
  59. package/src/components/assistant-ui/attachment.tsx +1 -4
  60. package/src/components/assistant-ui/error-boundary.tsx +119 -0
  61. package/src/components/assistant-ui/thread-list.tsx +3 -1
  62. package/src/components/assistant-ui/thread.tsx +7 -8
  63. package/src/components/ui/dialog.tsx +10 -1
  64. package/src/components/ui/popover.tsx +10 -12
  65. package/src/components/ui/tooltip.tsx +7 -2
  66. package/src/constants/tailwind.ts +2 -0
  67. package/src/contexts/ElementsProvider.tsx +29 -2
  68. package/src/contexts/portal-container-context.ts +4 -0
  69. package/src/contexts/portal-container.tsx +20 -0
  70. package/src/global.css +129 -16
  71. package/src/hooks/useAuth.ts +6 -16
  72. package/src/hooks/usePortalContainer.ts +16 -0
  73. package/src/hooks/useSession.ts +1 -3
  74. package/src/index.ts +5 -0
  75. package/src/lib/api.test.ts +5 -5
  76. package/src/lib/auth.ts +4 -4
  77. package/src/lib/errorTracking.config.ts +16 -0
  78. package/src/lib/errorTracking.ts +104 -0
  79. package/src/lib/tools.ts +37 -8
  80. package/src/types/index.ts +48 -16
  81. package/src/vite-env.d.ts +3 -0
  82. package/src/components/Chat/stories/ColorScheme.stories.tsx +0 -52
@@ -11,13 +11,6 @@ const meta: Meta<typeof Chat> = {
11
11
  parameters: {
12
12
  layout: 'fullscreen',
13
13
  },
14
- decorators: [
15
- (Story) => (
16
- <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
17
- <Story />
18
- </div>
19
- ),
20
- ],
21
14
  } satisfies Meta<typeof Chat>
22
15
 
23
16
  export default meta
@@ -8,13 +8,6 @@ const meta: Meta<typeof Chat> = {
8
8
  parameters: {
9
9
  layout: 'fullscreen',
10
10
  },
11
- decorators: [
12
- (Story) => (
13
- <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
14
- <Story />
15
- </div>
16
- ),
17
- ],
18
11
  } satisfies Meta<typeof Chat>
19
12
 
20
13
  export default meta
@@ -8,13 +8,6 @@ const meta: Meta<typeof Chat> = {
8
8
  parameters: {
9
9
  layout: 'fullscreen',
10
10
  },
11
- decorators: [
12
- (Story) => (
13
- <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
14
- <Story />
15
- </div>
16
- ),
17
- ],
18
11
  } satisfies Meta<typeof Chat>
19
12
 
20
13
  export default meta
@@ -8,13 +8,6 @@ const meta: Meta<typeof Chat> = {
8
8
  parameters: {
9
9
  layout: 'fullscreen',
10
10
  },
11
- decorators: [
12
- (Story) => (
13
- <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
14
- <Story />
15
- </div>
16
- ),
17
- ],
18
11
  } satisfies Meta<typeof Chat>
19
12
 
20
13
  export default meta
@@ -10,13 +10,6 @@ const meta: Meta<typeof Chat> = {
10
10
  parameters: {
11
11
  layout: 'fullscreen',
12
12
  },
13
- decorators: [
14
- (Story) => (
15
- <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
16
- <Story />
17
- </div>
18
- ),
19
- ],
20
13
  } satisfies Meta<typeof Chat>
21
14
 
22
15
  export default meta
@@ -44,6 +37,30 @@ SingleTool.parameters = {
44
37
  },
45
38
  }
46
39
 
40
+ export const SingleToolWithFunction: Story = () => <Chat />
41
+ SingleToolWithFunction.storyName =
42
+ 'Single Tool Requiring Approval with Function'
43
+ SingleToolWithFunction.parameters = {
44
+ elements: {
45
+ config: {
46
+ variant: 'standalone',
47
+ tools: {
48
+ toolsRequiringApproval: ({ toolName }: { toolName: string }) =>
49
+ toolName.endsWith('salutation'),
50
+ },
51
+ welcome: {
52
+ suggestions: [
53
+ {
54
+ title: 'Call a tool requiring approval',
55
+ label: 'Get a salutation',
56
+ action: 'Get a salutation',
57
+ },
58
+ ],
59
+ },
60
+ },
61
+ },
62
+ }
63
+
47
64
  export const MultipleGroupedTools: Story = () => <Chat />
48
65
  MultipleGroupedTools.storyName = 'Multiple Grouped Tools'
49
66
  MultipleGroupedTools.parameters = {
@@ -108,3 +125,30 @@ FrontendTool.parameters = {
108
125
  },
109
126
  },
110
127
  }
128
+
129
+ export const FrontendToolWithFunction: Story = () => <Chat />
130
+ FrontendToolWithFunction.storyName =
131
+ 'Frontend Tool Requiring Approval with Function'
132
+ FrontendToolWithFunction.parameters = {
133
+ elements: {
134
+ config: {
135
+ variant: 'standalone',
136
+ tools: {
137
+ frontendTools: {
138
+ deleteFile,
139
+ },
140
+ toolsRequiringApproval: ({ toolName }: { toolName: string }) =>
141
+ toolName.startsWith('delete'),
142
+ },
143
+ welcome: {
144
+ suggestions: [
145
+ {
146
+ title: 'Delete a file',
147
+ label: 'Delete a file',
148
+ action: 'Delete file with ID 123',
149
+ },
150
+ ],
151
+ },
152
+ },
153
+ },
154
+ }
@@ -9,13 +9,6 @@ const meta: Meta<typeof Chat> = {
9
9
  parameters: {
10
10
  layout: 'fullscreen',
11
11
  },
12
- decorators: [
13
- (Story) => (
14
- <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
15
- <Story />
16
- </div>
17
- ),
18
- ],
19
12
  } satisfies Meta<typeof Chat>
20
13
 
21
14
  export default meta
@@ -1,6 +1,7 @@
1
1
  import { Chat } from '..'
2
2
  import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite'
3
3
  import { ThreadList } from '@/components/assistant-ui/thread-list'
4
+ import { ROOT_SELECTOR } from '@/constants/tailwind'
4
5
 
5
6
  const meta: Meta<typeof Chat> = {
6
7
  title: 'Chat/Variants',
@@ -46,8 +47,10 @@ export const StandaloneWithHistory: StoryObj<typeof Chat> = {
46
47
  }
47
48
  StandaloneWithHistory.decorators = [
48
49
  (Story) => (
49
- <div className="m-auto flex h-screen w-full items-center justify-center border bg-linear-to-r from-violet-600 to-indigo-800">
50
- <Story />
50
+ <div className={ROOT_SELECTOR}>
51
+ <div className="m-auto flex h-screen w-full items-center justify-center border bg-linear-to-r from-violet-600 to-indigo-800">
52
+ <Story />
53
+ </div>
51
54
  </div>
52
55
  ),
53
56
  ]
@@ -1,4 +1,3 @@
1
- import React from 'react'
2
1
  import { Chat } from '..'
3
2
  import type { Meta, StoryFn } from '@storybook/react-vite'
4
3
 
@@ -8,13 +7,6 @@ const meta: Meta<typeof Chat> = {
8
7
  parameters: {
9
8
  layout: 'fullscreen',
10
9
  },
11
- decorators: [
12
- (Story) => (
13
- <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
14
- <Story />
15
- </div>
16
- ),
17
- ],
18
10
  } satisfies Meta<typeof Chat>
19
11
 
20
12
  export default meta
@@ -11,6 +11,7 @@ import {
11
11
  import { LazyMotion, domMax, AnimatePresence, MotionConfig } from 'motion/react'
12
12
  import * as m from 'motion/react-m'
13
13
 
14
+ import { ErrorBoundary } from '@/components/assistant-ui/error-boundary'
14
15
  import { Thread } from '@/components/assistant-ui/thread'
15
16
  import { ThreadList } from '@/components/assistant-ui/thread-list'
16
17
  import { useThemeProps } from '@/hooks/useThemeProps'
@@ -240,7 +241,9 @@ export const AssistantModal: FC<AssistantModalProps> = ({ className }) => {
240
241
 
241
242
  {/* Thread content */}
242
243
  <div className="aui-modal-thread w-full flex-1 overflow-hidden">
243
- <Thread />
244
+ <ErrorBoundary>
245
+ <Thread />
246
+ </ErrorBoundary>
244
247
  </div>
245
248
  </m.div>
246
249
  </m.div>
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { type FC } from 'react'
4
4
  import { Loader, PanelRightClose, PanelRightOpen } from 'lucide-react'
5
+ import { ErrorBoundary } from '@/components/assistant-ui/error-boundary'
5
6
  import { Thread } from '@/components/assistant-ui/thread'
6
7
  import { ThreadList } from '@/components/assistant-ui/thread-list'
7
8
  import { TooltipIconButton } from '@/components/assistant-ui/tooltip-icon-button'
@@ -36,10 +37,7 @@ export const AssistantSidecar: FC<AssistantSidecarProps> = ({ className }) => {
36
37
  return (
37
38
  <LazyMotion features={domMax}>
38
39
  <m.div
39
- initial={{
40
- width: dimensions?.default?.width ?? '400px',
41
- height: dimensions?.default?.height ?? '100vh',
42
- }}
40
+ initial={false}
43
41
  animate={{
44
42
  width: isExpanded
45
43
  ? (dimensions?.expanded?.width ?? '800px')
@@ -99,7 +97,9 @@ export const AssistantSidecar: FC<AssistantSidecarProps> = ({ className }) => {
99
97
 
100
98
  {/* Thread content */}
101
99
  <div className="aui-sidecar-content flex-1 overflow-hidden">
102
- <Thread />
100
+ <ErrorBoundary>
101
+ <Thread />
102
+ </ErrorBoundary>
103
103
  </div>
104
104
  </div>
105
105
  </m.div>
@@ -87,10 +87,7 @@ const AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => {
87
87
 
88
88
  return (
89
89
  <Dialog>
90
- <DialogTrigger
91
- className="aui-attachment-preview-trigger hover:bg-accent/50 cursor-pointer transition-colors"
92
- asChild
93
- >
90
+ <DialogTrigger className="aui-attachment-preview-trigger" asChild>
94
91
  {children}
95
92
  </DialogTrigger>
96
93
  <DialogContent className="aui-attachment-preview-dialog-content [&_svg]:text-background [&>button]:bg-foreground/60 [&>button]:hover:[&_svg]:text-destructive p-2 sm:max-w-3xl [&>button]:rounded-full [&>button]:p-1 [&>button]:opacity-100 [&>button]:!ring-0">
@@ -0,0 +1,119 @@
1
+ 'use client'
2
+
3
+ import { AlertCircle } from 'lucide-react'
4
+ import { Component, type ErrorInfo, type ReactNode } from 'react'
5
+ import { trackError } from '@/lib/errorTracking'
6
+ import { cn } from '@/lib/utils'
7
+ import { Button } from '../ui/button'
8
+
9
+ interface ErrorBoundaryProps {
10
+ children: ReactNode
11
+ fallback?: ReactNode
12
+ onError?: (error: Error, errorInfo: ErrorInfo) => void
13
+ onReset?: () => void
14
+ }
15
+
16
+ interface ErrorBoundaryState {
17
+ hasError: boolean
18
+ error: Error | null
19
+ resetKey: number
20
+ }
21
+
22
+ interface ErrorFallbackProps {
23
+ error: Error | null
24
+ onRetry: () => void
25
+ }
26
+
27
+ // eslint-disable-next-line react-refresh/only-export-components
28
+ const ErrorFallback = ({ error, onRetry }: ErrorFallbackProps) => {
29
+ return (
30
+ <div
31
+ className={cn(
32
+ 'aui-root aui-error-boundary bg-background flex h-full w-full flex-col items-center justify-center p-6'
33
+ )}
34
+ >
35
+ <div className="flex flex-col items-center gap-4 text-center">
36
+ <div className="text-destructive">
37
+ <AlertCircle className="size-12 stroke-[1.5px]" />
38
+ </div>
39
+ <div className="flex flex-col gap-2">
40
+ <h3 className="text-foreground text-xl font-semibold">
41
+ Something went wrong
42
+ </h3>
43
+ <p className="text-muted-foreground text-base">
44
+ An error occurred while loading the chat.
45
+ </p>
46
+ {error && (
47
+ <p className="text-muted-foreground/60 max-w-md truncate text-sm">
48
+ {error.message}
49
+ </p>
50
+ )}
51
+ </div>
52
+ <Button onClick={onRetry} variant="default" className="mt-2">
53
+ Try again
54
+ </Button>
55
+ </div>
56
+ </div>
57
+ )
58
+ }
59
+
60
+ // eslint-disable-next-line react-refresh/only-export-components
61
+ const Remounter = ({ children }: { children: ReactNode }) => <>{children}</>
62
+
63
+ /**
64
+ * Global error boundary for the Elements library. Catches unexpected errors and renders a fallback UI.
65
+ * We wrap the assistant-modal, assistant-sidecar, and chat components with this error boundary.
66
+ * Each variant needs to have the error boundary rendered at the appropriate level e.g if using
67
+ * the widget variant, then the error screen must be rendered within the widget modal.
68
+ * TODO: We should add more granular error boundaries (e.g wrapping AssistantMessage, ThreadWelcome, etc.)
69
+ * TODO: We should also wrap ChatHistory, which may yield its own errors.
70
+ */
71
+ export class ErrorBoundary extends Component<
72
+ ErrorBoundaryProps,
73
+ ErrorBoundaryState
74
+ > {
75
+ constructor(props: ErrorBoundaryProps) {
76
+ super(props)
77
+ this.state = { hasError: false, error: null, resetKey: 0 }
78
+ }
79
+
80
+ static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
81
+ return { hasError: true, error }
82
+ }
83
+
84
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
85
+ // Track error to Datadog RUM
86
+ trackError(error, {
87
+ source: 'error-boundary',
88
+ componentStack: errorInfo.componentStack ?? undefined,
89
+ })
90
+ this.props.onError?.(error, errorInfo)
91
+ }
92
+
93
+ handleRetry = () => {
94
+ // Increment resetKey to force remount of children, reinitializing the chat
95
+ this.setState((state) => ({
96
+ hasError: false,
97
+ error: null,
98
+ resetKey: state.resetKey + 1,
99
+ }))
100
+ this.props.onReset?.()
101
+ }
102
+
103
+ render(): ReactNode {
104
+ if (this.state.hasError) {
105
+ if (this.props.fallback) {
106
+ return this.props.fallback
107
+ }
108
+
109
+ return (
110
+ <ErrorFallback error={this.state.error} onRetry={this.handleRetry} />
111
+ )
112
+ }
113
+
114
+ // Use Remounter with key to force unmount/remount of children when retry is clicked
115
+ return (
116
+ <Remounter key={this.state.resetKey}>{this.props.children}</Remounter>
117
+ )
118
+ }
119
+ }
@@ -11,6 +11,7 @@ import { Skeleton } from '@/components/ui/skeleton'
11
11
  import { useRadius } from '@/hooks/useRadius'
12
12
  import { cn } from '@/lib/utils'
13
13
  import { useDensity } from '@/hooks/useDensity'
14
+ import { ROOT_SELECTOR } from '@/constants/tailwind'
14
15
 
15
16
  interface ThreadListProps {
16
17
  className?: string
@@ -23,7 +24,8 @@ export const ThreadList: FC<ThreadListProps> = ({ className }) => {
23
24
  className={cn(
24
25
  'aui-root aui-thread-list-root bg-background flex flex-col items-stretch',
25
26
  d('gap-sm'),
26
- className
27
+ className,
28
+ ROOT_SELECTOR
27
29
  )}
28
30
  >
29
31
  <div
@@ -52,19 +52,18 @@ import {
52
52
  } from '../ui/tooltip'
53
53
  import { ToolGroup } from './tool-group'
54
54
 
55
- const ApiKeyWarning = () => (
55
+ const StaticSessionWarning = () => (
56
56
  <div className="m-2 rounded-md border border-amber-500 bg-amber-100 px-4 py-3 text-sm text-amber-800 dark:border-amber-600 dark:bg-amber-900/30 dark:text-amber-200">
57
- <strong>Warning:</strong> You are using an API key directly in the client.
58
- Please{' '}
57
+ <strong>Warning:</strong> You are using a static session token in the
58
+ client. It will expire shortly. Please{' '}
59
59
  <a
60
60
  href="https://github.com/speakeasy-api/gram/tree/main/elements#setting-up-your-backend"
61
61
  target="_blank"
62
62
  rel="noopener noreferrer"
63
63
  className="text-amber-700 underline hover:text-amber-800 dark:text-amber-300 dark:hover:text-amber-200"
64
64
  >
65
- set up a session endpoint
66
- </a>{' '}
67
- before deploying to production.
65
+ set up a session endpoint to avoid this warning.
66
+ </a>
68
67
  </div>
69
68
  )
70
69
 
@@ -77,7 +76,7 @@ export const Thread: FC<ThreadProps> = ({ className }) => {
77
76
  const d = useDensity()
78
77
  const { config } = useElements()
79
78
  const components = config.components ?? {}
80
- const showApiKeyWarning = config.api && 'UNSAFE_apiKey' in config.api
79
+ const showStaticSessionWarning = config.api && 'sessionToken' in config.api
81
80
 
82
81
  return (
83
82
  <LazyMotion features={domAnimation}>
@@ -103,7 +102,7 @@ export const Thread: FC<ThreadProps> = ({ className }) => {
103
102
  )}
104
103
  </ThreadPrimitive.If>
105
104
 
106
- {showApiKeyWarning && <ApiKeyWarning />}
105
+ {showStaticSessionWarning && <StaticSessionWarning />}
107
106
 
108
107
  <ThreadPrimitive.Messages
109
108
  components={{
@@ -3,6 +3,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog'
3
3
  import { XIcon } from 'lucide-react'
4
4
 
5
5
  import { cn } from '@/lib/utils'
6
+ import { usePortalContainer } from '@/hooks/usePortalContainer'
6
7
 
7
8
  function Dialog({
8
9
  ...props
@@ -17,9 +18,17 @@ function DialogTrigger({
17
18
  }
18
19
 
19
20
  function DialogPortal({
21
+ container,
20
22
  ...props
21
23
  }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
22
- return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
24
+ const portalContainer = usePortalContainer()
25
+ return (
26
+ <DialogPrimitive.Portal
27
+ data-slot="dialog-portal"
28
+ container={container ?? portalContainer}
29
+ {...props}
30
+ />
31
+ )
23
32
  }
24
33
 
25
34
  function DialogClose({
@@ -22,18 +22,16 @@ function PopoverContent({
22
22
  ...props
23
23
  }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
24
24
  return (
25
- <PopoverPrimitive.Portal>
26
- <PopoverPrimitive.Content
27
- data-slot="popover-content"
28
- align={align}
29
- sideOffset={sideOffset}
30
- className={cn(
31
- 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-20 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
32
- className
33
- )}
34
- {...props}
35
- />
36
- </PopoverPrimitive.Portal>
25
+ <PopoverPrimitive.Content
26
+ data-slot="popover-content"
27
+ align={align}
28
+ sideOffset={sideOffset}
29
+ className={cn(
30
+ 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-20 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
31
+ className
32
+ )}
33
+ {...props}
34
+ />
37
35
  )
38
36
  }
39
37
 
@@ -4,6 +4,7 @@ import * as React from 'react'
4
4
  import * as TooltipPrimitive from '@radix-ui/react-tooltip'
5
5
 
6
6
  import { cn } from '@/lib/utils'
7
+ import { usePortalContainer } from '@/hooks/usePortalContainer'
7
8
 
8
9
  function TooltipProvider({
9
10
  delayDuration = 0,
@@ -38,10 +39,14 @@ function TooltipContent({
38
39
  className,
39
40
  sideOffset = 0,
40
41
  children,
42
+ container,
41
43
  ...props
42
- }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
44
+ }: React.ComponentProps<typeof TooltipPrimitive.Content> & {
45
+ container?: HTMLElement | null
46
+ }) {
47
+ const portalContainer = usePortalContainer()
43
48
  return (
44
- <TooltipPrimitive.Portal>
49
+ <TooltipPrimitive.Portal container={container ?? portalContainer}>
45
50
  <TooltipPrimitive.Content
46
51
  data-slot="tooltip-content"
47
52
  sideOffset={sideOffset}
@@ -0,0 +1,2 @@
1
+ // Used to scope elements styles to the Elements library
2
+ export const ROOT_SELECTOR = 'gram-elements'
@@ -2,6 +2,7 @@ import { FrontendTools } from '@/components/FrontendTools'
2
2
  import { useMCPTools } from '@/hooks/useMCPTools'
3
3
  import { useToolApproval } from '@/hooks/useToolApproval'
4
4
  import { getApiUrl } from '@/lib/api'
5
+ import { initErrorTracking, trackError } from '@/lib/errorTracking'
5
6
  import { MODELS } from '@/lib/models'
6
7
  import {
7
8
  clearFrontendToolApprovalConfig,
@@ -125,6 +126,15 @@ const ElementsProviderWithApproval = ({
125
126
  plugins
126
127
  )
127
128
 
129
+ // Initialize error tracking on mount
130
+ useEffect(() => {
131
+ initErrorTracking({
132
+ enabled: config.errorTracking?.enabled,
133
+ projectSlug: config.projectSlug,
134
+ variant: config.variant,
135
+ })
136
+ }, [])
137
+
128
138
  const { data: mcpTools } = useMCPTools({
129
139
  auth,
130
140
  mcp: config.mcp,
@@ -157,7 +167,7 @@ const ElementsProviderWithApproval = ({
157
167
 
158
168
  // Set up frontend tool approval config for runtime checking
159
169
  useEffect(() => {
160
- if (config.tools?.toolsRequiringApproval?.length) {
170
+ if (config.tools?.toolsRequiringApproval) {
161
171
  setFrontendToolApprovalConfig(
162
172
  getApprovalHelpers(),
163
173
  config.tools.toolsRequiringApproval
@@ -173,6 +183,10 @@ const ElementsProviderWithApproval = ({
173
183
  // but runtime is created using transport. The ref gets populated after runtime creation.
174
184
  const runtimeRef = useRef<ReturnType<typeof useChatRuntime> | null>(null)
175
185
 
186
+ // Generate a stable chat ID for server-side persistence (when history is disabled)
187
+ // When history is enabled, the thread adapter manages chat IDs instead
188
+ const chatIdRef = useRef<string | null>(null)
189
+
176
190
  // Create chat transport configuration
177
191
  const transport = useMemo<ChatTransport<UIMessage>>(
178
192
  () => ({
@@ -183,18 +197,29 @@ const ElementsProviderWithApproval = ({
183
197
  throw new Error('Session is loading')
184
198
  }
185
199
 
200
+ // Generate chat ID on first message if not already set
201
+ if (!chatIdRef.current) {
202
+ chatIdRef.current = crypto.randomUUID()
203
+ }
204
+
186
205
  const context = runtimeRef.current?.thread.getModelContext()
187
206
  const frontendTools = toAISDKTools(
188
207
  getEnabledTools(context?.tools ?? {})
189
208
  )
190
209
 
210
+ // Include Gram-Chat-ID header for chat persistence
211
+ const headersWithChatId = {
212
+ ...auth.headers,
213
+ 'Gram-Chat-ID': chatIdRef.current,
214
+ }
215
+
191
216
  // Create OpenRouter model (only needed when not using custom model)
192
217
  const openRouterModel = usingCustomModel
193
218
  ? null
194
219
  : createOpenRouter({
195
220
  baseURL: apiUrl,
196
221
  apiKey: 'unused, but must be set',
197
- headers: auth.headers,
222
+ headers: headersWithChatId,
198
223
  })
199
224
 
200
225
  if (config.languageModel) {
@@ -234,12 +259,14 @@ const ElementsProviderWithApproval = ({
234
259
  abortSignal,
235
260
  onError: ({ error }) => {
236
261
  console.error('Stream error in onError callback:', error)
262
+ trackError(error, { source: 'streaming' })
237
263
  },
238
264
  })
239
265
 
240
266
  return result.toUIMessageStream()
241
267
  } catch (error) {
242
268
  console.error('Error creating stream:', error)
269
+ trackError(error, { source: 'stream-creation' })
243
270
  throw error
244
271
  }
245
272
  },
@@ -0,0 +1,4 @@
1
+ import { createContext, type RefObject } from 'react'
2
+
3
+ export const PortalContainerContext =
4
+ createContext<RefObject<HTMLElement | null> | null>(null)
@@ -0,0 +1,20 @@
1
+ 'use client'
2
+
3
+ import { type RefObject } from 'react'
4
+ import { PortalContainerContext } from './portal-container-context'
5
+
6
+ export { PortalContainerContext }
7
+
8
+ export function PortalContainerProvider({
9
+ containerRef,
10
+ children,
11
+ }: {
12
+ containerRef: RefObject<HTMLElement | null>
13
+ children: React.ReactNode
14
+ }) {
15
+ return (
16
+ <PortalContainerContext.Provider value={containerRef}>
17
+ {children}
18
+ </PortalContainerContext.Provider>
19
+ )
20
+ }