@gram-ai/elements 1.26.1 → 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/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-DfqYP0CD.js → index-oO5BAmPI.js} +12578 -11964
- 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-ZLr2-8s7.cjs → profiler-CEpc7O5Q.cjs} +2 -2
- package/dist/{profiler-ZLr2-8s7.cjs.map → profiler-CEpc7O5Q.cjs.map} +1 -1
- package/dist/{profiler-WoFj2UH8.js → profiler-ECh1zoXF.js} +2 -2
- package/dist/{profiler-WoFj2UH8.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-DzQo16WK.js → startRecording-CmZjjJoz.js} +2 -2
- package/dist/{startRecording-DzQo16WK.js.map → startRecording-CmZjjJoz.js.map} +1 -1
- package/dist/{startRecording-BGnWDInp.cjs → startRecording-qDCAu4Q0.cjs} +2 -2
- package/dist/{startRecording-BGnWDInp.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/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-DdrZQXwQ.cjs +0 -147
- package/dist/index-DdrZQXwQ.cjs.map +0 -1
- package/dist/index-DfqYP0CD.js.map +0 -1
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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono adapter for Gram Elements server handlers.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { createHonoHandler } from '@gram-ai/elements/server/hono'
|
|
7
|
+
* import { Hono } from 'hono'
|
|
8
|
+
*
|
|
9
|
+
* const app = new Hono()
|
|
10
|
+
*
|
|
11
|
+
* app.post('/chat/session', createHonoHandler({
|
|
12
|
+
* embedOrigin: 'http://localhost:3000',
|
|
13
|
+
* userIdentifier: 'user-123',
|
|
14
|
+
* expiresAfter: 3600,
|
|
15
|
+
* }))
|
|
16
|
+
*
|
|
17
|
+
* export default app
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Context } from 'hono'
|
|
22
|
+
import { createChatSession, type SessionHandlerOptions } from './core'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a Hono route handler for the chat session endpoint.
|
|
26
|
+
*
|
|
27
|
+
* @param options - Session configuration options
|
|
28
|
+
* @returns Hono route handler
|
|
29
|
+
*/
|
|
30
|
+
export function createHonoHandler(
|
|
31
|
+
options:
|
|
32
|
+
| SessionHandlerOptions
|
|
33
|
+
| ((c: Context) => SessionHandlerOptions | Promise<SessionHandlerOptions>)
|
|
34
|
+
) {
|
|
35
|
+
return async (c: Context) => {
|
|
36
|
+
const projectSlug = c.req.header('gram-project')
|
|
37
|
+
|
|
38
|
+
if (!projectSlug) {
|
|
39
|
+
return c.json({ error: 'Missing Gram-Project header' }, 400)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sessionOptions =
|
|
43
|
+
typeof options === 'function' ? await options(c) : options
|
|
44
|
+
|
|
45
|
+
const result = await createChatSession({
|
|
46
|
+
projectSlug,
|
|
47
|
+
options: sessionOptions,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return new Response(result.body, {
|
|
51
|
+
status: result.status,
|
|
52
|
+
headers: result.headers,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router adapter for Gram Elements server handlers.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* // app/api/chat/session/route.ts
|
|
7
|
+
* import { createNextHandler } from '@gram-ai/elements/server/nextjs'
|
|
8
|
+
*
|
|
9
|
+
* export const POST = createNextHandler({
|
|
10
|
+
* embedOrigin: 'http://localhost:3000',
|
|
11
|
+
* userIdentifier: 'user-123',
|
|
12
|
+
* expiresAfter: 3600,
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createChatSession, type SessionHandlerOptions } from './core'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a Next.js App Router route handler for the chat session endpoint.
|
|
21
|
+
*
|
|
22
|
+
* @param options - Session configuration options
|
|
23
|
+
* @returns Next.js route handler
|
|
24
|
+
*/
|
|
25
|
+
export function createNextHandler(
|
|
26
|
+
options:
|
|
27
|
+
| SessionHandlerOptions
|
|
28
|
+
| ((
|
|
29
|
+
request: Request
|
|
30
|
+
) => SessionHandlerOptions | Promise<SessionHandlerOptions>)
|
|
31
|
+
) {
|
|
32
|
+
return async (request: Request) => {
|
|
33
|
+
const projectSlug = request.headers.get('gram-project')
|
|
34
|
+
|
|
35
|
+
if (!projectSlug) {
|
|
36
|
+
return new Response(
|
|
37
|
+
JSON.stringify({ error: 'Missing Gram-Project header' }),
|
|
38
|
+
{
|
|
39
|
+
status: 400,
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sessionOptions =
|
|
46
|
+
typeof options === 'function' ? await options(request) : options
|
|
47
|
+
|
|
48
|
+
const result = await createChatSession({
|
|
49
|
+
projectSlug,
|
|
50
|
+
options: sessionOptions,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return new Response(result.body, {
|
|
54
|
+
status: result.status,
|
|
55
|
+
headers: result.headers,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|