@gram-ai/elements 1.18.5 → 1.18.6

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 (85) hide show
  1. package/dist/components/Chat/stories/Sidecar.stories.d.ts +6 -0
  2. package/dist/elements.cjs +22 -21
  3. package/dist/elements.cjs.map +1 -0
  4. package/dist/elements.js +601 -591
  5. package/dist/elements.js.map +1 -0
  6. package/dist/index-Bj7jPiuy.cjs +1 -0
  7. package/dist/index-Bj7jPiuy.cjs.map +1 -0
  8. package/dist/index-CJRypLIa.js +1 -0
  9. package/dist/index-CJRypLIa.js.map +1 -0
  10. package/dist/plugins.cjs +1 -0
  11. package/dist/plugins.cjs.map +1 -0
  12. package/dist/plugins.js +1 -0
  13. package/dist/plugins.js.map +1 -0
  14. package/dist/server.cjs +1 -0
  15. package/dist/server.cjs.map +1 -0
  16. package/dist/server.js +1 -0
  17. package/dist/server.js.map +1 -0
  18. package/package.json +3 -2
  19. package/src/components/Chat/index.tsx +21 -0
  20. package/src/components/Chat/stories/ColorScheme.stories.tsx +52 -0
  21. package/src/components/Chat/stories/Composer.stories.tsx +42 -0
  22. package/src/components/Chat/stories/Customization.stories.tsx +88 -0
  23. package/src/components/Chat/stories/Density.stories.tsx +52 -0
  24. package/src/components/Chat/stories/FrontendTools.stories.tsx +145 -0
  25. package/src/components/Chat/stories/Modal.stories.tsx +84 -0
  26. package/src/components/Chat/stories/Model.stories.tsx +32 -0
  27. package/src/components/Chat/stories/Plugins.stories.tsx +50 -0
  28. package/src/components/Chat/stories/Radius.stories.tsx +52 -0
  29. package/src/components/Chat/stories/Sidecar.stories.tsx +27 -0
  30. package/src/components/Chat/stories/ToolApproval.stories.tsx +110 -0
  31. package/src/components/Chat/stories/Tools.stories.tsx +175 -0
  32. package/src/components/Chat/stories/Variants.stories.tsx +46 -0
  33. package/src/components/Chat/stories/Welcome.stories.tsx +42 -0
  34. package/src/components/FrontendTools/index.tsx +9 -0
  35. package/src/components/assistant-ui/assistant-modal.tsx +255 -0
  36. package/src/components/assistant-ui/assistant-sidecar.tsx +88 -0
  37. package/src/components/assistant-ui/attachment.tsx +233 -0
  38. package/src/components/assistant-ui/markdown-text.tsx +240 -0
  39. package/src/components/assistant-ui/reasoning.tsx +261 -0
  40. package/src/components/assistant-ui/thread-list.tsx +97 -0
  41. package/src/components/assistant-ui/thread.tsx +632 -0
  42. package/src/components/assistant-ui/tool-fallback.tsx +111 -0
  43. package/src/components/assistant-ui/tool-group.tsx +59 -0
  44. package/src/components/assistant-ui/tooltip-icon-button.tsx +57 -0
  45. package/src/components/ui/avatar.tsx +51 -0
  46. package/src/components/ui/button.tsx +27 -0
  47. package/src/components/ui/buttonVariants.ts +33 -0
  48. package/src/components/ui/collapsible.tsx +31 -0
  49. package/src/components/ui/dialog.tsx +141 -0
  50. package/src/components/ui/popover.tsx +46 -0
  51. package/src/components/ui/skeleton.tsx +13 -0
  52. package/src/components/ui/tool-ui.stories.tsx +146 -0
  53. package/src/components/ui/tool-ui.tsx +676 -0
  54. package/src/components/ui/tooltip.tsx +61 -0
  55. package/src/contexts/ElementsProvider.tsx +256 -0
  56. package/src/contexts/ToolApprovalContext.tsx +120 -0
  57. package/src/contexts/contexts.ts +10 -0
  58. package/src/global.css +136 -0
  59. package/src/hooks/useAuth.ts +71 -0
  60. package/src/hooks/useDensity.ts +110 -0
  61. package/src/hooks/useElements.ts +14 -0
  62. package/src/hooks/useExpanded.ts +20 -0
  63. package/src/hooks/useMCPTools.ts +73 -0
  64. package/src/hooks/usePluginComponents.ts +34 -0
  65. package/src/hooks/useRadius.ts +42 -0
  66. package/src/hooks/useSession.ts +38 -0
  67. package/src/hooks/useThemeProps.ts +24 -0
  68. package/src/hooks/useToolApproval.ts +16 -0
  69. package/src/index.ts +45 -0
  70. package/src/lib/api.test.ts +90 -0
  71. package/src/lib/api.ts +8 -0
  72. package/src/lib/auth.ts +10 -0
  73. package/src/lib/easing.ts +1 -0
  74. package/src/lib/humanize.ts +14 -0
  75. package/src/lib/models.ts +22 -0
  76. package/src/lib/tools.ts +210 -0
  77. package/src/lib/utils.ts +16 -0
  78. package/src/plugins/README.md +49 -0
  79. package/src/plugins/chart/component.tsx +102 -0
  80. package/src/plugins/chart/index.ts +27 -0
  81. package/src/plugins/index.ts +7 -0
  82. package/src/server.ts +89 -0
  83. package/src/types/index.ts +726 -0
  84. package/src/types/plugins.ts +65 -0
  85. package/src/vite-env.d.ts +12 -0
@@ -0,0 +1,210 @@
1
+ import { JSONSchema7, ToolSet, type ToolCallOptions } from 'ai'
2
+ import {
3
+ AssistantToolProps,
4
+ Tool,
5
+ makeAssistantTool,
6
+ } from '@assistant-ui/react'
7
+ import z from 'zod'
8
+ import { FC } from 'react'
9
+
10
+ /**
11
+ * Converts from assistant-ui tool format to the AI SDK tool shape
12
+ */
13
+ export const toAISDKTools = (tools: Record<string, Tool>) => {
14
+ return Object.fromEntries(
15
+ Object.entries(tools).map(([name, tool]) => [
16
+ name,
17
+ {
18
+ ...(tool.description ? { description: tool.description } : undefined),
19
+ parameters: (tool.parameters instanceof z.ZodType
20
+ ? z.toJSONSchema(tool.parameters)
21
+ : tool.parameters) as JSONSchema7,
22
+ },
23
+ ])
24
+ )
25
+ }
26
+
27
+ /**
28
+ * Returns only frontend tools that are enabled
29
+ */
30
+ export const getEnabledTools = (tools: Record<string, Tool>) => {
31
+ return Object.fromEntries(
32
+ Object.entries(tools).filter(
33
+ ([, tool]) => !tool.disabled && tool.type !== 'backend'
34
+ )
35
+ )
36
+ }
37
+
38
+ /**
39
+ * A frontend tool is a tool that is defined by the user and can be used in the chat.
40
+ */
41
+ export type FrontendTool<TArgs extends Record<string, unknown>, TResult> = FC<
42
+ AssistantToolProps<TArgs, TResult>
43
+ > & {
44
+ unstable_tool: AssistantToolProps<TArgs, TResult>
45
+ }
46
+
47
+ /**
48
+ * Module-level approval config that gets set by ElementsProvider at runtime.
49
+ * This allows defineFrontendTool to check approval status during execute.
50
+ */
51
+ let approvalConfig: {
52
+ helpers: ApprovalHelpers
53
+ toolsRequiringApproval: Set<string>
54
+ } | null = null
55
+
56
+ /**
57
+ * Sets the approval configuration. Called by ElementsProvider.
58
+ */
59
+ export function setFrontendToolApprovalConfig(
60
+ helpers: ApprovalHelpers,
61
+ toolsRequiringApproval: string[]
62
+ ): void {
63
+ approvalConfig = {
64
+ helpers,
65
+ toolsRequiringApproval: new Set(toolsRequiringApproval),
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Clears the approval configuration. Called when ElementsProvider unmounts.
71
+ */
72
+ export function clearFrontendToolApprovalConfig(): void {
73
+ approvalConfig = null
74
+ }
75
+
76
+ /**
77
+ * Make a frontend tool
78
+ */
79
+ export const defineFrontendTool = <
80
+ TArgs extends Record<string, unknown>,
81
+ TResult,
82
+ >(
83
+ tool: Tool,
84
+ name: string
85
+ ): FrontendTool<TArgs, TResult> => {
86
+ type ToolExecutionContext = Parameters<
87
+ NonNullable<Tool<Record<string, unknown>, void>['execute']>
88
+ >[1]
89
+ return makeAssistantTool({
90
+ ...tool,
91
+ execute: async (args: TArgs, context: ToolExecutionContext) => {
92
+ // Check if this tool requires approval at runtime
93
+ if (approvalConfig?.toolsRequiringApproval.has(name)) {
94
+ const { helpers } = approvalConfig
95
+ const toolCallId = context.toolCallId ?? ''
96
+
97
+ // Check if already approved (user chose "Approve always" previously)
98
+ if (!helpers.isToolApproved(name)) {
99
+ const approved = await helpers.requestApproval(name, toolCallId, args)
100
+
101
+ if (!approved) {
102
+ return {
103
+ content: [
104
+ {
105
+ type: 'text',
106
+ text: `Tool "${name}" execution was denied by the user. Please acknowledge this and continue without using this tool's result.`,
107
+ },
108
+ ],
109
+ isError: true,
110
+ } as TResult
111
+ }
112
+ }
113
+ }
114
+
115
+ return tool.execute?.(args, context)
116
+ },
117
+ toolName: name,
118
+ } as AssistantToolProps<TArgs, TResult>)
119
+ }
120
+
121
+ /**
122
+ * Helpers for requesting and tracking tool approval state.
123
+ */
124
+ export interface ApprovalHelpers {
125
+ requestApproval: (
126
+ toolName: string,
127
+ toolCallId: string,
128
+ args: unknown
129
+ ) => Promise<boolean>
130
+ isToolApproved: (toolName: string) => boolean
131
+ whitelistTool: (toolName: string) => void
132
+ }
133
+
134
+ /**
135
+ * Wraps tools with approval logic based on the approval config.
136
+ */
137
+ export function wrapToolsWithApproval(
138
+ tools: ToolSet,
139
+ toolsRequiringApproval: string[] | undefined,
140
+ approvalHelpers: ApprovalHelpers
141
+ ): ToolSet {
142
+ if (!toolsRequiringApproval || toolsRequiringApproval.length === 0) {
143
+ return tools
144
+ }
145
+
146
+ const approvalSet = new Set(toolsRequiringApproval)
147
+
148
+ return Object.fromEntries(
149
+ Object.entries(tools).map(([name, tool]) => {
150
+ if (!approvalSet.has(name)) {
151
+ return [name, tool]
152
+ }
153
+
154
+ const originalExecute = tool.execute
155
+ if (!originalExecute) {
156
+ return [name, tool]
157
+ }
158
+
159
+ return [
160
+ name,
161
+ {
162
+ ...tool,
163
+ execute: async (args: unknown, options?: ToolCallOptions) => {
164
+ const opts = (options ?? {}) as Parameters<
165
+ typeof originalExecute
166
+ >[1]
167
+ // Extract toolCallId from options
168
+ const toolCallId =
169
+ (opts as { toolCallId?: string }).toolCallId ?? ''
170
+
171
+ // Check if already approved (user chose "Approve always" previously)
172
+ if (approvalHelpers.isToolApproved(name)) {
173
+ return originalExecute(
174
+ args,
175
+ opts as Parameters<typeof originalExecute>[1]
176
+ )
177
+ }
178
+
179
+ // Request approval using the actual toolCallId from the stream
180
+ const approved = await approvalHelpers.requestApproval(
181
+ name,
182
+ toolCallId,
183
+ args
184
+ )
185
+
186
+ if (!approved) {
187
+ return {
188
+ content: [
189
+ {
190
+ type: 'text',
191
+ text: `Tool "${name}" execution was denied by the user. Please acknowledge this and continue without using this tool's result.`,
192
+ },
193
+ ],
194
+ isError: true,
195
+ }
196
+ }
197
+
198
+ // Note: Tool is marked as approved via the UI when user clicks "Approve always"
199
+ // (handled in tool-fallback.tsx via markToolApproved)
200
+
201
+ return originalExecute(
202
+ args,
203
+ opts as Parameters<typeof originalExecute>[1]
204
+ )
205
+ },
206
+ },
207
+ ]
208
+ })
209
+ ) as ToolSet
210
+ }
@@ -0,0 +1,16 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
7
+
8
+ export function assertNever(value: unknown): never {
9
+ throw new Error(`Unexpected value: ${value}`)
10
+ }
11
+
12
+ export function assert(condition: unknown, message: string): asserts condition {
13
+ if (!condition) {
14
+ throw new Error(message)
15
+ }
16
+ }
@@ -0,0 +1,49 @@
1
+ # Plugin Development Guide
2
+
3
+ This guide will help you create custom plugins for the Gram Elements library.
4
+
5
+ ## What are Plugins?
6
+
7
+ Plugins enable you to add custom rendering capabilities to the Gram Elements library. They allow you to transform markdown code blocks with specific language identifiers into rich, interactive components.
8
+
9
+ The typical plugin workflow is:
10
+
11
+ 1. **Extend System Prompt**: The plugin adds instructions to the system prompt, telling the LLM how to return data in a specific format
12
+ 2. **LLM Responds**: The LLM returns a code fence marked with your plugin's language identifier
13
+ 3. **Custom Rendering**: Your plugin's custom component renders the code block content
14
+
15
+ ## Plugin Interface
16
+
17
+ A plugin is defined by the following TypeScript interface:
18
+
19
+ ```typescript
20
+ interface Plugin {
21
+ // The language identifier for the code fence (e.g., "vega", "mermaid", "d3")
22
+ language: string
23
+
24
+ // Instructions for the LLM on how to use this plugin
25
+ prompt: string
26
+
27
+ // Your custom React component that renders the code block
28
+ SyntaxHighlighter: ComponentType<SyntaxHighlighterProps>
29
+
30
+ // Optional: Custom header component for the code block
31
+ CodeHeader?: ComponentType<CodeHeaderProps> | null
32
+
33
+ // Optional: Whether to override existing plugins with the same language
34
+ overrideExisting?: boolean
35
+ }
36
+ ```
37
+
38
+ ## Support
39
+
40
+ If you need help creating a plugin:
41
+
42
+ - Check existing plugins for examples
43
+ - Review the TypeScript types in `src/types/plugins.ts`
44
+ - Open an issue on GitHub
45
+ - Join our community Discord
46
+
47
+ ---
48
+
49
+ Happy plugin building! 🚀
@@ -0,0 +1,102 @@
1
+ 'use client'
2
+
3
+ import { useDensity } from '@/hooks/useDensity'
4
+ import { useRadius } from '@/hooks/useRadius'
5
+ import { cn } from '@/lib/utils'
6
+ import { useAssistantState } from '@assistant-ui/react'
7
+ import { SyntaxHighlighterProps } from '@assistant-ui/react-markdown'
8
+ import { AlertCircleIcon } from 'lucide-react'
9
+ import { FC, useEffect, useMemo, useRef, useState } from 'react'
10
+ import { parse, View, Warn } from 'vega'
11
+
12
+ export const ChartRenderer: FC<SyntaxHighlighterProps> = ({ code }) => {
13
+ const message = useAssistantState(({ message }) => message)
14
+ const containerRef = useRef<HTMLDivElement>(null)
15
+ const viewRef = useRef<View | null>(null)
16
+ const [error, setError] = useState<string | null>(null)
17
+ const messageIsComplete = message.status?.type === 'complete'
18
+ const r = useRadius()
19
+ const d = useDensity()
20
+
21
+ // Parse and validate JSON in useMemo - only recomputes when code changes
22
+ const parsedSpec = useMemo(() => {
23
+ const trimmedCode = code.trim()
24
+ if (!trimmedCode) return null
25
+
26
+ try {
27
+ return JSON.parse(trimmedCode) as Record<string, unknown>
28
+ } catch {
29
+ return null
30
+ }
31
+ }, [code])
32
+
33
+ // Only render when we have valid JSON AND message is complete
34
+ const shouldRender = messageIsComplete && parsedSpec !== null
35
+
36
+ useEffect(() => {
37
+ if (!containerRef.current || !shouldRender) {
38
+ return
39
+ }
40
+
41
+ setError(null)
42
+
43
+ const runChart = async () => {
44
+ try {
45
+ // Clean up any existing view
46
+ if (viewRef.current) {
47
+ viewRef.current.finalize()
48
+ viewRef.current = null
49
+ }
50
+
51
+ const chart = parse(parsedSpec)
52
+ const view = new View(chart, {
53
+ container: containerRef.current ?? undefined,
54
+ renderer: 'svg',
55
+ hover: true,
56
+ logLevel: Warn,
57
+ })
58
+ viewRef.current = view
59
+
60
+ await view.runAsync()
61
+ } catch (err) {
62
+ console.error('Failed to render chart:', err)
63
+ setError(err instanceof Error ? err.message : 'Failed to render chart')
64
+ }
65
+ }
66
+
67
+ runChart()
68
+
69
+ return () => {
70
+ if (viewRef.current) {
71
+ viewRef.current.finalize()
72
+ viewRef.current = null
73
+ }
74
+ }
75
+ }, [shouldRender, parsedSpec])
76
+
77
+ return (
78
+ <div
79
+ className={cn(
80
+ // the after:hidden is to prevent assistant-ui from showing its default code block loading indicator
81
+ 'relative flex min-h-[400px] w-fit max-w-full min-w-[400px] items-center justify-center border p-6 after:hidden',
82
+ r('lg'),
83
+ d('p-lg')
84
+ )}
85
+ >
86
+ {!shouldRender && !error && (
87
+ <div className="shimmer text-muted-foreground bg-background/80 absolute inset-0 z-10 flex items-center justify-center">
88
+ Rendering chart...
89
+ </div>
90
+ )}
91
+
92
+ {error && (
93
+ <div className="bg-background absolute inset-0 z-10 flex items-center justify-center gap-2 text-rose-500">
94
+ <AlertCircleIcon name="alert-circle" className="h-4 w-4" />
95
+ {error}
96
+ </div>
97
+ )}
98
+
99
+ <div ref={containerRef} className={!shouldRender ? 'hidden' : 'block'} />
100
+ </div>
101
+ )
102
+ }
@@ -0,0 +1,27 @@
1
+ import { Plugin } from '@/types/plugins'
2
+ import { ChartRenderer } from './component'
3
+
4
+ /**
5
+ * This plugin renders Vega charts.
6
+ */
7
+ export const chart: Plugin = {
8
+ language: 'vega',
9
+ prompt: `When a user requests a chart or visualization, respond with a valid Vega specification (https://vega.github.io/vega/) in a code block annotated with the language identifier 'vega'.
10
+
11
+ CRITICAL JSON REQUIREMENTS:
12
+ - The code block MUST contain ONLY valid, parseable JSON
13
+ - NO comments (no // or /* */ anywhere)
14
+ - NO trailing commas
15
+ - Use double quotes for all strings and keys
16
+ - NO text before or after the JSON object
17
+ - The JSON must start with { and end with }
18
+
19
+ CONTENT GUIDELINES:
20
+ - Outside the code block, describe trends and insights found in the data
21
+ - Do not describe visual properties or technical implementation details
22
+ - Do not mention "Vega" or other technical terms - this is user-facing
23
+
24
+ The Vega spec will be parsed with JSON.parse() - if it fails, the chart will error. Ensure strict JSON validity.`,
25
+ Component: ChartRenderer,
26
+ Header: undefined,
27
+ }
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from '@/types/plugins'
2
+ import { chart } from './chart'
3
+
4
+ export const recommended: Plugin[] = [chart]
5
+ export { chart } from './chart'
6
+
7
+ export type { Plugin } from '@/types/plugins'
package/src/server.ts ADDED
@@ -0,0 +1,89 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http'
2
+
3
+ type ServerHandler<T> = (
4
+ req: IncomingMessage,
5
+ res: ServerResponse,
6
+ options?: T
7
+ ) => Promise<void>
8
+
9
+ interface ServerHandlers {
10
+ /**
11
+ * Handler to create a new chat session token.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { createElementsServerHandlers } from '@gram-ai/elements/server'
16
+ * import express from 'express'
17
+ * const app = express()
18
+ * const handlers = createElementsServerHandlers()
19
+ * app.post('/chat/session', handlers.session)
20
+ * app.listen(3000)
21
+ * ```
22
+ */
23
+ session: ServerHandler<SessionHandlerOptions>
24
+ }
25
+
26
+ export const createElementsServerHandlers = (): ServerHandlers => {
27
+ return {
28
+ session: sessionHandler,
29
+ }
30
+ }
31
+
32
+ interface SessionHandlerOptions {
33
+ /**
34
+ * The origin from which the token will be used
35
+ */
36
+ embedOrigin: string
37
+
38
+ /**
39
+ * Free-form user identifier
40
+ */
41
+ userIdentifier: string
42
+
43
+ /**
44
+ * Token expiration in seconds (max / default 3600)
45
+ * @default 3600
46
+ */
47
+ expiresAfter?: number
48
+ }
49
+
50
+ const sessionHandler: ServerHandler<SessionHandlerOptions> = async (
51
+ req,
52
+ res,
53
+ options
54
+ ) => {
55
+ const base = process.env.GRAM_API_URL ?? 'https://app.getgram.ai'
56
+ if (req.method === 'POST') {
57
+ const projectSlug = Array.isArray(req.headers['gram-project'])
58
+ ? req.headers['gram-project'][0]
59
+ : req.headers['gram-project']
60
+
61
+ fetch(base + '/rpc/chatSessions.create', {
62
+ method: 'POST',
63
+ body: JSON.stringify({
64
+ embed_origin: options?.embedOrigin,
65
+ user_identifier: options?.userIdentifier,
66
+ expires_after: options?.expiresAfter,
67
+ }),
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ 'Gram-Project': typeof projectSlug === 'string' ? projectSlug : '',
71
+ 'Gram-Key': process.env.GRAM_API_KEY ?? '',
72
+ },
73
+ })
74
+ .then(async (response) => {
75
+ const body = await response.text()
76
+ res.writeHead(response.status, { 'Content-Type': 'application/json' })
77
+ res.end(body)
78
+ })
79
+ .catch((error) => {
80
+ console.error('Failed to create chat session:', error)
81
+ res.writeHead(500, { 'Content-Type': 'application/json' })
82
+ res.end(
83
+ JSON.stringify({
84
+ error: 'Failed to create chat session: ' + error.message,
85
+ })
86
+ )
87
+ })
88
+ }
89
+ }