Agent_PDF / web /src /components /UploadView.jsx
MohamedSameh77i's picture
Add upload user guide and harden landing page behavior
39cf1e2 verified
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 (
<>
<section className="upload-section">
<span className="sr-only" ref={liveRegionRef} aria-live="polite" aria-atomic="true" />
<div className="upload-hero">
<h1>
Extract Tables with <span className="gradient-text">AI Precision</span>
</h1>
<p>
Upload document images or PDFs and let our 5-phase pipeline detect tables,
recognize structure, perform OCR, and extract editable data — all in seconds.
</p>
</div>
<div
className={`upload-zone glass${dragOver ? ' drag-over' : ''}${isUploading ? ' is-uploading' : ''}`}
role="button"
tabIndex={0}
aria-label="Upload documents"
onDragOver={(e) => { 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()
}
}}
>
<div className="upload-zone-icon">📄</div>
<h3>{isUploading ? 'Uploading...' : 'Drop files here or click to browse'}</h3>
<p>Supports JPG, PNG, BMP, TIFF, and PDF up to {MAX_FILE_SIZE_MB} MB each</p>
<input
ref={fileRef}
type="file"
accept=".jpg,.jpeg,.png,.bmp,.tiff,.tif,.pdf"
multiple
onChange={(e) => { void handleFiles(e.target.files) }}
/>
<div className="upload-meta" aria-live="polite">
<span>{lastSelection.accepted} accepted</span>
<span>{lastSelection.rejected} rejected</span>
<span>{jobs.filter((job) => job.status === 'processing' || job.status === 'queued').length} active</span>
</div>
</div>
{validationErrors.length > 0 && (
<div className="upload-errors" role="alert">
{validationErrors.slice(0, 4).map((error) => (
<div key={error} className="upload-error-item">{error}</div>
))}
</div>
)}
<div className="pipeline-steps">
{[
['1', 'Table Detection'],
['2', 'Structure Recognition'],
['3', 'Text Detection'],
['4', 'OCR Recognition'],
['5', 'Cell Assignment'],
].map(([n, label]) => (
<div className="pipeline-step" key={n}>
<span className="step-num">{n}</span>
{label}
</div>
))}
</div>
<section className="upload-guide glass" aria-label="User guide">
<h3>User Guide</h3>
<div className="upload-guide-grid">
<div className="upload-guide-item">
<strong>1. Upload files</strong>
<p>Drag and drop or browse to select one or many images/PDFs.</p>
</div>
<div className="upload-guide-item">
<strong>2. Wait for processing</strong>
<p>Track queued and running jobs in the Upload Queue panel.</p>
</div>
<div className="upload-guide-item">
<strong>3. Review results</strong>
<p>Open any completed job to edit table structure and cell text.</p>
</div>
<div className="upload-guide-item">
<strong>4. Export data</strong>
<p>Export cleaned tables to CSV, Excel, or HTML from the studio.</p>
</div>
</div>
</section>
{!!activeJobs.length && (
<div className="upload-queue glass">
<div className="upload-queue-header">
<h3>Upload Queue</h3>
<span>{activeJobs.length} active</span>
</div>
<div className="upload-queue-list">
{activeJobs.map((job) => (
<div key={job.id} className="upload-queue-item">
<div>
<div className="upload-queue-name">{job.filename}</div>
<div className="upload-queue-meta">{job.status}</div>
</div>
<div className="upload-queue-actions">
{job.status === 'error' && (
<button type="button" className="btn btn-sm" onClick={() => onRetryJob?.(job)}>
Retry
</button>
)}
{job.status === 'processing' && <span className="badge">Running</span>}
</div>
</div>
))}
</div>
</div>
)}
</section>
{doneJobs.length > 0 && (
<section className="jobs-section">
<div className="jobs-header">
<h2>Processed Files</h2>
<span style={{ color: 'var(--text-muted)', fontSize: 14 }}>
{doneJobs.length} file{doneJobs.length !== 1 ? 's' : ''}
</span>
</div>
<div className="jobs-grid">
{doneJobs.map(job => (
<div key={job.id} className="job-card" onClick={() => onOpenJob(job)}>
<img
className="job-card-thumb"
src={job.imageUrl}
alt={job.filename}
loading="lazy"
/>
<div className="job-card-body">
<div>
<div className="job-card-name">{job.filename}</div>
<div className="job-card-meta">
{job.annotation?.tables?.length || 0} table(s) • {job.duration}s • {formatSize(job.size_bytes)}
</div>
</div>
<span className="badge badge-done">Done</span>
</div>
</div>
))}
</div>
</section>
)}
</>
)
}