ViralClipFactory / frontend /src /components /GenerationTracker.jsx
Alpha123B's picture
Add GenerationTracker component
1b26b87 verified
Raw
History Blame Contribute Delete
6.3 kB
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import toast from 'react-hot-toast';
import { generateClips, getClipsStatus } from '../api/client';
const STATUS_ICONS = {
pending: '⏳',
scripting: '📝',
voiceover: '🎙️',
visuals: '🎨',
assembling: '🔧',
done: '✅',
error: '❌',
};
const STATUS_LABELS = {
pending: 'Waiting...',
scripting: 'Writing Script',
voiceover: 'Generating Voice',
visuals: 'Creating Visuals',
assembling: 'Assembling Video',
done: 'Complete!',
error: 'Failed',
};
export default function GenerationTracker({ sessionId, clipConfigs, onComplete }) {
const [clips, setClips] = useState([]);
const [progress, setProgress] = useState(0);
const [started, setStarted] = useState(false);
useEffect(() => {
const startGeneration = async () => {
try {
const result = await generateClips(sessionId, clipConfigs);
setClips(result.clips);
setStarted(true);
} catch (err) {
toast.error(err.response?.data?.detail || 'Failed to start generation');
}
};
startGeneration();
}, [sessionId, clipConfigs]);
// Poll for progress
useEffect(() => {
if (!started) return;
const pollInterval = setInterval(async () => {
try {
const status = await getClipsStatus(sessionId);
setClips(status.clips);
setProgress(status.progress_percent);
if (status.status === 'completed' || status.progress_percent >= 100) {
clearInterval(pollInterval);
toast.success('All clips generated! 🎉');
onComplete(status);
}
} catch (err) {
console.error('Poll error:', err);
}
}, 3000);
return () => clearInterval(pollInterval);
}, [started, sessionId, onComplete]);
const getStageProgress = (status) => {
const stages = ['pending', 'scripting', 'voiceover', 'visuals', 'assembling', 'done'];
const idx = stages.indexOf(status);
return ((idx + 1) / stages.length) * 100;
};
return (
<div className="max-w-4xl mx-auto">
<div className="text-center mb-10">
<h2 className="text-3xl font-bold mb-2">⚡ Generating Your Clips</h2>
<p className="text-white/50">AI is working its magic. This usually takes 2-5 minutes per clip.</p>
</div>
{/* Overall Progress */}
<div className="card mb-8">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-white/60">Overall Progress</span>
<span className="text-sm font-mono text-primary-500">{Math.round(progress)}%</span>
</div>
<div className="w-full h-3 bg-dark-900 rounded-full overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-primary-500 via-accent-400 to-primary-500 rounded-full"
style={{ backgroundSize: '200% 100%' }}
animate={{ width: `${progress}%`, backgroundPosition: ['0% 0%', '100% 0%'] }}
transition={{ width: { duration: 0.5 }, backgroundPosition: { duration: 2, repeat: Infinity } }}
/>
</div>
</div>
{/* Per-Clip Status */}
<div className="space-y-4">
{clips.map((clip, i) => (
<motion.div
key={clip.clip_id}
className="card"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.1 }}
>
<div className="flex items-center gap-4">
{/* Clip Number */}
<div className="w-10 h-10 rounded-full bg-dark-900 flex items-center justify-center text-sm font-bold flex-shrink-0">
{i + 1}
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<h4 className="font-medium truncate">{clip.title || `Clip ${i + 1}`}</h4>
<div className="flex items-center gap-4 mt-1">
<span className="text-xs text-white/40">{clip.duration?.toFixed(1)}s</span>
<span className="text-xs text-green-400/60">Score: {clip.virality_score?.toFixed(0)}</span>
</div>
</div>
{/* Status */}
<div className="flex items-center gap-2 flex-shrink-0">
<span className="text-lg">{STATUS_ICONS[clip.status]}</span>
<span className={`text-sm font-medium ${clip.status === 'done' ? 'text-green-400' : clip.status === 'error' ? 'text-red-400' : 'text-white/60'}`}>
{STATUS_LABELS[clip.status]}
</span>
</div>
</div>
{/* Stage Progress Bar */}
{clip.status !== 'pending' && clip.status !== 'done' && clip.status !== 'error' && (
<div className="mt-3 flex gap-1">
{['scripting', 'voiceover', 'visuals', 'assembling'].map((stage) => {
const stages = ['scripting', 'voiceover', 'visuals', 'assembling'];
const currentIdx = stages.indexOf(clip.status);
const stageIdx = stages.indexOf(stage);
return (
<div key={stage} className="flex-1">
<div className={`h-1.5 rounded-full transition-all duration-500 ${
stageIdx < currentIdx ? 'bg-green-500' :
stageIdx === currentIdx ? 'bg-primary-500 animate-pulse' :
'bg-dark-900'
}`} />
<span className="text-[10px] text-white/30 mt-1 block text-center">
{stage.charAt(0).toUpperCase() + stage.slice(1)}
</span>
</div>
);
})}
</div>
)}
</motion.div>
))}
</div>
{/* Fun waiting messages */}
{progress < 100 && (
<motion.div
className="text-center mt-8 text-white/30 text-sm"
animate={{ opacity: [0.3, 0.7, 0.3] }}
transition={{ duration: 2, repeat: Infinity }}
>
🎬 Crafting viral content... every clip is a weapon 🔥
</motion.div>
)}
</div>
);
}