@agents24/chat-react 0.5.6 → 0.5.8

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.6",
3
+ "version": "0.5.8",
4
4
  "description": "Accessible web UI and complete default layout for Agents24 chat applications.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -290,9 +290,11 @@ export function ChatComposer({
290
290
  </Button>
291
291
  ) : (
292
292
  validationError ? (
293
- <Tooltip tabIndex={0}>
293
+ <Tooltip>
294
294
  <TooltipTrigger asChild>
295
- <Button size="icon" aria-label="Send message" disabled onClick={() => void submit()}><ArrowUp /></Button>
295
+ <span tabIndex={0} aria-label={validationError}>
296
+ <Button size="icon" aria-label="Send message" disabled onClick={() => void submit()}><ArrowUp /></Button>
297
+ </span>
296
298
  </TooltipTrigger>
297
299
  <TooltipContent>{validationError}</TooltipContent>
298
300
  </Tooltip>
@@ -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/30 px-3 sm:px-4"
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-7 px-4 pt-8 pb-16 sm:px-6 sm:pt-12"
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
- import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"
5
- import { useMessageScroller, useMessageScrollerVisibility } from "../ui/message-scroller"
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({ id: message.id, label: message.content.trim() || `Turn ${turns.length + 1}`, messageIds: [message.id], response: "" })
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.push(message.id)
17
- if (!turn.response) turn.response = message.content.trim()
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
- <nav aria-label="Transcript outline" className="pointer-events-none absolute end-2 top-1/2 z-20 hidden max-h-72 w-14 -translate-y-1/2 md:block xl:end-5">
34
- <div className="pointer-events-auto flex flex-col items-end gap-0.5 py-2">
35
- {turns.map((turn, index) => {
36
- const active = turn.messageIds.some((id) => visible.has(id) || currentAnchorId === id)
37
- const distance = focusIndex >= 0 ? Math.abs(index - focusIndex) : Number.POSITIVE_INFINITY
38
- const barWidth = focusIndex < 0
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-8", className)}
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 px-3.5 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end",
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}