@agents24/chat-react 0.5.6 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +11 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +11 -6
- package/dist/index.js.map +1 -1
- package/dist/latest-thread-scroller.d.ts +2 -1
- package/dist/scaffold/components/chat/turn-outline.d.ts +1 -1
- package/dist/scaffold/index.cjs +144 -133
- package/dist/scaffold/index.cjs.map +1 -1
- package/dist/scaffold/index.js +154 -143
- package/dist/scaffold/index.js.map +1 -1
- package/dist/styles.css +12 -5
- package/dist/ui/agent-chat-message.d.ts +4 -0
- package/dist/ui/index.cjs +138 -52
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.js +137 -52
- package/dist/ui/index.js.map +1 -1
- package/package.json +1 -1
- package/scaffold/src/components/chat/chat-message.tsx +12 -2
- package/scaffold/src/components/chat/chat-shell.tsx +58 -30
- package/scaffold/src/components/chat/turn-outline.tsx +17 -59
- package/scaffold/src/components/ui/message-scroller.tsx +1 -1
- package/scaffold/src/components/ui/message.tsx +1 -1
- package/scaffold/src/components/ui/sidebar.tsx +1 -1
package/package.json
CHANGED
|
@@ -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, ChatAttachmentRows, type ChatAttachmentContentResolver } from "@agents24/chat-react/ui"
|
|
4
|
+
import { AgentChatDefaultActions, AgentChatUserMessageActionRow, ChatAttachmentRows, type ChatAttachmentContentResolver } from "@agents24/chat-react/ui"
|
|
5
5
|
import * as React from "react"
|
|
6
6
|
|
|
7
7
|
import { MessageParts } from "./message-parts"
|
|
@@ -37,7 +37,7 @@ export function ChatMessage({
|
|
|
37
37
|
const isUser = message.role === "user"
|
|
38
38
|
return (
|
|
39
39
|
<Message align={isUser ? "end" : "start"}>
|
|
40
|
-
<MessageContent>
|
|
40
|
+
<MessageContent className={isUser ? "gap-0" : undefined}>
|
|
41
41
|
{isUser && message.attachments?.length ? (
|
|
42
42
|
<ChatAttachmentRows
|
|
43
43
|
attachments={message.attachments}
|
|
@@ -56,6 +56,16 @@ export function ChatMessage({
|
|
|
56
56
|
/>
|
|
57
57
|
</BubbleContent>
|
|
58
58
|
</Bubble>
|
|
59
|
+
{isUser && (
|
|
60
|
+
<AgentChatUserMessageActionRow>
|
|
61
|
+
<AgentChatDefaultActions
|
|
62
|
+
handlers={{
|
|
63
|
+
onCopy: (content) => { void navigator.clipboard.writeText(content).catch(() => undefined) },
|
|
64
|
+
}}
|
|
65
|
+
message={message}
|
|
66
|
+
/>
|
|
67
|
+
</AgentChatUserMessageActionRow>
|
|
68
|
+
)}
|
|
59
69
|
{!isUser && message.isFinal !== false && (
|
|
60
70
|
<MessageFooter className="gap-1 opacity-70 transition-opacity group-hover/message:opacity-100">
|
|
61
71
|
<AgentChatDefaultActions
|
|
@@ -105,6 +105,22 @@ export function ChatShell({
|
|
|
105
105
|
const activeTitle = controller.threads.find((thread) => thread.id === controller.activeThreadId)?.title
|
|
106
106
|
const hasBlockingError = Boolean(runtime.fatalError && controller.messages.length === 0)
|
|
107
107
|
const isEmptyState = controller.activeThreadId === null && controller.messages.length === 0 && !hasBlockingError
|
|
108
|
+
const composerOverlayRef = React.useRef<HTMLDivElement>(null)
|
|
109
|
+
const [composerOverlayHeight, setComposerOverlayHeight] = React.useState(192)
|
|
110
|
+
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
const overlay = composerOverlayRef.current
|
|
113
|
+
if (!overlay || isEmptyState) return
|
|
114
|
+
|
|
115
|
+
const updateHeight = () => {
|
|
116
|
+
setComposerOverlayHeight(Math.ceil(overlay.getBoundingClientRect().height))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
updateHeight()
|
|
120
|
+
const resizeObserver = new ResizeObserver(updateHeight)
|
|
121
|
+
resizeObserver.observe(overlay)
|
|
122
|
+
return () => resizeObserver.disconnect()
|
|
123
|
+
}, [isEmptyState])
|
|
108
124
|
|
|
109
125
|
return (
|
|
110
126
|
<SidebarProvider
|
|
@@ -127,7 +143,7 @@ export function ChatShell({
|
|
|
127
143
|
/>}
|
|
128
144
|
<SidebarInset className="h-svh min-h-0 overflow-hidden bg-background">
|
|
129
145
|
<header
|
|
130
|
-
className="flex h-12 shrink-0 items-center gap-2 border-b border-border/
|
|
146
|
+
className="flex h-12 shrink-0 items-center gap-2 border-b border-border/50 px-3 sm:px-4"
|
|
131
147
|
data-agents24-chat-header=""
|
|
132
148
|
>
|
|
133
149
|
<CollapsedHeaderActions onNew={runtime.newThread} />
|
|
@@ -184,7 +200,8 @@ export function ChatShell({
|
|
|
184
200
|
<MessageScroller className="relative min-h-0 flex-1">
|
|
185
201
|
<MessageScrollerViewport preserveScrollOnPrepend>
|
|
186
202
|
<MessageScrollerContent
|
|
187
|
-
className="mx-auto w-full max-w-3xl gap-
|
|
203
|
+
className="mx-auto w-full max-w-3xl gap-3 px-4 pt-8 pb-0 sm:px-6 sm:pt-12"
|
|
204
|
+
style={{ paddingBottom: composerOverlayHeight + 72 }}
|
|
188
205
|
>
|
|
189
206
|
{controller.hasOlderTurns && (
|
|
190
207
|
<Button className="self-center" variant="ghost" size="sm" disabled={controller.isLoadingOlder} onClick={() => { void controller.loadOlder().catch(() => undefined) }}>
|
|
@@ -231,36 +248,47 @@ export function ChatShell({
|
|
|
231
248
|
</MessageScrollerContent>
|
|
232
249
|
</MessageScrollerViewport>
|
|
233
250
|
<TurnOutline messages={controller.messages} />
|
|
234
|
-
<MessageScrollerButton />
|
|
235
251
|
</MessageScroller>
|
|
252
|
+
|
|
253
|
+
<div
|
|
254
|
+
ref={composerOverlayRef}
|
|
255
|
+
className="pointer-events-none absolute inset-x-0 bottom-0 z-30 overflow-visible pt-10"
|
|
256
|
+
style={{
|
|
257
|
+
background: "linear-gradient(to bottom, transparent 0%, color-mix(in oklch, var(--background) 85%, transparent) 45%, var(--background) 65%, var(--background) 100%)",
|
|
258
|
+
}}
|
|
259
|
+
>
|
|
260
|
+
<div className="pointer-events-auto absolute inset-x-0 top-0 flex -translate-y-full justify-center pb-3">
|
|
261
|
+
<MessageScrollerButton className="!relative !inset-auto !translate-x-0 rtl:!translate-x-0" />
|
|
262
|
+
</div>
|
|
263
|
+
<div className="pointer-events-auto">
|
|
264
|
+
<ChatComposer
|
|
265
|
+
inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
|
|
266
|
+
contextWindow={runtime.contextWindow}
|
|
267
|
+
disabled={runtime.isBootstrapping || runtime.isChangingAgent || hasBlockingError || isUploading}
|
|
268
|
+
focusKey={controller.activeThreadId}
|
|
269
|
+
isCancelling={isCancelling}
|
|
270
|
+
isRunning={isRunning}
|
|
271
|
+
agentId={runtime.agentId}
|
|
272
|
+
agents={runtime.agents}
|
|
273
|
+
onAgentChange={(agentId) => { void runtime.changeAgent(agentId) }}
|
|
274
|
+
onStop={() => { void controller.cancelRun().catch(() => undefined) }}
|
|
275
|
+
modelId={runtime.modelId}
|
|
276
|
+
models={runtime.models}
|
|
277
|
+
onModelChange={runtime.changeModel}
|
|
278
|
+
onSubmit={(text, files) => runtime.submit({
|
|
279
|
+
text,
|
|
280
|
+
files: files.map((file) => ({
|
|
281
|
+
data: file.source,
|
|
282
|
+
mediaType: file.mediaType,
|
|
283
|
+
name: file.filename,
|
|
284
|
+
})),
|
|
285
|
+
attachments: files,
|
|
286
|
+
})}
|
|
287
|
+
utilityRow="below"
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
236
291
|
</MessageScrollerProvider>
|
|
237
|
-
<div className="relative z-30 shrink-0 bg-background pt-3">
|
|
238
|
-
<ChatComposer
|
|
239
|
-
inputContract={runtime.inputContract || LOADING_INPUT_CONTRACT}
|
|
240
|
-
contextWindow={runtime.contextWindow}
|
|
241
|
-
disabled={runtime.isBootstrapping || runtime.isChangingAgent || hasBlockingError || isUploading}
|
|
242
|
-
focusKey={controller.activeThreadId}
|
|
243
|
-
isCancelling={isCancelling}
|
|
244
|
-
isRunning={isRunning}
|
|
245
|
-
agentId={runtime.agentId}
|
|
246
|
-
agents={runtime.agents}
|
|
247
|
-
onAgentChange={(agentId) => { void runtime.changeAgent(agentId) }}
|
|
248
|
-
onStop={() => { void controller.cancelRun().catch(() => undefined) }}
|
|
249
|
-
modelId={runtime.modelId}
|
|
250
|
-
models={runtime.models}
|
|
251
|
-
onModelChange={runtime.changeModel}
|
|
252
|
-
onSubmit={(text, files) => runtime.submit({
|
|
253
|
-
text,
|
|
254
|
-
files: files.map((file) => ({
|
|
255
|
-
data: file.source,
|
|
256
|
-
mediaType: file.mediaType,
|
|
257
|
-
name: file.filename,
|
|
258
|
-
})),
|
|
259
|
-
attachments: files,
|
|
260
|
-
})}
|
|
261
|
-
utilityRow="below"
|
|
262
|
-
/>
|
|
263
|
-
</div>
|
|
264
292
|
</>
|
|
265
293
|
)}
|
|
266
294
|
</section>
|
|
@@ -1,73 +1,31 @@
|
|
|
1
|
+
import { LatestThreadScroller, type LatestThreadScrollerOutlineItem } from "@agents24/chat-react"
|
|
1
2
|
import type { ChatMessage } from "@agents24/react"
|
|
2
|
-
import { useState } from "react"
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { cn } from "../../lib/utils"
|
|
7
|
-
|
|
8
|
-
type Turn = { id: string; label: string; messageIds: string[]; response: string }
|
|
9
|
-
|
|
10
|
-
function turnsFromMessages(messages: ChatMessage[]): Turn[] {
|
|
11
|
-
return messages.reduce<Turn[]>((turns, message) => {
|
|
4
|
+
function turnsFromMessages(messages: ChatMessage[]): LatestThreadScrollerOutlineItem[] {
|
|
5
|
+
return messages.reduce<LatestThreadScrollerOutlineItem[]>((turns, message) => {
|
|
12
6
|
if (message.role === "user") {
|
|
13
|
-
turns.push({
|
|
7
|
+
turns.push({
|
|
8
|
+
id: message.id,
|
|
9
|
+
label: message.content.trim() || `Turn ${turns.length + 1}`,
|
|
10
|
+
messageIds: [message.id],
|
|
11
|
+
responsePreview: "",
|
|
12
|
+
})
|
|
14
13
|
} else if (turns.length) {
|
|
15
14
|
const turn = turns[turns.length - 1]
|
|
16
|
-
turn.messageIds
|
|
17
|
-
if (!turn.
|
|
15
|
+
turn.messageIds?.push(message.id)
|
|
16
|
+
if (!turn.responsePreview) turn.responsePreview = message.content.trim()
|
|
18
17
|
}
|
|
19
18
|
return turns
|
|
20
19
|
}, [])
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
export function TurnOutline({ messages }: { messages: ChatMessage[] }) {
|
|
24
|
-
const turns = turnsFromMessages(messages)
|
|
25
|
-
const { scrollToMessage } = useMessageScroller()
|
|
26
|
-
const { currentAnchorId, visibleMessageIds } = useMessageScrollerVisibility()
|
|
27
|
-
const visible = new Set(visibleMessageIds)
|
|
28
|
-
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
|
|
29
|
-
const focusIndex = hoveredIndex ?? -1
|
|
30
|
-
if (turns.length < 2) return null
|
|
31
|
-
|
|
32
23
|
return (
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
? 12
|
|
40
|
-
: distance === 0 ? 50 : distance === 1 ? 36 : distance === 2 ? 23 : 10
|
|
41
|
-
const barOpacity = focusIndex < 0
|
|
42
|
-
? active ? 1 : 0.4
|
|
43
|
-
: distance === 0 ? 1 : distance === 1 ? 0.78 : distance === 2 ? 0.58 : 0.36
|
|
44
|
-
return (
|
|
45
|
-
<Tooltip key={turn.id}>
|
|
46
|
-
<TooltipTrigger asChild>
|
|
47
|
-
<button
|
|
48
|
-
type="button"
|
|
49
|
-
aria-label={`Jump to ${turn.label}`}
|
|
50
|
-
className="group relative flex h-2 w-14 items-center justify-end outline-none after:pointer-events-auto after:absolute after:-inset-y-0.5 after:inset-x-0 after:content-['']"
|
|
51
|
-
onBlur={() => setHoveredIndex(null)}
|
|
52
|
-
onFocus={() => setHoveredIndex(index)}
|
|
53
|
-
onMouseEnter={() => setHoveredIndex(index)}
|
|
54
|
-
onMouseLeave={() => setHoveredIndex(null)}
|
|
55
|
-
onClick={() => scrollToMessage(turn.id, { align: "start", behavior: "smooth" })}
|
|
56
|
-
>
|
|
57
|
-
<span
|
|
58
|
-
className={cn("h-0.5 rounded-full bg-muted-foreground/40 transition-[width,background-color,opacity] duration-150 ease-out group-hover:bg-foreground group-focus-visible:bg-foreground", active && "bg-foreground")}
|
|
59
|
-
style={{ opacity: barOpacity, width: barWidth }}
|
|
60
|
-
/>
|
|
61
|
-
</button>
|
|
62
|
-
</TooltipTrigger>
|
|
63
|
-
<TooltipContent side="left" className="w-64 flex-col items-stretch gap-0 overflow-hidden p-3">
|
|
64
|
-
<p className="line-clamp-2 max-w-full wrap-break-word text-sm font-medium">{turn.label}</p>
|
|
65
|
-
{turn.response && <p className="mt-1 line-clamp-2 max-w-full wrap-break-word text-xs opacity-70">{turn.response}</p>}
|
|
66
|
-
</TooltipContent>
|
|
67
|
-
</Tooltip>
|
|
68
|
-
)
|
|
69
|
-
})}
|
|
70
|
-
</div>
|
|
71
|
-
</nav>
|
|
24
|
+
<LatestThreadScroller.Outline
|
|
25
|
+
items={turnsFromMessages(messages)}
|
|
26
|
+
minSideGutterForRail={0}
|
|
27
|
+
side="left"
|
|
28
|
+
verticalOffset="3.5rem"
|
|
29
|
+
/>
|
|
72
30
|
)
|
|
73
31
|
}
|
|
@@ -55,7 +55,7 @@ function MessageScrollerContent({
|
|
|
55
55
|
return (
|
|
56
56
|
<MessageScrollerPrimitive.Content
|
|
57
57
|
data-slot="message-scroller-content"
|
|
58
|
-
className={cn("flex h-max min-h-full flex-col gap-
|
|
58
|
+
className={cn("flex h-max min-h-full flex-col gap-3", className)}
|
|
59
59
|
{...props}
|
|
60
60
|
/>
|
|
61
61
|
)
|
|
@@ -74,7 +74,7 @@ function MessageFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
74
74
|
<div
|
|
75
75
|
data-slot="message-footer"
|
|
76
76
|
className={cn(
|
|
77
|
-
"flex max-w-full min-w-0 items-center
|
|
77
|
+
"flex max-w-full min-w-0 items-center pl-3.5 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end",
|
|
78
78
|
className
|
|
79
79
|
)}
|
|
80
80
|
{...props}
|
|
@@ -215,7 +215,7 @@ function Sidebar({
|
|
|
215
215
|
// Adjust the padding for floating and inset variants.
|
|
216
216
|
variant === "floating" || variant === "inset"
|
|
217
217
|
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
|
218
|
-
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
|
218
|
+
: "border-border/50 group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
|
219
219
|
className
|
|
220
220
|
)}
|
|
221
221
|
{...props}
|