@agents24/chat-react 0.5.4 → 0.5.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.
Files changed (34) hide show
  1. package/README.md +8 -2
  2. package/compatibility.json +4 -4
  3. package/dist/runtime/index.cjs +127 -18
  4. package/dist/runtime/index.cjs.map +1 -1
  5. package/dist/runtime/index.js +125 -18
  6. package/dist/runtime/index.js.map +1 -1
  7. package/dist/runtime/use-agent-chat-runtime.d.ts +17 -2
  8. package/dist/scaffold/app.d.ts +1 -0
  9. package/dist/scaffold/components/chat/chat-composer.d.ts +10 -2
  10. package/dist/scaffold/components/chat/chat-message.d.ts +3 -1
  11. package/dist/scaffold/components/chat/chat-shell.d.ts +3 -1
  12. package/dist/scaffold/index.cjs +415 -253
  13. package/dist/scaffold/index.cjs.map +1 -1
  14. package/dist/scaffold/index.js +408 -240
  15. package/dist/scaffold/index.js.map +1 -1
  16. package/dist/styles.css +18 -4
  17. package/dist/ui/adapters.d.ts +14 -2
  18. package/dist/ui/agent-chat-composer.d.ts +2 -9
  19. package/dist/ui/agent-chat-message.d.ts +3 -1
  20. package/dist/ui/composer-attachments.d.ts +11 -0
  21. package/dist/ui/index.cjs +653 -343
  22. package/dist/ui/index.cjs.map +1 -1
  23. package/dist/ui/index.d.ts +1 -0
  24. package/dist/ui/index.js +638 -328
  25. package/dist/ui/index.js.map +1 -1
  26. package/package.json +4 -3
  27. package/scaffold/src/app.tsx +2 -1
  28. package/scaffold/src/chat.css +13 -1
  29. package/scaffold/src/components/chat/chat-composer.tsx +107 -27
  30. package/scaffold/src/components/chat/chat-message.tsx +8 -9
  31. package/scaffold/src/components/chat/chat-shell.tsx +24 -11
  32. package/scaffold/src/components/ui/button.tsx +1 -1
  33. package/scaffold/src/components/ui/dropdown-menu.tsx +1 -1
  34. package/scaffold/src/components/ui/sidebar.tsx +4 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents24/chat-react",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Accessible web UI and complete default layout for Agents24 chat applications.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -58,8 +58,9 @@
58
58
  "prepack": "pnpm run build && pnpm run check:boundaries"
59
59
  },
60
60
  "dependencies": {
61
- "@agents24/client": "0.2.2",
62
- "@agents24/react": "0.2.2",
61
+ "@untitledui/file-icons": "0.0.9",
62
+ "@agents24/client": "0.2.4",
63
+ "@agents24/react": "0.2.4",
63
64
  "@fontsource-variable/inter": "^5.3.0",
64
65
  "@fontsource-variable/jetbrains-mono": "^5.3.0",
65
66
  "@fontsource-variable/merriweather": "^5.3.0",
@@ -30,6 +30,7 @@ export type Agents24ChatAppProps = {
30
30
  onModelChange?: (modelId: string) => void
31
31
  identity?: Agents24ChatIdentity
32
32
  components?: Agents24ChatScaffoldComponents
33
+ sidebar?: React.ReactNode
33
34
  }
34
35
 
35
36
  export type Agents24ChatScaffoldComponents = {
@@ -58,7 +59,7 @@ function RuntimeChat({
58
59
  ),
59
60
  })
60
61
 
61
- return <ChatShell components={components} identity={identity} runtime={runtime} />
62
+ return <ChatShell components={components} identity={identity} runtime={runtime} sidebar={props.sidebar} />
62
63
  }
63
64
 
64
65
  export function App(props: Agents24ChatAppProps) {
@@ -10,12 +10,24 @@
10
10
  [data-agents24-theme-root],
11
11
  [data-agents24-theme-root] * {
12
12
  box-sizing: border-box;
13
+ -ms-overflow-style: none;
14
+ scrollbar-width: none;
15
+ --tw-inset-ring-shadow: 0 0 #0000 !important;
16
+ --tw-ring-shadow: 0 0 #0000 !important;
17
+ }
18
+
19
+ [data-agents24-theme-root] ::-webkit-scrollbar {
20
+ display: none;
21
+ }
22
+
23
+ [data-agents24-theme-root] :focus-visible {
24
+ outline: none !important;
13
25
  }
14
26
 
15
27
  [data-agents24-theme-root] .a24-message-response {
16
28
  max-width: 100%;
17
29
  min-width: 0;
18
- padding-inline: 3px;
30
+ padding-inline: 6px;
19
31
  width: 100%;
20
32
  }
21
33
 
@@ -1,10 +1,21 @@
1
- import { ArrowUp, LoaderCircle, Mic, Square, X } from "lucide-react"
1
+ import { ArrowUp, ChevronDown, LoaderCircle, Mic, Square } from "lucide-react"
2
2
  import * as React from "react"
3
- import { AgentChatContextStatus, useAudioRecorder } from "@agents24/chat-react/ui"
3
+ import {
4
+ AgentChatContextStatus,
5
+ ChatAttachmentRows,
6
+ createAgentChatComposerFile,
7
+ revokeAgentChatComposerFiles,
8
+ useAudioRecorder,
9
+ type AgentChatComposerFile,
10
+ } from "@agents24/chat-react/ui"
4
11
  import { canonicalInputMimeType, inputMimeTypes, validateAgentChatSubmission, type AgentChatInputContract } from "@agents24/chat-react/runtime"
5
12
  import type { ContextWindow } from "@agents24/client/protocol"
6
13
 
7
14
  import { Button } from "../ui/button"
15
+ import {
16
+ DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem,
17
+ DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,
18
+ } from "../ui/dropdown-menu"
8
19
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
9
20
  import { Textarea } from "../ui/textarea"
10
21
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"
@@ -14,14 +25,18 @@ type ChatComposerProps = {
14
25
  inputContract: AgentChatInputContract
15
26
  disabled?: boolean
16
27
  contextWindow?: ContextWindow | null
28
+ focusKey?: string | null
17
29
  forceExpanded?: boolean
18
30
  isCancelling?: boolean
19
31
  isRunning: boolean
32
+ agentId: string | null
33
+ agents: Array<{ id: string; label: string }>
20
34
  modelId: string | null
21
35
  models: Array<{ id: string; label: string }>
22
36
  onModelChange: (modelId: string) => void
37
+ onAgentChange: (agentId: string) => void
23
38
  onStop: () => void
24
- onSubmit: (text: string, files: File[]) => Promise<void>
39
+ onSubmit: (text: string, files: AgentChatComposerFile[]) => Promise<void>
25
40
  utilityRow?: "inline" | "below"
26
41
  }
27
42
 
@@ -29,45 +44,61 @@ export function ChatComposer({
29
44
  inputContract,
30
45
  disabled,
31
46
  contextWindow,
47
+ focusKey,
32
48
  forceExpanded = false,
33
49
  isCancelling = false,
34
50
  isRunning,
51
+ agentId,
52
+ agents,
35
53
  modelId,
36
54
  models,
37
55
  onModelChange,
56
+ onAgentChange,
38
57
  onStop,
39
58
  onSubmit,
40
59
  utilityRow = "inline",
41
60
  }: ChatComposerProps) {
42
61
  const [text, setText] = React.useState("")
43
- const [files, setFiles] = React.useState<File[]>([])
62
+ const [files, setFiles] = React.useState<AgentChatComposerFile[]>([])
44
63
  const [attachmentError, setAttachmentError] = React.useState<string | null>(null)
45
64
  const inputRef = React.useRef<HTMLInputElement>(null)
46
65
  const textareaRef = React.useRef<HTMLTextAreaElement>(null)
66
+ const filesRef = React.useRef<AgentChatComposerFile[]>([])
67
+ const inFlightFilesRef = React.useRef<AgentChatComposerFile[]>([])
47
68
  const submittingRef = React.useRef(false)
48
69
  const draftRevisionRef = React.useRef(0)
49
70
  const attachmentMimeTypes = inputMimeTypes(inputContract)
50
71
  const allowAttachments = attachmentMimeTypes.length > 0
51
72
  const recordingAllowed = inputContract.modalities.audio.recording_enabled
73
+ React.useEffect(() => {
74
+ filesRef.current = files
75
+ }, [files])
76
+ React.useEffect(() => () => {
77
+ revokeAgentChatComposerFiles(filesRef.current)
78
+ revokeAgentChatComposerFiles(inFlightFilesRef.current)
79
+ }, [])
80
+ React.useEffect(() => {
81
+ if (disabled || isRunning) return
82
+ const frame = window.requestAnimationFrame(() => textareaRef.current?.focus({ preventScroll: true }))
83
+ return () => window.cancelAnimationFrame(frame)
84
+ }, [disabled, focusKey, isRunning])
52
85
  const addRecording = React.useCallback((file: File) => {
53
86
  draftRevisionRef.current += 1
54
- setFiles((current) => [...current, file])
87
+ setFiles((current) => [...current, createAgentChatComposerFile(file)])
55
88
  }, [])
56
89
  const recorder = useAudioRecorder(addRecording)
57
90
  const isExpanded = forceExpanded || text.includes("\n") || text.length > 62
58
91
  const validationError = validateAgentChatSubmission(
59
92
  inputContract,
60
93
  text,
61
- files.map((file) => ({ data: file, mediaType: file.type })),
94
+ files.map((file) => ({ data: file.source, mediaType: file.mediaType })),
62
95
  )
63
96
 
97
+ const selectedAgent = agents.find((item) => item.id === agentId)
98
+ const selectedModel = models.find((item) => item.id === modelId)
64
99
  const modelSelector = models.length > 0 ? (
65
100
  <div className="w-fit max-w-40">
66
- <Select
67
- disabled={disabled || isRunning}
68
- onValueChange={onModelChange}
69
- value={modelId || undefined}
70
- >
101
+ <Select disabled={disabled || isRunning} onValueChange={onModelChange} value={modelId || undefined}>
71
102
  <SelectTrigger aria-label="Runtime model">
72
103
  <SelectValue placeholder="Model" />
73
104
  </SelectTrigger>
@@ -77,6 +108,47 @@ export function ChatComposer({
77
108
  </Select>
78
109
  </div>
79
110
  ) : null
111
+ const runtimeSelector = agents.length > 1 ? (
112
+ <DropdownMenu>
113
+ <DropdownMenuTrigger asChild>
114
+ <Button
115
+ variant="ghost"
116
+ className="h-8 max-w-64 gap-1.5 bg-transparent px-2.5 text-xs font-normal hover:bg-muted/40 disabled:opacity-100"
117
+ disabled={disabled || isRunning}
118
+ aria-label="Runtime configuration"
119
+ >
120
+ <span className="truncate">
121
+ {[selectedAgent?.label, selectedModel?.label].filter(Boolean).join(" · ") || "Runtime"}
122
+ </span>
123
+ <ChevronDown className="size-3.5 text-muted-foreground" />
124
+ </Button>
125
+ </DropdownMenuTrigger>
126
+ <DropdownMenuContent align="start" className="w-64 rounded-lg">
127
+ <DropdownMenuSub>
128
+ <DropdownMenuSubTrigger className="rounded-lg">
129
+ <span>Agent</span><span className="ml-auto max-w-32 truncate text-muted-foreground">{selectedAgent?.label}</span>
130
+ </DropdownMenuSubTrigger>
131
+ <DropdownMenuSubContent className="w-56 rounded-lg">
132
+ <DropdownMenuRadioGroup value={agentId || undefined} onValueChange={onAgentChange}>
133
+ {agents.map((agent) => <DropdownMenuRadioItem className="rounded-lg" key={agent.id} value={agent.id}>{agent.label}</DropdownMenuRadioItem>)}
134
+ </DropdownMenuRadioGroup>
135
+ </DropdownMenuSubContent>
136
+ </DropdownMenuSub>
137
+ {models.length > 0 ? (
138
+ <DropdownMenuSub>
139
+ <DropdownMenuSubTrigger className="rounded-lg">
140
+ <span>Model</span><span className="ml-auto max-w-32 truncate text-muted-foreground">{selectedModel?.label}</span>
141
+ </DropdownMenuSubTrigger>
142
+ <DropdownMenuSubContent className="max-h-64 w-56 rounded-lg">
143
+ <DropdownMenuRadioGroup value={modelId || undefined} onValueChange={onModelChange}>
144
+ {models.map((model) => <DropdownMenuRadioItem className="rounded-lg" key={model.id} value={model.id}>{model.label}</DropdownMenuRadioItem>)}
145
+ </DropdownMenuRadioGroup>
146
+ </DropdownMenuSubContent>
147
+ </DropdownMenuSub>
148
+ ) : null}
149
+ </DropdownMenuContent>
150
+ </DropdownMenu>
151
+ ) : modelSelector
80
152
  const hasContextWindow = Number.isFinite(Number(contextWindow?.max_tokens)) && Number(contextWindow?.max_tokens) > 0
81
153
  const showUtilityRow = utilityRow === "below"
82
154
 
@@ -87,16 +159,23 @@ export function ChatComposer({
87
159
  const clearedRevision = draftRevisionRef.current + 1
88
160
  draftRevisionRef.current = clearedRevision
89
161
  submittingRef.current = true
162
+ inFlightFilesRef.current = selectedFiles
90
163
  setText("")
91
164
  setFiles([])
92
165
  textareaRef.current?.focus()
93
166
  try {
94
167
  await onSubmit(value, selectedFiles)
168
+ revokeAgentChatComposerFiles(selectedFiles)
169
+ inFlightFilesRef.current = []
95
170
  } catch {
96
171
  if (draftRevisionRef.current === clearedRevision) {
97
172
  draftRevisionRef.current += 1
98
173
  setText(value)
99
174
  setFiles(selectedFiles)
175
+ inFlightFilesRef.current = []
176
+ } else {
177
+ revokeAgentChatComposerFiles(selectedFiles)
178
+ inFlightFilesRef.current = []
100
179
  }
101
180
  } finally {
102
181
  submittingRef.current = false
@@ -109,18 +188,19 @@ export function ChatComposer({
109
188
  data-agents24-chat-composer=""
110
189
  >
111
190
  {allowAttachments && files.length > 0 && (
112
- <div className="mb-2 flex min-w-0 gap-2 overflow-x-auto">
113
- {files.map((file, index) => (
114
- <div key={`${file.name}-${index}`} className="flex max-w-48 items-center gap-2 border bg-card px-2 py-1 text-xs">
115
- <span className="truncate">{file.name}</span>
116
- <Button variant="ghost" size="icon-xs" aria-label={`Remove ${file.name}`} onClick={() => {
117
- draftRevisionRef.current += 1
118
- setFiles((current) => current.filter((_, itemIndex) => itemIndex !== index))
119
- }}>
120
- <X />
121
- </Button>
122
- </div>
123
- ))}
191
+ <div className="mb-2 min-w-0">
192
+ <ChatAttachmentRows
193
+ attachments={files}
194
+ onRemove={(attachment) => {
195
+ if (!attachment.id) return
196
+ draftRevisionRef.current += 1
197
+ setFiles((current) => {
198
+ const removed = current.find((file) => file.id === attachment.id)
199
+ if (removed) revokeAgentChatComposerFiles([removed])
200
+ return current.filter((file) => file.id !== attachment.id)
201
+ })
202
+ }}
203
+ />
124
204
  </div>
125
205
  )}
126
206
  {attachmentError || recorder.error ? <p className="mb-2 text-xs text-destructive" role="alert">{attachmentError || recorder.error}</p> : null}
@@ -145,7 +225,7 @@ export function ChatComposer({
145
225
  attachmentMimeTypes.includes(canonicalInputMimeType(file.type))
146
226
  ))
147
227
  draftRevisionRef.current += 1
148
- setFiles((current) => [...current, ...accepted])
228
+ setFiles((current) => [...current, ...accepted.map(createAgentChatComposerFile)])
149
229
  setAttachmentError(
150
230
  accepted.length === selected.length
151
231
  ? null
@@ -185,7 +265,7 @@ export function ChatComposer({
185
265
  />
186
266
  <div className="agents24-composer-actions flex items-center gap-1">
187
267
  {utilityRow === "inline" ? <AgentChatContextStatus contextWindow={contextWindow} /> : null}
188
- {utilityRow === "inline" ? modelSelector : null}
268
+ {utilityRow === "inline" ? runtimeSelector : null}
189
269
  {recordingAllowed ? (
190
270
  <Button
191
271
  variant={recorder.isRecording ? "default" : "ghost"}
@@ -224,11 +304,11 @@ export function ChatComposer({
224
304
  </div>
225
305
  {showUtilityRow ? (
226
306
  <div
227
- aria-hidden={!modelSelector && !hasContextWindow ? "true" : undefined}
307
+ aria-hidden={!runtimeSelector && !hasContextWindow ? "true" : undefined}
228
308
  className="mt-2 flex min-h-8 items-center justify-between gap-2 px-1"
229
309
  data-agents24-chat-composer-utility=""
230
310
  >
231
- <div className="min-w-0">{modelSelector}</div>
311
+ <div className="min-w-0">{runtimeSelector}</div>
232
312
  <AgentChatContextStatus contextWindow={contextWindow} />
233
313
  </div>
234
314
  ) : null}
@@ -1,7 +1,7 @@
1
1
  import type { ChatHitlPart, ChatMessage as ChatMessageModel } from "@agents24/react"
2
2
  import type { FeedbackReason, HitlResponse } from "@agents24/client"
3
3
  import type { AgentChatRuntimeHitlActionState } from "@agents24/chat-react/runtime"
4
- import { AgentChatDefaultActions, ChatAttachment, ChatAttachments } from "@agents24/chat-react/ui"
4
+ import { AgentChatDefaultActions, ChatAttachmentRows, type ChatAttachmentContentResolver } from "@agents24/chat-react/ui"
5
5
  import * as React from "react"
6
6
 
7
7
  import { MessageParts } from "./message-parts"
@@ -20,6 +20,7 @@ type Props = {
20
20
  input: { rating: "like" | "dislike" | null; reason?: FeedbackReason | null; comment?: string | null },
21
21
  ) => Promise<void>
22
22
  Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>
23
+ resolveAttachmentContent?: ChatAttachmentContentResolver
23
24
  }
24
25
 
25
26
  export function ChatMessage({
@@ -30,6 +31,7 @@ export function ChatMessage({
30
31
  onHitlAction,
31
32
  onRegenerate,
32
33
  onSetFeedback,
34
+ resolveAttachmentContent,
33
35
  Timeline,
34
36
  }: Props) {
35
37
  const isUser = message.role === "user"
@@ -37,14 +39,11 @@ export function ChatMessage({
37
39
  <Message align={isUser ? "end" : "start"}>
38
40
  <MessageContent>
39
41
  {isUser && message.attachments?.length ? (
40
- <ChatAttachments className="mb-2 justify-end">
41
- {message.attachments.map((attachment, index) => (
42
- <ChatAttachment
43
- data={attachment}
44
- key={String(attachment.id || attachment.url || attachment.filename || index)}
45
- />
46
- ))}
47
- </ChatAttachments>
42
+ <ChatAttachmentRows
43
+ attachments={message.attachments}
44
+ className="mb-2"
45
+ resolveContent={resolveAttachmentContent}
46
+ />
48
47
  ) : null}
49
48
  <Bubble variant={isUser ? "secondary" : "ghost"} align={isUser ? "end" : "start"} className={isUser ? "max-w-[min(75%,42rem)]" : "max-w-full overflow-visible"}>
50
49
  <BubbleContent className={isUser ? "px-4 py-2.5" : "w-full overflow-visible"}>
@@ -87,10 +87,12 @@ export function ChatShell({
87
87
  components,
88
88
  identity,
89
89
  runtime,
90
+ sidebar,
90
91
  }: {
91
92
  components?: Agents24ChatScaffoldComponents
92
93
  identity: Agents24ChatIdentity
93
94
  runtime: AgentChatRuntime
95
+ sidebar?: React.ReactNode
94
96
  }) {
95
97
  const { controller } = runtime
96
98
  const isRunning =
@@ -98,7 +100,7 @@ export function ChatShell({
98
100
  || controller.runState === "reconnecting"
99
101
  || controller.runState === "paused"
100
102
  || controller.runState === "cancelling"
101
- || runtime.isSubmitting
103
+ const isUploading = controller.runState === "uploading" || runtime.isUploading
102
104
  const isCancelling = controller.runState === "cancelling"
103
105
  const activeTitle = controller.threads.find((thread) => thread.id === controller.activeThreadId)?.title
104
106
  const hasBlockingError = Boolean(runtime.fatalError && controller.messages.length === 0)
@@ -111,7 +113,7 @@ export function ChatShell({
111
113
  data-agents24-chat-layout=""
112
114
  >
113
115
  <PersistentSidebarTrigger />
114
- <ThreadSidebar
116
+ {sidebar || <ThreadSidebar
115
117
  activeThreadId={controller.activeThreadId}
116
118
  hasMore={controller.hasMoreThreads}
117
119
  identity={identity}
@@ -122,7 +124,7 @@ export function ChatShell({
122
124
  onLoadMore={controller.loadMoreThreads}
123
125
  onNew={runtime.newThread}
124
126
  onOpen={runtime.openThread}
125
- />
127
+ />}
126
128
  <SidebarInset className="h-svh min-h-0 overflow-hidden bg-background">
127
129
  <header
128
130
  className="flex h-12 shrink-0 items-center gap-2 border-b border-border/30 px-3 sm:px-4"
@@ -149,10 +151,14 @@ export function ChatShell({
149
151
  <ChatComposer
150
152
  inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
151
153
  contextWindow={runtime.contextWindow}
152
- disabled={runtime.isBootstrapping}
154
+ disabled={runtime.isBootstrapping || runtime.isChangingAgent || isUploading}
155
+ focusKey={controller.activeThreadId}
153
156
  forceExpanded
154
157
  isCancelling={isCancelling}
155
158
  isRunning={isRunning}
159
+ agentId={runtime.agentId}
160
+ agents={runtime.agents}
161
+ onAgentChange={(agentId) => { void runtime.changeAgent(agentId) }}
156
162
  onStop={() => { void controller.cancelRun().catch(() => undefined) }}
157
163
  modelId={runtime.modelId}
158
164
  models={runtime.models}
@@ -160,10 +166,11 @@ export function ChatShell({
160
166
  onSubmit={(text, files) => runtime.submit({
161
167
  text,
162
168
  files: files.map((file) => ({
163
- data: file,
164
- mediaType: file.type || "application/octet-stream",
165
- name: file.name,
169
+ data: file.source,
170
+ mediaType: file.mediaType,
171
+ name: file.filename,
166
172
  })),
173
+ attachments: files,
167
174
  })}
168
175
  />
169
176
  </div>
@@ -216,6 +223,7 @@ export function ChatShell({
216
223
  onHitlAction={runtime.onHitlAction}
217
224
  onRegenerate={() => controller.regenerate(message)}
218
225
  onSetFeedback={(input) => controller.setFeedback(message, input)}
226
+ resolveAttachmentContent={runtime.resolveAttachmentContent}
219
227
  Timeline={components?.Timeline}
220
228
  />
221
229
  </MessageScrollerItem>
@@ -230,9 +238,13 @@ export function ChatShell({
230
238
  <ChatComposer
231
239
  inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
232
240
  contextWindow={runtime.contextWindow}
233
- disabled={runtime.isBootstrapping || hasBlockingError}
241
+ disabled={runtime.isBootstrapping || runtime.isChangingAgent || hasBlockingError || isUploading}
242
+ focusKey={controller.activeThreadId}
234
243
  isCancelling={isCancelling}
235
244
  isRunning={isRunning}
245
+ agentId={runtime.agentId}
246
+ agents={runtime.agents}
247
+ onAgentChange={(agentId) => { void runtime.changeAgent(agentId) }}
236
248
  onStop={() => { void controller.cancelRun().catch(() => undefined) }}
237
249
  modelId={runtime.modelId}
238
250
  models={runtime.models}
@@ -240,10 +252,11 @@ export function ChatShell({
240
252
  onSubmit={(text, files) => runtime.submit({
241
253
  text,
242
254
  files: files.map((file) => ({
243
- data: file,
244
- mediaType: file.type || "application/octet-stream",
245
- name: file.name,
255
+ data: file.source,
256
+ mediaType: file.mediaType,
257
+ name: file.filename,
246
258
  })),
259
+ attachments: files,
247
260
  })}
248
261
  utilityRow="below"
249
262
  />
@@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
5
5
  import { cn } from "../../lib/utils"
6
6
 
7
7
  const buttonVariants = cva(
8
- "group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
+ "group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
9
  {
10
10
  variants: {
11
11
  variant: {
@@ -246,7 +246,7 @@ function DropdownMenuSubContent({
246
246
  return (
247
247
  <DropdownMenuPrimitive.SubContent
248
248
  data-slot="dropdown-menu-sub-content"
249
- className={cn("z-50 min-w-36 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-3xl bg-popover p-1.5 text-popover-foreground shadow-lg ring-1 ring-foreground/5 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
249
+ className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-36 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-3xl bg-popover p-1.5 text-popover-foreground shadow-lg ring-1 ring-foreground/5 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
250
250
  {...props}
251
251
  />
252
252
  )
@@ -451,7 +451,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
451
451
  }
452
452
 
453
453
  const sidebarMenuButtonVariants = cva(
454
- "peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-xl px-3 py-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
454
+ "peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-xl px-3 py-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
455
455
  {
456
456
  variants: {
457
457
  variant: {
@@ -540,7 +540,7 @@ function SidebarMenuAction({
540
540
  className={cn(
541
541
  "absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-xl p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-2 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
542
542
  showOnHover &&
543
- "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0",
543
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0",
544
544
  className
545
545
  )}
546
546
  {...props}
@@ -557,7 +557,7 @@ function SidebarMenuBadge({
557
557
  data-slot="sidebar-menu-badge"
558
558
  data-sidebar="menu-badge"
559
559
  className={cn(
560
- "pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-xl px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground",
560
+ "pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-xl px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
561
561
  className
562
562
  )}
563
563
  {...props}
@@ -641,7 +641,7 @@ function SidebarMenuSubButton({
641
641
  data-size={size}
642
642
  data-active={isActive}
643
643
  className={cn(
644
- "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-xl px-3 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
644
+ "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-xl px-3 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
645
645
  className
646
646
  )}
647
647
  {...props}