@agents24/chat-react 0.5.4 → 0.5.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents24/chat-react",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Accessible web UI and complete default layout for Agents24 chat applications.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -58,8 +58,8 @@
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
+ "@agents24/client": "0.2.3",
62
+ "@agents24/react": "0.2.3",
63
63
  "@fontsource-variable/inter": "^5.3.0",
64
64
  "@fontsource-variable/jetbrains-mono": "^5.3.0",
65
65
  "@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,14 @@
1
- import { ArrowUp, LoaderCircle, Mic, Square, X } from "lucide-react"
1
+ import { ArrowUp, ChevronDown, LoaderCircle, Mic, Square, X } from "lucide-react"
2
2
  import * as React from "react"
3
3
  import { AgentChatContextStatus, useAudioRecorder } from "@agents24/chat-react/ui"
4
4
  import { canonicalInputMimeType, inputMimeTypes, validateAgentChatSubmission, type AgentChatInputContract } from "@agents24/chat-react/runtime"
5
5
  import type { ContextWindow } from "@agents24/client/protocol"
6
6
 
7
7
  import { Button } from "../ui/button"
8
+ import {
9
+ DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem,
10
+ DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,
11
+ } from "../ui/dropdown-menu"
8
12
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
9
13
  import { Textarea } from "../ui/textarea"
10
14
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"
@@ -14,12 +18,16 @@ type ChatComposerProps = {
14
18
  inputContract: AgentChatInputContract
15
19
  disabled?: boolean
16
20
  contextWindow?: ContextWindow | null
21
+ focusKey?: string | null
17
22
  forceExpanded?: boolean
18
23
  isCancelling?: boolean
19
24
  isRunning: boolean
25
+ agentId: string | null
26
+ agents: Array<{ id: string; label: string }>
20
27
  modelId: string | null
21
28
  models: Array<{ id: string; label: string }>
22
29
  onModelChange: (modelId: string) => void
30
+ onAgentChange: (agentId: string) => void
23
31
  onStop: () => void
24
32
  onSubmit: (text: string, files: File[]) => Promise<void>
25
33
  utilityRow?: "inline" | "below"
@@ -29,12 +37,16 @@ export function ChatComposer({
29
37
  inputContract,
30
38
  disabled,
31
39
  contextWindow,
40
+ focusKey,
32
41
  forceExpanded = false,
33
42
  isCancelling = false,
34
43
  isRunning,
44
+ agentId,
45
+ agents,
35
46
  modelId,
36
47
  models,
37
48
  onModelChange,
49
+ onAgentChange,
38
50
  onStop,
39
51
  onSubmit,
40
52
  utilityRow = "inline",
@@ -49,6 +61,11 @@ export function ChatComposer({
49
61
  const attachmentMimeTypes = inputMimeTypes(inputContract)
50
62
  const allowAttachments = attachmentMimeTypes.length > 0
51
63
  const recordingAllowed = inputContract.modalities.audio.recording_enabled
64
+ React.useEffect(() => {
65
+ if (disabled || isRunning) return
66
+ const frame = window.requestAnimationFrame(() => textareaRef.current?.focus({ preventScroll: true }))
67
+ return () => window.cancelAnimationFrame(frame)
68
+ }, [disabled, focusKey, isRunning])
52
69
  const addRecording = React.useCallback((file: File) => {
53
70
  draftRevisionRef.current += 1
54
71
  setFiles((current) => [...current, file])
@@ -61,13 +78,11 @@ export function ChatComposer({
61
78
  files.map((file) => ({ data: file, mediaType: file.type })),
62
79
  )
63
80
 
81
+ const selectedAgent = agents.find((item) => item.id === agentId)
82
+ const selectedModel = models.find((item) => item.id === modelId)
64
83
  const modelSelector = models.length > 0 ? (
65
84
  <div className="w-fit max-w-40">
66
- <Select
67
- disabled={disabled || isRunning}
68
- onValueChange={onModelChange}
69
- value={modelId || undefined}
70
- >
85
+ <Select disabled={disabled || isRunning} onValueChange={onModelChange} value={modelId || undefined}>
71
86
  <SelectTrigger aria-label="Runtime model">
72
87
  <SelectValue placeholder="Model" />
73
88
  </SelectTrigger>
@@ -77,6 +92,47 @@ export function ChatComposer({
77
92
  </Select>
78
93
  </div>
79
94
  ) : null
95
+ const runtimeSelector = agents.length > 1 ? (
96
+ <DropdownMenu>
97
+ <DropdownMenuTrigger asChild>
98
+ <Button
99
+ variant="ghost"
100
+ 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"
101
+ disabled={disabled || isRunning}
102
+ aria-label="Runtime configuration"
103
+ >
104
+ <span className="truncate">
105
+ {[selectedAgent?.label, selectedModel?.label].filter(Boolean).join(" · ") || "Runtime"}
106
+ </span>
107
+ <ChevronDown className="size-3.5 text-muted-foreground" />
108
+ </Button>
109
+ </DropdownMenuTrigger>
110
+ <DropdownMenuContent align="start" className="w-64 rounded-lg">
111
+ <DropdownMenuSub>
112
+ <DropdownMenuSubTrigger className="rounded-lg">
113
+ <span>Agent</span><span className="ml-auto max-w-32 truncate text-muted-foreground">{selectedAgent?.label}</span>
114
+ </DropdownMenuSubTrigger>
115
+ <DropdownMenuSubContent className="w-56 rounded-lg">
116
+ <DropdownMenuRadioGroup value={agentId || undefined} onValueChange={onAgentChange}>
117
+ {agents.map((agent) => <DropdownMenuRadioItem className="rounded-lg" key={agent.id} value={agent.id}>{agent.label}</DropdownMenuRadioItem>)}
118
+ </DropdownMenuRadioGroup>
119
+ </DropdownMenuSubContent>
120
+ </DropdownMenuSub>
121
+ {models.length > 0 ? (
122
+ <DropdownMenuSub>
123
+ <DropdownMenuSubTrigger className="rounded-lg">
124
+ <span>Model</span><span className="ml-auto max-w-32 truncate text-muted-foreground">{selectedModel?.label}</span>
125
+ </DropdownMenuSubTrigger>
126
+ <DropdownMenuSubContent className="max-h-64 w-56 rounded-lg">
127
+ <DropdownMenuRadioGroup value={modelId || undefined} onValueChange={onModelChange}>
128
+ {models.map((model) => <DropdownMenuRadioItem className="rounded-lg" key={model.id} value={model.id}>{model.label}</DropdownMenuRadioItem>)}
129
+ </DropdownMenuRadioGroup>
130
+ </DropdownMenuSubContent>
131
+ </DropdownMenuSub>
132
+ ) : null}
133
+ </DropdownMenuContent>
134
+ </DropdownMenu>
135
+ ) : modelSelector
80
136
  const hasContextWindow = Number.isFinite(Number(contextWindow?.max_tokens)) && Number(contextWindow?.max_tokens) > 0
81
137
  const showUtilityRow = utilityRow === "below"
82
138
 
@@ -185,7 +241,7 @@ export function ChatComposer({
185
241
  />
186
242
  <div className="agents24-composer-actions flex items-center gap-1">
187
243
  {utilityRow === "inline" ? <AgentChatContextStatus contextWindow={contextWindow} /> : null}
188
- {utilityRow === "inline" ? modelSelector : null}
244
+ {utilityRow === "inline" ? runtimeSelector : null}
189
245
  {recordingAllowed ? (
190
246
  <Button
191
247
  variant={recorder.isRecording ? "default" : "ghost"}
@@ -224,11 +280,11 @@ export function ChatComposer({
224
280
  </div>
225
281
  {showUtilityRow ? (
226
282
  <div
227
- aria-hidden={!modelSelector && !hasContextWindow ? "true" : undefined}
283
+ aria-hidden={!runtimeSelector && !hasContextWindow ? "true" : undefined}
228
284
  className="mt-2 flex min-h-8 items-center justify-between gap-2 px-1"
229
285
  data-agents24-chat-composer-utility=""
230
286
  >
231
- <div className="min-w-0">{modelSelector}</div>
287
+ <div className="min-w-0">{runtimeSelector}</div>
232
288
  <AgentChatContextStatus contextWindow={contextWindow} />
233
289
  </div>
234
290
  ) : null}
@@ -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 =
@@ -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}
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}
@@ -230,9 +236,13 @@ export function ChatShell({
230
236
  <ChatComposer
231
237
  inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
232
238
  contextWindow={runtime.contextWindow}
233
- disabled={runtime.isBootstrapping || hasBlockingError}
239
+ disabled={runtime.isBootstrapping || runtime.isChangingAgent || hasBlockingError}
240
+ focusKey={controller.activeThreadId}
234
241
  isCancelling={isCancelling}
235
242
  isRunning={isRunning}
243
+ agentId={runtime.agentId}
244
+ agents={runtime.agents}
245
+ onAgentChange={(agentId) => { void runtime.changeAgent(agentId) }}
236
246
  onStop={() => { void controller.cancelRun().catch(() => undefined) }}
237
247
  modelId={runtime.modelId}
238
248
  models={runtime.models}
@@ -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}