import { useCallback, useEffect, useRef, useState } from 'react' import './index.css' import UploadView from './components/UploadView' import ProcessingView from './components/ProcessingView' import ResultsView from './components/ResultsView' import HealthDashboard from './components/HealthDashboard' import LandingPage from './components/LandingPage' import API from 'api' function hasSeenLanding() { try { return window.sessionStorage.getItem('landingSeen') === '1' } catch { return false } } function markLandingSeen() { try { window.sessionStorage.setItem('landingSeen', '1') } catch { // Ignore storage failures (private mode / blocked storage). } } export default function App() { const [showLanding, setShowLanding] = useState(() => !hasSeenLanding()) const [tool, setTool] = useState('extractor') const [view, setView] = useState('upload') const [jobs, setJobs] = useState([]) const [activeJob, setActiveJob] = useState(null) const pollersRef = useRef(new Map()) const updateJob = useCallback((jobId, updates) => { setJobs((currentJobs) => currentJobs.map((job) => ( job.id === jobId ? { ...job, ...updates } : job ))) setActiveJob((currentJob) => ( currentJob?.id === jobId ? { ...currentJob, ...updates } : currentJob )) }, []) const stopPolling = useCallback((jobId) => { const poller = pollersRef.current.get(jobId) if (poller) { window.clearInterval(poller) pollersRef.current.delete(jobId) } }, []) const pollJob = useCallback((jobId) => { if (pollersRef.current.has(jobId)) { return } const interval = window.setInterval(async () => { try { const statusResponse = await fetch(`${API}/api/status/${jobId}`) const statusData = await statusResponse.json() if (statusData.status === 'done') { stopPolling(jobId) const resultsResponse = await fetch(`${API}/api/results/${jobId}`) const resultsData = await resultsResponse.json() updateJob(jobId, { status: 'done', annotation: resultsData.annotation, duration: resultsData.duration, }) setActiveJob((currentJob) => { if (currentJob?.id === jobId) { setView('results') return { ...currentJob, status: 'done', annotation: resultsData.annotation, duration: resultsData.duration, } } return currentJob }) } else if (statusData.status === 'error') { stopPolling(jobId) updateJob(jobId, { status: 'error', error: statusData.error, }) } } catch (error) { console.error('Polling failed', error) } }, 1000) pollersRef.current.set(jobId, interval) }, [stopPolling, updateJob]) useEffect(() => () => { for (const poller of pollersRef.current.values()) { window.clearInterval(poller) } pollersRef.current.clear() }, []) const handleUpload = useCallback(async (files) => { const newJobs = [] for (const file of files) { const formData = new FormData() formData.append('file', file) try { const response = await fetch(`${API}/api/upload-and-process`, { method: 'POST', body: formData, }) const data = await response.json() if (!response.ok || !data?.job_id) { console.error('Upload failed', data) continue } newJobs.push({ id: data.job_id, filename: data.filename, status: data.status || 'queued', annotation: null, duration: null, imageUrl: `${API}/api/image/${data.job_id}`, size_bytes: file.size, }) } catch (error) { console.error('Upload failed', error) } } if (!newJobs.length) { return } setJobs((currentJobs) => [...newJobs, ...currentJobs]) setActiveJob(newJobs[0]) setView('processing') newJobs.forEach((job) => pollJob(job.id)) }, [pollJob]) const handleRetryJob = useCallback(async (job) => { if (!job?.id) return try { const response = await fetch(`${API}/api/process/${job.id}`, { method: 'POST', }) if (!response.ok) { throw new Error(`Retry failed with status ${response.status}`) } updateJob(job.id, { status: 'queued', error: null }) pollJob(job.id) } catch (error) { console.error('Retry failed', error) } }, [pollJob, updateJob]) const handleOpenJob = useCallback((job) => { setTool('extractor') setActiveJob(job) if (job.status === 'done') { setView('results') return } setView('processing') pollJob(job.id) }, [pollJob]) const handleGoHome = useCallback(() => { setView('upload') setActiveJob(null) }, []) const handleDismissLanding = useCallback(() => { markLandingSeen() setShowLanding(false) }, []) const showStudio = tool === 'extractor' && view === 'results' && activeJob const processedCount = jobs.filter((job) => job.status === 'done').length return ( <> {showLanding && }
{!showStudio && (
)} {tool === 'health' ? ( ) : ( <> {view === 'upload' && ( )} {view === 'processing' && activeJob && ( )} {view === 'results' && activeJob && ( )} )}
) }