import React, { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useGeneration } from '@/context/GenerationContext'; // Icons const CheckIcon = () => ( ); const SpinnerIcon = () => ( ); const ClockIcon = () => ( ); const VideoIcon = () => ( ); const WaveformIcon = () => ( ); const BrainIcon = () => ( ); const DownloadIcon = () => ( ); const ImageIcon = () => ( ); interface ActivityLog { id: string; message: string; timestamp: Date; type: 'info' | 'success' | 'warning' | 'processing'; icon?: 'video' | 'audio' | 'brain' | 'download' | 'image'; } const XIcon = () => ( ); export const GenerationProgress: React.FC = () => { const { state, cancelGeneration } = useGeneration(); const { progress, provider, generatedVideos, segments, isCancelling, activeTaskIds } = state; const [elapsedTime, setElapsedTime] = useState(0); const [activityLog, setActivityLog] = useState([]); const [startTime] = useState(() => Date.now()); const lastMessageRef = useRef(''); const logContainerRef = useRef(null); const percentage = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0; const accentColor = provider === 'kling' ? 'coral' : 'electric'; const accentClass = accentColor === 'coral' ? 'text-coral-400' : 'text-electric-400'; const accentBg = accentColor === 'coral' ? 'bg-coral-500' : 'bg-electric-500'; const accentBorder = accentColor === 'coral' ? 'border-coral-500' : 'border-electric-500'; // Update elapsed time useEffect(() => { const interval = setInterval(() => { setElapsedTime(Math.floor((Date.now() - startTime) / 1000)); }, 1000); return () => clearInterval(interval); }, [startTime]); // Track progress messages and add to activity log useEffect(() => { if (progress.message && progress.message !== lastMessageRef.current) { lastMessageRef.current = progress.message; // Determine log type and icon based on message content let type: ActivityLog['type'] = 'info'; let icon: ActivityLog['icon'] = undefined; const msg = progress.message.toLowerCase(); if (msg.includes('complete') || msg.includes('success') || msg.includes('✅')) { type = 'success'; } else if (msg.includes('warning') || msg.includes('⚠️') || msg.includes('fallback')) { type = 'warning'; } else if (msg.includes('generating') || msg.includes('processing') || msg.includes('submitting')) { type = 'processing'; icon = 'video'; } if (msg.includes('whisper') || msg.includes('audio') || msg.includes('transcri')) { icon = 'audio'; } else if (msg.includes('prompt') || msg.includes('refin') || msg.includes('gpt')) { icon = 'brain'; } else if (msg.includes('download')) { icon = 'download'; } else if (msg.includes('image') || msg.includes('frame') || msg.includes('upload')) { icon = 'image'; } else if (msg.includes('video') || msg.includes('segment')) { icon = 'video'; } const newLog: ActivityLog = { id: `${Date.now()}-${Math.random()}`, message: progress.message, timestamp: new Date(), type, icon, }; setActivityLog(prev => [...prev.slice(-20), newLog]); // Keep last 20 entries } }, [progress.message]); // Auto-scroll activity log useEffect(() => { if (logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; } }, [activityLog]); // Format time const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; }; // Estimate remaining time const estimatedTotal = progress.current > 0 ? Math.round((elapsedTime / progress.current) * progress.total) : 0; const remainingTime = Math.max(0, estimatedTotal - elapsedTime); // Get icon component const getIcon = (iconType?: ActivityLog['icon']) => { switch (iconType) { case 'video': return ; case 'audio': return ; case 'brain': return ; case 'download': return ; case 'image': return ; default: return ; } }; return ( {/* Header with Time */} Generating Videos {provider === 'kling' ? 'KIE API' : 'Replicate'} • {segments.length} segments Elapsed: {formatTime(elapsedTime)} {progress.current > 0 && remainingTime > 0 && ( Est. remaining: {formatTime(remainingTime)} )} {activeTaskIds.length > 0 && ( {isCancelling ? 'Cancelling...' : 'Cancel Generation'} )} {/* Main Progress Area */} {/* Left: Circular Progress */} {/* Background circle */} {/* Center content */} {percentage}% {generatedVideos.length} / {progress.total} videos {/* Current Status */} {progress.message} {/* Right: Segment Progress & Activity Log */} {/* Segment Cards */} Segment Progress {segments.map((segment, index) => { const isCompleted = index < generatedVideos.length; const isCurrent = index === generatedVideos.length && index < progress.total; return ( {/* Status indicator */} {isCompleted ? : isCurrent ? : {index + 1}} Segment {index + 1} {segment.action_timeline?.dialogue?.substring(0, 40) || 'Processing...'}... {/* Thumbnail preview when completed */} {isCompleted && generatedVideos[index]?.thumbnails?.[0] && ( )} ); })} {/* Activity Log */} Live Activity {activityLog.map((log) => ( {log.type === 'success' ? : getIcon(log.icon)} {log.message} {log.timestamp.toLocaleTimeString()} ))} {activityLog.length === 0 && ( Waiting for activity... )} {/* Bottom Progress Bar */} Overall Progress {percentage}% {/* Shimmer effect */} {/* Step indicators */} 0 ? accentClass : ''}> {generatedVideos.length > 0 ? '✓' : '○'} Generating = segments.length / 2 ? accentClass : ''}> {generatedVideos.length >= segments.length / 2 ? '✓' : '○'} Halfway {generatedVideos.length === segments.length ? '✓' : '○'} Complete {/* Tips */} 💡 Each video typically takes 1-2 minutes. Stay on this page to track progress. ); };
{provider === 'kling' ? 'KIE API' : 'Replicate'} • {segments.length} segments
{progress.message}
{log.message}
{log.timestamp.toLocaleTimeString()}