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 &&