Spaces:
Sleeping
Sleeping
Download frontend/src/components/GenerationProgress.tsx from userIdc2024/Video_AdGenesis_App: direct link, hf CLI and curl.
- Browser
- Download file 19.9 kB
-
https://huggingface.co/spaces/userIdc2024/Video_AdGenesis_App/resolve/main/frontend/src/components/GenerationProgress.tsx
- Command line
-
hf download hf://spaces/userIdc2024/Video_AdGenesis_App/frontend/src/components/GenerationProgress.tsx
-
curl -L -o GenerationProgress.tsx https://huggingface.co/spaces/userIdc2024/Video_AdGenesis_App/resolve/main/frontend/src/components/GenerationProgress.tsx
19.9 kB
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { useGeneration } from '@/context/GenerationContext'; | |
| import { SegmentGenerationProgress } from './SegmentGenerationProgress'; | |
| import { SegmentPromptsViewer } from './SegmentPromptsViewer'; | |
| // Icons | |
| const CheckIcon = () => ( | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> | |
| </svg> | |
| ); | |
| const SpinnerIcon = () => ( | |
| <svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"> | |
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> | |
| <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /> | |
| </svg> | |
| ); | |
| const ClockIcon = () => ( | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <circle cx="12" cy="12" r="10" strokeWidth={2} /> | |
| <path strokeLinecap="round" strokeWidth={2} d="M12 6v6l4 2" /> | |
| </svg> | |
| ); | |
| const VideoIcon = () => ( | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /> | |
| </svg> | |
| ); | |
| const WaveformIcon = () => ( | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" /> | |
| </svg> | |
| ); | |
| const BrainIcon = () => ( | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /> | |
| </svg> | |
| ); | |
| const DownloadIcon = () => ( | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /> | |
| </svg> | |
| ); | |
| const ImageIcon = () => ( | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> | |
| </svg> | |
| ); | |
| interface ActivityLog { | |
| id: string; | |
| message: string; | |
| timestamp: Date; | |
| type: 'info' | 'success' | 'warning' | 'processing'; | |
| icon?: 'video' | 'audio' | 'brain' | 'download' | 'image'; | |
| } | |
| const XIcon = () => ( | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| ); | |
| export const GenerationProgress: React.FC = () => { | |
| const { state, cancelGeneration } = useGeneration(); | |
| const { progress, provider, generatedVideos, segments, isCancelling, step } = state; | |
| // Show enhanced UX during prompt generation (streaming) | |
| // Use estimated count if segments not yet loaded, or actual count if available | |
| if (step === 'generating_prompts') { | |
| const estimatedCount = segments.length > 0 ? segments.length : (progress.total > 0 ? progress.total : 5); | |
| const accentColor = provider === 'kling' ? 'coral' : 'electric'; | |
| return ( | |
| <div className="w-full max-w-4xl mx-auto space-y-8"> | |
| <SegmentGenerationProgress | |
| segmentsCount={Math.max(estimatedCount, segments.length)} | |
| onCancel={cancelGeneration} | |
| /> | |
| {segments.length > 0 && ( | |
| <SegmentPromptsViewer segments={segments} accentColor={accentColor} /> | |
| )} | |
| </div> | |
| ); | |
| } | |
| const [elapsedTime, setElapsedTime] = useState(0); | |
| const [activityLog, setActivityLog] = useState<ActivityLog[]>([]); | |
| const [startTime] = useState(() => Date.now()); | |
| const lastMessageRef = useRef<string>(''); | |
| const logContainerRef = useRef<HTMLDivElement>(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 <VideoIcon />; | |
| case 'audio': return <WaveformIcon />; | |
| case 'brain': return <BrainIcon />; | |
| case 'download': return <DownloadIcon />; | |
| case 'image': return <ImageIcon />; | |
| default: return <SpinnerIcon />; | |
| } | |
| }; | |
| return ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| className="min-h-[60vh] flex flex-col items-center justify-center p-4 md:p-8" | |
| > | |
| <div className="max-w-4xl w-full"> | |
| {/* Header with Time */} | |
| <div className="flex items-center justify-between mb-8"> | |
| <div> | |
| <h2 className="text-2xl font-bold text-void-100">Generating Videos</h2> | |
| <p className="text-void-400 text-sm mt-1"> | |
| {provider === 'kling' ? 'KIE API' : 'Replicate'} β’ {segments.length} segments | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-4"> | |
| <div className="flex items-center gap-6 text-sm"> | |
| <div className="flex items-center gap-2 text-void-400"> | |
| <ClockIcon /> | |
| <span>Elapsed: <span className="text-void-200 font-mono">{formatTime(elapsedTime)}</span></span> | |
| </div> | |
| {progress.current > 0 && remainingTime > 0 && ( | |
| <div className="flex items-center gap-2 text-void-400"> | |
| <span>Est. remaining: <span className={`font-mono ${accentClass}`}>{formatTime(remainingTime)}</span></span> | |
| </div> | |
| )} | |
| </div> | |
| <button | |
| onClick={cancelGeneration} | |
| disabled={isCancelling} | |
| className={` | |
| px-4 py-2 rounded-lg font-medium text-sm transition-all | |
| flex items-center gap-2 | |
| ${isCancelling | |
| ? 'bg-void-700 text-void-400 cursor-not-allowed' | |
| : 'bg-red-500/20 hover:bg-red-500/30 text-red-400 hover:text-red-300 border border-red-500/30' | |
| } | |
| `} | |
| > | |
| <XIcon /> | |
| {isCancelling ? 'Cancelling...' : 'Cancel Generation'} | |
| </button> | |
| </div> | |
| </div> | |
| {/* Main Progress Area */} | |
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8"> | |
| {/* Left: Circular Progress */} | |
| <div className="lg:col-span-1 flex flex-col items-center justify-center"> | |
| <div className="relative w-48 h-48"> | |
| {/* Background circle */} | |
| <svg className="w-full h-full -rotate-90"> | |
| <circle | |
| cx="96" | |
| cy="96" | |
| r="88" | |
| fill="none" | |
| stroke="currentColor" | |
| strokeWidth="8" | |
| className="text-void-800" | |
| /> | |
| <motion.circle | |
| cx="96" | |
| cy="96" | |
| r="88" | |
| fill="none" | |
| stroke={accentColor === 'coral' ? '#ff6b6b' : '#22b8cf'} | |
| strokeWidth="8" | |
| strokeLinecap="round" | |
| initial={{ strokeDasharray: '0 553' }} | |
| animate={{ strokeDasharray: `${(percentage / 100) * 553} 553` }} | |
| transition={{ duration: 0.5, ease: 'easeOut' }} | |
| /> | |
| </svg> | |
| {/* Center content */} | |
| <div className="absolute inset-0 flex flex-col items-center justify-center"> | |
| <span className={`text-5xl font-bold ${accentClass}`}> | |
| {percentage}% | |
| </span> | |
| <span className="text-void-400 text-sm mt-1"> | |
| {generatedVideos.length} / {progress.total} videos | |
| </span> | |
| </div> | |
| </div> | |
| {/* Current Status */} | |
| <motion.div | |
| key={progress.message} | |
| initial={{ opacity: 0, y: 5 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="mt-6 text-center" | |
| > | |
| <p className="text-void-200 font-medium">{progress.message}</p> | |
| </motion.div> | |
| </div> | |
| {/* Right: Segment Progress & Activity Log */} | |
| <div className="lg:col-span-2 space-y-6"> | |
| {/* Segment Cards */} | |
| <div className="card p-4"> | |
| <h3 className="text-sm font-semibold text-void-400 mb-4">Segment Progress</h3> | |
| <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3"> | |
| {segments.map((segment, index) => { | |
| const isCompleted = index < generatedVideos.length; | |
| const isCurrent = index === generatedVideos.length && index < progress.total; | |
| return ( | |
| <motion.div | |
| key={index} | |
| initial={{ opacity: 0, scale: 0.9 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| transition={{ delay: index * 0.05 }} | |
| className={` | |
| relative p-3 rounded-lg border-2 transition-all | |
| ${isCompleted | |
| ? `${accentBorder} bg-void-900` | |
| : isCurrent | |
| ? `${accentBorder} bg-void-800 animate-pulse-slow` | |
| : 'border-void-700 bg-void-900/50' | |
| } | |
| `} | |
| > | |
| {/* Status indicator */} | |
| <div className={` | |
| absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full flex items-center justify-center | |
| ${isCompleted | |
| ? accentBg + ' text-white' | |
| : isCurrent | |
| ? accentBg + ' text-white' | |
| : 'bg-void-700 text-void-500' | |
| } | |
| `}> | |
| {isCompleted ? <CheckIcon /> : isCurrent ? <SpinnerIcon /> : <span className="text-xs">{index + 1}</span>} | |
| </div> | |
| <div className="text-sm font-medium text-void-200"> | |
| Segment {index + 1} | |
| </div> | |
| <div className="text-xs text-void-500 mt-1 line-clamp-2"> | |
| {segment.action_timeline?.dialogue?.substring(0, 40) || 'Processing...'}... | |
| </div> | |
| {/* Thumbnail preview when completed */} | |
| {isCompleted && generatedVideos[index]?.thumbnails?.[0] && ( | |
| <div className="mt-2 rounded overflow-hidden aspect-video bg-void-950"> | |
| <img | |
| src={generatedVideos[index].thumbnails[0]} | |
| alt={`Segment ${index + 1}`} | |
| className="w-full h-full object-cover" | |
| /> | |
| </div> | |
| )} | |
| </motion.div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| {/* Activity Log */} | |
| <div className="card p-4"> | |
| <h3 className="text-sm font-semibold text-void-400 mb-3 flex items-center gap-2"> | |
| <span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" /> | |
| Live Activity | |
| </h3> | |
| <div | |
| ref={logContainerRef} | |
| className="h-48 overflow-y-auto space-y-2 scrollbar-thin scrollbar-thumb-void-600 scrollbar-track-void-800" | |
| > | |
| <AnimatePresence initial={false}> | |
| {activityLog.map((log) => ( | |
| <motion.div | |
| key={log.id} | |
| initial={{ opacity: 0, x: -20 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| exit={{ opacity: 0 }} | |
| className={` | |
| flex items-start gap-3 p-2 rounded-lg text-sm | |
| ${log.type === 'success' ? 'bg-green-500/10 text-green-400' : | |
| log.type === 'warning' ? 'bg-amber-500/10 text-amber-400' : | |
| log.type === 'processing' ? `${accentColor === 'coral' ? 'bg-coral-500/10 text-coral-400' : 'bg-electric-500/10 text-electric-400'}` : | |
| 'bg-void-800/50 text-void-300' | |
| } | |
| `} | |
| > | |
| <div className={` | |
| flex-shrink-0 w-6 h-6 rounded flex items-center justify-center | |
| ${log.type === 'success' ? 'bg-green-500/20' : | |
| log.type === 'warning' ? 'bg-amber-500/20' : | |
| log.type === 'processing' ? `${accentColor === 'coral' ? 'bg-coral-500/20' : 'bg-electric-500/20'}` : | |
| 'bg-void-700' | |
| } | |
| `}> | |
| {log.type === 'success' ? <CheckIcon /> : getIcon(log.icon)} | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <p className="truncate">{log.message}</p> | |
| <p className="text-xs opacity-60 mt-0.5"> | |
| {log.timestamp.toLocaleTimeString()} | |
| </p> | |
| </div> | |
| </motion.div> | |
| ))} | |
| </AnimatePresence> | |
| {activityLog.length === 0 && ( | |
| <div className="text-void-500 text-center py-8"> | |
| Waiting for activity... | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Segment Prompts Viewer - Show during video generation */} | |
| {segments.length > 0 && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.3 }} | |
| className="mb-8" | |
| > | |
| <SegmentPromptsViewer | |
| segments={segments} | |
| accentColor={accentColor} | |
| /> | |
| </motion.div> | |
| )} | |
| {/* Bottom Progress Bar */} | |
| <div className="card p-4"> | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-sm text-void-400">Overall Progress</span> | |
| <span className={`text-sm font-medium ${accentClass}`}>{percentage}%</span> | |
| </div> | |
| <div className="w-full bg-void-800 rounded-full h-3 overflow-hidden"> | |
| <motion.div | |
| className={`h-full ${accentBg} relative`} | |
| initial={{ width: 0 }} | |
| animate={{ width: `${percentage}%` }} | |
| transition={{ duration: 0.5 }} | |
| > | |
| {/* Shimmer effect */} | |
| <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-shimmer" /> | |
| </motion.div> | |
| </div> | |
| {/* Step indicators */} | |
| <div className="flex justify-between mt-4 text-xs text-void-500"> | |
| <div className={generatedVideos.length > 0 ? accentClass : ''}> | |
| {generatedVideos.length > 0 ? 'β' : 'β'} Generating | |
| </div> | |
| <div className={generatedVideos.length >= segments.length / 2 ? accentClass : ''}> | |
| {generatedVideos.length >= segments.length / 2 ? 'β' : 'β'} Halfway | |
| </div> | |
| <div className={generatedVideos.length === segments.length ? accentClass : ''}> | |
| {generatedVideos.length === segments.length ? 'β' : 'β'} Complete | |
| </div> | |
| </div> | |
| </div> | |
| {/* Tips */} | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| transition={{ delay: 0.5 }} | |
| className="mt-6 text-center text-void-500 text-sm" | |
| > | |
| π‘ Each video typically takes 1-2 minutes. Stay on this page to track progress. | |
| </motion.div> | |
| </div> | |
| </motion.div> | |
| ); | |
| }; | |