@gram-ai/elements 1.20.0 → 1.20.2
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/index.d.ts +5 -1
- package/dist/components/Chat/stories/ErrorBoundary.stories.d.ts +8 -0
- package/dist/components/Chat/stories/Variants.stories.d.ts +2 -1
- package/dist/components/assistant-ui/assistant-modal.d.ts +5 -1
- package/dist/components/assistant-ui/assistant-sidecar.d.ts +5 -1
- package/dist/components/assistant-ui/error-boundary.d.ts +28 -0
- package/dist/components/assistant-ui/thread-list.d.ts +5 -1
- package/dist/components/assistant-ui/thread.d.ts +5 -1
- package/dist/elements.cjs +42 -42
- package/dist/elements.cjs.map +1 -1
- package/dist/elements.css +1 -1
- package/dist/elements.js +6010 -5965
- package/dist/elements.js.map +1 -1
- package/dist/{index-DaF9fGY-.js → index-BwdTXSZG.js} +4 -3
- package/dist/{index-DaF9fGY-.js.map → index-BwdTXSZG.js.map} +1 -1
- package/dist/{index-B52U8PL6.cjs → index-D8g4LkEy.cjs} +3 -3
- package/dist/{index-B52U8PL6.cjs.map → index-D8g4LkEy.cjs.map} +1 -1
- package/dist/plugins.cjs +1 -1
- package/dist/plugins.js +1 -1
- package/package.json +4 -2
- package/src/components/Chat/index.tsx +15 -3
- package/src/components/Chat/stories/ErrorBoundary.stories.tsx +221 -0
- package/src/components/Chat/stories/Variants.stories.tsx +28 -1
- package/src/components/assistant-ui/assistant-modal.tsx +11 -3
- package/src/components/assistant-ui/assistant-sidecar.tsx +11 -3
- package/src/components/assistant-ui/error-boundary.tsx +113 -0
- package/src/components/assistant-ui/thread-list.tsx +8 -3
- package/src/components/assistant-ui/thread.tsx +7 -2
- package/src/contexts/ElementsProvider.tsx +16 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { AlertCircle } from 'lucide-react'
|
|
4
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
import { Button } from '../ui/button'
|
|
7
|
+
|
|
8
|
+
interface ErrorBoundaryProps {
|
|
9
|
+
children: ReactNode
|
|
10
|
+
fallback?: ReactNode
|
|
11
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
|
12
|
+
onReset?: () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ErrorBoundaryState {
|
|
16
|
+
hasError: boolean
|
|
17
|
+
error: Error | null
|
|
18
|
+
resetKey: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ErrorFallbackProps {
|
|
22
|
+
error: Error | null
|
|
23
|
+
onRetry: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
27
|
+
const ErrorFallback = ({ error, onRetry }: ErrorFallbackProps) => {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={cn(
|
|
31
|
+
'aui-root aui-error-boundary bg-background flex h-full w-full flex-col items-center justify-center p-6'
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
<div className="flex flex-col items-center gap-4 text-center">
|
|
35
|
+
<div className="text-destructive">
|
|
36
|
+
<AlertCircle className="size-12 stroke-[1.5px]" />
|
|
37
|
+
</div>
|
|
38
|
+
<div className="flex flex-col gap-2">
|
|
39
|
+
<h3 className="text-foreground text-xl font-semibold">
|
|
40
|
+
Something went wrong
|
|
41
|
+
</h3>
|
|
42
|
+
<p className="text-muted-foreground text-base">
|
|
43
|
+
An error occurred while loading the chat.
|
|
44
|
+
</p>
|
|
45
|
+
{error && (
|
|
46
|
+
<p className="text-muted-foreground/60 max-w-md truncate text-sm">
|
|
47
|
+
{error.message}
|
|
48
|
+
</p>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
<Button onClick={onRetry} variant="default" className="mt-2">
|
|
52
|
+
Try again
|
|
53
|
+
</Button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
60
|
+
const Remounter = ({ children }: { children: ReactNode }) => <>{children}</>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Global error boundary for the Elements library. Catches unexpected errors and renders a fallback UI.
|
|
64
|
+
* We wrap the assistant-modal, assistant-sidecar, and chat components with this error boundary.
|
|
65
|
+
* Each variant needs to have the error boundary rendered at the appropriate level e.g if using
|
|
66
|
+
* the widget variant, then the error screen must be rendered within the widget modal.
|
|
67
|
+
* TODO: We should add more granular error boundaries (e.g wrapping AssistantMessage, ThreadWelcome, etc.)
|
|
68
|
+
* TODO: We should also wrap ChatHistory, which may yield its own errors.
|
|
69
|
+
*/
|
|
70
|
+
export class ErrorBoundary extends Component<
|
|
71
|
+
ErrorBoundaryProps,
|
|
72
|
+
ErrorBoundaryState
|
|
73
|
+
> {
|
|
74
|
+
constructor(props: ErrorBoundaryProps) {
|
|
75
|
+
super(props)
|
|
76
|
+
this.state = { hasError: false, error: null, resetKey: 0 }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
|
80
|
+
return { hasError: true, error }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
|
84
|
+
this.props.onError?.(error, errorInfo)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
handleRetry = () => {
|
|
88
|
+
// Increment resetKey to force remount of children, reinitializing the chat
|
|
89
|
+
this.setState((state) => ({
|
|
90
|
+
hasError: false,
|
|
91
|
+
error: null,
|
|
92
|
+
resetKey: state.resetKey + 1,
|
|
93
|
+
}))
|
|
94
|
+
this.props.onReset?.()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render(): ReactNode {
|
|
98
|
+
if (this.state.hasError) {
|
|
99
|
+
if (this.props.fallback) {
|
|
100
|
+
return this.props.fallback
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<ErrorFallback error={this.state.error} onRetry={this.handleRetry} />
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Use Remounter with key to force unmount/remount of children when retry is clicked
|
|
109
|
+
return (
|
|
110
|
+
<Remounter key={this.state.resetKey}>{this.props.children}</Remounter>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -12,13 +12,18 @@ import { useRadius } from '@/hooks/useRadius'
|
|
|
12
12
|
import { cn } from '@/lib/utils'
|
|
13
13
|
import { useDensity } from '@/hooks/useDensity'
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
interface ThreadListProps {
|
|
16
|
+
className?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const ThreadList: FC<ThreadListProps> = ({ className }) => {
|
|
16
20
|
const d = useDensity()
|
|
17
21
|
return (
|
|
18
22
|
<ThreadListPrimitive.Root
|
|
19
23
|
className={cn(
|
|
20
|
-
'aui-root aui-thread-list-root flex flex-col items-stretch',
|
|
21
|
-
d('gap-sm')
|
|
24
|
+
'aui-root aui-thread-list-root bg-background flex flex-col items-stretch',
|
|
25
|
+
d('gap-sm'),
|
|
26
|
+
className
|
|
22
27
|
)}
|
|
23
28
|
>
|
|
24
29
|
<div
|
|
@@ -68,7 +68,11 @@ const ApiKeyWarning = () => (
|
|
|
68
68
|
</div>
|
|
69
69
|
)
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
interface ThreadProps {
|
|
72
|
+
className?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const Thread: FC<ThreadProps> = ({ className }) => {
|
|
72
76
|
const themeProps = useThemeProps()
|
|
73
77
|
const d = useDensity()
|
|
74
78
|
const { config } = useElements()
|
|
@@ -81,7 +85,8 @@ export const Thread: FC = () => {
|
|
|
81
85
|
<ThreadPrimitive.Root
|
|
82
86
|
className={cn(
|
|
83
87
|
'aui-root aui-thread-root bg-background @container flex h-full flex-col',
|
|
84
|
-
themeProps.className
|
|
88
|
+
themeProps.className,
|
|
89
|
+
className
|
|
85
90
|
)}
|
|
86
91
|
>
|
|
87
92
|
<ThreadPrimitive.Viewport
|
|
@@ -173,6 +173,10 @@ const ElementsProviderWithApproval = ({
|
|
|
173
173
|
// but runtime is created using transport. The ref gets populated after runtime creation.
|
|
174
174
|
const runtimeRef = useRef<ReturnType<typeof useChatRuntime> | null>(null)
|
|
175
175
|
|
|
176
|
+
// Generate a stable chat ID for server-side persistence (when history is disabled)
|
|
177
|
+
// When history is enabled, the thread adapter manages chat IDs instead
|
|
178
|
+
const chatIdRef = useRef<string | null>(null)
|
|
179
|
+
|
|
176
180
|
// Create chat transport configuration
|
|
177
181
|
const transport = useMemo<ChatTransport<UIMessage>>(
|
|
178
182
|
() => ({
|
|
@@ -183,18 +187,29 @@ const ElementsProviderWithApproval = ({
|
|
|
183
187
|
throw new Error('Session is loading')
|
|
184
188
|
}
|
|
185
189
|
|
|
190
|
+
// Generate chat ID on first message if not already set
|
|
191
|
+
if (!chatIdRef.current) {
|
|
192
|
+
chatIdRef.current = crypto.randomUUID()
|
|
193
|
+
}
|
|
194
|
+
|
|
186
195
|
const context = runtimeRef.current?.thread.getModelContext()
|
|
187
196
|
const frontendTools = toAISDKTools(
|
|
188
197
|
getEnabledTools(context?.tools ?? {})
|
|
189
198
|
)
|
|
190
199
|
|
|
200
|
+
// Include Gram-Chat-ID header for chat persistence
|
|
201
|
+
const headersWithChatId = {
|
|
202
|
+
...auth.headers,
|
|
203
|
+
'Gram-Chat-ID': chatIdRef.current,
|
|
204
|
+
}
|
|
205
|
+
|
|
191
206
|
// Create OpenRouter model (only needed when not using custom model)
|
|
192
207
|
const openRouterModel = usingCustomModel
|
|
193
208
|
? null
|
|
194
209
|
: createOpenRouter({
|
|
195
210
|
baseURL: apiUrl,
|
|
196
211
|
apiKey: 'unused, but must be set',
|
|
197
|
-
headers:
|
|
212
|
+
headers: headersWithChatId,
|
|
198
213
|
})
|
|
199
214
|
|
|
200
215
|
if (config.languageModel) {
|