@agents24/chat-react 0.5.0 → 0.5.1
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/README.md +90 -21
- package/compatibility.json +10 -10
- package/dist/chunk-SSUO4EPD.js +189 -0
- package/dist/chunk-SSUO4EPD.js.map +1 -0
- package/dist/chunk-ZCNHLFRB.js +108 -0
- package/dist/chunk-ZCNHLFRB.js.map +1 -0
- package/dist/index.cjs +112 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +362 -68
- package/dist/index.js.map +1 -1
- package/dist/mcp-oauth.d.ts +13 -0
- package/dist/runtime/index.cjs +594 -0
- package/dist/runtime/index.cjs.map +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +458 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/input-contract.d.ts +20 -0
- package/dist/runtime/use-agent-chat-runtime.d.ts +83 -0
- package/dist/scaffold/app.d.ts +30 -0
- package/dist/scaffold/components/chat/chat-composer.d.ts +21 -0
- package/dist/scaffold/components/chat/chat-message.d.ts +21 -0
- package/dist/scaffold/components/chat/chat-shell.d.ts +8 -0
- package/dist/scaffold/components/chat/collapsible-user-message.d.ts +3 -0
- package/dist/scaffold/components/chat/external-link-dialog.d.ts +2 -0
- package/dist/scaffold/components/chat/identity-control.d.ts +4 -0
- package/dist/scaffold/components/chat/message-parts.d.ts +12 -0
- package/dist/scaffold/components/chat/theme-menu.d.ts +1 -0
- package/dist/scaffold/components/chat/thread-sidebar.d.ts +16 -0
- package/dist/scaffold/components/chat/timeline-loading.d.ts +7 -0
- package/dist/scaffold/components/chat/turn-outline.d.ts +4 -0
- package/dist/scaffold/components/theme-provider.d.ts +20 -0
- package/dist/scaffold/components/ui/attachment.d.ts +24 -0
- package/dist/scaffold/components/ui/bubble.d.ts +17 -0
- package/dist/scaffold/components/ui/button-group.d.ts +11 -0
- package/dist/scaffold/components/ui/button.d.ts +10 -0
- package/dist/scaffold/components/ui/dialog.d.ts +17 -0
- package/dist/scaffold/components/ui/dropdown-menu.d.ts +29 -0
- package/dist/scaffold/components/ui/input.d.ts +3 -0
- package/dist/scaffold/components/ui/marker.d.ts +11 -0
- package/dist/scaffold/components/ui/message-scroller.d.ts +10 -0
- package/dist/scaffold/components/ui/message.d.ts +10 -0
- package/dist/scaffold/components/ui/popover.d.ts +6 -0
- package/dist/scaffold/components/ui/select.d.ts +8 -0
- package/dist/scaffold/components/ui/separator.d.ts +4 -0
- package/dist/scaffold/components/ui/sheet.d.ts +14 -0
- package/dist/scaffold/components/ui/sidebar.d.ts +69 -0
- package/dist/scaffold/components/ui/skeleton.d.ts +2 -0
- package/dist/scaffold/components/ui/textarea.d.ts +3 -0
- package/dist/scaffold/components/ui/tooltip.d.ts +15 -0
- package/dist/scaffold/hooks/use-mobile.d.ts +1 -0
- package/dist/scaffold/identity.d.ts +10 -0
- package/dist/scaffold/index.cjs +2270 -0
- package/dist/scaffold/index.cjs.map +1 -0
- package/dist/scaffold/index.d.ts +14 -0
- package/dist/scaffold/index.js +2244 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/lib/utils.d.ts +2 -0
- package/dist/styles.css +137 -378
- package/dist/types.d.ts +3 -13
- package/dist/ui/adapters.d.ts +1 -1
- package/dist/ui/agent-chat-actions.d.ts +12 -5
- package/dist/ui/agent-chat-composer.d.ts +2 -1
- package/dist/ui/agent-chat-context-compression-row.d.ts +5 -0
- package/dist/ui/agent-chat-context-status.d.ts +13 -0
- package/dist/ui/agent-chat-message.d.ts +7 -5
- package/dist/ui/agent-response-timeline.d.ts +29 -7
- package/dist/ui/ask-user-interaction-state.d.ts +20 -0
- package/dist/ui/ask-user-interaction.d.ts +8 -0
- package/dist/ui/index.cjs +1391 -271
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.js +2380 -65
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/tool-sessions.d.ts +70 -0
- package/dist/ui/use-audio-recorder.d.ts +8 -0
- package/package.json +27 -12
- package/scaffold/components.json +25 -0
- package/scaffold/manifest.json +28 -0
- package/scaffold/src/app.tsx +71 -0
- package/scaffold/src/chat.css +54 -0
- package/scaffold/src/components/chat/chat-composer.tsx +237 -0
- package/scaffold/src/components/chat/chat-message.tsx +76 -0
- package/scaffold/src/components/chat/chat-shell.tsx +257 -0
- package/scaffold/src/components/chat/collapsible-user-message.tsx +37 -0
- package/scaffold/src/components/chat/external-link-dialog.tsx +48 -0
- package/scaffold/src/components/chat/identity-control.tsx +69 -0
- package/scaffold/src/components/chat/message-parts.tsx +83 -0
- package/scaffold/src/components/chat/theme-menu.tsx +79 -0
- package/scaffold/src/components/chat/thread-sidebar.tsx +114 -0
- package/scaffold/src/components/chat/timeline-loading.ts +17 -0
- package/scaffold/src/components/chat/turn-outline.tsx +73 -0
- package/scaffold/src/components/theme-provider.tsx +91 -0
- package/scaffold/src/components/ui/attachment.tsx +204 -0
- package/scaffold/src/components/ui/bubble.tsx +125 -0
- package/scaffold/src/components/ui/button-group.tsx +83 -0
- package/scaffold/src/components/ui/button.tsx +65 -0
- package/scaffold/src/components/ui/dialog.tsx +171 -0
- package/scaffold/src/components/ui/dropdown-menu.tsx +271 -0
- package/scaffold/src/components/ui/input.tsx +19 -0
- package/scaffold/src/components/ui/marker.tsx +69 -0
- package/scaffold/src/components/ui/message-scroller.tsx +129 -0
- package/scaffold/src/components/ui/message.tsx +92 -0
- package/scaffold/src/components/ui/popover.tsx +40 -0
- package/scaffold/src/components/ui/select.tsx +92 -0
- package/scaffold/src/components/ui/separator.tsx +28 -0
- package/scaffold/src/components/ui/sheet.tsx +150 -0
- package/scaffold/src/components/ui/sidebar.tsx +677 -0
- package/scaffold/src/components/ui/skeleton.tsx +13 -0
- package/scaffold/src/components/ui/textarea.tsx +18 -0
- package/scaffold/src/components/ui/tooltip.tsx +56 -0
- package/scaffold/src/hooks/use-mobile.ts +24 -0
- package/scaffold/src/identity.ts +17 -0
- package/scaffold/src/index.css +222 -0
- package/scaffold/src/index.tsx +39 -0
- package/scaffold/src/lib/utils.ts +6 -0
- package/scaffold/styles.css +3 -0
- package/dist/chunk-CHAU7FNW.js +0 -140
- package/dist/chunk-CHAU7FNW.js.map +0 -1
- package/dist/chunk-LI67M74F.js +0 -445
- package/dist/chunk-LI67M74F.js.map +0 -1
- package/dist/chunk-R7WFL3YR.js +0 -1327
- package/dist/chunk-R7WFL3YR.js.map +0 -1
- package/dist/renderers.d.ts +0 -10
- package/dist/templates/agent-chat-app.d.ts +0 -18
- package/dist/templates/agent-chat-layout.d.ts +0 -77
- package/dist/templates/agent-chat-shell.d.ts +0 -37
- package/dist/templates/appearance-control.d.ts +0 -5
- package/dist/templates/index.cjs +0 -2600
- package/dist/templates/index.cjs.map +0 -1
- package/dist/templates/index.d.ts +0 -4
- package/dist/templates/index.js +0 -846
- 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
|
+
}
|