import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { ScanLine, Search, Globe, MousePointer2, BarChart3, Puzzle, Clapperboard, MessageSquare, Focus, Play, } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { SceneOutline } from '@/lib/types/generation'; // Step-specific visualizers export function StepVisualizer({ stepId, outlines, webSearchSources, }: { stepId: string; outlines?: SceneOutline[] | null; webSearchSources?: Array<{ title: string; url: string }>; }) { switch (stepId) { case 'pdf-analysis': return ; case 'web-search': return ; case 'outline': return ; case 'agent-generation': return ; case 'slide-content': return ; case 'actions': return ; default: return null; } } // PDF: Document with scanning laser line function PdfScanVisualizer() { return (
{[80, 60, 90, 45, 70].map((w, i) => ( ))}
{/* Scanning laser */}
); } // Web Search: Miniature search engine results page with animated query + result rows function WebSearchVisualizer({ sources }: { sources: Array<{ title: string; url: string }> }) { const [activeResult, setActiveResult] = useState(0); // Cycle through result highlight when we have sources useEffect(() => { if (sources.length === 0) return; const timer = setInterval(() => { setActiveResult((prev) => (prev + 1) % Math.min(sources.length, 4)); }, 1400); return () => clearInterval(timer); }, [sources.length]); // Placeholder results for skeleton state const skeletonResults = [ { titleW: 70, urlW: 45, snippetW: [90, 60] }, { titleW: 55, urlW: 50, snippetW: [80, 75] }, { titleW: 65, urlW: 40, snippetW: [85, 50] }, { titleW: 50, urlW: 55, snippetW: [70, 65] }, ]; const ROW_H = 38; return (
{/* Background glow */} {/* Search results card */}
{/* Search bar header */}
{/* Results list */}
{/* Sliding highlight */} {sources.length > 0 && ( )} {sources.length === 0 ? // Skeleton: pulsing result placeholders skeletonResults.map((item, i) => (
{item.snippetW.map((w, j) => (
))}
)) : // Live results sources.slice(0, 4).map((source, i) => { const isActive = i === activeResult; return (
{source.title}
{source.url.replace(/^https?:\/\/(www\.)?/, '').slice(0, 32)}
); })}
{/* Scanning beam */}
{/* Source count badge */} {sources.length > 0 && ( {sources.length} )}
); } // Outline: Streams real outline data as it arrives from SSE function StreamingOutlineVisualizer({ outlines }: { outlines: SceneOutline[] }) { // Build display lines from outlines const allLines: string[] = []; outlines.forEach((outline, i) => { allLines.push(`${i + 1}. ${outline.title}`); outline.keyPoints?.slice(0, 2).forEach((kp) => { const text = kp.length > 18 ? kp.substring(0, 18) + '...' : kp; allLines.push(` • ${text}`); }); }); return (
{allLines.length === 0 ? ( // Waiting for first outline — show placeholder skeleton
{[60, 80, 50, 70].map((w, i) => ( ))}
) : ( allLines.map((line, i) => ( {line} )) )}
); } // Content: Cycles through distinct representations of Slides, Quiz, PBL, Interactive function AgentGenerationVisualizer() { return (
{[0, 1, 2].map((i) => (
?
))}
); } function ContentVisualizer() { const [index, setIndex] = useState(0); // 0: Slide (Blue) // 1: Quiz (Purple) // 2: PBL (Amber) // 3: Interactive (Emerald) const totalTypes = 4; useEffect(() => { const timer = setInterval(() => { setIndex((prev) => (prev + 1) % totalTypes); }, 3200); return () => clearInterval(timer); }, []); const variants = { enter: { x: 50, opacity: 0, scale: 0.9, rotateY: -15 }, center: { x: 0, opacity: 1, scale: 1, rotateY: 0 }, exit: { x: -50, opacity: 0, scale: 0.9, rotateY: 15 }, }; const getTheme = (idx: number) => { switch (idx) { case 0: return { color: 'blue', label: 'SLIDE', badge: 'bg-blue-100 text-blue-600 border-blue-200 dark:bg-blue-900/40 dark:text-blue-300 dark:border-blue-800', }; case 1: return { color: 'purple', label: 'QUIZ', badge: 'bg-purple-100 text-purple-600 border-purple-200 dark:bg-purple-900/40 dark:text-purple-300 dark:border-purple-800', }; case 2: return { color: 'amber', label: 'PBL', badge: 'bg-amber-100 text-amber-600 border-amber-200 dark:bg-amber-900/40 dark:text-amber-300 dark:border-amber-800', }; case 3: return { color: 'emerald', label: 'WEB', badge: 'bg-emerald-100 text-emerald-600 border-emerald-200 dark:bg-emerald-900/40 dark:text-emerald-300 dark:border-emerald-800', }; default: return { color: 'blue', label: '', badge: '' }; } }; const theme = getTheme(index); return (
{/* Background glow based on current theme */} {/* Subtle orbiting rings (pushed back, slower) */} {[0, 1].map((i) => ( ))} {/* Main Content Container */}
{/* Consistent Badge - Now outside content logic */} {theme.label} {/* --- SLIDE TYPE --- */} {index === 0 && (
{[0.8, 0.9, 0.6, 0.7].map((w, i) => ( ))}
)} {/* --- QUIZ TYPE --- */} {index === 1 && (
{[0, 1, 2, 3].map((i) => (
))}
)} {/* --- PBL TYPE --- */} {index === 2 && (
{[0, 1, 2].map((col) => (
{[0, 1].map((card) => (
))} ))}
)} {/* --- INTERACTIVE TYPE --- */} {index === 3 && (
{/* Browser Chrome - Padded right to avoid badge */}
{[1, 2, 3].map((i) => (
))}
)} {/* Scanning beam (shared) */}
); } // Actions: Timeline of speech, spotlight, and interactions being orchestrated function ActionsVisualizer() { const [activeIdx, setActiveIdx] = useState(0); const actionItems = [ { icon: MessageSquare, label: 'Speech', color: 'text-violet-500', activeBg: 'bg-violet-500/10', activeBorder: 'border-violet-200 dark:border-violet-800', }, { icon: Focus, label: 'Spotlight', color: 'text-amber-500', activeBg: 'bg-amber-500/10', activeBorder: 'border-amber-200 dark:border-amber-800', }, { icon: MessageSquare, label: 'Speech', color: 'text-violet-500', activeBg: 'bg-violet-500/10', activeBorder: 'border-violet-200 dark:border-violet-800', }, { icon: Play, label: 'Interact', color: 'text-emerald-500', activeBg: 'bg-emerald-500/10', activeBorder: 'border-emerald-200 dark:border-emerald-800', }, { icon: MessageSquare, label: 'Speech', color: 'text-violet-500', activeBg: 'bg-violet-500/10', activeBorder: 'border-violet-200 dark:border-violet-800', }, ]; // Row height (py-1.5 = 6px×2 padding + icon ~16px) + gap 6px ≈ 34px per row const ROW_H = 34; useEffect(() => { const timer = setInterval(() => { setActiveIdx((prev) => (prev + 1) % actionItems.length); }, 1600); return () => clearInterval(timer); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return (
{/* Background pulse */} {/* Timeline card */}
{/* Header */}
{/* Action items */}
{/* Sliding highlight — absolute, animates via y transform, no layout impact */} {actionItems.map((item, i) => { const Icon = item.icon; const isActive = i === activeIdx; const isPast = i < activeIdx; return (
{item.label}
{/* Pulsing dot — always rendered, opacity-controlled, no layout shift */} ); })}
); }