Video_AdGenesis_App / frontend /src /components /GenerationProgress.tsx
sushilideaclan01's picture
improve the flow
d51b30c
Raw History Blame Contribute Delete
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>
);
};