@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.
- package/README.md +83 -15
- package/dist/components/Chat/stories/ConnectionConfiguration.stories.d.ts +1 -1
- package/dist/components/ui/calendar.d.ts +25 -0
- package/dist/components/ui/time-range-picker.d.ts +46 -0
- package/dist/components/ui/time-range-picker.stories.d.ts +37 -0
- package/dist/components/ui/tool-ui.d.ts +16 -1
- package/dist/core-Cqad6-xW.js +36 -0
- package/dist/core-Cqad6-xW.js.map +1 -0
- package/dist/core-DBxmxwCi.cjs +2 -0
- package/dist/core-DBxmxwCi.cjs.map +1 -0
- package/dist/elements.cjs +1 -1
- package/dist/elements.css +1 -1
- package/dist/elements.js +18 -14
- package/dist/hooks/useModel.d.ts +2 -0
- package/dist/index-CP-wWZCV.cjs +172 -0
- package/dist/index-CP-wWZCV.cjs.map +1 -0
- package/dist/{index-BJnv49-A.js → index-oO5BAmPI.js} +12667 -12048
- package/dist/index-oO5BAmPI.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/lib/auth.d.ts +12 -4
- package/dist/lib/models.d.ts +1 -1
- package/dist/{profiler-DCWYDZ1F.cjs → profiler-CEpc7O5Q.cjs} +2 -2
- package/dist/{profiler-DCWYDZ1F.cjs.map → profiler-CEpc7O5Q.cjs.map} +1 -1
- package/dist/{profiler-D4Tw5ecI.js → profiler-ECh1zoXF.js} +2 -2
- package/dist/{profiler-D4Tw5ecI.js.map → profiler-ECh1zoXF.js.map} +1 -1
- package/dist/server/bun.cjs +2 -0
- package/dist/server/bun.cjs.map +1 -0
- package/dist/server/bun.d.ts +8 -0
- package/dist/server/bun.js +26 -0
- package/dist/server/bun.js.map +1 -0
- package/dist/server/core.d.ts +37 -0
- package/dist/server/express.cjs +2 -0
- package/dist/server/express.cjs.map +1 -0
- package/dist/server/express.d.ts +9 -0
- package/dist/server/express.js +21 -0
- package/dist/server/express.js.map +1 -0
- package/dist/server/fastify.cjs +2 -0
- package/dist/server/fastify.cjs.map +1 -0
- package/dist/server/fastify.d.ts +9 -0
- package/dist/server/fastify.js +19 -0
- package/dist/server/fastify.js.map +1 -0
- package/dist/server/hono.cjs +2 -0
- package/dist/server/hono.cjs.map +1 -0
- package/dist/server/hono.d.ts +9 -0
- package/dist/server/hono.js +20 -0
- package/dist/server/hono.js.map +1 -0
- package/dist/server/nextjs.cjs +2 -0
- package/dist/server/nextjs.cjs.map +1 -0
- package/dist/server/nextjs.d.ts +8 -0
- package/dist/server/nextjs.js +26 -0
- package/dist/server/nextjs.js.map +1 -0
- package/dist/server/tanstack-start.cjs +2 -0
- package/dist/server/tanstack-start.cjs.map +1 -0
- package/dist/server/tanstack-start.d.ts +25 -0
- package/dist/server/tanstack-start.js +39 -0
- package/dist/server/tanstack-start.js.map +1 -0
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +10 -16
- package/dist/server.js +22 -29
- package/dist/server.js.map +1 -1
- package/dist/{startRecording-BHhcCWQE.js → startRecording-CmZjjJoz.js} +2 -2
- package/dist/{startRecording-BHhcCWQE.js.map → startRecording-CmZjjJoz.js.map} +1 -1
- package/dist/{startRecording-3sTskM3H.cjs → startRecording-qDCAu4Q0.cjs} +2 -2
- package/dist/{startRecording-3sTskM3H.cjs.map → startRecording-qDCAu4Q0.cjs.map} +1 -1
- package/dist/types/index.d.ts +22 -10
- package/package.json +63 -3
- package/src/components/Chat/stories/ConnectionConfiguration.stories.tsx +6 -8
- package/src/components/assistant-ui/thread.tsx +8 -14
- package/src/components/ui/calendar.tsx +262 -0
- package/src/components/ui/time-range-picker.stories.tsx +249 -0
- package/src/components/ui/time-range-picker.tsx +675 -0
- package/src/components/ui/tool-ui.tsx +31 -2
- package/src/hooks/useAuth.ts +59 -7
- package/src/hooks/useFollowOnSuggestions.ts +7 -14
- package/src/hooks/useModel.ts +30 -0
- package/src/index.ts +17 -0
- package/src/lib/api.test.ts +4 -4
- package/src/lib/auth.ts +34 -4
- package/src/lib/models.ts +1 -0
- package/src/server/bun.ts +63 -0
- package/src/server/core.ts +84 -0
- package/src/server/express.ts +60 -0
- package/src/server/fastify.ts +61 -0
- package/src/server/hono.ts +55 -0
- package/src/server/nextjs.ts +58 -0
- package/src/server/tanstack-start.ts +110 -0
- package/src/server.ts +37 -49
- package/src/types/index.ts +25 -9
- package/dist/index-BJnv49-A.js.map +0 -1
- package/dist/index-ChW-CSuu.cjs +0 -147
- 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
|
-
{
|
|
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
|
package/src/hooks/useAuth.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { useReplayContext } from '@/contexts/ReplayContext'
|
|
2
|
-
import {
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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 {
|
|
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'
|
package/src/lib/api.test.ts
CHANGED
|
@@ -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',
|
|
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: {
|
|
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: '',
|
|
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///',
|
|
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 {
|
|
1
|
+
import {
|
|
2
|
+
ApiConfig,
|
|
3
|
+
BaseApiConfig,
|
|
4
|
+
DangerousApiKeyAuthConfig,
|
|
5
|
+
GetSessionFn,
|
|
6
|
+
SessionAuthConfig,
|
|
7
|
+
StaticSessionAuthConfig,
|
|
8
|
+
UnifiedSessionAuthConfig,
|
|
9
|
+
} from '@/types'
|
|
2
10
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
}
|