@gram-ai/elements 1.26.0 → 1.27.0

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 (92) hide show
  1. package/README.md +83 -15
  2. package/dist/components/Chat/stories/ConnectionConfiguration.stories.d.ts +1 -1
  3. package/dist/components/ui/calendar.d.ts +25 -0
  4. package/dist/components/ui/time-range-picker.d.ts +46 -0
  5. package/dist/components/ui/time-range-picker.stories.d.ts +37 -0
  6. package/dist/components/ui/tool-ui.d.ts +16 -1
  7. package/dist/core-Cqad6-xW.js +36 -0
  8. package/dist/core-Cqad6-xW.js.map +1 -0
  9. package/dist/core-DBxmxwCi.cjs +2 -0
  10. package/dist/core-DBxmxwCi.cjs.map +1 -0
  11. package/dist/elements.cjs +1 -1
  12. package/dist/elements.css +1 -1
  13. package/dist/elements.js +18 -14
  14. package/dist/hooks/useModel.d.ts +2 -0
  15. package/dist/index-CP-wWZCV.cjs +172 -0
  16. package/dist/index-CP-wWZCV.cjs.map +1 -0
  17. package/dist/{index-BJnv49-A.js → index-oO5BAmPI.js} +12667 -12048
  18. package/dist/index-oO5BAmPI.js.map +1 -0
  19. package/dist/index.d.ts +5 -1
  20. package/dist/lib/auth.d.ts +12 -4
  21. package/dist/lib/models.d.ts +1 -1
  22. package/dist/{profiler-DCWYDZ1F.cjs → profiler-CEpc7O5Q.cjs} +2 -2
  23. package/dist/{profiler-DCWYDZ1F.cjs.map → profiler-CEpc7O5Q.cjs.map} +1 -1
  24. package/dist/{profiler-D4Tw5ecI.js → profiler-ECh1zoXF.js} +2 -2
  25. package/dist/{profiler-D4Tw5ecI.js.map → profiler-ECh1zoXF.js.map} +1 -1
  26. package/dist/server/bun.cjs +2 -0
  27. package/dist/server/bun.cjs.map +1 -0
  28. package/dist/server/bun.d.ts +8 -0
  29. package/dist/server/bun.js +26 -0
  30. package/dist/server/bun.js.map +1 -0
  31. package/dist/server/core.d.ts +37 -0
  32. package/dist/server/express.cjs +2 -0
  33. package/dist/server/express.cjs.map +1 -0
  34. package/dist/server/express.d.ts +9 -0
  35. package/dist/server/express.js +21 -0
  36. package/dist/server/express.js.map +1 -0
  37. package/dist/server/fastify.cjs +2 -0
  38. package/dist/server/fastify.cjs.map +1 -0
  39. package/dist/server/fastify.d.ts +9 -0
  40. package/dist/server/fastify.js +19 -0
  41. package/dist/server/fastify.js.map +1 -0
  42. package/dist/server/hono.cjs +2 -0
  43. package/dist/server/hono.cjs.map +1 -0
  44. package/dist/server/hono.d.ts +9 -0
  45. package/dist/server/hono.js +20 -0
  46. package/dist/server/hono.js.map +1 -0
  47. package/dist/server/nextjs.cjs +2 -0
  48. package/dist/server/nextjs.cjs.map +1 -0
  49. package/dist/server/nextjs.d.ts +8 -0
  50. package/dist/server/nextjs.js +26 -0
  51. package/dist/server/nextjs.js.map +1 -0
  52. package/dist/server/tanstack-start.cjs +2 -0
  53. package/dist/server/tanstack-start.cjs.map +1 -0
  54. package/dist/server/tanstack-start.d.ts +25 -0
  55. package/dist/server/tanstack-start.js +39 -0
  56. package/dist/server/tanstack-start.js.map +1 -0
  57. package/dist/server.cjs +1 -1
  58. package/dist/server.cjs.map +1 -1
  59. package/dist/server.d.ts +10 -16
  60. package/dist/server.js +22 -29
  61. package/dist/server.js.map +1 -1
  62. package/dist/{startRecording-BHhcCWQE.js → startRecording-CmZjjJoz.js} +2 -2
  63. package/dist/{startRecording-BHhcCWQE.js.map → startRecording-CmZjjJoz.js.map} +1 -1
  64. package/dist/{startRecording-3sTskM3H.cjs → startRecording-qDCAu4Q0.cjs} +2 -2
  65. package/dist/{startRecording-3sTskM3H.cjs.map → startRecording-qDCAu4Q0.cjs.map} +1 -1
  66. package/dist/types/index.d.ts +22 -10
  67. package/package.json +63 -3
  68. package/src/components/Chat/stories/ConnectionConfiguration.stories.tsx +6 -8
  69. package/src/components/assistant-ui/thread.tsx +8 -14
  70. package/src/components/ui/calendar.tsx +262 -0
  71. package/src/components/ui/time-range-picker.stories.tsx +249 -0
  72. package/src/components/ui/time-range-picker.tsx +675 -0
  73. package/src/components/ui/tool-ui.tsx +31 -2
  74. package/src/hooks/useAuth.ts +59 -7
  75. package/src/hooks/useFollowOnSuggestions.ts +7 -14
  76. package/src/hooks/useModel.ts +30 -0
  77. package/src/index.ts +17 -0
  78. package/src/lib/api.test.ts +4 -4
  79. package/src/lib/auth.ts +34 -4
  80. package/src/lib/models.ts +1 -0
  81. package/src/server/bun.ts +63 -0
  82. package/src/server/core.ts +84 -0
  83. package/src/server/express.ts +60 -0
  84. package/src/server/fastify.ts +61 -0
  85. package/src/server/hono.ts +55 -0
  86. package/src/server/nextjs.ts +58 -0
  87. package/src/server/tanstack-start.ts +110 -0
  88. package/src/server.ts +37 -49
  89. package/src/types/index.ts +25 -9
  90. package/dist/index-BJnv49-A.js.map +0 -1
  91. package/dist/index-ChW-CSuu.cjs +0 -147
  92. package/dist/index-ChW-CSuu.cjs.map +0 -1
@@ -47,6 +47,20 @@ type ContentItem =
47
47
  | { type: 'text'; text: string; _meta?: { 'getgram.ai/mime-type'?: string } }
48
48
  | { type: 'image'; data: string; _meta?: { 'getgram.ai/mime-type'?: string } }
49
49
 
50
+ /** MCP tool annotations providing hints about tool behavior */
51
+ interface ToolAnnotations {
52
+ /** Human-readable display name for the tool */
53
+ title?: string
54
+ /** If true, the tool does not modify its environment */
55
+ readOnlyHint?: boolean
56
+ /** If true, the tool may perform destructive updates */
57
+ destructiveHint?: boolean
58
+ /** If true, repeated calls with same args have no additional effect */
59
+ idempotentHint?: boolean
60
+ /** If true, tool interacts with external entities */
61
+ openWorldHint?: boolean
62
+ }
63
+
50
64
  interface ToolUIProps {
51
65
  /** Display name of the tool */
52
66
  name: string
@@ -64,6 +78,8 @@ interface ToolUIProps {
64
78
  defaultExpanded?: boolean
65
79
  /** Additional class names */
66
80
  className?: string
81
+ /** MCP tool annotations */
82
+ annotations?: ToolAnnotations
67
83
  /** Approval callbacks */
68
84
  onApproveOnce?: () => void
69
85
  onApproveForSession?: () => void
@@ -410,10 +426,13 @@ function ToolUI({
410
426
  result,
411
427
  defaultExpanded = false,
412
428
  className,
429
+ annotations,
413
430
  onApproveOnce,
414
431
  onApproveForSession,
415
432
  onDeny,
416
433
  }: ToolUIProps) {
434
+ // Use annotation title if available, otherwise fall back to name
435
+ const displayName = annotations?.title || name
417
436
  const isApprovalPending =
418
437
  status === 'approval' && onApproveOnce !== undefined && onDeny !== undefined
419
438
  // Auto-expand when approval is pending, collapse when approved
@@ -486,7 +505,7 @@ function ToolUI({
486
505
  !provider && isApprovalPending && 'shimmer'
487
506
  )}
488
507
  >
489
- {name}
508
+ {displayName}
490
509
  </span>
491
510
  {hasContent && (
492
511
  <ChevronDownIcon
@@ -528,10 +547,20 @@ function ToolUI({
528
547
  data-slot="tool-ui-approval-actions"
529
548
  className="border-border flex flex-col gap-2 border-t px-4 py-3 @[320px]:flex-row @[320px]:items-center @[320px]:justify-end"
530
549
  >
531
- <div className="@[320px]:mr-auto">
550
+ <div className="flex items-center gap-2 @[320px]:mr-auto">
532
551
  <span className="text-muted-foreground text-sm">
533
552
  This tool requires approval
534
553
  </span>
554
+ {annotations?.readOnlyHint && (
555
+ <span className="bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-xs">
556
+ Read-only
557
+ </span>
558
+ )}
559
+ {annotations?.destructiveHint && !annotations?.readOnlyHint && (
560
+ <span className="rounded bg-amber-500/10 px-1.5 py-0.5 text-xs text-amber-600 dark:text-amber-400">
561
+ Destructive
562
+ </span>
563
+ )}
535
564
  </div>
536
565
  <div className="flex items-center gap-2 self-end">
537
566
  <Button
@@ -1,9 +1,17 @@
1
1
  import { useReplayContext } from '@/contexts/ReplayContext'
2
- import { hasExplicitSessionAuth, isStaticSessionAuth } from '@/lib/auth'
2
+ import {
3
+ hasExplicitSessionAuth,
4
+ isDangerousApiKeyAuth,
5
+ isStaticSessionAuth,
6
+ isUnifiedFunctionSession,
7
+ isUnifiedStaticSession,
8
+ } from '@/lib/auth'
3
9
  import { useMemo } from 'react'
4
- import { ApiConfig } from '../types'
10
+ import { ApiConfig, GetSessionFn } from '../types'
5
11
  import { useSession } from './useSession'
6
12
 
13
+ declare const __GRAM_API_URL__: string | undefined
14
+
7
15
  export type Auth =
8
16
  | {
9
17
  headers: Record<string, string>
@@ -30,6 +38,28 @@ async function defaultGetSession(init: {
30
38
  return data.client_token
31
39
  }
32
40
 
41
+ function createDangerousApiKeySessionFn(
42
+ apiKey: string,
43
+ apiUrl: string
44
+ ): GetSessionFn {
45
+ return async ({ projectSlug }) => {
46
+ const response = await fetch(`${apiUrl}/rpc/chatSessions.create`, {
47
+ method: 'POST',
48
+ headers: {
49
+ 'Content-Type': 'application/json',
50
+ 'Gram-Key': apiKey,
51
+ 'Gram-Project': projectSlug,
52
+ },
53
+ body: JSON.stringify({
54
+ embed_origin: window.location.origin,
55
+ user_identifier: 'elements-dev',
56
+ }),
57
+ })
58
+ const data = await response.json()
59
+ return data.client_token
60
+ }
61
+ }
62
+
33
63
  /**
34
64
  * Hook to fetch or retrieve the session token for the chat.
35
65
  * @returns The session token string or null
@@ -44,20 +74,42 @@ export const useAuth = ({
44
74
  const replayCtx = useReplayContext()
45
75
  const isReplay = replayCtx?.isReplay ?? false
46
76
 
77
+ const apiUrl = useMemo(() => {
78
+ const envUrl =
79
+ typeof __GRAM_API_URL__ !== 'undefined' ? __GRAM_API_URL__ : undefined
80
+ const url = auth?.url || envUrl || 'https://app.getgram.ai'
81
+ return url.replace(/\/+$/, '')
82
+ }, [auth?.url])
83
+
47
84
  const getSession = useMemo(() => {
48
85
  // In replay mode, skip session fetching entirely
49
86
  if (isReplay) {
50
87
  return null
51
88
  }
89
+ // dangerousApiKey — exchange key for session via API
90
+ if (isDangerousApiKeyAuth(auth)) {
91
+ return createDangerousApiKeySessionFn(auth.dangerousApiKey, apiUrl)
92
+ }
93
+ // Unified session: static string
94
+ if (isUnifiedStaticSession(auth)) {
95
+ return () => Promise.resolve(auth.session)
96
+ }
97
+ // Unified session: function
98
+ if (isUnifiedFunctionSession(auth)) {
99
+ return auth.session
100
+ }
101
+ // Legacy: static sessionToken (deprecated)
52
102
  if (isStaticSessionAuth(auth)) {
53
103
  return () => Promise.resolve(auth.sessionToken)
54
104
  }
55
- return !isStaticSessionAuth(auth) && hasExplicitSessionAuth(auth)
56
- ? auth.sessionFn
57
- : defaultGetSession
58
- }, [auth, isReplay])
105
+ // Legacy: explicit sessionFn (deprecated)
106
+ if (hasExplicitSessionAuth(auth)) {
107
+ return auth.sessionFn
108
+ }
109
+ return defaultGetSession
110
+ }, [auth, isReplay, apiUrl])
59
111
 
60
- // The session request is only neccessary if we are not using static session auth
112
+ // The session request is only necessary if we are not using static session auth
61
113
  // configuration. If a custom session fetcher is provided, we use it,
62
114
  // otherwise we fallback to the default session fetcher
63
115
  const session = useSession({
@@ -1,12 +1,12 @@
1
1
  import { useReplayContext } from '@/contexts/ReplayContext'
2
- import { useAssistantState } from '@assistant-ui/react'
3
- import { useCallback, useEffect, useRef, useState } from 'react'
4
- import { useElements } from './useElements'
5
2
  import { getApiUrl } from '@/lib/api'
6
- import { useAuth } from './useAuth'
7
- import { createOpenRouter } from '@openrouter/ai-sdk-provider'
3
+ import { useAssistantState } from '@assistant-ui/react'
8
4
  import { generateObject } from 'ai'
5
+ import { useCallback, useEffect, useRef, useState } from 'react'
9
6
  import { z } from 'zod'
7
+ import { useAuth } from './useAuth'
8
+ import { useElements } from './useElements'
9
+ import { useModel } from './useModel'
10
10
 
11
11
  export interface FollowOnSuggestion {
12
12
  id: string
@@ -46,6 +46,8 @@ export function useFollowOnSuggestions(): {
46
46
  projectSlug: config.projectSlug,
47
47
  })
48
48
 
49
+ const model = useModel(SUGGESTIONS_MODEL)
50
+
49
51
  // Check if follow-up suggestions are enabled (default: true)
50
52
  // Disable in replay mode since we don't need AI-generated suggestions
51
53
  const isEnabled = !isReplay && config.thread?.followUpSuggestions !== false
@@ -104,15 +106,6 @@ export function useFollowOnSuggestions(): {
104
106
  setIsLoading(true)
105
107
 
106
108
  try {
107
- // Create OpenRouter client
108
- const openRouter = createOpenRouter({
109
- baseURL: apiUrl,
110
- apiKey: 'unused, but must be set',
111
- headers: auth.headers,
112
- })
113
-
114
- const model = openRouter.chat(SUGGESTIONS_MODEL)
115
-
116
109
  // Check if the assistant is asking a question
117
110
  if (lastAssistantMessage) {
118
111
  try {
@@ -0,0 +1,30 @@
1
+ import { getApiUrl } from '@/lib/api'
2
+ import { createOpenRouter } from '@openrouter/ai-sdk-provider'
3
+ import { LanguageModel } from 'ai'
4
+ import { useAuth } from './useAuth'
5
+ import { useElements } from './useElements'
6
+
7
+ // Creates an OpenRouter client to be used for "internal Gram" usage, such as follow-on suggestions
8
+ export const useModel = (
9
+ model: string = 'openai/gpt-4o-mini'
10
+ ): LanguageModel => {
11
+ const { config } = useElements()
12
+
13
+ const auth = useAuth({
14
+ auth: config.api,
15
+ projectSlug: config.projectSlug,
16
+ })
17
+
18
+ const apiUrl = getApiUrl(config)
19
+
20
+ const openRouter = createOpenRouter({
21
+ baseURL: apiUrl,
22
+ apiKey: 'unused, but must be set',
23
+ headers: {
24
+ ...auth.headers,
25
+ 'X-Gram-Source': 'gram',
26
+ },
27
+ })
28
+
29
+ return openRouter.chat(model)
30
+ }
package/src/index.ts CHANGED
@@ -43,6 +43,7 @@ export type {
43
43
  ColorScheme,
44
44
  ComponentOverrides,
45
45
  ComposerConfig,
46
+ DangerousApiKeyAuthConfig,
46
47
  DENSITIES,
47
48
  Density,
48
49
  Dimension,
@@ -62,6 +63,7 @@ export type {
62
63
  ThemeConfig,
63
64
  ToolMentionsConfig,
64
65
  ToolsConfig,
66
+ UnifiedSessionAuthConfig,
65
67
  Variant,
66
68
  VARIANTS,
67
69
  WelcomeConfig,
@@ -70,3 +72,18 @@ export type {
70
72
  export { MODELS } from './lib/models'
71
73
 
72
74
  export type { Plugin } from './types/plugins'
75
+
76
+ // Time Range Picker
77
+ export {
78
+ TimeRangePicker,
79
+ getPresetRange,
80
+ PRESETS,
81
+ } from '@/components/ui/time-range-picker'
82
+ export type {
83
+ TimeRange,
84
+ TimeRangePreset,
85
+ TimeRangePickerProps,
86
+ DateRangePreset,
87
+ } from '@/components/ui/time-range-picker'
88
+ export { Calendar } from '@/components/ui/calendar'
89
+ export type { CalendarProps } from '@/components/ui/calendar'
@@ -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', sessionToken: 'test-key' },
19
+ api: { url: 'https://config.example.com', session: '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: { sessionToken: 'test-key' },
29
+ api: { session: '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: '', sessionToken: 'test-key' },
66
+ api: { url: '', session: '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///', sessionToken: 'test-key' },
76
+ api: { url: 'https://config.example.com///', session: 'test-key' },
77
77
  }
78
78
 
79
79
  expect(getApiUrl(config)).toBe('https://config.example.com')
package/src/lib/auth.ts CHANGED
@@ -1,17 +1,47 @@
1
- import { ApiConfig, SessionAuthConfig, StaticSessionAuthConfig } from '@/types'
1
+ import {
2
+ ApiConfig,
3
+ BaseApiConfig,
4
+ DangerousApiKeyAuthConfig,
5
+ GetSessionFn,
6
+ SessionAuthConfig,
7
+ StaticSessionAuthConfig,
8
+ UnifiedSessionAuthConfig,
9
+ } from '@/types'
2
10
 
3
- /**
4
- * Checks if the auth config is an API key auth config
5
- */
11
+ export function isDangerousApiKeyAuth(
12
+ auth: ApiConfig | undefined
13
+ ): auth is DangerousApiKeyAuthConfig {
14
+ return !!auth && 'dangerousApiKey' in auth
15
+ }
16
+
17
+ export function isUnifiedStaticSession(
18
+ auth: ApiConfig | undefined
19
+ ): auth is UnifiedSessionAuthConfig & { session: string } {
20
+ return !!auth && 'session' in auth && typeof auth.session === 'string'
21
+ }
22
+
23
+ export function isUnifiedFunctionSession(
24
+ auth: ApiConfig | undefined
25
+ ): auth is BaseApiConfig & { session: GetSessionFn } {
26
+ return !!auth && 'session' in auth && typeof auth.session === 'function'
27
+ }
28
+
29
+ /** @deprecated Legacy check for `{ sessionToken }` configs. */
6
30
  export function isStaticSessionAuth(
7
31
  auth: ApiConfig | undefined
8
32
  ): auth is StaticSessionAuthConfig {
9
33
  return !!auth && 'sessionToken' in auth
10
34
  }
11
35
 
36
+ /** @deprecated Legacy check for `{ sessionFn }` configs. */
12
37
  export function hasExplicitSessionAuth(
13
38
  auth: ApiConfig | undefined
14
39
  ): auth is SessionAuthConfig {
15
40
  if (!auth) return false
16
41
  return 'sessionFn' in auth
17
42
  }
43
+
44
+ /** Returns true when either the legacy `sessionToken` or the unified static `session` string is used. */
45
+ export function isAnyStaticSession(auth: ApiConfig | undefined): boolean {
46
+ return isStaticSessionAuth(auth) || isUnifiedStaticSession(auth)
47
+ }
package/src/lib/models.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // List of openrouter models available to the user
2
2
  // This list should be updated to match the model whitelist on the backend side.
3
3
  export const MODELS = [
4
+ 'anthropic/claude-opus-4.6',
4
5
  'anthropic/claude-sonnet-4.5',
5
6
  'anthropic/claude-haiku-4.5',
6
7
  'anthropic/claude-sonnet-4',
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Bun adapter for Gram Elements server handlers.
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { createBunHandler } from '@gram-ai/elements/server/bun'
7
+ *
8
+ * const handler = createBunHandler({
9
+ * embedOrigin: 'http://localhost:3000',
10
+ * userIdentifier: 'user-123',
11
+ * expiresAfter: 3600,
12
+ * })
13
+ *
14
+ * Bun.serve({
15
+ * routes: {
16
+ * '/chat/session': { POST: handler },
17
+ * },
18
+ * })
19
+ * ```
20
+ */
21
+
22
+ import { createChatSession, type SessionHandlerOptions } from './core'
23
+
24
+ /**
25
+ * Create a Bun route handler for the chat session endpoint.
26
+ *
27
+ * @param options - Session configuration options
28
+ * @returns Bun route handler
29
+ */
30
+ export function createBunHandler(
31
+ options:
32
+ | SessionHandlerOptions
33
+ | ((
34
+ request: Request
35
+ ) => SessionHandlerOptions | Promise<SessionHandlerOptions>)
36
+ ) {
37
+ return async (request: Request) => {
38
+ const projectSlug = request.headers.get('gram-project')
39
+
40
+ if (!projectSlug) {
41
+ return new Response(
42
+ JSON.stringify({ error: 'Missing Gram-Project header' }),
43
+ {
44
+ status: 400,
45
+ headers: { 'Content-Type': 'application/json' },
46
+ }
47
+ )
48
+ }
49
+
50
+ const sessionOptions =
51
+ typeof options === 'function' ? await options(request) : options
52
+
53
+ const result = await createChatSession({
54
+ projectSlug,
55
+ options: sessionOptions,
56
+ })
57
+
58
+ return new Response(result.body, {
59
+ status: result.status,
60
+ headers: result.headers,
61
+ })
62
+ }
63
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Core session handler logic shared across all server adapters.
3
+ * This module contains the framework-agnostic business logic for creating chat sessions.
4
+ */
5
+
6
+ export interface SessionHandlerOptions {
7
+ /**
8
+ * The origin from which the token will be used
9
+ */
10
+ embedOrigin: string
11
+
12
+ /**
13
+ * Free-form user identifier
14
+ */
15
+ userIdentifier: string
16
+
17
+ /**
18
+ * Token expiration in seconds (max / default 3600)
19
+ * @default 3600
20
+ */
21
+ expiresAfter?: number
22
+
23
+ /**
24
+ * Gram API key. If not provided, falls back to the `GRAM_API_KEY` environment variable.
25
+ */
26
+ apiKey?: string
27
+ }
28
+
29
+ export interface CreateSessionRequest {
30
+ projectSlug: string
31
+ options: SessionHandlerOptions
32
+ }
33
+
34
+ export interface CreateSessionResponse {
35
+ status: number
36
+ body: string
37
+ headers: Record<string, string>
38
+ }
39
+
40
+ /**
41
+ * Core function to create a chat session by calling Gram's API.
42
+ * This is framework-agnostic and can be used by any adapter.
43
+ */
44
+ export async function createChatSession(
45
+ request: CreateSessionRequest
46
+ ): Promise<CreateSessionResponse> {
47
+ const base = process.env.GRAM_API_URL ?? 'https://app.getgram.ai'
48
+
49
+ try {
50
+ const response = await fetch(base + '/rpc/chatSessions.create', {
51
+ method: 'POST',
52
+ body: JSON.stringify({
53
+ embed_origin: request.options.embedOrigin,
54
+ user_identifier: request.options.userIdentifier,
55
+ expires_after: request.options.expiresAfter,
56
+ }),
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ 'Gram-Project': request.projectSlug,
60
+ 'Gram-Key': request.options.apiKey ?? process.env.GRAM_API_KEY ?? '',
61
+ },
62
+ })
63
+
64
+ const body = await response.text()
65
+
66
+ return {
67
+ status: response.status,
68
+ body,
69
+ headers: { 'Content-Type': 'application/json' },
70
+ }
71
+ } catch (error) {
72
+ const errorMessage =
73
+ error instanceof Error ? error.message : 'Unknown error'
74
+ console.error('Failed to create chat session:', error)
75
+
76
+ return {
77
+ status: 500,
78
+ body: JSON.stringify({
79
+ error: 'Failed to create chat session: ' + errorMessage,
80
+ }),
81
+ headers: { 'Content-Type': 'application/json' },
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Express adapter for Gram Elements server handlers.
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { createExpressHandler } from '@gram-ai/elements/server/express'
7
+ * import express from 'express'
8
+ *
9
+ * const app = express()
10
+ * app.use(express.json())
11
+ *
12
+ * app.post('/chat/session', createExpressHandler({
13
+ * embedOrigin: 'http://localhost:3000',
14
+ * userIdentifier: 'user-123',
15
+ * expiresAfter: 3600,
16
+ * }))
17
+ *
18
+ * app.listen(3000)
19
+ * ```
20
+ */
21
+
22
+ import type { Request, Response } from 'express'
23
+ import { createChatSession, type SessionHandlerOptions } from './core'
24
+
25
+ /**
26
+ * Create an Express request handler for the chat session endpoint.
27
+ *
28
+ * @param options - Session configuration options
29
+ * @returns Express request handler
30
+ */
31
+ export function createExpressHandler(
32
+ options:
33
+ | SessionHandlerOptions
34
+ | ((req: Request) => SessionHandlerOptions | Promise<SessionHandlerOptions>)
35
+ ) {
36
+ return async (req: Request, res: Response) => {
37
+ const projectSlug = Array.isArray(req.headers['gram-project'])
38
+ ? req.headers['gram-project'][0]
39
+ : req.headers['gram-project']
40
+
41
+ if (!projectSlug) {
42
+ res.status(400).json({ error: 'Missing Gram-Project header' })
43
+ return
44
+ }
45
+
46
+ const sessionOptions =
47
+ typeof options === 'function' ? await options(req) : options
48
+
49
+ const result = await createChatSession({
50
+ projectSlug,
51
+ options: sessionOptions,
52
+ })
53
+
54
+ res.status(result.status)
55
+ Object.entries(result.headers).forEach(([key, value]) => {
56
+ res.setHeader(key, value)
57
+ })
58
+ res.send(result.body)
59
+ }
60
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Fastify adapter for Gram Elements server handlers.
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { createFastifyHandler } from '@gram-ai/elements/server/fastify'
7
+ * import Fastify from 'fastify'
8
+ *
9
+ * const fastify = Fastify()
10
+ *
11
+ * fastify.post('/chat/session', createFastifyHandler({
12
+ * embedOrigin: 'http://localhost:3000',
13
+ * userIdentifier: 'user-123',
14
+ * expiresAfter: 3600,
15
+ * }))
16
+ *
17
+ * fastify.listen({ port: 3000 })
18
+ * ```
19
+ */
20
+
21
+ import type { FastifyRequest, FastifyReply } from 'fastify'
22
+ import { createChatSession, type SessionHandlerOptions } from './core'
23
+
24
+ /**
25
+ * Create a Fastify route handler for the chat session endpoint.
26
+ *
27
+ * @param options - Session configuration options
28
+ * @returns Fastify route handler
29
+ */
30
+ export function createFastifyHandler(
31
+ options:
32
+ | SessionHandlerOptions
33
+ | ((
34
+ request: FastifyRequest
35
+ ) => SessionHandlerOptions | Promise<SessionHandlerOptions>)
36
+ ) {
37
+ return async (request: FastifyRequest, reply: FastifyReply) => {
38
+ const projectSlug = Array.isArray(request.headers['gram-project'])
39
+ ? request.headers['gram-project'][0]
40
+ : request.headers['gram-project']
41
+
42
+ if (!projectSlug) {
43
+ reply.code(400).send({ error: 'Missing Gram-Project header' })
44
+ return
45
+ }
46
+
47
+ const sessionOptions =
48
+ typeof options === 'function' ? await options(request) : options
49
+
50
+ const result = await createChatSession({
51
+ projectSlug,
52
+ options: sessionOptions,
53
+ })
54
+
55
+ reply
56
+ .code(result.status)
57
+ .headers(result.headers)
58
+ .type('application/json')
59
+ .send(result.body)
60
+ }
61
+ }