@gram-ai/elements 1.18.5 → 1.18.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Chat/stories/Sidecar.stories.d.ts +6 -0
- package/dist/elements.cjs +22 -21
- package/dist/elements.cjs.map +1 -0
- package/dist/elements.js +601 -591
- 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/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 +3 -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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useElements } from './useElements'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Density class mappings for different UI elements
|
|
5
|
+
*/
|
|
6
|
+
const densityClasses = {
|
|
7
|
+
compact: {
|
|
8
|
+
// Padding - small increments (1, 1.5, 2, 2.5, 3)
|
|
9
|
+
'p-xs': 'p-1',
|
|
10
|
+
'p-sm': 'p-1.5',
|
|
11
|
+
'p-md': 'p-2',
|
|
12
|
+
'p-lg': 'p-2.5',
|
|
13
|
+
'p-xl': 'p-3',
|
|
14
|
+
'px-xs': 'px-1',
|
|
15
|
+
'px-sm': 'px-1.5',
|
|
16
|
+
'px-md': 'px-2',
|
|
17
|
+
'px-lg': 'px-2.5',
|
|
18
|
+
'px-xl': 'px-3',
|
|
19
|
+
'py-xs': 'py-1',
|
|
20
|
+
'py-sm': 'py-1.5',
|
|
21
|
+
'py-md': 'py-2',
|
|
22
|
+
'py-lg': 'py-2.5',
|
|
23
|
+
'py-xl': 'py-3',
|
|
24
|
+
// Gaps - small increments
|
|
25
|
+
'gap-sm': 'gap-1',
|
|
26
|
+
'gap-md': 'gap-1.5',
|
|
27
|
+
'gap-lg': 'gap-2',
|
|
28
|
+
'gap-xl': 'gap-2.5',
|
|
29
|
+
// Heights
|
|
30
|
+
'h-header': 'h-10',
|
|
31
|
+
'h-input': 'min-h-10',
|
|
32
|
+
// Text
|
|
33
|
+
'text-base': 'text-sm',
|
|
34
|
+
'text-title': 'text-xl',
|
|
35
|
+
'text-subtitle': 'text-sm',
|
|
36
|
+
},
|
|
37
|
+
normal: {
|
|
38
|
+
// Padding - medium increments (1, 2, 3, 4, 5, 6)
|
|
39
|
+
'p-xs': 'p-1',
|
|
40
|
+
'p-sm': 'p-2',
|
|
41
|
+
'p-md': 'p-3',
|
|
42
|
+
'p-lg': 'p-4',
|
|
43
|
+
'p-xl': 'p-6',
|
|
44
|
+
'px-xs': 'px-1',
|
|
45
|
+
'px-sm': 'px-2',
|
|
46
|
+
'px-md': 'px-3',
|
|
47
|
+
'px-lg': 'px-4',
|
|
48
|
+
'px-xl': 'px-6',
|
|
49
|
+
'py-xs': 'py-1',
|
|
50
|
+
'py-sm': 'py-2',
|
|
51
|
+
'py-md': 'py-3',
|
|
52
|
+
'py-lg': 'py-4',
|
|
53
|
+
'py-xl': 'py-6',
|
|
54
|
+
// Gaps - medium increments
|
|
55
|
+
'gap-sm': 'gap-1.5',
|
|
56
|
+
'gap-md': 'gap-2',
|
|
57
|
+
'gap-lg': 'gap-3',
|
|
58
|
+
'gap-xl': 'gap-4',
|
|
59
|
+
// Heights
|
|
60
|
+
'h-header': 'h-12',
|
|
61
|
+
'h-input': 'min-h-12',
|
|
62
|
+
// Text
|
|
63
|
+
'text-base': 'text-base',
|
|
64
|
+
'text-title': 'text-2xl',
|
|
65
|
+
'text-subtitle': 'text-base',
|
|
66
|
+
},
|
|
67
|
+
spacious: {
|
|
68
|
+
// Padding - large increments (2, 3, 4, 6, 8, 10)
|
|
69
|
+
'p-xs': 'p-2',
|
|
70
|
+
'p-sm': 'p-3',
|
|
71
|
+
'p-md': 'p-4',
|
|
72
|
+
'p-lg': 'p-6',
|
|
73
|
+
'p-xl': 'p-10',
|
|
74
|
+
'px-xs': 'px-2',
|
|
75
|
+
'px-sm': 'px-3',
|
|
76
|
+
'px-md': 'px-4',
|
|
77
|
+
'px-lg': 'px-6',
|
|
78
|
+
'px-xl': 'px-10',
|
|
79
|
+
'py-xs': 'py-2',
|
|
80
|
+
'py-sm': 'py-3',
|
|
81
|
+
'py-md': 'py-4',
|
|
82
|
+
'py-lg': 'py-6',
|
|
83
|
+
'py-xl': 'py-10',
|
|
84
|
+
// Gaps - large increments
|
|
85
|
+
'gap-sm': 'gap-2',
|
|
86
|
+
'gap-md': 'gap-3',
|
|
87
|
+
'gap-lg': 'gap-4',
|
|
88
|
+
'gap-xl': 'gap-6',
|
|
89
|
+
// Heights
|
|
90
|
+
'h-header': 'h-14',
|
|
91
|
+
'h-input': 'min-h-16',
|
|
92
|
+
// Text
|
|
93
|
+
'text-base': 'text-lg',
|
|
94
|
+
'text-title': 'text-3xl',
|
|
95
|
+
'text-subtitle': 'text-lg',
|
|
96
|
+
},
|
|
97
|
+
} as const
|
|
98
|
+
|
|
99
|
+
type DensityToken = keyof (typeof densityClasses)['normal']
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Hook to get density classes based on theme config
|
|
103
|
+
* Use: const d = useDensity(); then d('p-md') returns the appropriate padding class
|
|
104
|
+
*/
|
|
105
|
+
export const useDensity = () => {
|
|
106
|
+
const { config } = useElements()
|
|
107
|
+
const density = config.theme?.density ?? 'normal'
|
|
108
|
+
|
|
109
|
+
return (token: DensityToken) => densityClasses[density][token]
|
|
110
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { ElementsContext } from '@/contexts/contexts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @private Internal hook to access the ElementsContext
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
export const useElements = () => {
|
|
9
|
+
const context = useContext(ElementsContext)
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error('useElements must be used within a ElementsProvider')
|
|
12
|
+
}
|
|
13
|
+
return context
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction } from 'react'
|
|
2
|
+
import { useElements } from './useElements'
|
|
3
|
+
|
|
4
|
+
interface UseExpandedAPI {
|
|
5
|
+
expandable: boolean
|
|
6
|
+
isExpanded: boolean
|
|
7
|
+
defaultExpanded: boolean
|
|
8
|
+
setIsExpanded: Dispatch<SetStateAction<boolean>>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const useExpanded = (): UseExpandedAPI => {
|
|
12
|
+
const { config, isExpanded, setIsExpanded } = useElements()
|
|
13
|
+
const defaultExpanded = config.modal?.defaultExpanded ?? false
|
|
14
|
+
return {
|
|
15
|
+
expandable: config.modal?.expandable ?? false,
|
|
16
|
+
isExpanded,
|
|
17
|
+
setIsExpanded,
|
|
18
|
+
defaultExpanded,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { assert } from '@/lib/utils'
|
|
2
|
+
import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp'
|
|
3
|
+
import { useQuery, type UseQueryResult } from '@tanstack/react-query'
|
|
4
|
+
import { Auth } from './useAuth'
|
|
5
|
+
|
|
6
|
+
type MCPToolsResult = Awaited<
|
|
7
|
+
ReturnType<Awaited<ReturnType<typeof createMCPClient>>['tools']>
|
|
8
|
+
>
|
|
9
|
+
|
|
10
|
+
export function useMCPTools({
|
|
11
|
+
auth,
|
|
12
|
+
mcp,
|
|
13
|
+
environment,
|
|
14
|
+
}: {
|
|
15
|
+
auth: Auth
|
|
16
|
+
mcp: string | undefined
|
|
17
|
+
environment: Record<string, unknown>
|
|
18
|
+
}): UseQueryResult<MCPToolsResult, Error> {
|
|
19
|
+
const authQueryKey = Object.entries(auth.headers ?? {}).map(
|
|
20
|
+
(k, v) => `${k}:${v}`
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const queryResult = useQuery({
|
|
24
|
+
queryKey: ['mcpTools', mcp, ...authQueryKey],
|
|
25
|
+
queryFn: async () => {
|
|
26
|
+
assert(!auth.isLoading, 'No auth found')
|
|
27
|
+
assert(mcp, 'No MCP URL found')
|
|
28
|
+
|
|
29
|
+
const mcpClient = await createMCPClient({
|
|
30
|
+
name: 'gram-elements-mcp-client',
|
|
31
|
+
transport: {
|
|
32
|
+
type: 'http',
|
|
33
|
+
url: mcp,
|
|
34
|
+
headers: {
|
|
35
|
+
...transformEnvironmentToHeaders(environment ?? {}),
|
|
36
|
+
...auth.headers,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const mcpTools = await mcpClient.tools()
|
|
42
|
+
return mcpTools
|
|
43
|
+
},
|
|
44
|
+
enabled: !auth.isLoading && !!mcp,
|
|
45
|
+
staleTime: Infinity,
|
|
46
|
+
gcTime: Infinity,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return queryResult
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const HEADER_PREFIX = 'MCP-'
|
|
53
|
+
|
|
54
|
+
function transformEnvironmentToHeaders(environment: Record<string, unknown>) {
|
|
55
|
+
if (typeof environment !== 'object' || environment === null) {
|
|
56
|
+
return {}
|
|
57
|
+
}
|
|
58
|
+
return Object.entries(environment).reduce(
|
|
59
|
+
(acc, [key, value]) => {
|
|
60
|
+
// Normalize key: replace underscores with dashes
|
|
61
|
+
const normalizedKey = key.replace(/_/g, '-')
|
|
62
|
+
|
|
63
|
+
// Add MCP- prefix if it doesn't already have it
|
|
64
|
+
const headerKey = normalizedKey.startsWith(HEADER_PREFIX)
|
|
65
|
+
? normalizedKey
|
|
66
|
+
: `${HEADER_PREFIX}${normalizedKey}`
|
|
67
|
+
|
|
68
|
+
acc[headerKey] = value as string
|
|
69
|
+
return acc
|
|
70
|
+
},
|
|
71
|
+
{} as Record<string, string>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CodeHeaderProps,
|
|
3
|
+
SyntaxHighlighterProps,
|
|
4
|
+
} from '@assistant-ui/react-markdown'
|
|
5
|
+
import { ComponentType, useMemo } from 'react'
|
|
6
|
+
import { Plugin } from '@/types/plugins'
|
|
7
|
+
|
|
8
|
+
type ComponentsByLanguage =
|
|
9
|
+
| Record<
|
|
10
|
+
string,
|
|
11
|
+
{
|
|
12
|
+
CodeHeader?: ComponentType<CodeHeaderProps> | undefined
|
|
13
|
+
SyntaxHighlighter?: ComponentType<SyntaxHighlighterProps> | undefined
|
|
14
|
+
}
|
|
15
|
+
>
|
|
16
|
+
| undefined
|
|
17
|
+
|
|
18
|
+
export function useComponentsByLanguage(plugins: Plugin[]) {
|
|
19
|
+
return useMemo(() => {
|
|
20
|
+
return plugins.reduce((acc, plugin) => {
|
|
21
|
+
if (acc?.[plugin.language] && !plugin.overrideExisting) {
|
|
22
|
+
return acc
|
|
23
|
+
}
|
|
24
|
+
acc = {
|
|
25
|
+
...acc,
|
|
26
|
+
[plugin.language]: {
|
|
27
|
+
CodeHeader: plugin.Header ?? (() => null),
|
|
28
|
+
SyntaxHighlighter: plugin.Component ?? undefined,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
return acc
|
|
32
|
+
}, {} as ComponentsByLanguage)
|
|
33
|
+
}, [plugins])
|
|
34
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Radius } from '@/types'
|
|
2
|
+
import { useElements } from './useElements'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Radius class mappings for different UI elements
|
|
6
|
+
*/
|
|
7
|
+
const radiusClasses: Record<Radius, Record<RadiusSize, string>> = {
|
|
8
|
+
sharp: {
|
|
9
|
+
sm: 'rounded-sm',
|
|
10
|
+
md: 'rounded',
|
|
11
|
+
lg: 'rounded-md',
|
|
12
|
+
xl: 'rounded-lg',
|
|
13
|
+
full: 'rounded-lg',
|
|
14
|
+
},
|
|
15
|
+
soft: {
|
|
16
|
+
sm: 'rounded',
|
|
17
|
+
md: 'rounded-lg',
|
|
18
|
+
lg: 'rounded-xl',
|
|
19
|
+
xl: 'rounded-2xl',
|
|
20
|
+
full: 'rounded-full',
|
|
21
|
+
},
|
|
22
|
+
round: {
|
|
23
|
+
sm: 'rounded-lg',
|
|
24
|
+
md: 'rounded-xl',
|
|
25
|
+
lg: 'rounded-2xl',
|
|
26
|
+
xl: 'rounded-3xl',
|
|
27
|
+
full: 'rounded-full',
|
|
28
|
+
},
|
|
29
|
+
} as const
|
|
30
|
+
|
|
31
|
+
type RadiusSize = 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Hook to get radius classes based on theme config
|
|
35
|
+
* Use: const r = useRadius(); then r('lg') returns the appropriate rounded class
|
|
36
|
+
*/
|
|
37
|
+
export const useRadius = () => {
|
|
38
|
+
const { config } = useElements()
|
|
39
|
+
const radius = config.theme?.radius ?? 'soft'
|
|
40
|
+
|
|
41
|
+
return (size: RadiusSize) => radiusClasses[radius][size]
|
|
42
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { GetSessionFn } from '@/types'
|
|
2
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to fetch or retrieve the session token for the chat.
|
|
6
|
+
* @returns The session token string or null
|
|
7
|
+
*/
|
|
8
|
+
export const useSession = ({
|
|
9
|
+
getSession,
|
|
10
|
+
projectSlug,
|
|
11
|
+
enabled,
|
|
12
|
+
}: {
|
|
13
|
+
getSession: GetSessionFn | null
|
|
14
|
+
projectSlug: string
|
|
15
|
+
enabled: boolean
|
|
16
|
+
}): string | null => {
|
|
17
|
+
const queryClient = useQueryClient()
|
|
18
|
+
const queryKey = ['chatSession', projectSlug]
|
|
19
|
+
|
|
20
|
+
const queryState = queryClient.getQueryState(queryKey)
|
|
21
|
+
const hasData = queryState?.data !== undefined
|
|
22
|
+
// Check if data is stale - with staleTime: Infinity, data never becomes stale
|
|
23
|
+
// but we check dataUpdatedAt to determine if we should refetch
|
|
24
|
+
const dataUpdatedAt = queryState?.dataUpdatedAt ?? 0
|
|
25
|
+
const staleTime = Infinity // Matches the staleTime in useQuery options
|
|
26
|
+
const isStale = hasData && Date.now() - dataUpdatedAt > staleTime
|
|
27
|
+
const shouldFetch = !hasData || isStale
|
|
28
|
+
|
|
29
|
+
const { data: fetchedSessionToken } = useQuery({
|
|
30
|
+
queryKey,
|
|
31
|
+
queryFn: () => getSession!({ projectSlug }),
|
|
32
|
+
enabled: enabled && shouldFetch && getSession !== null,
|
|
33
|
+
staleTime: Infinity, // Session tokens don't need to be refetched
|
|
34
|
+
gcTime: Infinity, // Keep in cache indefinitely
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return fetchedSessionToken ?? null
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useElements } from './useElements'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to get theme-related props including dark mode class
|
|
6
|
+
*/
|
|
7
|
+
export const useThemeProps = () => {
|
|
8
|
+
const { config } = useElements()
|
|
9
|
+
const theme = config.theme ?? {}
|
|
10
|
+
|
|
11
|
+
return useMemo(() => {
|
|
12
|
+
const { colorScheme = 'light' } = theme
|
|
13
|
+
|
|
14
|
+
const isDark =
|
|
15
|
+
colorScheme === 'dark' ||
|
|
16
|
+
(colorScheme === 'system' &&
|
|
17
|
+
typeof window !== 'undefined' &&
|
|
18
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
className: isDark ? 'dark' : undefined,
|
|
22
|
+
} as const
|
|
23
|
+
}, [theme])
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { ToolApprovalContext } from '@/contexts/contexts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to access the tool approval context for managing human-in-the-loop
|
|
6
|
+
* tool execution approval.
|
|
7
|
+
*/
|
|
8
|
+
export const useToolApproval = () => {
|
|
9
|
+
const context = useContext(ToolApprovalContext)
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'useToolApproval must be used within a ToolApprovalProvider'
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
return context
|
|
16
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Side-effect import to include CSS in build (consumers import via @gram-ai/elements/elements.css)
|
|
2
|
+
import './global.css'
|
|
3
|
+
|
|
4
|
+
// Context Providers
|
|
5
|
+
export { ElementsProvider as GramElementsProvider } from './contexts/ElementsProvider'
|
|
6
|
+
export { useElements as useGramElements } from './hooks/useElements'
|
|
7
|
+
|
|
8
|
+
// Core Components
|
|
9
|
+
export { Chat } from '@/components/Chat'
|
|
10
|
+
|
|
11
|
+
// Frontend Tools
|
|
12
|
+
export { defineFrontendTool } from './lib/tools'
|
|
13
|
+
export type { FrontendTool } from './lib/tools'
|
|
14
|
+
|
|
15
|
+
// Types
|
|
16
|
+
export type {
|
|
17
|
+
AttachmentsConfig,
|
|
18
|
+
COLOR_SCHEMES,
|
|
19
|
+
ColorScheme,
|
|
20
|
+
ComponentOverrides,
|
|
21
|
+
ComposerConfig,
|
|
22
|
+
DENSITIES,
|
|
23
|
+
Density,
|
|
24
|
+
Dimension,
|
|
25
|
+
Dimensions,
|
|
26
|
+
ElementsConfig,
|
|
27
|
+
GetSessionFn,
|
|
28
|
+
ModalConfig,
|
|
29
|
+
ModalTriggerPosition,
|
|
30
|
+
Model,
|
|
31
|
+
ModelConfig,
|
|
32
|
+
RADII,
|
|
33
|
+
Radius,
|
|
34
|
+
SidecarConfig,
|
|
35
|
+
Suggestion,
|
|
36
|
+
ThemeConfig,
|
|
37
|
+
ToolsConfig,
|
|
38
|
+
Variant,
|
|
39
|
+
VARIANTS,
|
|
40
|
+
WelcomeConfig,
|
|
41
|
+
} from './types'
|
|
42
|
+
|
|
43
|
+
export { MODELS } from './lib/models'
|
|
44
|
+
|
|
45
|
+
export type { Plugin } from './types/plugins'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import type { ElementsConfig } from '@/types'
|
|
3
|
+
|
|
4
|
+
describe('getApiUrl', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.resetModules()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
async function loadGetApiUrl(gramApiUrl: string | undefined) {
|
|
10
|
+
vi.stubGlobal('__GRAM_API_URL__', gramApiUrl)
|
|
11
|
+
const { getApiUrl } = await import('./api')
|
|
12
|
+
return getApiUrl
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
it('uses config.api.url when set', async () => {
|
|
16
|
+
const getApiUrl = await loadGetApiUrl('https://env.example.com')
|
|
17
|
+
const config: ElementsConfig = {
|
|
18
|
+
projectSlug: 'test',
|
|
19
|
+
api: { url: 'https://config.example.com', UNSAFE_apiKey: 'test-key' },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
expect(getApiUrl(config)).toBe('https://config.example.com')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('falls back to __GRAM_API_URL__ when config.api.url is not set', async () => {
|
|
26
|
+
const getApiUrl = await loadGetApiUrl('https://env.example.com')
|
|
27
|
+
const config: ElementsConfig = {
|
|
28
|
+
projectSlug: 'test',
|
|
29
|
+
api: { UNSAFE_apiKey: 'test-key' },
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
expect(getApiUrl(config)).toBe('https://env.example.com')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('falls back to __GRAM_API_URL__ when config.api is undefined', async () => {
|
|
36
|
+
const getApiUrl = await loadGetApiUrl('https://env.example.com')
|
|
37
|
+
const config: ElementsConfig = {
|
|
38
|
+
projectSlug: 'test',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
expect(getApiUrl(config)).toBe('https://env.example.com')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('falls back to default URL when both config.api.url and __GRAM_API_URL__ are not set', async () => {
|
|
45
|
+
const getApiUrl = await loadGetApiUrl('')
|
|
46
|
+
const config: ElementsConfig = {
|
|
47
|
+
projectSlug: 'test',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
expect(getApiUrl(config)).toBe('https://app.getgram.ai')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('falls back to default URL when __GRAM_API_URL__ is undefined', async () => {
|
|
54
|
+
const getApiUrl = await loadGetApiUrl(undefined)
|
|
55
|
+
const config: ElementsConfig = {
|
|
56
|
+
projectSlug: 'test',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
expect(getApiUrl(config)).toBe('https://app.getgram.ai')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('skips empty string config.api.url and uses __GRAM_API_URL__', async () => {
|
|
63
|
+
const getApiUrl = await loadGetApiUrl('https://env.example.com')
|
|
64
|
+
const config: ElementsConfig = {
|
|
65
|
+
projectSlug: 'test',
|
|
66
|
+
api: { url: '', UNSAFE_apiKey: 'test-key' },
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
expect(getApiUrl(config)).toBe('https://env.example.com')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('removes trailing slashes from the URL', async () => {
|
|
73
|
+
const getApiUrl = await loadGetApiUrl('')
|
|
74
|
+
const config: ElementsConfig = {
|
|
75
|
+
projectSlug: 'test',
|
|
76
|
+
api: { url: 'https://config.example.com///', UNSAFE_apiKey: 'test-key' },
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
expect(getApiUrl(config)).toBe('https://config.example.com')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('removes trailing slashes from __GRAM_API_URL__', async () => {
|
|
83
|
+
const getApiUrl = await loadGetApiUrl('https://env.example.com//')
|
|
84
|
+
const config: ElementsConfig = {
|
|
85
|
+
projectSlug: 'test',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
expect(getApiUrl(config)).toBe('https://env.example.com')
|
|
89
|
+
})
|
|
90
|
+
})
|
package/src/lib/api.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ElementsConfig } from '@/types'
|
|
2
|
+
|
|
3
|
+
export function getApiUrl(config: ElementsConfig): string {
|
|
4
|
+
// The api.url in the config should take precedence over the __GRAM_API_URL__ environment variable
|
|
5
|
+
// because it is a user-defined override
|
|
6
|
+
const apiURL = config.api?.url || __GRAM_API_URL__ || 'https://app.getgram.ai'
|
|
7
|
+
return apiURL.replace(/\/+$/, '') // Remove trailing slashes
|
|
8
|
+
}
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ApiKeyAuthConfig, AuthConfig } from '@/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if the auth config is an API key auth config
|
|
5
|
+
*/
|
|
6
|
+
export function isApiKeyAuth(
|
|
7
|
+
auth: AuthConfig | undefined
|
|
8
|
+
): auth is ApiKeyAuthConfig {
|
|
9
|
+
return !!auth && 'UNSAFE_apiKey' in auth
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const EASE_OUT_QUINT = [0.23, 1, 0.32, 1] as const
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// humanize tool name:
|
|
2
|
+
// - split camel case into words
|
|
3
|
+
// - capitalize first letter of each word
|
|
4
|
+
// - remove hyphens / underscores
|
|
5
|
+
// - title case the string
|
|
6
|
+
export function humanizeToolName(toolName: string): string {
|
|
7
|
+
return toolName
|
|
8
|
+
.replace(/[-_]/g, ' ') // Replace hyphens and underscores with spaces
|
|
9
|
+
.split(/(?=[A-Z])/) // Split on camelCase boundaries
|
|
10
|
+
.join(' ') // Join with spaces
|
|
11
|
+
.split(/\s+/) // Split on any whitespace to normalize
|
|
12
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) // Title case each word
|
|
13
|
+
.join(' ')
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// List of openrouter models available to the user
|
|
2
|
+
// This list should be updated to match the model whitelist on the backend side.
|
|
3
|
+
export const MODELS = [
|
|
4
|
+
'anthropic/claude-sonnet-4.5',
|
|
5
|
+
'anthropic/claude-haiku-4.5',
|
|
6
|
+
'anthropic/claude-sonnet-4',
|
|
7
|
+
'anthropic/claude-opus-4.5',
|
|
8
|
+
'openai/gpt-4o',
|
|
9
|
+
'openai/gpt-4o-mini',
|
|
10
|
+
'openai/gpt-5.1-codex',
|
|
11
|
+
'openai/gpt-5',
|
|
12
|
+
'openai/gpt-5.1',
|
|
13
|
+
'openai/gpt-4.1',
|
|
14
|
+
'anthropic/claude-3.7-sonnet',
|
|
15
|
+
'anthropic/claude-opus-4',
|
|
16
|
+
'google/gemini-2.5-pro-preview',
|
|
17
|
+
'google/gemini-3-pro-preview',
|
|
18
|
+
'moonshotai/kimi-k2',
|
|
19
|
+
'mistralai/mistral-medium-3',
|
|
20
|
+
'mistralai/mistral-medium-3.1',
|
|
21
|
+
'mistralai/codestral-2501',
|
|
22
|
+
] as const
|