@gram-ai/elements 1.18.4 → 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.
- package/dist/components/Chat/stories/Sidecar.stories.d.ts +6 -0
- package/dist/elements.cjs +23 -22
- package/dist/elements.cjs.map +1 -0
- package/dist/elements.js +609 -599
- package/dist/elements.js.map +1 -0
- package/dist/index-Bj7jPiuy.cjs +1 -0
- package/dist/index-Bj7jPiuy.cjs.map +1 -0
- package/dist/index-CJRypLIa.js +1 -0
- package/dist/index-CJRypLIa.js.map +1 -0
- package/dist/lib/api.d.ts +2 -0
- package/dist/lib/api.test.d.ts +1 -0
- package/dist/plugins.cjs +1 -0
- package/dist/plugins.cjs.map +1 -0
- package/dist/plugins.js +1 -0
- package/dist/plugins.js.map +1 -0
- package/dist/server.cjs +1 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -0
- package/package.json +6 -2
- package/src/components/Chat/index.tsx +21 -0
- package/src/components/Chat/stories/ColorScheme.stories.tsx +52 -0
- package/src/components/Chat/stories/Composer.stories.tsx +42 -0
- package/src/components/Chat/stories/Customization.stories.tsx +88 -0
- package/src/components/Chat/stories/Density.stories.tsx +52 -0
- package/src/components/Chat/stories/FrontendTools.stories.tsx +145 -0
- package/src/components/Chat/stories/Modal.stories.tsx +84 -0
- package/src/components/Chat/stories/Model.stories.tsx +32 -0
- package/src/components/Chat/stories/Plugins.stories.tsx +50 -0
- package/src/components/Chat/stories/Radius.stories.tsx +52 -0
- package/src/components/Chat/stories/Sidecar.stories.tsx +27 -0
- package/src/components/Chat/stories/ToolApproval.stories.tsx +110 -0
- package/src/components/Chat/stories/Tools.stories.tsx +175 -0
- package/src/components/Chat/stories/Variants.stories.tsx +46 -0
- package/src/components/Chat/stories/Welcome.stories.tsx +42 -0
- package/src/components/FrontendTools/index.tsx +9 -0
- package/src/components/assistant-ui/assistant-modal.tsx +255 -0
- package/src/components/assistant-ui/assistant-sidecar.tsx +88 -0
- package/src/components/assistant-ui/attachment.tsx +233 -0
- package/src/components/assistant-ui/markdown-text.tsx +240 -0
- package/src/components/assistant-ui/reasoning.tsx +261 -0
- package/src/components/assistant-ui/thread-list.tsx +97 -0
- package/src/components/assistant-ui/thread.tsx +632 -0
- package/src/components/assistant-ui/tool-fallback.tsx +111 -0
- package/src/components/assistant-ui/tool-group.tsx +59 -0
- package/src/components/assistant-ui/tooltip-icon-button.tsx +57 -0
- package/src/components/ui/avatar.tsx +51 -0
- package/src/components/ui/button.tsx +27 -0
- package/src/components/ui/buttonVariants.ts +33 -0
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/dialog.tsx +141 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/tool-ui.stories.tsx +146 -0
- package/src/components/ui/tool-ui.tsx +676 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/contexts/ElementsProvider.tsx +256 -0
- package/src/contexts/ToolApprovalContext.tsx +120 -0
- package/src/contexts/contexts.ts +10 -0
- package/src/global.css +136 -0
- package/src/hooks/useAuth.ts +71 -0
- package/src/hooks/useDensity.ts +110 -0
- package/src/hooks/useElements.ts +14 -0
- package/src/hooks/useExpanded.ts +20 -0
- package/src/hooks/useMCPTools.ts +73 -0
- package/src/hooks/usePluginComponents.ts +34 -0
- package/src/hooks/useRadius.ts +42 -0
- package/src/hooks/useSession.ts +38 -0
- package/src/hooks/useThemeProps.ts +24 -0
- package/src/hooks/useToolApproval.ts +16 -0
- package/src/index.ts +45 -0
- package/src/lib/api.test.ts +90 -0
- package/src/lib/api.ts +8 -0
- package/src/lib/auth.ts +10 -0
- package/src/lib/easing.ts +1 -0
- package/src/lib/humanize.ts +14 -0
- package/src/lib/models.ts +22 -0
- package/src/lib/tools.ts +210 -0
- package/src/lib/utils.ts +16 -0
- package/src/plugins/README.md +49 -0
- package/src/plugins/chart/component.tsx +102 -0
- package/src/plugins/chart/index.ts +27 -0
- package/src/plugins/index.ts +7 -0
- package/src/server.ts +89 -0
- package/src/types/index.ts +726 -0
- package/src/types/plugins.ts +65 -0
- package/src/vite-env.d.ts +12 -0
package/src/lib/tools.ts
ADDED
|
@@ -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
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -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
|
+
}
|
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
|
+
}
|