@agents24/chat-react 0.5.0 → 0.5.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.
Files changed (133) hide show
  1. package/README.md +90 -21
  2. package/compatibility.json +10 -10
  3. package/dist/chunk-SSUO4EPD.js +189 -0
  4. package/dist/chunk-SSUO4EPD.js.map +1 -0
  5. package/dist/chunk-ZCNHLFRB.js +108 -0
  6. package/dist/chunk-ZCNHLFRB.js.map +1 -0
  7. package/dist/index.cjs +112 -95
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +0 -1
  10. package/dist/index.js +362 -68
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp-oauth.d.ts +13 -0
  13. package/dist/runtime/index.cjs +594 -0
  14. package/dist/runtime/index.cjs.map +1 -0
  15. package/dist/runtime/index.d.ts +2 -0
  16. package/dist/runtime/index.js +458 -0
  17. package/dist/runtime/index.js.map +1 -0
  18. package/dist/runtime/input-contract.d.ts +20 -0
  19. package/dist/runtime/use-agent-chat-runtime.d.ts +83 -0
  20. package/dist/scaffold/app.d.ts +30 -0
  21. package/dist/scaffold/components/chat/chat-composer.d.ts +21 -0
  22. package/dist/scaffold/components/chat/chat-message.d.ts +21 -0
  23. package/dist/scaffold/components/chat/chat-shell.d.ts +8 -0
  24. package/dist/scaffold/components/chat/collapsible-user-message.d.ts +3 -0
  25. package/dist/scaffold/components/chat/external-link-dialog.d.ts +2 -0
  26. package/dist/scaffold/components/chat/identity-control.d.ts +4 -0
  27. package/dist/scaffold/components/chat/message-parts.d.ts +12 -0
  28. package/dist/scaffold/components/chat/theme-menu.d.ts +1 -0
  29. package/dist/scaffold/components/chat/thread-sidebar.d.ts +16 -0
  30. package/dist/scaffold/components/chat/timeline-loading.d.ts +7 -0
  31. package/dist/scaffold/components/chat/turn-outline.d.ts +4 -0
  32. package/dist/scaffold/components/theme-provider.d.ts +20 -0
  33. package/dist/scaffold/components/ui/attachment.d.ts +24 -0
  34. package/dist/scaffold/components/ui/bubble.d.ts +17 -0
  35. package/dist/scaffold/components/ui/button-group.d.ts +11 -0
  36. package/dist/scaffold/components/ui/button.d.ts +10 -0
  37. package/dist/scaffold/components/ui/dialog.d.ts +17 -0
  38. package/dist/scaffold/components/ui/dropdown-menu.d.ts +29 -0
  39. package/dist/scaffold/components/ui/input.d.ts +3 -0
  40. package/dist/scaffold/components/ui/marker.d.ts +11 -0
  41. package/dist/scaffold/components/ui/message-scroller.d.ts +10 -0
  42. package/dist/scaffold/components/ui/message.d.ts +10 -0
  43. package/dist/scaffold/components/ui/popover.d.ts +6 -0
  44. package/dist/scaffold/components/ui/select.d.ts +8 -0
  45. package/dist/scaffold/components/ui/separator.d.ts +4 -0
  46. package/dist/scaffold/components/ui/sheet.d.ts +14 -0
  47. package/dist/scaffold/components/ui/sidebar.d.ts +69 -0
  48. package/dist/scaffold/components/ui/skeleton.d.ts +2 -0
  49. package/dist/scaffold/components/ui/textarea.d.ts +3 -0
  50. package/dist/scaffold/components/ui/tooltip.d.ts +15 -0
  51. package/dist/scaffold/hooks/use-mobile.d.ts +1 -0
  52. package/dist/scaffold/identity.d.ts +10 -0
  53. package/dist/scaffold/index.cjs +2270 -0
  54. package/dist/scaffold/index.cjs.map +1 -0
  55. package/dist/scaffold/index.d.ts +14 -0
  56. package/dist/scaffold/index.js +2244 -0
  57. package/dist/scaffold/index.js.map +1 -0
  58. package/dist/scaffold/lib/utils.d.ts +2 -0
  59. package/dist/styles.css +137 -378
  60. package/dist/types.d.ts +3 -13
  61. package/dist/ui/adapters.d.ts +1 -1
  62. package/dist/ui/agent-chat-actions.d.ts +12 -5
  63. package/dist/ui/agent-chat-composer.d.ts +2 -1
  64. package/dist/ui/agent-chat-context-compression-row.d.ts +5 -0
  65. package/dist/ui/agent-chat-context-status.d.ts +13 -0
  66. package/dist/ui/agent-chat-message.d.ts +7 -5
  67. package/dist/ui/agent-response-timeline.d.ts +29 -7
  68. package/dist/ui/ask-user-interaction-state.d.ts +20 -0
  69. package/dist/ui/ask-user-interaction.d.ts +8 -0
  70. package/dist/ui/index.cjs +1391 -271
  71. package/dist/ui/index.cjs.map +1 -1
  72. package/dist/ui/index.d.ts +5 -0
  73. package/dist/ui/index.js +2380 -65
  74. package/dist/ui/index.js.map +1 -1
  75. package/dist/ui/tool-sessions.d.ts +70 -0
  76. package/dist/ui/use-audio-recorder.d.ts +8 -0
  77. package/package.json +27 -12
  78. package/scaffold/components.json +25 -0
  79. package/scaffold/manifest.json +28 -0
  80. package/scaffold/src/app.tsx +71 -0
  81. package/scaffold/src/chat.css +54 -0
  82. package/scaffold/src/components/chat/chat-composer.tsx +237 -0
  83. package/scaffold/src/components/chat/chat-message.tsx +76 -0
  84. package/scaffold/src/components/chat/chat-shell.tsx +257 -0
  85. package/scaffold/src/components/chat/collapsible-user-message.tsx +37 -0
  86. package/scaffold/src/components/chat/external-link-dialog.tsx +48 -0
  87. package/scaffold/src/components/chat/identity-control.tsx +69 -0
  88. package/scaffold/src/components/chat/message-parts.tsx +83 -0
  89. package/scaffold/src/components/chat/theme-menu.tsx +79 -0
  90. package/scaffold/src/components/chat/thread-sidebar.tsx +114 -0
  91. package/scaffold/src/components/chat/timeline-loading.ts +17 -0
  92. package/scaffold/src/components/chat/turn-outline.tsx +73 -0
  93. package/scaffold/src/components/theme-provider.tsx +91 -0
  94. package/scaffold/src/components/ui/attachment.tsx +204 -0
  95. package/scaffold/src/components/ui/bubble.tsx +125 -0
  96. package/scaffold/src/components/ui/button-group.tsx +83 -0
  97. package/scaffold/src/components/ui/button.tsx +65 -0
  98. package/scaffold/src/components/ui/dialog.tsx +171 -0
  99. package/scaffold/src/components/ui/dropdown-menu.tsx +271 -0
  100. package/scaffold/src/components/ui/input.tsx +19 -0
  101. package/scaffold/src/components/ui/marker.tsx +69 -0
  102. package/scaffold/src/components/ui/message-scroller.tsx +129 -0
  103. package/scaffold/src/components/ui/message.tsx +92 -0
  104. package/scaffold/src/components/ui/popover.tsx +40 -0
  105. package/scaffold/src/components/ui/select.tsx +92 -0
  106. package/scaffold/src/components/ui/separator.tsx +28 -0
  107. package/scaffold/src/components/ui/sheet.tsx +150 -0
  108. package/scaffold/src/components/ui/sidebar.tsx +677 -0
  109. package/scaffold/src/components/ui/skeleton.tsx +13 -0
  110. package/scaffold/src/components/ui/textarea.tsx +18 -0
  111. package/scaffold/src/components/ui/tooltip.tsx +56 -0
  112. package/scaffold/src/hooks/use-mobile.ts +24 -0
  113. package/scaffold/src/identity.ts +17 -0
  114. package/scaffold/src/index.css +222 -0
  115. package/scaffold/src/index.tsx +39 -0
  116. package/scaffold/src/lib/utils.ts +6 -0
  117. package/scaffold/styles.css +3 -0
  118. package/dist/chunk-CHAU7FNW.js +0 -140
  119. package/dist/chunk-CHAU7FNW.js.map +0 -1
  120. package/dist/chunk-LI67M74F.js +0 -445
  121. package/dist/chunk-LI67M74F.js.map +0 -1
  122. package/dist/chunk-R7WFL3YR.js +0 -1327
  123. package/dist/chunk-R7WFL3YR.js.map +0 -1
  124. package/dist/renderers.d.ts +0 -10
  125. package/dist/templates/agent-chat-app.d.ts +0 -18
  126. package/dist/templates/agent-chat-layout.d.ts +0 -77
  127. package/dist/templates/agent-chat-shell.d.ts +0 -37
  128. package/dist/templates/appearance-control.d.ts +0 -5
  129. package/dist/templates/index.cjs +0 -2600
  130. package/dist/templates/index.cjs.map +0 -1
  131. package/dist/templates/index.d.ts +0 -4
  132. package/dist/templates/index.js +0 -846
  133. package/dist/templates/index.js.map +0 -1
@@ -0,0 +1,257 @@
1
+ import {
2
+ agentChatRuntimeErrorMessage,
3
+ type AgentChatRuntime,
4
+ } from "@agents24/chat-react/runtime"
5
+ import type { Agents24ChatScaffoldComponents } from "../../app"
6
+ import { CircleAlert, LoaderCircle, SquarePen, X } from "lucide-react"
7
+ import * as React from "react"
8
+ import type { Agents24ChatIdentity } from "../../identity"
9
+
10
+ import { ChatComposer } from "./chat-composer"
11
+ import { ChatMessage } from "./chat-message"
12
+ import { ThemeMenu } from "./theme-menu"
13
+ import { ThreadSidebar } from "./thread-sidebar"
14
+ import { TurnOutline } from "./turn-outline"
15
+ import { isScaffoldTimelineLoading } from "./timeline-loading"
16
+ import { Button } from "../ui/button"
17
+ import {
18
+ MessageScroller,
19
+ MessageScrollerButton,
20
+ MessageScrollerContent,
21
+ MessageScrollerItem,
22
+ MessageScrollerProvider,
23
+ MessageScrollerViewport,
24
+ } from "../ui/message-scroller"
25
+ import { SidebarInset, SidebarProvider, SidebarTrigger, useSidebar } from "../ui/sidebar"
26
+
27
+ const LOADING_INPUT_CONTRACT = {
28
+ version: "agents24.runtime-inputs.v2" as const,
29
+ modalities: {
30
+ text: { enabled: false, required: false },
31
+ files: { enabled: false, required: false, allowed_mime_types: [] },
32
+ images: { enabled: false, required: false, allowed_mime_types: [] },
33
+ audio: { enabled: false, required: false, allowed_mime_types: [], recording_enabled: false },
34
+ },
35
+ limits: { max_attachments_per_turn: 0, max_file_bytes: 0, max_total_bytes: 0 },
36
+ retention: { mode: "thread_lifetime" as const },
37
+ }
38
+
39
+ function PersistentSidebarTrigger() {
40
+ return (
41
+ <div className="pointer-events-none fixed start-3 top-0 z-[60] flex h-12 items-center">
42
+ <SidebarTrigger className="pointer-events-auto [&_svg]:size-[18px]" />
43
+ </div>
44
+ )
45
+ }
46
+
47
+ function CollapsedHeaderActions({ onNew }: { onNew: () => void }) {
48
+ const { isMobile, openMobile, state } = useSidebar()
49
+ const sidebarVisible = isMobile ? openMobile : state === "expanded"
50
+ if (sidebarVisible) return null
51
+ return (
52
+ <>
53
+ <div className="h-8 w-17 shrink-0" aria-hidden="true" />
54
+ <div className="pointer-events-none fixed start-12 top-0 z-[60] flex h-12 items-center">
55
+ <Button className="pointer-events-auto" variant="ghost" size="icon-sm" aria-label="New chat" onClick={onNew}>
56
+ <SquarePen />
57
+ </Button>
58
+ </div>
59
+ </>
60
+ )
61
+ }
62
+
63
+ function OperationErrorNotice({ runtime }: { runtime: AgentChatRuntime }) {
64
+ const error = runtime.operationError || runtime.backgroundError
65
+ if (!error) return null
66
+ return (
67
+ <div className="pointer-events-none absolute inset-x-0 top-3 z-40 flex justify-center px-4">
68
+ <div className="pointer-events-auto flex max-w-lg items-start gap-2 rounded-lg border border-destructive/20 bg-background px-3 py-2 text-sm shadow-md" role="alert">
69
+ <CircleAlert className="mt-0.5 size-4 shrink-0 text-destructive" />
70
+ <span className="min-w-0 flex-1 wrap-break-word">
71
+ {agentChatRuntimeErrorMessage(error) || "The chat could not complete that action."}
72
+ </span>
73
+ <Button
74
+ aria-label="Dismiss error"
75
+ onClick={runtime.operationError ? runtime.clearOperationError : runtime.controller.clearBackgroundError}
76
+ size="icon-xs"
77
+ variant="ghost"
78
+ >
79
+ <X />
80
+ </Button>
81
+ </div>
82
+ </div>
83
+ )
84
+ }
85
+
86
+ export function ChatShell({
87
+ components,
88
+ identity,
89
+ runtime,
90
+ }: {
91
+ components?: Agents24ChatScaffoldComponents
92
+ identity: Agents24ChatIdentity
93
+ runtime: AgentChatRuntime
94
+ }) {
95
+ const { controller } = runtime
96
+ const isRunning =
97
+ controller.runState === "streaming"
98
+ || controller.runState === "reconnecting"
99
+ || controller.runState === "paused"
100
+ || controller.runState === "cancelling"
101
+ || runtime.isSubmitting
102
+ const isCancelling = controller.runState === "cancelling"
103
+ const activeTitle = controller.threads.find((thread) => thread.id === controller.activeThreadId)?.title
104
+ const hasBlockingError = Boolean(runtime.fatalError && controller.messages.length === 0)
105
+ const isEmptyState = controller.activeThreadId === null && controller.messages.length === 0 && !hasBlockingError
106
+
107
+ return (
108
+ <SidebarProvider
109
+ className="h-svh min-h-0 overflow-hidden"
110
+ data-agents24-chat=""
111
+ data-agents24-chat-layout=""
112
+ >
113
+ <PersistentSidebarTrigger />
114
+ <ThreadSidebar
115
+ activeThreadId={controller.activeThreadId}
116
+ hasMore={controller.hasMoreThreads}
117
+ identity={identity}
118
+ isLoading={controller.isLoadingThreads}
119
+ isLoadingMore={controller.isLoadingMoreThreads}
120
+ threads={controller.threads}
121
+ onDelete={runtime.deleteThread}
122
+ onLoadMore={controller.loadMoreThreads}
123
+ onNew={runtime.newThread}
124
+ onOpen={runtime.openThread}
125
+ />
126
+ <SidebarInset className="h-svh min-h-0 overflow-hidden bg-background">
127
+ <header
128
+ className="flex h-12 shrink-0 items-center gap-2 border-b border-border/30 px-3 sm:px-4"
129
+ data-agents24-chat-header=""
130
+ >
131
+ <CollapsedHeaderActions onNew={runtime.newThread} />
132
+ <div className="min-w-0 flex-1">
133
+ <h1 className="truncate text-sm font-medium">{String(activeTitle || "New chat")}</h1>
134
+ </div>
135
+ <ThemeMenu />
136
+ </header>
137
+
138
+ <section
139
+ aria-label="Conversation"
140
+ className="relative flex min-h-0 flex-1 flex-col overflow-hidden"
141
+ data-agents24-chat-transcript=""
142
+ >
143
+ <OperationErrorNotice runtime={runtime} />
144
+ {isEmptyState ? (
145
+ <div className="absolute inset-0 flex min-h-0 flex-col justify-center pb-20">
146
+ <div className="mx-auto mb-8 w-full max-w-3xl px-4 text-center sm:px-6">
147
+ <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">Ready when you are.</h2>
148
+ </div>
149
+ <ChatComposer
150
+ inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
151
+ contextWindow={runtime.contextWindow}
152
+ disabled={runtime.isBootstrapping}
153
+ forceExpanded
154
+ isCancelling={isCancelling}
155
+ isRunning={isRunning}
156
+ onStop={() => { void controller.cancelRun().catch(() => undefined) }}
157
+ modelId={runtime.modelId}
158
+ models={runtime.models}
159
+ onModelChange={runtime.changeModel}
160
+ onSubmit={(text, files) => runtime.submit({
161
+ text,
162
+ files: files.map((file) => ({
163
+ data: file,
164
+ mediaType: file.type || "application/octet-stream",
165
+ name: file.name,
166
+ })),
167
+ })}
168
+ />
169
+ </div>
170
+ ) : (
171
+ <>
172
+ <MessageScrollerProvider
173
+ autoScroll
174
+ defaultScrollPosition="last-anchor"
175
+ scrollPreviousItemPeek={64}
176
+ >
177
+ <MessageScroller className="relative min-h-0 flex-1">
178
+ <MessageScrollerViewport preserveScrollOnPrepend>
179
+ <MessageScrollerContent
180
+ className="mx-auto w-full max-w-3xl gap-7 px-4 pt-8 pb-16 sm:px-6 sm:pt-12"
181
+ >
182
+ {controller.hasOlderTurns && (
183
+ <Button className="self-center" variant="ghost" size="sm" disabled={controller.isLoadingOlder} onClick={() => { void controller.loadOlder().catch(() => undefined) }}>
184
+ {controller.isLoadingOlder && <LoaderCircle className="animate-spin" />} Load earlier messages
185
+ </Button>
186
+ )}
187
+ {runtime.isBootstrapping && (
188
+ <div aria-label="Loading conversation" className="flex flex-1 items-center justify-center py-24 text-muted-foreground" role="status">
189
+ <LoaderCircle className="size-4 animate-spin" />
190
+ </div>
191
+ )}
192
+ {hasBlockingError && (
193
+ <div className="flex flex-1 flex-col items-center justify-center gap-3 py-24 text-center">
194
+ <CircleAlert className="size-5 text-destructive" />
195
+ <div><h2 className="font-medium">Couldn’t connect</h2><p className="mt-1 max-w-md text-sm text-muted-foreground">{runtime.errorMessage}</p></div>
196
+ <Button variant="outline" onClick={() => void runtime.retryBootstrap()}>Try again</Button>
197
+ </div>
198
+ )}
199
+ {controller.messages.map((message) => (
200
+ <MessageScrollerItem
201
+ key={message.id}
202
+ messageId={message.id}
203
+ scrollAnchor={message.role === "user"}
204
+ className="overflow-visible [content-visibility:visible]"
205
+ >
206
+ <ChatMessage
207
+ allowFeedback={runtime.allowFeedback}
208
+ getHitlActionState={runtime.getHitlActionState}
209
+ isLoading={isScaffoldTimelineLoading({
210
+ activeRunId: controller.activeRunId,
211
+ message,
212
+ runState: controller.runState,
213
+ streamingMessageId: controller.streamingMessageId,
214
+ })}
215
+ message={message}
216
+ onHitlAction={runtime.onHitlAction}
217
+ onRegenerate={() => controller.regenerate(message)}
218
+ onSetFeedback={(input) => controller.setFeedback(message, input)}
219
+ Timeline={components?.Timeline}
220
+ />
221
+ </MessageScrollerItem>
222
+ ))}
223
+ </MessageScrollerContent>
224
+ </MessageScrollerViewport>
225
+ <TurnOutline messages={controller.messages} />
226
+ <MessageScrollerButton />
227
+ </MessageScroller>
228
+ </MessageScrollerProvider>
229
+ <div className="relative z-30 shrink-0 bg-background pt-3">
230
+ <ChatComposer
231
+ inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
232
+ contextWindow={runtime.contextWindow}
233
+ disabled={runtime.isBootstrapping || hasBlockingError}
234
+ isCancelling={isCancelling}
235
+ isRunning={isRunning}
236
+ onStop={() => { void controller.cancelRun().catch(() => undefined) }}
237
+ modelId={runtime.modelId}
238
+ models={runtime.models}
239
+ onModelChange={runtime.changeModel}
240
+ onSubmit={(text, files) => runtime.submit({
241
+ text,
242
+ files: files.map((file) => ({
243
+ data: file,
244
+ mediaType: file.type || "application/octet-stream",
245
+ name: file.name,
246
+ })),
247
+ })}
248
+ utilityRow="below"
249
+ />
250
+ </div>
251
+ </>
252
+ )}
253
+ </section>
254
+ </SidebarInset>
255
+ </SidebarProvider>
256
+ )
257
+ }
@@ -0,0 +1,37 @@
1
+ import { ChevronDown, ChevronUp } from "lucide-react"
2
+ import * as React from "react"
3
+
4
+ import { Button } from "../ui/button"
5
+
6
+ const collapseThreshold = 360
7
+ export function CollapsibleUserMessage({ content }: { content: string }) {
8
+ const [expanded, setExpanded] = React.useState(false)
9
+ const contentId = React.useId()
10
+ const collapsible = content.length > collapseThreshold
11
+ const Icon = expanded ? ChevronUp : ChevronDown
12
+
13
+ return (
14
+ <div className="flex min-w-0 flex-col gap-2">
15
+ <div
16
+ id={contentId}
17
+ className={`max-w-none min-w-0 text-[15px] leading-7 whitespace-pre-wrap wrap-break-word ${collapsible && !expanded ? "agents24-user-message-collapsed" : ""}`}
18
+ >
19
+ {content}
20
+ </div>
21
+ {collapsible && (
22
+ <Button
23
+ variant="ghost"
24
+ size="sm"
25
+ type="button"
26
+ aria-controls={contentId}
27
+ aria-expanded={expanded}
28
+ className="h-auto w-fit px-0 py-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
29
+ onClick={() => setExpanded((value) => !value)}
30
+ >
31
+ {expanded ? "Show less" : "Show more"}
32
+ <Icon />
33
+ </Button>
34
+ )}
35
+ </div>
36
+ )
37
+ }
@@ -0,0 +1,48 @@
1
+ import type { LinkSafetyModalProps } from "streamdown"
2
+ import * as React from "react"
3
+
4
+ import { Button } from "../ui/button"
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "../ui/dialog"
13
+
14
+ export function ExternalLinkDialog({ isOpen, onClose, onConfirm, url }: LinkSafetyModalProps) {
15
+ const [copied, setCopied] = React.useState(false)
16
+
17
+ React.useEffect(() => {
18
+ if (!isOpen) setCopied(false)
19
+ }, [isOpen])
20
+
21
+ const copyUrl = async () => {
22
+ await navigator.clipboard.writeText(url)
23
+ setCopied(true)
24
+ }
25
+
26
+ const openUrl = () => {
27
+ onConfirm()
28
+ onClose()
29
+ }
30
+
31
+ return (
32
+ <Dialog open={isOpen} onOpenChange={(open) => { if (!open) onClose() }}>
33
+ <DialogContent className="max-h-[calc(100dvh-2rem)] min-w-0 overflow-hidden sm:max-w-md">
34
+ <DialogHeader>
35
+ <DialogTitle>Open external link?</DialogTitle>
36
+ <DialogDescription>You’re about to visit an external website.</DialogDescription>
37
+ </DialogHeader>
38
+ <div className="max-h-32 overflow-auto rounded-md bg-muted p-3 font-mono text-xs wrap-break-word">
39
+ {url}
40
+ </div>
41
+ <DialogFooter>
42
+ <Button variant="outline" onClick={() => void copyUrl()}>{copied ? "Copied" : "Copy link"}</Button>
43
+ <Button onClick={openUrl}>Open link</Button>
44
+ </DialogFooter>
45
+ </DialogContent>
46
+ </Dialog>
47
+ )
48
+ }
@@ -0,0 +1,69 @@
1
+ import { LogIn, LogOut, UserRound } from "lucide-react"
2
+
3
+ import type { Agents24ChatIdentity } from "../../identity"
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuLabel,
9
+ DropdownMenuSeparator,
10
+ DropdownMenuTrigger,
11
+ } from "../ui/dropdown-menu"
12
+ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "../ui/sidebar"
13
+
14
+ function identityLabel(identity: Agents24ChatIdentity): string {
15
+ if (identity.displayName) return identity.displayName
16
+ if (identity.state === "authenticated") return "Signed in"
17
+ if (identity.state === "loading") return "Loading identity"
18
+ if (identity.state === "error") return "Identity unavailable"
19
+ if (identity.state === "signed_out") return "Sign in"
20
+ return "Guest"
21
+ }
22
+
23
+ function IdentityAvatar({ identity }: { identity: Agents24ChatIdentity }) {
24
+ const label = identityLabel(identity)
25
+ if (identity.avatarUrl) {
26
+ return <img alt="" className="size-6 rounded-full object-cover" src={identity.avatarUrl} />
27
+ }
28
+ return (
29
+ <span className="flex size-6 items-center justify-center rounded-full bg-sidebar-accent text-[10px] font-semibold" aria-hidden="true">
30
+ {identity.displayName ? identity.displayName.slice(0, 1).toUpperCase() : <UserRound className="size-3.5" />}
31
+ </span>
32
+ )
33
+ }
34
+
35
+ export function IdentityControl({ identity }: { identity: Agents24ChatIdentity }) {
36
+ const label = identityLabel(identity)
37
+ const canSignOut = identity.state === "authenticated" && Boolean(identity.onSignOut)
38
+ const canSignIn = (identity.state === "guest" || identity.state === "signed_out" || identity.state === "error") && Boolean(identity.onSignIn)
39
+ return (
40
+ <SidebarMenu className="w-full min-w-0">
41
+ <SidebarMenuItem className="w-full min-w-0">
42
+ <DropdownMenu>
43
+ <DropdownMenuTrigger asChild>
44
+ <SidebarMenuButton
45
+ size="lg"
46
+ aria-label={`Account: ${label}`}
47
+ className="w-full min-w-0 justify-start"
48
+ >
49
+ <IdentityAvatar identity={identity} />
50
+ <span className="min-w-0 flex-1 overflow-hidden text-start">
51
+ <span className="block truncate text-sm font-medium">{label}</span>
52
+ {identity.secondaryText ? <span className="block truncate text-xs text-muted-foreground">{identity.secondaryText}</span> : null}
53
+ </span>
54
+ </SidebarMenuButton>
55
+ </DropdownMenuTrigger>
56
+ <DropdownMenuContent align="start" side="top" sideOffset={8}>
57
+ <DropdownMenuLabel>
58
+ <span className="block truncate">{label}</span>
59
+ {identity.secondaryText ? <span className="mt-0.5 block truncate text-xs font-normal text-muted-foreground">{identity.secondaryText}</span> : null}
60
+ </DropdownMenuLabel>
61
+ {(canSignIn || canSignOut) ? <DropdownMenuSeparator /> : null}
62
+ {canSignIn ? <DropdownMenuItem onSelect={() => identity.onSignIn?.()}><LogIn />Sign in</DropdownMenuItem> : null}
63
+ {canSignOut ? <DropdownMenuItem onSelect={() => identity.onSignOut?.()}><LogOut />Sign out</DropdownMenuItem> : null}
64
+ </DropdownMenuContent>
65
+ </DropdownMenu>
66
+ </SidebarMenuItem>
67
+ </SidebarMenu>
68
+ )
69
+ }
@@ -0,0 +1,83 @@
1
+ import type { ChatHitlPart, ChatMessage } from "@agents24/react"
2
+ import type { HitlResponse } from "@agents24/client"
3
+ import {
4
+ AgentChatPart,
5
+ AgentResponseTimeline,
6
+ } from "@agents24/chat-react/ui"
7
+ import type { AgentChatRuntimeHitlActionState } from "@agents24/chat-react/runtime"
8
+ import * as React from "react"
9
+ import type { LinkSafetyConfig } from "streamdown"
10
+
11
+ import { CollapsibleUserMessage } from "./collapsible-user-message"
12
+ import { ExternalLinkDialog } from "./external-link-dialog"
13
+ import type { Agents24ChatTimelineSlotProps } from "../../app"
14
+
15
+ const linkSafety = {
16
+ enabled: true,
17
+ renderModal: (props) => <ExternalLinkDialog {...props} />,
18
+ } satisfies LinkSafetyConfig
19
+
20
+ function ScaffoldTimeline(props: Agents24ChatTimelineSlotProps) {
21
+ return (
22
+ <AgentResponseTimeline
23
+ {...props}
24
+ responseProps={{
25
+ className: "text-[15px] leading-7",
26
+ linkSafety,
27
+ }}
28
+ />
29
+ )
30
+ }
31
+
32
+ export function MessageParts({
33
+ message,
34
+ getHitlActionState,
35
+ isLoading,
36
+ onHitlAction,
37
+ Timeline = ScaffoldTimeline,
38
+ }: {
39
+ message: ChatMessage
40
+ getHitlActionState: (part: ChatHitlPart) => AgentChatRuntimeHitlActionState | undefined
41
+ isLoading: boolean
42
+ onHitlAction: (part: ChatHitlPart, response: HitlResponse) => Promise<void>
43
+ Timeline?: React.ComponentType<Agents24ChatTimelineSlotProps>
44
+ }) {
45
+ if (message.role === "assistant") {
46
+ return (
47
+ <Timeline
48
+ getHitlActionState={getHitlActionState}
49
+ isLoading={isLoading}
50
+ message={message}
51
+ onHitlAction={(part, response) => {
52
+ void onHitlAction(part, response).catch(() => undefined)
53
+ }}
54
+ parts={message.parts}
55
+ />
56
+ )
57
+ }
58
+
59
+ const text = message.parts
60
+ .filter((part) => part.kind === "text")
61
+ .map((part) => part.kind === "text" ? part.text : "")
62
+ .join("") || message.content
63
+ const supportingParts = message.parts.filter((part) => (
64
+ part.kind !== "text" && part.kind !== "data"
65
+ ))
66
+
67
+ return (
68
+ <>
69
+ {supportingParts.map((part) => (
70
+ <AgentChatPart
71
+ getHitlActionState={getHitlActionState}
72
+ key={part.id}
73
+ message={message}
74
+ onHitlAction={(hitlPart, response) => {
75
+ void onHitlAction(hitlPart, response).catch(() => undefined)
76
+ }}
77
+ part={part}
78
+ />
79
+ ))}
80
+ {text ? <CollapsibleUserMessage content={text} /> : null}
81
+ </>
82
+ )
83
+ }
@@ -0,0 +1,79 @@
1
+ import { Laptop, Moon, Sun } from "lucide-react"
2
+ import * as React from "react"
3
+
4
+ import { useTheme } from "../theme-provider"
5
+ import { Button } from "../ui/button"
6
+
7
+ const options = [
8
+ { value: "system" as const, label: "System", icon: Laptop },
9
+ { value: "light" as const, label: "Light", icon: Sun },
10
+ { value: "dark" as const, label: "Dark", icon: Moon },
11
+ ]
12
+
13
+ export function ThemeMenu() {
14
+ const { appearance, setAppearance } = useTheme()
15
+ const [open, setOpen] = React.useState(false)
16
+ const rootRef = React.useRef<HTMLDivElement>(null)
17
+ const firstItemRef = React.useRef<HTMLButtonElement>(null)
18
+ const menuId = React.useId()
19
+ const ActiveIcon = options.find((item) => item.value === appearance)?.icon ?? Laptop
20
+
21
+ React.useEffect(() => {
22
+ if (!open) return
23
+ firstItemRef.current?.focus()
24
+ const closeOnOutsidePointer = (event: PointerEvent) => {
25
+ if (!rootRef.current?.contains(event.target as Node)) setOpen(false)
26
+ }
27
+ const closeOnEscape = (event: KeyboardEvent) => {
28
+ if (event.key === "Escape") setOpen(false)
29
+ }
30
+ document.addEventListener("pointerdown", closeOnOutsidePointer)
31
+ document.addEventListener("keydown", closeOnEscape)
32
+ return () => {
33
+ document.removeEventListener("pointerdown", closeOnOutsidePointer)
34
+ document.removeEventListener("keydown", closeOnEscape)
35
+ }
36
+ }, [open])
37
+
38
+ const label = appearance === "system" ? "System" : appearance === "light" ? "Light" : "Dark"
39
+ return (
40
+ <div ref={rootRef} className="relative">
41
+ <Button
42
+ aria-controls={menuId}
43
+ aria-expanded={open}
44
+ aria-haspopup="menu"
45
+ aria-label={`Appearance: ${label}`}
46
+ onClick={() => setOpen((value) => !value)}
47
+ size="icon-sm"
48
+ variant="ghost"
49
+ >
50
+ <ActiveIcon />
51
+ </Button>
52
+ {open ? (
53
+ <div
54
+ id={menuId}
55
+ className="absolute end-0 top-full z-50 mt-1 flex min-w-44 flex-col rounded-xl border bg-popover p-1 text-popover-foreground shadow-md"
56
+ role="menu"
57
+ >
58
+ {options.map(({ value, label: optionLabel, icon: Icon }, index) => (
59
+ <Button
60
+ key={value}
61
+ ref={index === 0 ? firstItemRef : undefined}
62
+ aria-current={appearance === value ? "true" : undefined}
63
+ className="w-full justify-start"
64
+ onClick={() => {
65
+ setAppearance(value)
66
+ setOpen(false)
67
+ }}
68
+ role="menuitem"
69
+ size="sm"
70
+ variant="ghost"
71
+ >
72
+ <Icon /> {optionLabel}
73
+ </Button>
74
+ ))}
75
+ </div>
76
+ ) : null}
77
+ </div>
78
+ )
79
+ }