mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-05 12:25:04 -05:00
Compare commits
2 Commits
ntindle/wa
...
swiftyos/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88ebef601a | ||
|
|
d919bd5f54 |
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import type { ChatMessageData } from "../ChatMessage/useChatMessage";
|
import type { ChatMessageData } from "../ChatMessage/useChatMessage";
|
||||||
import { StreamingMessage } from "../StreamingMessage/StreamingMessage";
|
import { StreamingMessage } from "../StreamingMessage/StreamingMessage";
|
||||||
import { ThinkingMessage } from "../ThinkingMessage/ThinkingMessage";
|
import { ThinkingMessage } from "../ThinkingMessage/ThinkingMessage";
|
||||||
@@ -31,6 +32,29 @@ export function MessageList({
|
|||||||
isStreaming,
|
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 (
|
return (
|
||||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||||
{/* Top fade shadow */}
|
{/* Top fade shadow */}
|
||||||
@@ -92,10 +116,15 @@ export function MessageList({
|
|||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Render thinking message when streaming but no chunks yet */}
|
{/* 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 */}
|
{/* Render streaming message if active (wait for thinking animation to complete) */}
|
||||||
{isStreaming && streamingChunks.length > 0 && (
|
{isStreaming && streamingChunks.length > 0 && !showThinkingMessage && (
|
||||||
<StreamingMessage
|
<StreamingMessage
|
||||||
chunks={streamingChunks}
|
chunks={streamingChunks}
|
||||||
onComplete={onStreamComplete}
|
onComplete={onStreamComplete}
|
||||||
|
|||||||
@@ -1,28 +1,41 @@
|
|||||||
|
import { Progress } from "@/components/atoms/Progress/Progress";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
|
import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
|
||||||
|
import { useAsymptoticProgress } from "../ToolCallMessage/useAsymptoticProgress";
|
||||||
|
|
||||||
export interface ThinkingMessageProps {
|
export interface ThinkingMessageProps {
|
||||||
className?: string;
|
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 [showSlowLoader, setShowSlowLoader] = useState(false);
|
||||||
const [showCoffeeMessage, setShowCoffeeMessage] = useState(false);
|
const [showCoffeeMessage, setShowCoffeeMessage] = useState(false);
|
||||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const coffeeTimerRef = 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(() => {
|
useEffect(() => {
|
||||||
if (timerRef.current === null) {
|
if (timerRef.current === null) {
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
setShowSlowLoader(true);
|
setShowSlowLoader(true);
|
||||||
}, 8000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coffeeTimerRef.current === null) {
|
if (coffeeTimerRef.current === null) {
|
||||||
coffeeTimerRef.current = setTimeout(() => {
|
coffeeTimerRef.current = setTimeout(() => {
|
||||||
setShowCoffeeMessage(true);
|
setShowCoffeeMessage(true);
|
||||||
}, 10000);
|
}, 8000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -49,9 +78,18 @@ export function ThinkingMessage({ className }: ThinkingMessageProps) {
|
|||||||
<AIChatBubble>
|
<AIChatBubble>
|
||||||
<div className="transition-all duration-500 ease-in-out">
|
<div className="transition-all duration-500 ease-in-out">
|
||||||
{showCoffeeMessage ? (
|
{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">
|
<div className="flex flex-col items-center gap-3">
|
||||||
This could take a few minutes, grab a coffee ☕️
|
<div className="flex w-full max-w-[280px] flex-col gap-1.5">
|
||||||
</span>
|
<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 ? (
|
) : 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">
|
<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...
|
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