New thinking/agent animation + UI (#3302)

* implement new thinking animation ui

* implement agent thinking animation
This commit is contained in:
Sean Hatfield 2025-02-27 07:36:28 +08:00 committed by GitHub
parent 23d5f368d9
commit 6f1938c598
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 128 additions and 126 deletions

View File

@ -1,10 +1,8 @@
import React, { useState } from "react";
import {
CaretDown,
CircleNotch,
Check,
CheckCircle,
} from "@phosphor-icons/react";
import { CaretDown } from "@phosphor-icons/react";
import AgentAnimation from "@/media/animations/agent-animation.webm";
import AgentStatic from "@/media/animations/agent-static.png";
export default function StatusResponse({
messages = [],
@ -21,94 +19,85 @@ export default function StatusResponse({
}
return (
<div className="flex justify-center items-end w-full">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col relative">
<div
onClick={handleExpandClick}
className={`${!previousThoughts?.length ? "cursor-text" : "cursor-pointer hover:bg-theme-sidebar-item-hover transition-all duration-200"} bg-theme-bg-chat-input rounded-full py-2 px-4 flex items-center gap-x-2 border border-theme-sidebar-border`}
>
{isThinking ? (
<CircleNotch
className="w-4 h-4 text-theme-text-secondary animate-spin"
aria-label="Agent is thinking..."
/>
) : showCheckmark ? (
<CheckCircle
className="w-4 h-4 text-green-400 transition-all duration-300"
aria-label="Thought complete"
/>
) : null}
<div className="flex-1 overflow-hidden">
<span
key={currentThought.content}
className="text-xs text-theme-text-secondary font-mono inline-block w-full animate-thoughtTransition"
>
{currentThought.content}
</span>
</div>
<div className="flex items-center gap-x-2">
{previousThoughts?.length > 0 && (
<div
data-tooltip-id="expand-cot"
data-tooltip-content={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
className="border-none text-theme-text-secondary hover:text-theme-text-primary transition-colors p-1 rounded-full hover:bg-theme-sidebar-item-hover"
aria-label={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
>
<CaretDown
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
/>
</div>
)}
</div>
</div>
{/* Previous thoughts dropdown */}
{previousThoughts?.length > 0 && (
<div className="flex justify-center w-full">
<div className="w-full max-w-[80%] flex flex-col">
<div className=" w-full max-w-[800px]">
<div
key={`cot-list-${currentThought.uuid}`}
className={`mt-2 bg-theme-bg-chat-input backdrop-blur-sm rounded-lg overflow-hidden transition-all duration-300 border border-theme-sidebar-border ${
isExpanded
? "max-h-[300px] overflow-y-auto opacity-100"
: "max-h-0 opacity-0"
}`}
onClick={handleExpandClick}
style={{ borderRadius: "6px" }}
className={`${!previousThoughts?.length ? "" : `${previousThoughts?.length ? "hover:bg-theme-sidebar-item-hover" : ""}`} items-start bg-theme-bg-chat-input py-2 px-4 flex gap-x-2`}
>
<div className="p-2">
{previousThoughts.map((thought, index) => (
<div
key={`cot-${thought.uuid || index}`}
className="flex gap-x-2"
<div className="w-7 h-7 flex justify-center flex-shrink-0 items-center">
{isThinking ? (
<video
autoPlay
loop
muted
playsInline
className="w-8 h-8 scale-150 transition-opacity duration-200 light:invert light:opacity-50"
data-tooltip-id="agent-thinking"
data-tooltip-content="Agent is thinking..."
aria-label="Agent is thinking..."
>
<p className="text-xs text-theme-text-secondary font-mono">
{index + 1}/{previousThoughts.length}
</p>
<div
className="flex items-center gap-x-3 p-2 animate-fadeUpIn"
style={{ animationDelay: `${index * 50}ms` }}
>
<span className="text-xs text-theme-text-secondary font-mono">
{thought.content}
<source src={AgentAnimation} type="video/webm" />
</video>
) : (
<img
src={AgentStatic}
alt="Agent complete"
className="w-6 h-6 transition-opacity duration-200 light:invert light:opacity-50"
data-tooltip-id="agent-thinking"
data-tooltip-content="Agent has finished thinking"
aria-label="Agent has finished thinking"
/>
)}
</div>
<div className="flex-1 min-w-0">
<div
className={`overflow-hidden transition-all duration-300 ease-in-out ${isExpanded ? "max-h-[500px]" : "max-h-6"}`}
>
<div className="text-theme-text-secondary font-mono leading-6">
{!isExpanded ? (
<span className="block w-full truncate mt-[2px]">
{currentThought.content}
</span>
</div>
</div>
))}
{/* Append current thought to the end */}
<div key={`cot-${currentThought.uuid}`} className="flex gap-x-2">
<p className="text-xs text-theme-text-secondary font-mono">
{previousThoughts.length + 1}/{previousThoughts.length + 1}
</p>
<div className="flex items-center gap-x-3 p-2 animate-fadeUpIn">
<span className="text-xs text-theme-text-secondary font-mono">
{currentThought.content}
</span>
) : (
<>
{previousThoughts.map((thought, index) => (
<div
key={`cot-${thought.uuid || index}`}
className="mb-2"
>
{thought.content}
</div>
))}
<div>{currentThought.content}</div>
</>
)}
</div>
</div>
</div>
<div className="flex items-center gap-x-2">
{previousThoughts?.length > 0 && (
<button
onClick={handleExpandClick}
data-tooltip-id="expand-cot"
data-tooltip-content={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
className="border-none text-theme-text-secondary hover:text-theme-text-primary transition-colors p-1 rounded-full hover:bg-theme-sidebar-item-hover"
aria-label={
isExpanded ? "Hide thought chain" : "Show thought chain"
}
>
<CaretDown
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
/>
</button>
)}
</div>
</div>
)}
</div>
</div>
</div>
);

View File

@ -1,9 +1,10 @@
import { useState, forwardRef, useImperativeHandle } from "react";
import renderMarkdown from "@/utils/chat/markdown";
import { Brain, CaretDown } from "@phosphor-icons/react";
import { CaretDown } from "@phosphor-icons/react";
import DOMPurify from "dompurify";
import truncate from "truncate";
import { isMobile } from "react-device-detect";
import ThinkingAnimation from "@/media/animations/thinking-animation.webm";
import ThinkingStatic from "@/media/animations/thinking-static.png";
const THOUGHT_KEYWORDS = ["thought", "thinking", "think", "thought_chain"];
const CLOSING_TAGS = [...THOUGHT_KEYWORDS, "response", "answer"];
@ -61,46 +62,57 @@ export const ThoughtChainComponent = forwardRef(
<div
style={{
transition: "all 0.1s ease-in-out",
borderRadius: isExpanded || autoExpand ? "6px" : "24px",
borderRadius: "6px",
}}
className={`${isExpanded || autoExpand ? "" : `${canExpand ? "hover:bg-theme-sidebar-item-hover" : ""}`} items-start bg-theme-bg-chat-input py-2 px-4 flex gap-x-2 border border-theme-sidebar-border`}
className={`${isExpanded || autoExpand ? "" : `${canExpand ? "hover:bg-theme-sidebar-item-hover" : ""}`} items-start bg-theme-bg-chat-input py-2 px-4 flex gap-x-2`}
>
{isThinking || isComplete ? (
<Brain
data-tooltip-id="cot-thinking"
data-tooltip-content={
isThinking
? "Model is thinking..."
: "Model has finished thinking"
}
className={`w-4 h-4 mt-1 ${isThinking ? "text-blue-500 animate-pulse" : "text-green-400"}`}
aria-label={
isThinking
? "Model is thinking..."
: "Model has finished thinking"
}
/>
) : null}
<div className="flex-1 overflow-hidden">
{!isExpanded && !autoExpand ? (
<span
className="text-theme-text-secondary font-mono inline-block w-full"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
truncate(tagStrippedContent, THOUGHT_PREVIEW_LENGTH)
),
}}
/>
) : (
<span
className="text-theme-text-secondary font-mono inline-block w-full"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
renderMarkdown(tagStrippedContent)
),
}}
/>
)}
<div
className={`w-7 h-7 flex justify-center flex-shrink-0 ${!isExpanded && !autoExpand ? "items-center" : "items-start pt-[2px]"}`}
>
{isThinking || isComplete ? (
<>
<video
autoPlay
loop
muted
playsInline
className={`w-7 h-7 transition-opacity duration-200 light:invert light:opacity-50 ${isThinking ? "opacity-100" : "opacity-0 hidden"}`}
data-tooltip-id="cot-thinking"
data-tooltip-content="Model is thinking..."
aria-label="Model is thinking..."
>
<source src={ThinkingAnimation} type="video/webm" />
</video>
<img
src={ThinkingStatic}
alt="Thinking complete"
className={`w-6 h-6 transition-opacity duration-200 light:invert light:opacity-50 ${!isThinking && isComplete ? "opacity-100" : "opacity-0 hidden"}`}
data-tooltip-id="cot-thinking"
data-tooltip-content="Model has finished thinking"
aria-label="Model has finished thinking"
/>
</>
) : null}
</div>
<div className="flex-1 min-w-0">
<div
className={`overflow-hidden transition-all transform duration-300 ease-in-out origin-top ${isExpanded || autoExpand ? "max-h-[500px]" : "max-h-6"}`}
>
<div
className={`text-theme-text-secondary font-mono leading-6 ${isExpanded || autoExpand ? "-ml-[5.5px] -mt-[4px]" : "mt-[2px]"}`}
>
<span
className={`block w-full ${!isExpanded && !autoExpand ? "truncate" : ""}`}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
isExpanded || autoExpand
? renderMarkdown(tagStrippedContent)
: tagStrippedContent
),
}}
/>
</div>
</div>
</div>
<div className="flex items-center gap-x-2">
{!autoExpand && canExpand ? (
@ -127,3 +139,4 @@ export const ThoughtChainComponent = forwardRef(
);
}
);
ThoughtChainComponent.displayName = "ThoughtChainComponent";

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB