diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/MessageList.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/MessageList.tsx
index 01d107c64e..240210aa17 100644
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/MessageList.tsx
+++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/MessageList.tsx
@@ -1,6 +1,7 @@
"use client";
import { cn } from "@/lib/utils";
+import { useEffect, useState } from "react";
import type { ChatMessageData } from "../ChatMessage/useChatMessage";
import { StreamingMessage } from "../StreamingMessage/StreamingMessage";
import { ThinkingMessage } from "../ThinkingMessage/ThinkingMessage";
@@ -31,6 +32,29 @@ export function MessageList({
isStreaming,
});
+ const [showThinkingMessage, setShowThinkingMessage] = useState(false);
+ const [thinkingComplete, setThinkingComplete] = useState(false);
+
+ // Manage thinking message visibility and completion state
+ useEffect(() => {
+ if (isStreaming && streamingChunks.length === 0) {
+ // Start showing thinking message
+ setShowThinkingMessage(true);
+ setThinkingComplete(false);
+ } else if (streamingChunks.length > 0 && showThinkingMessage) {
+ // Chunks arrived - trigger completion animation
+ setThinkingComplete(true);
+ } else if (!isStreaming) {
+ // Streaming ended completely - reset state
+ setShowThinkingMessage(false);
+ setThinkingComplete(false);
+ }
+ }, [isStreaming, streamingChunks.length, showThinkingMessage]);
+
+ function handleThinkingAnimationComplete() {
+ setShowThinkingMessage(false);
+ }
+
return (
{/* Top fade shadow */}
@@ -92,10 +116,15 @@ export function MessageList({
})()}
{/* Render thinking message when streaming but no chunks yet */}
- {isStreaming && streamingChunks.length === 0 &&
}
+ {showThinkingMessage && (
+
+ )}
- {/* Render streaming message if active */}
- {isStreaming && streamingChunks.length > 0 && (
+ {/* Render streaming message if active (wait for thinking animation to complete) */}
+ {isStreaming && streamingChunks.length > 0 && !showThinkingMessage && (
void;
}
-export function ThinkingMessage({ className }: ThinkingMessageProps) {
+export function ThinkingMessage({
+ className,
+ isComplete = false,
+ onAnimationComplete,
+}: ThinkingMessageProps) {
const [showSlowLoader, setShowSlowLoader] = useState(false);
const [showCoffeeMessage, setShowCoffeeMessage] = useState(false);
const timerRef = useRef(null);
const coffeeTimerRef = useRef(null);
- const progress = useAsymptoticProgress(showCoffeeMessage);
+ const delayTimerRef = useRef(null);
+ const { progress, isAnimationDone } = useAsymptoticProgress(
+ showCoffeeMessage,
+ isComplete,
+ );
useEffect(() => {
if (timerRef.current === null) {
timerRef.current = setTimeout(() => {
setShowSlowLoader(true);
- }, 8000);
+ }, 3000);
}
if (coffeeTimerRef.current === null) {
coffeeTimerRef.current = setTimeout(() => {
setShowCoffeeMessage(true);
- }, 10000);
+ }, 8000);
}
return () => {
@@ -40,6 +50,22 @@ export function ThinkingMessage({ className }: ThinkingMessageProps) {
};
}, []);
+ // Handle completion animation delay before unmounting
+ useEffect(() => {
+ if (isAnimationDone && onAnimationComplete) {
+ delayTimerRef.current = setTimeout(() => {
+ onAnimationComplete();
+ }, 200); // 200ms delay after animation completes
+ }
+
+ return () => {
+ if (delayTimerRef.current) {
+ clearTimeout(delayTimerRef.current);
+ delayTimerRef.current = null;
+ }
+ };
+ }, [isAnimationDone, onAnimationComplete]);
+
return (
(null);
+ const animationFrameRef = useRef(null);
+ // Handle asymptotic progress when active but not complete
useEffect(() => {
- if (!isActive) {
- setProgress(0);
- elapsedTimeRef.current = 0;
+ if (!isActive || isComplete) {
+ if (!isComplete) {
+ setProgress(0);
+ elapsedTimeRef.current = 0;
+ setIsAnimationDone(false);
+ completionStartProgressRef.current = null;
+ }
return;
}
@@ -44,7 +70,48 @@ export function useAsymptoticProgress(
}, intervalMs);
return () => clearInterval(interval);
- }, [isActive, halfLifeSeconds, maxProgress, intervalMs]);
+ }, [isActive, isComplete, halfLifeSeconds, maxProgress, intervalMs]);
- return progress;
+ // Handle completion animation
+ useEffect(() => {
+ if (!isComplete) {
+ return;
+ }
+
+ // Capture the starting progress when completion begins
+ if (completionStartProgressRef.current === null) {
+ completionStartProgressRef.current = progress;
+ }
+
+ const startProgress = completionStartProgressRef.current;
+ const animationDuration = 300; // 300ms
+ const startTime = performance.now();
+
+ function animate(currentTime: number) {
+ const elapsed = currentTime - startTime;
+ const t = Math.min(elapsed / animationDuration, 1);
+
+ // Cubic Ease Out from current progress to maxProgress
+ const easedProgress =
+ startProgress + (maxProgress - startProgress) * cubicEaseOut(t);
+ setProgress(easedProgress);
+
+ if (t < 1) {
+ animationFrameRef.current = requestAnimationFrame(animate);
+ } else {
+ setProgress(maxProgress);
+ setIsAnimationDone(true);
+ }
+ }
+
+ animationFrameRef.current = requestAnimationFrame(animate);
+
+ return () => {
+ if (animationFrameRef.current !== null) {
+ cancelAnimationFrame(animationFrameRef.current);
+ }
+ };
+ }, [isComplete, maxProgress]);
+
+ return { progress, isAnimationDone };
}