Spaces:
Sleeping
Sleeping
| import React, { useEffect, useRef, useState } from 'react'; | |
| import ConfidenceBar from './ConfidenceBar'; | |
| import FrameTable from './FrameTable'; | |
| import DMCASection from './DMCASection'; | |
| const MAX_ATTEMPTS = 90; // 90 Γ 2s = 3 minutes max before giving up | |
| export default function VideoPolling({ jobId, onComplete, onError }) { | |
| const [status, setStatus] = useState('processing'); | |
| const [statusText, setStatusText] = useState('Connecting to serverβ¦'); | |
| const [result, setResult] = useState(null); | |
| const [error, setError] = useState(null); | |
| const attemptsRef = useRef(0); | |
| // FIX: use a ref to track if component is still mounted β prevents | |
| // setState calls after unmount which cause React warnings & ghost updates | |
| const mountedRef = useRef(true); | |
| useEffect(() => { | |
| mountedRef.current = true; | |
| return () => { mountedRef.current = false; }; | |
| }, []); | |
| useEffect(() => { | |
| if (!jobId) return; | |
| // FIX: reset attempts on every new jobId so a re-upload starts fresh | |
| attemptsRef.current = 0; | |
| function poll() { | |
| if (!mountedRef.current) return; // component unmounted β stop silently | |
| attemptsRef.current++; | |
| // FIX: hard cap β stop polling after MAX_ATTEMPTS instead of running forever | |
| if (attemptsRef.current > MAX_ATTEMPTS) { | |
| const msg = 'Processing timed out. The video may be too long or the server is busy.'; | |
| if (mountedRef.current) { | |
| setStatus('error'); | |
| setError(msg); | |
| } | |
| // FIX: notify App.jsx so it can clear jobId and show error in parent | |
| if (onError) onError(msg); | |
| return; | |
| } | |
| if (mountedRef.current) { | |
| setStatusText(`Checking⦠(attempt ${attemptsRef.current} / ${MAX_ATTEMPTS})`); | |
| } | |
| fetch(`/status/${jobId}`) | |
| .then(r => { | |
| if (!r.ok) throw new Error(`Server returned ${r.status}`); | |
| return r.json(); | |
| }) | |
| .then(data => { | |
| if (!mountedRef.current) return; | |
| if (data.status === 'processing') { | |
| setTimeout(poll, 2000); | |
| } else if (data.status === 'done') { | |
| setResult(data); | |
| setStatus('done'); | |
| // FIX: lift result up to App.jsx so ResultSection can render there too | |
| if (onComplete) onComplete(data); | |
| } else { | |
| // status === 'error' or anything unexpected | |
| const msg = data.error || 'Unknown server error.'; | |
| setStatus('error'); | |
| setError(msg); | |
| if (onError) onError(msg); | |
| } | |
| }) | |
| .catch(() => { | |
| if (!mountedRef.current) return; | |
| // Network hiccup β keep retrying until MAX_ATTEMPTS | |
| setTimeout(poll, 3000); | |
| }); | |
| } | |
| poll(); | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [jobId]); | |
| // ββ Rendering βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if (status === 'processing') { | |
| return ( | |
| <div className="video-processing-box"> | |
| <div className="processing-spinner"></div> | |
| <div className="processing-label">Analysing your videoβ¦</div> | |
| <div className="processing-sub"> | |
| Sampling frames & running inference β this usually takes under a minute. | |
| </div> | |
| <div className="pulse-bar"><div className="pulse-fill"></div></div> | |
| <div className="poll-status-text">{statusText}</div> | |
| </div> | |
| ); | |
| } | |
| if (status === 'error') { | |
| return ( | |
| <div className="poll-error-box"> | |
| β Processing failed: {error} | |
| </div> | |
| ); | |
| } | |
| // status === 'done' β render result inline inside VideoPolling | |
| // (App.jsx also gets the result via onComplete for its own ResultSection) | |
| if (!result) return null; | |
| const isFake = result.result !== 'REAL' && result.result !== 'REAL IMAGE'; | |
| const label = isFake ? 'β Deepfake Detected in Video' : 'β Real Video'; | |
| const badgeStyle = isFake | |
| ? { background: 'rgba(239,68,68,0.2)', color: '#f87171', border: '1px solid #ef4444' } | |
| : { background: 'rgba(34,197,94,0.2)', color: '#4ade80', border: '1px solid #22c55e' }; | |
| const barColor = isFake ? '#ef4444' : '#22c55e'; | |
| return ( | |
| <div style={{ marginTop: '35px', borderTop: '1px solid var(--glass-border)', paddingTop: '20px' }}> | |
| <div style={{ | |
| ...badgeStyle, | |
| padding: '8px 16px', | |
| borderRadius: '99px', | |
| display: 'inline-block', | |
| marginBottom: '15px', | |
| }}> | |
| {label} | |
| </div> | |
| <p style={{ fontSize: '14px', color: 'var(--text-muted)' }}> | |
| Confidence Score: {result.confidence}% | |
| </p> | |
| <ConfidenceBar confidence={result.confidence} color={barColor} /> | |
| <FrameTable frameResults={result.frame_results} /> | |
| {isFake && <DMCASection mediaType="video" />} | |
| </div> | |
| ); | |
| } |