"use client"; import { Folder, FileText, Search, Globe, Check, Sparkles, Terminal, Play, Pause, } from "lucide-react"; import { motion, AnimatePresence } from "motion/react"; import { useState, useEffect, useRef } from "react"; import { Tooltip } from "@/components/workspace/tooltip"; type AnimationPhase = | "idle" | "user-input" | "scanning" | "load-skill" | "load-template" | "researching" | "load-frontend" | "building" | "load-deploy" | "deploying" | "done"; interface FileItem { name: string; type: "folder" | "file"; indent: number; highlight?: boolean; active?: boolean; done?: boolean; dragging?: boolean; } const searchSteps = [ { type: "search", text: "mRNA lipid nanoparticle delivery 2024" }, { type: "fetch", text: "nature.com/articles/s41587-024..." }, { type: "search", text: "LNP ionizable lipids efficiency" }, { type: "fetch", text: "pubs.acs.org/doi/10.1021/..." }, { type: "search", text: "targeted mRNA tissue-specific" }, ]; // Animation duration configuration - adjust the duration for each step here const ANIMATION_DELAYS = { "user-input": 0, // User input phase duration (milliseconds) scanning: 2000, // Scanning phase duration "load-skill": 1500, // Load skill phase duration "load-template": 1200, // Load template phase duration researching: 800, // Researching phase duration "load-frontend": 800, // Load frontend phase duration building: 1200, // Building phase duration "load-deploy": 2500, // Load deploy phase duration deploying: 1200, // Deploying phase duration done: 2500, // Done phase duration (final step) } as const; export default function ProgressiveSkillsAnimation() { const [phase, setPhase] = useState("idle"); const [searchIndex, setSearchIndex] = useState(0); const [buildIndex, setBuildIndex] = useState(0); const [, setChatMessages] = useState([]); const [, setShowWorkspace] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [hasPlayed, setHasPlayed] = useState(false); const [hasAutoPlayed, setHasAutoPlayed] = useState(false); const chatMessagesRef = useRef(null); const containerRef = useRef(null); const timeoutsRef = useRef([]); // Additional display duration after the final step (done) completes, used to show the final result const FINAL_DISPLAY_DURATION = 3000; // milliseconds // Play animation only when isPlaying is true useEffect(() => { if (!isPlaying) { // Clear all timeouts when paused timeoutsRef.current.forEach(clearTimeout); timeoutsRef.current = []; return; } const timeline = [ { phase: "user-input" as const, delay: ANIMATION_DELAYS["user-input"] }, { phase: "scanning" as const, delay: ANIMATION_DELAYS.scanning }, { phase: "load-skill" as const, delay: ANIMATION_DELAYS["load-skill"] }, { phase: "load-template" as const, delay: ANIMATION_DELAYS["load-template"], }, { phase: "researching" as const, delay: ANIMATION_DELAYS.researching }, { phase: "load-frontend" as const, delay: ANIMATION_DELAYS["load-frontend"], }, { phase: "building" as const, delay: ANIMATION_DELAYS.building }, { phase: "load-deploy" as const, delay: ANIMATION_DELAYS["load-deploy"] }, { phase: "deploying" as const, delay: ANIMATION_DELAYS.deploying }, { phase: "done" as const, delay: ANIMATION_DELAYS.done }, ]; let totalDelay = 0; const timeouts: NodeJS.Timeout[] = []; timeline.forEach(({ phase, delay }) => { totalDelay += delay; timeouts.push(setTimeout(() => setPhase(phase), totalDelay)); }); // Reset after animation completes // Total duration for the final step = ANIMATION_DELAYS["done"] + FINAL_DISPLAY_DURATION timeouts.push( setTimeout(() => { setPhase("idle"); setChatMessages([]); setSearchIndex(0); setBuildIndex(0); setShowWorkspace(false); setIsPlaying(false); }, totalDelay + FINAL_DISPLAY_DURATION), ); timeoutsRef.current = timeouts; return () => { timeouts.forEach(clearTimeout); timeoutsRef.current = []; }; }, [isPlaying]); const handlePlay = () => { setIsPlaying(true); setHasPlayed(true); setPhase("idle"); setChatMessages([]); setSearchIndex(0); setBuildIndex(0); setShowWorkspace(false); }; const handleTogglePlayPause = () => { if (isPlaying) { setIsPlaying(false); } else { // If animation hasn't started or is at idle, restart from beginning if (phase === "idle") { handlePlay(); } else { // Resume from current phase setIsPlaying(true); } } }; // Auto-play when component enters viewport for the first time useEffect(() => { if (hasAutoPlayed || !containerRef.current) return; const containerElement = containerRef.current; const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting && !hasAutoPlayed && !isPlaying) { setHasAutoPlayed(true); // Small delay before auto-playing for better UX setTimeout(() => { setIsPlaying(true); setHasPlayed(true); setPhase("idle"); setChatMessages([]); setSearchIndex(0); setBuildIndex(0); setShowWorkspace(false); }, 300); } }); }, { threshold: 0.3, // Trigger when 30% of the component is visible rootMargin: "0px", }, ); observer.observe(containerElement); return () => { if (containerElement) { observer.unobserve(containerElement); } }; }, [hasAutoPlayed, isPlaying]); // Handle search animation useEffect(() => { if (phase === "researching" && searchIndex < searchSteps.length) { const timer = setTimeout(() => { setSearchIndex((i) => i + 1); }, 350); return () => clearTimeout(timer); } }, [phase, searchIndex]); // Handle build animation useEffect(() => { if (phase === "building" && buildIndex < 3) { const timer = setTimeout(() => { setBuildIndex((i) => i + 1); }, 600); return () => clearTimeout(timer); } if (phase === "building") { setShowWorkspace(true); } }, [phase, buildIndex]); // Auto scroll chat to bottom when messages change useEffect(() => { if (chatMessagesRef.current && phase !== "idle") { chatMessagesRef.current.scrollTo({ top: chatMessagesRef.current.scrollHeight, behavior: "smooth", }); } }, [phase, searchIndex, buildIndex]); const getFileTree = (): FileItem[] => { const base: FileItem[] = [ { name: "deep-search", type: "folder", indent: 0, highlight: phase === "scanning", active: ["load-skill", "load-template", "researching"].includes(phase), done: [ "researching", "load-frontend", "building", "load-deploy", "deploying", "done", ].includes(phase), }, { name: "SKILL.md", type: "file", indent: 1, highlight: phase === "scanning", dragging: phase === "load-skill", done: [ "load-template", "researching", "load-frontend", "building", "load-deploy", "deploying", "done", ].includes(phase), }, { name: "biotech.md", type: "file", indent: 1, highlight: phase === "load-template", dragging: phase === "load-template", done: [ "researching", "load-frontend", "building", "load-deploy", "deploying", "done", ].includes(phase), }, { name: "computer-science.md", type: "file", indent: 1 }, { name: "physics.md", type: "file", indent: 1 }, { name: "frontend-design", type: "folder", indent: 0, highlight: phase === "scanning", active: ["load-frontend", "building"].includes(phase), done: ["building", "load-deploy", "deploying", "done"].includes(phase), }, { name: "SKILL.md", type: "file", indent: 1, highlight: phase === "scanning", dragging: phase === "load-frontend", done: ["building", "load-deploy", "deploying", "done"].includes(phase), }, { name: "deploy", type: "folder", indent: 0, highlight: phase === "scanning", active: ["load-deploy", "deploying"].includes(phase), done: ["deploying", "done"].includes(phase), }, { name: "SKILL.md", type: "file", indent: 1, highlight: phase === "scanning", dragging: phase === "load-deploy", done: ["deploying", "done"].includes(phase), }, { name: "scripts", type: "folder", indent: 1, done: ["deploying", "done"].includes(phase), }, { name: "deploy.sh", type: "file", indent: 2, done: ["deploying", "done"].includes(phase), }, ]; return base; }; const workspaceFiles = ["index.html", "index.css", "index.js"]; return (
{/* Overlay and Play Button */} {!isPlaying && !hasPlayed && (
Click to play
)}
{/* Bottom Left Play/Pause Button */} {isPlaying ? ( ) : ( )}
{/* Left: File Tree */}
/mnt/skills/
{getFileTree().map((item, index) => ( {item.type === "folder" ? ( ) : ( )} {item.name} {item.done && } {item.highlight && !item.done && ( )} ))}
{/* Right: Chat Interface */}
{/* Chat Header */}
DeerFlow Agent
{/* Chat Messages */}
{/* User Message */} {phase !== "idle" && (

Research mRNA delivery, build a landing page, deploy to Vercel

)}
{/* Agent Messages */} {phase !== "idle" && phase !== "user-input" && ( {/* Found Skills */} {[ "scanning", "load-skill", "load-template", "researching", "load-frontend", "building", "load-deploy", "deploying", "done", ].includes(phase) && (
Found 3 skills
)} {/* Researching Section */} {[ "load-skill", "load-template", "researching", "load-frontend", "building", "load-deploy", "deploying", "done", ].includes(phase) && (

🔬 Researching...
{/* Loading SKILL.md */} {[ "load-skill", "load-template", "researching", "load-frontend", "building", "load-deploy", "deploying", "done", ].includes(phase) && (
Loading deep-search/SKILL.md...
)} {/* Loading biotech.md */} {[ "load-template", "researching", "load-frontend", "building", "load-deploy", "deploying", "done", ].includes(phase) && (
Found biotech related topic, loading deep-search/biotech.md...
)}
{/* Search steps */} {phase === "researching" && (
{searchSteps.slice(0, searchIndex).map((step, i) => ( {step.type === "search" ? ( ) : ( )} {step.text} ))}
)} {[ "load-frontend", "building", "load-deploy", "deploying", "done", ].includes(phase) && (
{searchSteps.map((step, i) => ( {step.type === "search" ? ( ) : ( )} {step.text} ))}
)}
)} {/* Building */} {["building", "load-deploy", "deploying", "done"].includes( phase, ) && (
🔨 Building...
Loading frontend-design/SKILL.md...
{workspaceFiles.slice(0, buildIndex).map((file) => ( Generating {file}... ))}
)} {/* Deploying */} {["load-deploy", "deploying", "done"].includes(phase) && (
🚀 Deploying...
Loading deploy/SKILL.md...
{["deploying", "done"].includes(phase) && ( Executing scripts/deploy.sh )}
{phase === "done" && (
✅ Live at biotech-startup.vercel.app
)}
)}
)}
{/* Chat Input (decorative) */}
Ask DeerFlow anything...
); }