mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-05 12:25:04 -05:00
Compare commits
2 Commits
claude/add
...
swiftyos/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88ebef601a | ||
|
|
d919bd5f54 |
@@ -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 (
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
{/* Top fade shadow */}
|
||||
@@ -92,10 +116,15 @@ export function MessageList({
|
||||
})()}
|
||||
|
||||
{/* Render thinking message when streaming but no chunks yet */}
|
||||
{isStreaming && streamingChunks.length === 0 && <ThinkingMessage />}
|
||||
{showThinkingMessage && (
|
||||
<ThinkingMessage
|
||||
isComplete={thinkingComplete}
|
||||
onAnimationComplete={handleThinkingAnimationComplete}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 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 && (
|
||||
<StreamingMessage
|
||||
chunks={streamingChunks}
|
||||
onComplete={onStreamComplete}
|
||||
|
||||
@@ -1,28 +1,41 @@
|
||||
import { Progress } from "@/components/atoms/Progress/Progress";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
|
||||
import { useAsymptoticProgress } from "../ToolCallMessage/useAsymptoticProgress";
|
||||
|
||||
export interface ThinkingMessageProps {
|
||||
className?: string;
|
||||
isComplete?: boolean;
|
||||
onAnimationComplete?: () => 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<NodeJS.Timeout | null>(null);
|
||||
const coffeeTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const delayTimerRef = useRef<NodeJS.Timeout | null>(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 () => {
|
||||
@@ -37,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 (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -49,9 +78,18 @@ export function ThinkingMessage({ className }: ThinkingMessageProps) {
|
||||
<AIChatBubble>
|
||||
<div className="transition-all duration-500 ease-in-out">
|
||||
{showCoffeeMessage ? (
|
||||
<span className="inline-block animate-shimmer bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent">
|
||||
This could take a few minutes, grab a coffee ☕️
|
||||
</span>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="flex w-full max-w-[280px] flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between text-xs text-neutral-500">
|
||||
<span>Working on it...</span>
|
||||
<span>{Math.round(progress)}%</span>
|
||||
</div>
|
||||
<Progress value={progress} className="h-2 w-full" />
|
||||
</div>
|
||||
<span className="inline-block animate-shimmer bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent">
|
||||
This could take a few minutes, grab a coffee ☕️
|
||||
</span>
|
||||
</div>
|
||||
) : showSlowLoader ? (
|
||||
<span className="inline-block animate-shimmer bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent">
|
||||
Taking a bit more time...
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* Cubic Ease Out easing function: 1 - (1 - t)^3
|
||||
* Starts fast and decelerates smoothly to a stop.
|
||||
*/
|
||||
function cubicEaseOut(t: number): number {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
export interface AsymptoticProgressResult {
|
||||
progress: number;
|
||||
isAnimationDone: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that returns a progress value that starts fast and slows down,
|
||||
* asymptotically approaching but never reaching the max value.
|
||||
*
|
||||
* Uses a half-life formula: progress = max * (1 - 0.5^(time/halfLife))
|
||||
* This creates the "game loading bar" effect where:
|
||||
* - 50% is reached at halfLifeSeconds
|
||||
* - 75% is reached at 2 * halfLifeSeconds
|
||||
* - 87.5% is reached at 3 * halfLifeSeconds
|
||||
* - and so on...
|
||||
*
|
||||
* When isComplete is set to true, animates from current progress to 100%
|
||||
* using Cubic Ease Out over 300ms.
|
||||
*
|
||||
* @param isActive - Whether the progress should be animating
|
||||
* @param isComplete - Whether to animate to 100% (completion animation)
|
||||
* @param halfLifeSeconds - Time in seconds to reach 50% progress (default: 30)
|
||||
* @param maxProgress - Maximum progress value to approach (default: 100)
|
||||
* @param intervalMs - Update interval in milliseconds (default: 100)
|
||||
* @returns Object with current progress value and whether completion animation is done
|
||||
*/
|
||||
export function useAsymptoticProgress(
|
||||
isActive: boolean,
|
||||
isComplete = false,
|
||||
halfLifeSeconds = 30,
|
||||
maxProgress = 100,
|
||||
intervalMs = 100,
|
||||
): AsymptoticProgressResult {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [isAnimationDone, setIsAnimationDone] = useState(false);
|
||||
const elapsedTimeRef = useRef(0);
|
||||
const completionStartProgressRef = useRef<number | null>(null);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
|
||||
// Handle asymptotic progress when active but not complete
|
||||
useEffect(() => {
|
||||
if (!isActive || isComplete) {
|
||||
if (!isComplete) {
|
||||
setProgress(0);
|
||||
elapsedTimeRef.current = 0;
|
||||
setIsAnimationDone(false);
|
||||
completionStartProgressRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
elapsedTimeRef.current += intervalMs / 1000;
|
||||
// Half-life approach: progress = max * (1 - 0.5^(time/halfLife))
|
||||
// At t=halfLife: 50%, at t=2*halfLife: 75%, at t=3*halfLife: 87.5%, etc.
|
||||
const newProgress =
|
||||
maxProgress *
|
||||
(1 - Math.pow(0.5, elapsedTimeRef.current / halfLifeSeconds));
|
||||
setProgress(newProgress);
|
||||
}, intervalMs);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isActive, isComplete, halfLifeSeconds, maxProgress, intervalMs]);
|
||||
|
||||
// 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user