@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
package/src/global.css CHANGED
@@ -1,6 +1,7 @@
1
- @import "tailwindcss";
2
- @import "tw-animate-css";
3
- @import "tw-shimmer";
1
+ @import 'tw-animate-css';
2
+ @import 'tw-shimmer';
3
+ @layer theme, base, components, utilities;
4
+ @import 'tailwindcss/theme.css' layer(theme);
4
5
 
5
6
  @custom-variant dark (&:is(.dark *));
6
7
 
@@ -42,7 +43,20 @@
42
43
  --color-sidebar-ring: var(--sidebar-ring);
43
44
  }
44
45
 
45
- :root {
46
+ /* Scope ALL variables to .gram-elements instead of :root */
47
+ .gram-elements {
48
+ /* Scoped preflight/base styles */
49
+ font-family:
50
+ ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
51
+ 'Segoe UI Symbol', 'Noto Color Emoji';
52
+ font-feature-settings: normal;
53
+ font-variation-settings: normal;
54
+ -webkit-font-smoothing: antialiased;
55
+ -moz-osx-font-smoothing: grayscale;
56
+ line-height: 1.5;
57
+ -webkit-tap-highlight-color: transparent;
58
+ tab-size: 4;
59
+
46
60
  /* Theme: Radius - set via data-radius attribute */
47
61
  --radius: 0.625rem;
48
62
  --background: oklch(1 0 0);
@@ -78,7 +92,9 @@
78
92
  --sidebar-ring: oklch(0.705 0.015 286.067);
79
93
  }
80
94
 
81
- .dark {
95
+ /* Dark mode scoped to .gram-elements */
96
+ .gram-elements.dark,
97
+ .dark .gram-elements {
82
98
  --background: oklch(0.141 0.005 285.823);
83
99
  --foreground: oklch(0.985 0 0);
84
100
  --card: oklch(0.21 0.006 285.885);
@@ -113,24 +129,121 @@
113
129
  }
114
130
 
115
131
  @layer base {
116
- * {
117
- @apply border-border outline-ring/50;
118
- }
119
- body {
120
- @apply bg-background text-foreground;
132
+ .gram-elements {
133
+ @tailwind utilities;
134
+
135
+ /* We can't use tailwind's preflight styles here because we need to scope them to .gram-elements so therefore we just include a version of them inline here */
136
+ *,
137
+ *::before,
138
+ *::after {
139
+ box-sizing: border-box;
140
+ border-width: 0;
141
+ border-style: solid;
142
+ @apply border-border outline-ring/50;
143
+ }
144
+
145
+ /* Reset margins and padding for common elements */
146
+ h1,
147
+ h2,
148
+ h3,
149
+ h4,
150
+ h5,
151
+ h6,
152
+ p,
153
+ blockquote,
154
+ pre,
155
+ ul,
156
+ ol,
157
+ figure {
158
+ margin: 0;
159
+ padding: 0;
160
+ }
161
+
162
+ /* List style reset */
163
+ ul,
164
+ ol {
165
+ list-style: none;
166
+ }
167
+
168
+ /* Image defaults */
169
+ img,
170
+ svg,
171
+ video,
172
+ canvas,
173
+ audio,
174
+ iframe,
175
+ embed,
176
+ object {
177
+ display: block;
178
+ }
179
+
180
+ img,
181
+ video {
182
+ max-width: 100%;
183
+ height: auto;
184
+ }
185
+
186
+ /* Button/input reset */
187
+ button,
188
+ input,
189
+ optgroup,
190
+ select,
191
+ textarea {
192
+ font-family: inherit;
193
+ font-feature-settings: inherit;
194
+ font-variation-settings: inherit;
195
+ font-size: 100%;
196
+ font-weight: inherit;
197
+ line-height: inherit;
198
+ letter-spacing: inherit;
199
+ color: inherit;
200
+ margin: 0;
201
+ padding: 0;
202
+ }
203
+
204
+ button,
205
+ select {
206
+ text-transform: none;
207
+ }
208
+
209
+ button,
210
+ input:where([type='button']),
211
+ input:where([type='reset']),
212
+ input:where([type='submit']) {
213
+ appearance: button;
214
+ -webkit-appearance: button;
215
+ background-color: transparent;
216
+ background-image: none;
217
+ }
218
+
219
+ a {
220
+ color: inherit;
221
+ text-decoration: inherit;
222
+ }
223
+
224
+ /* Table reset */
225
+ table {
226
+ text-indent: 0;
227
+ border-color: inherit;
228
+ border-collapse: collapse;
229
+ }
121
230
  }
122
231
  }
123
232
 
124
- /* Theme: Radius variants via data attribute */
125
- [data-radius='sharp'] {
233
+ /* Theme: Radius variants via data attribute - scoped */
234
+ .gram-elements[data-radius='sharp'],
235
+ [data-radius='sharp'] .gram-elements {
126
236
  --radius: 0.25rem;
127
237
  }
128
- [data-radius='soft'] {
238
+ .gram-elements[data-radius='soft'],
239
+ [data-radius='soft'] .gram-elements {
129
240
  --radius: 0.625rem;
130
241
  }
131
- [data-radius='round'] {
242
+ .gram-elements[data-radius='round'],
243
+ [data-radius='round'] .gram-elements {
132
244
  --radius: 1rem;
133
245
  }
134
- [data-radius='pill'] {
246
+ .gram-elements[data-radius='pill'],
247
+ [data-radius='pill'] .gram-elements {
135
248
  --radius: 9999px;
136
- }
249
+ }
@@ -1,7 +1,7 @@
1
- import { hasExplicitSessionAuth, isApiKeyAuth } from '@/lib/auth'
1
+ import { hasExplicitSessionAuth, isStaticSessionAuth } from '@/lib/auth'
2
+ import { useMemo } from 'react'
2
3
  import { ApiConfig } from '../types'
3
4
  import { useSession } from './useSession'
4
- import { useMemo } from 'react'
5
5
 
6
6
  export type Auth =
7
7
  | {
@@ -38,33 +38,23 @@ export const useAuth = ({
38
38
  projectSlug: string
39
39
  }): Auth => {
40
40
  const getSession = useMemo(() => {
41
- if (isApiKeyAuth(auth)) {
42
- return null
41
+ if (isStaticSessionAuth(auth)) {
42
+ return () => Promise.resolve(auth.sessionToken)
43
43
  }
44
- return !isApiKeyAuth(auth) && hasExplicitSessionAuth(auth)
44
+ return !isStaticSessionAuth(auth) && hasExplicitSessionAuth(auth)
45
45
  ? auth.sessionFn
46
46
  : defaultGetSession
47
47
  }, [auth])
48
+
48
49
  // The session request is only neccessary if we are not using an API key auth
49
50
  // configuration. If a custom session fetcher is provided, we use it,
50
51
  // otherwise we fallback to the default session fetcher
51
52
  const session = useSession({
52
53
  // We want to check it's NOT API key auth, as the default auth scheme is session auth (if the user hasn't provided an explicit API config, we have a session auth config by default)
53
- enabled: !isApiKeyAuth(auth),
54
54
  getSession,
55
55
  projectSlug,
56
56
  })
57
57
 
58
- if (isApiKeyAuth(auth)) {
59
- return {
60
- headers: {
61
- 'Gram-Project': projectSlug,
62
- 'Gram-Key': auth.UNSAFE_apiKey,
63
- },
64
- isLoading: false,
65
- }
66
- }
67
-
68
58
  return !session
69
59
  ? {
70
60
  isLoading: true,
@@ -0,0 +1,16 @@
1
+ 'use client'
2
+
3
+ import { useContext } from 'react'
4
+ import { PortalContainerContext } from '@/contexts/portal-container-context'
5
+
6
+ /**
7
+ * Because we do not want Tailwind to leak from the Elements library, and
8
+ * because some UI elements such as Dialogs and Tooltips need to be rendered in
9
+ * a different container than the root element, we need to use a portal
10
+ * container, which renders any tooltips, dialogs etc within the .gram-elements
11
+ * scope so that they still inherit the Elements CSS
12
+ */
13
+ export function usePortalContainer(): HTMLElement | null {
14
+ const ref = useContext(PortalContainerContext)
15
+ return ref?.current ?? null
16
+ }
@@ -8,11 +8,9 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
8
8
  export const useSession = ({
9
9
  getSession,
10
10
  projectSlug,
11
- enabled,
12
11
  }: {
13
12
  getSession: GetSessionFn | null
14
13
  projectSlug: string
15
- enabled: boolean
16
14
  }): string | null => {
17
15
  const queryClient = useQueryClient()
18
16
  const queryKey = ['chatSession', projectSlug]
@@ -29,7 +27,7 @@ export const useSession = ({
29
27
  const { data: fetchedSessionToken } = useQuery({
30
28
  queryKey,
31
29
  queryFn: () => getSession!({ projectSlug }),
32
- enabled: enabled && shouldFetch && getSession !== null,
30
+ enabled: shouldFetch && getSession !== null,
33
31
  staleTime: Infinity, // Session tokens don't need to be refetched
34
32
  gcTime: Infinity, // Keep in cache indefinitely
35
33
  })
package/src/index.ts CHANGED
@@ -15,6 +15,10 @@ export { ThreadList as ChatHistory } from '@/components/assistant-ui/thread-list
15
15
  export { defineFrontendTool } from './lib/tools'
16
16
  export type { FrontendTool } from './lib/tools'
17
17
 
18
+ // Error Tracking
19
+ export { trackError } from './lib/errorTracking'
20
+ export type { ErrorContext } from './lib/errorTracking'
21
+
18
22
  // Types
19
23
  export type {
20
24
  AttachmentsConfig,
@@ -27,6 +31,7 @@ export type {
27
31
  Dimension,
28
32
  Dimensions,
29
33
  ElementsConfig,
34
+ ErrorTrackingConfigOption,
30
35
  GetSessionFn,
31
36
  HistoryConfig,
32
37
  ModalConfig,
@@ -1,5 +1,5 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
1
  import type { ElementsConfig } from '@/types'
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
3
3
 
4
4
  describe('getApiUrl', () => {
5
5
  beforeEach(() => {
@@ -16,7 +16,7 @@ describe('getApiUrl', () => {
16
16
  const getApiUrl = await loadGetApiUrl('https://env.example.com')
17
17
  const config: ElementsConfig = {
18
18
  projectSlug: 'test',
19
- api: { url: 'https://config.example.com', UNSAFE_apiKey: 'test-key' },
19
+ api: { url: 'https://config.example.com', sessionToken: 'test-key' },
20
20
  }
21
21
 
22
22
  expect(getApiUrl(config)).toBe('https://config.example.com')
@@ -26,7 +26,7 @@ describe('getApiUrl', () => {
26
26
  const getApiUrl = await loadGetApiUrl('https://env.example.com')
27
27
  const config: ElementsConfig = {
28
28
  projectSlug: 'test',
29
- api: { UNSAFE_apiKey: 'test-key' },
29
+ api: { sessionToken: 'test-key' },
30
30
  }
31
31
 
32
32
  expect(getApiUrl(config)).toBe('https://env.example.com')
@@ -63,7 +63,7 @@ describe('getApiUrl', () => {
63
63
  const getApiUrl = await loadGetApiUrl('https://env.example.com')
64
64
  const config: ElementsConfig = {
65
65
  projectSlug: 'test',
66
- api: { url: '', UNSAFE_apiKey: 'test-key' },
66
+ api: { url: '', sessionToken: 'test-key' },
67
67
  }
68
68
 
69
69
  expect(getApiUrl(config)).toBe('https://env.example.com')
@@ -73,7 +73,7 @@ describe('getApiUrl', () => {
73
73
  const getApiUrl = await loadGetApiUrl('')
74
74
  const config: ElementsConfig = {
75
75
  projectSlug: 'test',
76
- api: { url: 'https://config.example.com///', UNSAFE_apiKey: 'test-key' },
76
+ api: { url: 'https://config.example.com///', sessionToken: 'test-key' },
77
77
  }
78
78
 
79
79
  expect(getApiUrl(config)).toBe('https://config.example.com')
package/src/lib/auth.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { ApiKeyAuthConfig, ApiConfig, SessionAuthConfig } from '@/types'
1
+ import { ApiConfig, SessionAuthConfig, StaticSessionAuthConfig } from '@/types'
2
2
 
3
3
  /**
4
4
  * Checks if the auth config is an API key auth config
5
5
  */
6
- export function isApiKeyAuth(
6
+ export function isStaticSessionAuth(
7
7
  auth: ApiConfig | undefined
8
- ): auth is ApiKeyAuthConfig {
9
- return !!auth && 'UNSAFE_apiKey' in auth
8
+ ): auth is StaticSessionAuthConfig {
9
+ return !!auth && 'sessionToken' in auth
10
10
  }
11
11
 
12
12
  export function hasExplicitSessionAuth(
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Datadog RUM configuration for Gram Elements.
3
+ * Values are injected at build time via environment variables.
4
+ * These client tokens are designed to be client-side safe.
5
+ *
6
+ * Required env vars for build:
7
+ * - VITE_DATADOG_APPLICATION_ID
8
+ * - VITE_DATADOG_CLIENT_TOKEN
9
+ * - VITE_DATADOG_SITE (optional, defaults to datadoghq.com)
10
+ */
11
+ export const DATADOG_CONFIG = {
12
+ applicationId: import.meta.env.VITE_DATADOG_APPLICATION_ID ?? '',
13
+ clientToken: import.meta.env.VITE_DATADOG_CLIENT_TOKEN ?? '',
14
+ site: import.meta.env.VITE_DATADOG_SITE ?? 'datadoghq.com',
15
+ service: 'gram-elements',
16
+ } as const
@@ -0,0 +1,104 @@
1
+ import { datadogRum } from '@datadog/browser-rum'
2
+ import { DATADOG_CONFIG } from './errorTracking.config'
3
+
4
+ let initialized = false
5
+ let enabled = true
6
+
7
+ export interface ErrorTrackingConfig {
8
+ enabled?: boolean
9
+ projectSlug?: string
10
+ variant?: string
11
+ }
12
+
13
+ export interface ErrorContext {
14
+ source: 'error-boundary' | 'streaming' | 'stream-creation' | 'custom'
15
+ componentStack?: string
16
+ [key: string]: unknown
17
+ }
18
+
19
+ /**
20
+ * Initialize Datadog RUM for error tracking.
21
+ * Should be called once when the ElementsProvider mounts.
22
+ */
23
+ export function initErrorTracking(config: ErrorTrackingConfig = {}): void {
24
+ // Check if explicitly disabled
25
+ if (config.enabled === false) {
26
+ enabled = false
27
+ return
28
+ }
29
+
30
+ // Prevent double initialization
31
+ if (initialized) {
32
+ return
33
+ }
34
+
35
+ // Skip if credentials not configured (e.g., local dev without env vars)
36
+ if (!DATADOG_CONFIG.applicationId || !DATADOG_CONFIG.clientToken) {
37
+ enabled = false
38
+ return
39
+ }
40
+
41
+ try {
42
+ datadogRum.init({
43
+ applicationId: DATADOG_CONFIG.applicationId,
44
+ clientToken: DATADOG_CONFIG.clientToken,
45
+ site: DATADOG_CONFIG.site,
46
+ service: DATADOG_CONFIG.service,
47
+ env: process.env.NODE_ENV || 'production',
48
+ sessionSampleRate: 100,
49
+ sessionReplaySampleRate: 100,
50
+ trackUserInteractions: true,
51
+ trackResources: true,
52
+ trackLongTasks: true,
53
+
54
+ // Note: we need to mask everything, not just user input, as sensitive data may be echo-ed
55
+ // back in the LLM messages or the user messages in the chat window
56
+ defaultPrivacyLevel: 'mask',
57
+ })
58
+
59
+ // Set global context
60
+ if (config.projectSlug) {
61
+ datadogRum.setGlobalContextProperty('projectSlug', config.projectSlug)
62
+ }
63
+ if (config.variant) {
64
+ datadogRum.setGlobalContextProperty('variant', config.variant)
65
+ }
66
+
67
+ initialized = true
68
+ } catch (error) {
69
+ console.warn('[Elements] Failed to initialize Datadog RUM:', error)
70
+ enabled = false
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Track an error to Datadog RUM.
76
+ * Includes context about where the error originated.
77
+ */
78
+ export function trackError(
79
+ error: Error | unknown,
80
+ context: ErrorContext
81
+ ): void {
82
+ if (!enabled || !initialized) {
83
+ return
84
+ }
85
+
86
+ const errorObj = error instanceof Error ? error : new Error(String(error))
87
+
88
+ try {
89
+ datadogRum.addError(errorObj, {
90
+ ...context,
91
+ timestamp: new Date().toISOString(),
92
+ })
93
+ } catch (e) {
94
+ // Silently fail - we don't want error tracking to cause more errors
95
+ console.warn('[Elements] Failed to track error:', e)
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Check if error tracking is currently enabled.
101
+ */
102
+ export function isErrorTrackingEnabled(): boolean {
103
+ return enabled && initialized
104
+ }
package/src/lib/tools.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  } from '@assistant-ui/react'
7
7
  import z from 'zod'
8
8
  import { FC } from 'react'
9
+ import type { ToolsRequiringApproval } from '@/types'
9
10
 
10
11
  /**
11
12
  * Converts from assistant-ui tool format to the AI SDK tool shape
@@ -50,7 +51,7 @@ export type FrontendTool<TArgs extends Record<string, unknown>, TResult> = FC<
50
51
  */
51
52
  let approvalConfig: {
52
53
  helpers: ApprovalHelpers
53
- toolsRequiringApproval: Set<string>
54
+ requiresApproval: (toolName: string) => boolean
54
55
  } | null = null
55
56
 
56
57
  /**
@@ -58,11 +59,12 @@ let approvalConfig: {
58
59
  */
59
60
  export function setFrontendToolApprovalConfig(
60
61
  helpers: ApprovalHelpers,
61
- toolsRequiringApproval: string[]
62
+ toolsRequiringApproval: ToolsRequiringApproval
62
63
  ): void {
64
+ const requiresApproval = createRequiresApprovalFn(toolsRequiringApproval)
63
65
  approvalConfig = {
64
66
  helpers,
65
- toolsRequiringApproval: new Set(toolsRequiringApproval),
67
+ requiresApproval,
66
68
  }
67
69
  }
68
70
 
@@ -73,6 +75,25 @@ export function clearFrontendToolApprovalConfig(): void {
73
75
  approvalConfig = null
74
76
  }
75
77
 
78
+ /**
79
+ * Creates a function that checks if a tool requires approval.
80
+ * Handles both array and function-based configurations.
81
+ */
82
+ function createRequiresApprovalFn(
83
+ toolsRequiringApproval: ToolsRequiringApproval | undefined
84
+ ): (toolName: string) => boolean {
85
+ if (!toolsRequiringApproval) {
86
+ return () => false
87
+ }
88
+
89
+ if (typeof toolsRequiringApproval === 'function') {
90
+ return (toolName: string) => toolsRequiringApproval({ toolName })
91
+ }
92
+
93
+ const approvalSet = new Set(toolsRequiringApproval)
94
+ return (toolName: string) => approvalSet.has(toolName)
95
+ }
96
+
76
97
  /**
77
98
  * Make a frontend tool
78
99
  */
@@ -90,7 +111,7 @@ export const defineFrontendTool = <
90
111
  ...tool,
91
112
  execute: async (args: TArgs, context: ToolExecutionContext) => {
92
113
  // Check if this tool requires approval at runtime
93
- if (approvalConfig?.toolsRequiringApproval.has(name)) {
114
+ if (approvalConfig?.requiresApproval(name)) {
94
115
  const { helpers } = approvalConfig
95
116
  const toolCallId = context.toolCallId ?? ''
96
117
 
@@ -136,18 +157,26 @@ export interface ApprovalHelpers {
136
157
  */
137
158
  export function wrapToolsWithApproval(
138
159
  tools: ToolSet,
139
- toolsRequiringApproval: string[] | undefined,
160
+ toolsRequiringApproval: ToolsRequiringApproval | undefined,
140
161
  approvalHelpers: ApprovalHelpers
141
162
  ): ToolSet {
142
- if (!toolsRequiringApproval || toolsRequiringApproval.length === 0) {
163
+ if (!toolsRequiringApproval) {
143
164
  return tools
144
165
  }
145
166
 
146
- const approvalSet = new Set(toolsRequiringApproval)
167
+ // Handle empty array case
168
+ if (
169
+ Array.isArray(toolsRequiringApproval) &&
170
+ toolsRequiringApproval.length === 0
171
+ ) {
172
+ return tools
173
+ }
174
+
175
+ const requiresApproval = createRequiresApprovalFn(toolsRequiringApproval)
147
176
 
148
177
  return Object.fromEntries(
149
178
  Object.entries(tools).map(([name, tool]) => {
150
- if (!approvalSet.has(name)) {
179
+ if (!requiresApproval(name)) {
151
180
  return [name, tool]
152
181
  }
153
182
 
@@ -288,6 +288,30 @@ export interface ElementsConfig {
288
288
  * }
289
289
  */
290
290
  api?: ApiConfig
291
+
292
+ /**
293
+ * Error tracking configuration.
294
+ * By default, errors are reported to help improve the Elements library.
295
+ *
296
+ * @example
297
+ * const config: ElementsConfig = {
298
+ * errorTracking: {
299
+ * enabled: false, // Opt out of error reporting
300
+ * },
301
+ * }
302
+ */
303
+ errorTracking?: ErrorTrackingConfigOption
304
+ }
305
+
306
+ /**
307
+ * Configuration for error tracking.
308
+ */
309
+ export interface ErrorTrackingConfigOption {
310
+ /**
311
+ * Set to false to disable error reporting.
312
+ * @default true
313
+ */
314
+ enabled?: boolean
291
315
  }
292
316
 
293
317
  /**
@@ -325,32 +349,27 @@ export type SessionAuthConfig = {
325
349
  }
326
350
 
327
351
  /**
328
- * The API key auth config is used to authenticate the Elements library using an API key only.
329
- *
330
- * NOTE: This is not recommended for production use, and a warning
331
- * will be displayed in the chat interface if you use this config.
332
- * Define a session endpoint instead to avoid this warning.
352
+ * The static session auth config is used to authenticate the Elements library using a static session token only.
333
353
  *
334
354
  * @example
335
355
  * const config: ElementsConfig = {
336
356
  * api: {
337
- * UNSAFE_apiKey: 'your-api-key',
357
+ * sessionToken: 'your-session-token',
338
358
  * },
339
359
  * }
340
360
  */
341
- export type ApiKeyAuthConfig = {
361
+ export type StaticSessionAuthConfig = {
342
362
  /**
343
- * The API key to use if you haven't yet configured a session endpoint.
344
- * Do not use this in production.
363
+ * A static session token to use if you haven't yet configured a session endpoint.
345
364
  *
346
365
  * @example
347
366
  * const config: ElementsConfig = {
348
367
  * api: {
349
- * UNSAFE_apiKey: 'your-api-key',
368
+ * sessionToken: 'your-session-token',
350
369
  * },
351
370
  * }
352
371
  */
353
- UNSAFE_apiKey: string
372
+ sessionToken: string
354
373
  }
355
374
 
356
375
  /**
@@ -359,7 +378,7 @@ export type ApiKeyAuthConfig = {
359
378
  export type ApiConfig =
360
379
  | BaseApiConfig
361
380
  | (BaseApiConfig & SessionAuthConfig)
362
- | (BaseApiConfig & ApiKeyAuthConfig)
381
+ | (BaseApiConfig & StaticSessionAuthConfig)
363
382
 
364
383
  /**
365
384
  * The LLM model to use for the Elements library.
@@ -490,6 +509,10 @@ export interface ComponentOverrides {
490
509
  >
491
510
  }
492
511
 
512
+ export type ToolsRequiringApproval =
513
+ | string[]
514
+ | (({ toolName }: { toolName: string }) => boolean)
515
+
493
516
  /**
494
517
  * ToolsConfig is used to configure tool support in the Elements library.
495
518
  * At the moment, you can override the default React components used by
@@ -504,7 +527,6 @@ export interface ComponentOverrides {
504
527
  * },
505
528
  * }
506
529
  */
507
-
508
530
  export interface ToolsConfig {
509
531
  /**
510
532
  * Whether individual tool calls within a group should be expanded by default.
@@ -584,17 +606,27 @@ export interface ToolsConfig {
584
606
 
585
607
  /**
586
608
  * List of tool names that require confirmation from the end user before
587
- * being executed. The user can choose to approve once or approve for the
609
+ * being executed. A function can also be provided to dynamically determine if a tool requires approval.
610
+ * The user can choose to approve once or approve for the
588
611
  * entire session via the UI.
589
612
  *
590
- * @example
613
+ * @example Using an array of tool names
591
614
  * ```ts
592
615
  * tools: {
593
616
  * toolsRequiringApproval: ['delete_file', 'send_email'],
594
617
  * }
595
618
  * ```
619
+ *
620
+ * @example Using a function to dynamically determine if a tool requires approval
621
+ * ```ts
622
+ * tools: {
623
+ * toolsRequiringApproval: (toolName) => {
624
+ * return toolName.startsWith('protected_')
625
+ * },
626
+ * }
627
+ * ```
596
628
  */
597
- toolsRequiringApproval?: string[]
629
+ toolsRequiringApproval?: ToolsRequiringApproval
598
630
  }
599
631
 
600
632
  export interface WelcomeConfig {