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 (
{/* Pulsing dot — always rendered, opacity-controlled, no layout shift */}
);
})}
);
}