import { useEffect, useState, useRef } from 'react' import { CheckCircle2, Circle, Zap, Film, FileText } from 'lucide-react' import { useWebSocket } from '../hooks/useWebSocket' import { getJob } from '../utils/api' /** * Processing view with progress indicators * BUG FIX: data.result doesn't exist — backend returns full JobResult. * BUG FIX: Added polling fallback (every 2.5s) when WebSocket drops. * Props: { jobId, jobType, onComplete } */ function ProcessingView({ jobId, jobType = 'clip', onComplete }) { const { jobData } = useWebSocket(jobId) const [jobInfo, setJobInfo] = useState(null) const [error, setError] = useState(null) const pollingRef = useRef(null) const completedRef = useRef(false) const clipSteps = [ { id: 'downloading', label: 'Downloading' }, { id: 'transcribing', label: 'Transcribing Audio' }, { id: 'analyzing', label: 'Analyzing Clips' }, { id: 'editing', label: 'Editing Video' }, { id: 'captioning', label: 'Adding Captions' }, { id: 'completed', label: 'Done' }, ] const articleSteps = [ { id: 'processing', label: 'Scraping Article' }, { id: 'processing', label: 'Generating Script' }, { id: 'processing', label: 'Creating Voiceover' }, { id: 'processing', label: 'Assembling Reel' }, { id: 'completed', label: 'Done' }, ] const steps = jobType === 'article' ? articleSteps : clipSteps const handleJobData = (data) => { if (!data) return setJobInfo(data) if (data.status === 'completed' && !completedRef.current) { completedRef.current = true clearInterval(pollingRef.current) // BUG FIX: pass `data` (the full JobResult), NOT `data.result` onComplete(data) } if (data.status === 'failed') { clearInterval(pollingRef.current) setError(data.error || 'Processing failed. Please try again.') } } // WebSocket updates useEffect(() => { if (jobData) handleJobData(jobData) }, [jobData]) // eslint-disable-line // Polling fallback — fires every 2.5s regardless of WebSocket useEffect(() => { if (!jobId) return pollingRef.current = setInterval(async () => { try { const data = await getJob(jobId) handleJobData(data) } catch (err) { console.warn('[poll]', err.message) } }, 2500) return () => clearInterval(pollingRef.current) }, [jobId]) // eslint-disable-line // Map backend status string → step index const statusToStep = { queued: 0, downloading: 0, transcribing: 1, analyzing: 2, editing: 3, captioning: 4, completed: 5, failed: 0, } const currentStepIndex = statusToStep[jobInfo?.status] ?? 0 const progressPct = jobInfo?.progress ?? Math.round(((currentStepIndex + 1) / steps.length) * 100) return (
{/* Header */}
{jobType === 'article' ? : }

{jobType === 'article' ? 'Creating Your Reels' : 'Creating Your Clips'}

{jobType === 'article' ? 'Turning the article into viral social content…' : 'Hold tight, AI magic in progress…'}

{/* Main Card */}
{/* Source */} {jobInfo?.source_url && (

Processing

{jobInfo.source_url}

)} {/* Progress Bar */}
Overall Progress {progressPct}%
{jobInfo?.message && (

{jobInfo.message}

)}
{/* Steps */}
{steps.map((step, idx) => { const done = idx < currentStepIndex const active = idx === currentStepIndex return (
{done ? ( ) : active ? (
) : ( )}
{step.label} {active && (
{[0, 0.1, 0.2].map((delay, i) => ( ))}
)}
) })}
{/* Tip */}

{jobType === 'article' ? 'Article → Reel' : 'Pro Tip'}

{jobType === 'article' ? 'Your article is being condensed into a punchy ~45-second reel with AI voiceover and a chainstreet.io call-to-action.' : 'ClipCraft AI analyzes emotional beats, engagement patterns, and viewer retention to find the perfect viral moments.'}

{error && (
{error}
)}
) } export default ProcessingView