supposedly better tool call animation
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useRef, useState } from "preact/hooks";
|
||||
import type { JSX } from "preact";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import type { ComponentChildren, JSX } from "preact";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChatAttachmentList } from "@/components/chat/chat-attachment-list";
|
||||
import { getMessageAttachments, type Message } from "@/lib/api";
|
||||
@@ -142,6 +142,14 @@ function buildMessageRenderItems(messages: Message[]) {
|
||||
return items;
|
||||
}
|
||||
|
||||
function getToolCallMessageIDs(messages: Message[]) {
|
||||
const ids = new Set<string>();
|
||||
for (const message of messages) {
|
||||
if (message.role === "tool" && asToolLogMetadata(message.metadata)) ids.add(message.id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function getToolStackHeight(messageCount: number, expanded: boolean) {
|
||||
const visibleCount = Math.min(messageCount, COLLAPSED_TOOL_STACK_LIMIT);
|
||||
return expanded
|
||||
@@ -246,10 +254,10 @@ function ToolCallCard({
|
||||
className={cn(
|
||||
"inline-flex min-w-0 items-start gap-3 overflow-hidden rounded-xl border px-3 py-2.5 shadow-[inset_0_1px_0_hsl(180_100%_88%_/_0.06)]",
|
||||
isFailed
|
||||
? "border-rose-400/34 bg-[linear-gradient(90deg,hsl(350_72%_44%_/_0.18),hsl(342_66%_9%_/_0.72))]"
|
||||
? "border-rose-400/44 bg-[linear-gradient(90deg,hsl(350_64%_20%),hsl(342_58%_9%))]"
|
||||
: isInitiated
|
||||
? "border-amber-300/34 bg-[linear-gradient(90deg,hsl(43_74%_30%_/_0.34),hsl(260_48%_13%_/_0.74))]"
|
||||
: "border-cyan-400/34 bg-[linear-gradient(90deg,hsl(184_89%_21%_/_0.70),hsl(208_66%_12%_/_0.78))]",
|
||||
? "border-amber-300/44 bg-[linear-gradient(90deg,hsl(43_72%_20%),hsl(260_48%_13%))]"
|
||||
: "border-cyan-400/44 bg-[linear-gradient(90deg,hsl(184_82%_14%),hsl(208_66%_10%))]",
|
||||
className
|
||||
)}
|
||||
style={style}
|
||||
@@ -280,15 +288,40 @@ function ToolCallCard({
|
||||
);
|
||||
}
|
||||
|
||||
function ToolCallStackCardSurface({
|
||||
messageID,
|
||||
animateEntry,
|
||||
isHidden,
|
||||
children,
|
||||
}: {
|
||||
messageID: string;
|
||||
animateEntry: boolean;
|
||||
isHidden: boolean;
|
||||
children: ComponentChildren;
|
||||
}) {
|
||||
const [shouldAnimateEntry] = useState(() => animateEntry);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("tool-call-stack-card-surface", shouldAnimateEntry && !isHidden && "tool-call-stack-card-enter")}
|
||||
data-tool-stack-card-id={messageID}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolCallStack({
|
||||
groupKey,
|
||||
messages,
|
||||
expanded,
|
||||
entryMessageIDs,
|
||||
onToggle,
|
||||
}: {
|
||||
groupKey: string;
|
||||
messages: Message[];
|
||||
expanded: boolean;
|
||||
entryMessageIDs: Set<string>;
|
||||
onToggle: (groupKey: string) => void;
|
||||
}) {
|
||||
const hiddenCount = Math.max(0, messages.length - COLLAPSED_TOOL_STACK_LIMIT);
|
||||
@@ -324,6 +357,7 @@ function ToolCallStack({
|
||||
{messages.map((message, index) => {
|
||||
const depth = messages.length - index - 1;
|
||||
const isHidden = !expanded && depth >= COLLAPSED_TOOL_STACK_LIMIT;
|
||||
const shouldAnimateEntry = entryMessageIDs.has(message.id) && !isHidden;
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
@@ -335,12 +369,9 @@ function ToolCallStack({
|
||||
style={getToolStackStyle(index, messages.length, expanded, motionDirection)}
|
||||
aria-hidden={isHidden ? "true" : undefined}
|
||||
>
|
||||
<div
|
||||
className={cn("tool-call-stack-card-surface", !isHidden && "tool-call-stack-card-enter")}
|
||||
data-tool-stack-card-id={message.id}
|
||||
>
|
||||
<ToolCallStackCardSurface messageID={message.id} animateEntry={shouldAnimateEntry} isHidden={isHidden}>
|
||||
<ToolCallCard message={message} className="tool-call-stack-card-glass w-full max-w-full" />
|
||||
</div>
|
||||
</ToolCallStackCardSurface>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -367,8 +398,26 @@ function ToolCallStack({
|
||||
export function ChatMessagesPanel({ messages, isLoading, isSending }: Props) {
|
||||
const hasPendingAssistant = messages.some((message) => message.id.startsWith("temp-assistant-") && message.content.trim().length === 0);
|
||||
const renderItems = useMemo(() => buildMessageRenderItems(messages), [messages]);
|
||||
const toolCallMessageIDs = useMemo(() => getToolCallMessageIDs(messages), [messages]);
|
||||
const seenToolCallMessageIDsRef = useRef<Set<string> | null>(null);
|
||||
const entryToolCallMessageIDs = useMemo(() => {
|
||||
const seenIDs = seenToolCallMessageIDsRef.current;
|
||||
if (!seenIDs) return new Set<string>();
|
||||
const entryIDs = new Set<string>();
|
||||
for (const id of toolCallMessageIDs) {
|
||||
if (!seenIDs.has(id)) entryIDs.add(id);
|
||||
}
|
||||
return entryIDs;
|
||||
}, [toolCallMessageIDs]);
|
||||
const [expandedToolGroups, setExpandedToolGroups] = useState<Set<string>>(() => new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (!toolCallMessageIDs.size) return;
|
||||
const seenIDs = seenToolCallMessageIDsRef.current ?? new Set<string>();
|
||||
for (const id of toolCallMessageIDs) seenIDs.add(id);
|
||||
seenToolCallMessageIDsRef.current = seenIDs;
|
||||
}, [toolCallMessageIDs]);
|
||||
|
||||
const toggleToolGroup = (groupKey: string) => {
|
||||
setExpandedToolGroups((current) => {
|
||||
const next = new Set(current);
|
||||
@@ -390,6 +439,7 @@ export function ChatMessagesPanel({ messages, isLoading, isSending }: Props) {
|
||||
groupKey={item.key}
|
||||
messages={item.messages}
|
||||
expanded={expandedToolGroups.has(item.key)}
|
||||
entryMessageIDs={entryToolCallMessageIDs}
|
||||
onToggle={toggleToolGroup}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -177,7 +177,7 @@ textarea {
|
||||
}
|
||||
|
||||
.tool-call-stack-card-glass {
|
||||
backdrop-filter: blur(10px);
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.tool-call-stack-card-enter {
|
||||
|
||||
Reference in New Issue
Block a user