Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |