import { useState, useRef, useCallback } from 'react' const MAX_FILE_SIZE_MB = 30 const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024 function formatSize(bytes) { if (!Number.isFinite(bytes) || bytes <= 0) return '0 B' const units = ['B', 'KB', 'MB', 'GB'] let value = bytes let idx = 0 while (value >= 1024 && idx < units.length - 1) { value /= 1024 idx += 1 } return `${value.toFixed(value >= 10 || idx === 0 ? 0 : 1)} ${units[idx]}` } export default function UploadView({ onUpload, jobs, onOpenJob, onRetryJob }) { const [dragOver, setDragOver] = useState(false) const [isUploading, setIsUploading] = useState(false) const [validationErrors, setValidationErrors] = useState([]) const [lastSelection, setLastSelection] = useState({ accepted: 0, rejected: 0 }) const fileRef = useRef() const liveRegionRef = useRef() const announce = useCallback((message) => { if (!liveRegionRef.current) return liveRegionRef.current.textContent = '' window.requestAnimationFrame(() => { if (liveRegionRef.current) { liveRegionRef.current.textContent = message } }) }, []) const handleFiles = useCallback(async (files) => { const accepted = [] const rejected = [] for (const file of Array.from(files || [])) { const validType = /\.(jpe?g|png|bmp|tiff?|pdf)$/i.test(file.name) if (!validType) { rejected.push(`${file.name}: unsupported file type`) continue } if (file.size > MAX_FILE_SIZE_BYTES) { rejected.push(`${file.name}: exceeds ${MAX_FILE_SIZE_MB} MB`) continue } accepted.push(file) } setValidationErrors(rejected) setLastSelection({ accepted: accepted.length, rejected: rejected.length }) if (!accepted.length) { announce('No valid files selected.') return } setIsUploading(true) announce(`Uploading ${accepted.length} file${accepted.length > 1 ? 's' : ''}.`) try { await onUpload(accepted) announce(`${accepted.length} file${accepted.length > 1 ? 's' : ''} queued for processing.`) } finally { setIsUploading(false) } }, [onUpload]) const onDrop = useCallback((e) => { e.preventDefault() setDragOver(false) void handleFiles(e.dataTransfer.files) }, [handleFiles]) const doneJobs = jobs.filter(j => j.status === 'done') const activeJobs = jobs.filter(j => j.status !== 'done').slice(0, 8) return ( <>

Extract Tables with AI Precision

Upload document images or PDFs and let our 5-phase pipeline detect tables, recognize structure, perform OCR, and extract editable data — all in seconds.

{ e.preventDefault(); setDragOver(true) }} onDragLeave={() => setDragOver(false)} onDrop={onDrop} onClick={() => fileRef.current?.click()} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() fileRef.current?.click() } }} >
📄

{isUploading ? 'Uploading...' : 'Drop files here or click to browse'}

Supports JPG, PNG, BMP, TIFF, and PDF up to {MAX_FILE_SIZE_MB} MB each

{ void handleFiles(e.target.files) }} />
{lastSelection.accepted} accepted {lastSelection.rejected} rejected {jobs.filter((job) => job.status === 'processing' || job.status === 'queued').length} active
{validationErrors.length > 0 && (
{validationErrors.slice(0, 4).map((error) => (
{error}
))}
)}
{[ ['1', 'Table Detection'], ['2', 'Structure Recognition'], ['3', 'Text Detection'], ['4', 'OCR Recognition'], ['5', 'Cell Assignment'], ].map(([n, label]) => (
{n} {label}
))}

User Guide

1. Upload files

Drag and drop or browse to select one or many images/PDFs.

2. Wait for processing

Track queued and running jobs in the Upload Queue panel.

3. Review results

Open any completed job to edit table structure and cell text.

4. Export data

Export cleaned tables to CSV, Excel, or HTML from the studio.

{!!activeJobs.length && (

Upload Queue

{activeJobs.length} active
{activeJobs.map((job) => (
{job.filename}
{job.status}
{job.status === 'error' && ( )} {job.status === 'processing' && Running}
))}
)}
{doneJobs.length > 0 && (

Processed Files

{doneJobs.length} file{doneJobs.length !== 1 ? 's' : ''}
{doneJobs.map(job => (
onOpenJob(job)}> {job.filename}
{job.filename}
{job.annotation?.tables?.length || 0} table(s) • {job.duration}s • {formatSize(job.size_bytes)}
Done
))}
)} ) }