import { useState, useRef, useEffect } from 'react'; import type { DragEvent, ChangeEvent } from 'react'; import { PageParseAPI, INDIAN_LANGUAGES } from '../services/api'; import type { ProcessingJob } from '../services/api'; import { UploadCloud, FileText, CheckCircle2, AlertCircle, Mic, Square, Camera, Files } from 'lucide-react'; interface FileUploaderProps { onJobStarted: () => void; onJobFinished: () => void; isAirgapped: boolean; } export const FileUploader = ({ onJobStarted, onJobFinished, isAirgapped }: FileUploaderProps) => { const [isDragActive, setIsDragActive] = useState(false); const [currentJob, setCurrentJob] = useState(null); const [activeJobs, setActiveJobs] = useState([]); const [filePreview, setFilePreview] = useState(null); const [selectedLanguage, setSelectedLanguage] = useState('English'); const [schemaType, setSchemaType] = useState('auto'); const [isRecording, setIsRecording] = useState(false); const [recordingDuration, setRecordingDuration] = useState(0); const [imageToConfigure, setImageToConfigure] = useState(null); const [brightness, setBrightness] = useState(100); const [contrast, setContrast] = useState(100); const [binarize, setBinarize] = useState(false); const [threshold, setThreshold] = useState(128); const [rotation, setRotation] = useState(0); const fileInputRef = useRef(null); const batchInputRef = useRef(null); const cameraInputRef = useRef(null); const mediaRecorderRef = useRef(null); const audioChunksRef = useRef([]); const timerRef = useRef(null); const canvasRef = useRef(null); useEffect(() => { if (!imageToConfigure || !canvasRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; const img = new Image(); img.src = URL.createObjectURL(imageToConfigure); img.onload = () => { const is90 = Math.abs(rotation % 180) === 90; canvas.width = is90 ? img.height : img.width; canvas.height = is90 ? img.width : img.height; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.translate(canvas.width / 2, canvas.height / 2); ctx.rotate((rotation * Math.PI) / 180); ctx.drawImage(img, -img.width / 2, -img.height / 2); ctx.restore(); let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); let data = imgData.data; const bFactor = brightness / 100; const cFactor = contrast / 100; for (let i = 0; i < data.length; i += 4) { for (let c = 0; c < 3; c++) { let val = data[i + c]; val = val * bFactor; val = ((val - 128) * cFactor) + 128; data[i + c] = Math.min(255, Math.max(0, val)); } if (binarize) { const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]; const binVal = gray < threshold ? 0 : 255; data[i] = binVal; data[i + 1] = binVal; data[i + 2] = binVal; } } ctx.putImageData(imgData, 0, 0); }; }, [imageToConfigure, brightness, contrast, binarize, threshold, rotation]); const startRecording = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); audioChunksRef.current = []; try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const recorder = new MediaRecorder(stream); mediaRecorderRef.current = recorder; recorder.ondataavailable = (event) => { if (event.data.size > 0) audioChunksRef.current.push(event.data); }; recorder.onstop = () => { const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/wav' }); const file = new File([audioBlob], `recorded_voice_${Date.now()}.wav`, { type: 'audio/wav' }); processSelectedFile(file); stream.getTracks().forEach(track => track.stop()); }; recorder.start(); setIsRecording(true); setRecordingDuration(0); timerRef.current = setInterval(() => setRecordingDuration(prev => prev + 1), 1000); } catch (err) { alert("Microphone access error: " + err); } }; const stopRecording = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (mediaRecorderRef.current && isRecording) { mediaRecorderRef.current.stop(); setIsRecording(false); if (timerRef.current) clearInterval(timerRef.current); } }; const takePhoto = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); const track = stream.getVideoTracks()[0]; const imageCapture = new (window as any).ImageCapture(track); const photoBlob = await imageCapture.takePhoto(); track.stop(); const file = new File([photoBlob], `camera_${Date.now()}.jpg`, { type: 'image/jpeg' }); processSelectedFile(file); } catch (err) { cameraInputRef.current?.click(); } }; const uploadProcessedFile = async (file: File) => { try { const job = await PageParseAPI.uploadPage(file, selectedLanguage, schemaType, (updatedJob) => { setCurrentJob(updatedJob); if (updatedJob.status === 'saved' || updatedJob.status === 'failed') { onJobFinished(); setTimeout(() => { setCurrentJob(null); setFilePreview(null); }, 3000); } }); setCurrentJob(job); onJobStarted(); } catch (err: any) { console.error(err); setCurrentJob({ id: 'err', filename: file.name, status: 'failed', progress: 0, error: err.message || 'Processing failed' }); onJobFinished(); } }; const uploadBatchFiles = async (files: FileList) => { onJobStarted(); const jobs: ProcessingJob[] = []; for (const file of Array.from(files)) { jobs.push({ id: Math.random().toString(36).substring(2, 9), filename: file.name, status: 'queued', progress: 0 }); } setActiveJobs(jobs); for (let i = 0; i < files.length; i++) { const file = files[i]; try { await PageParseAPI.uploadPage(file, selectedLanguage, schemaType, (updatedJob) => { setActiveJobs(prev => prev.map(j => j.id === updatedJob.id ? updatedJob : j)); }); } catch (err) { console.error(`Failed to upload ${file.name}:`, err); } } onJobFinished(); setTimeout(() => setActiveJobs([]), 5000); }; const confirmAndProcess = () => { const canvas = canvasRef.current; if (!canvas || !imageToConfigure) return; canvas.toBlob((blob) => { if (blob) { const processedFile = new File([blob], imageToConfigure.name, { type: 'image/jpeg' }); setImageToConfigure(null); uploadProcessedFile(processedFile); } }, 'image/jpeg', 0.9); }; const handleDrag = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === "dragenter" || e.type === "dragover") setIsDragActive(true); else if (e.type === "dragleave") setIsDragActive(false); }; const processSelectedFile = async (file: File) => { if (!file) return; if (file.type.startsWith('image/')) { setImageToConfigure(file); const reader = new FileReader(); reader.onloadend = () => setFilePreview(reader.result as string); reader.readAsDataURL(file); } else { setFilePreview(null); uploadProcessedFile(file); } }; const handleDrop = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragActive(false); if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { if (e.dataTransfer.files.length > 1) { uploadBatchFiles(e.dataTransfer.files); } else { processSelectedFile(e.dataTransfer.files[0]); } } }; const handleChange = (e: ChangeEvent) => { if (e.target.files && e.target.files[0]) processSelectedFile(e.target.files[0]); }; const handleBatchChange = (e: ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { uploadBatchFiles(e.target.files); } }; const handleCameraCapture = (e: ChangeEvent) => { if (e.target.files && e.target.files[0]) processSelectedFile(e.target.files[0]); }; const getStatusText = (status: string) => { const map: Record = { queued: 'Queued', preprocessing: 'OpenCV Preprocessing', ocr: 'OCR Inference (CPU)', extracting: 'SLM + GBNF Structuring', saved: 'Saved to SQLite', failed: 'Failed', }; return map[status] || status; }; const schemaOptions = [ { value: 'auto', label: 'Auto-Detect' }, { value: 'todo', label: 'Planner / To-Do' }, { value: 'meeting', label: 'Meeting Minutes' }, { value: 'recipe', label: 'Recipe Card' }, { value: 'survey', label: 'Survey Form' }, { value: 'notes', label: 'General Notes' }, ]; return (

Local Ingestion

{!currentJob && activeJobs.length === 0 && (
)}
{activeJobs.length > 0 ? (
Batch Upload ({activeJobs.length} files)
{activeJobs.map(job => (
{job.filename} {getStatusText(job.status)} {job.status === 'saved' && } {job.status === 'failed' && }
))}
) : !currentJob ? ( imageToConfigure ? (

Adjust Handwritten Sheet

Brightness: {brightness}%
setBrightness(Number(e.target.value))} style={{ width: '100%' }} />
Contrast: {contrast}%
setContrast(Number(e.target.value))} style={{ width: '100%' }} />
Rotation: {rotation}°
setRotation(Number(e.target.value))} style={{ width: '100%' }} />
{binarize && (
Threshold: {threshold}
setThreshold(Number(e.target.value))} style={{ width: '100%' }} />
)}
e.stopPropagation()}>
) : (
fileInputRef.current?.click()} >
Drop files here
Images · Audio · Video · PDF · DOCX · TXT · CSV
e.stopPropagation()}> {!isRecording ? ( ) : ( )}
{isAirgapped &&
Air-gap mode active — no network calls
}
) ) : (
{filePreview ? : }
{currentJob.filename}
{getStatusText(currentJob.status)}
{['queued', 'preprocessing', 'ocr', 'extracting', 'saved'].map((step, i) => { const stages = ['queued', 'preprocessing', 'ocr', 'extracting', 'saved']; const idx = stages.indexOf(currentJob.status); const isActive = i <= idx && currentJob.status !== 'failed'; const isCurrent = i === idx; return (
{i < idx || currentJob.status === 'saved' ? : i === idx && currentJob.status === 'failed' ? : i + 1}
{['Queue', 'OpenCV', 'OCR', 'SLM', 'SQLite'][i]}
); })}
{currentJob.status === 'saved' ? 'Complete' : currentJob.status === 'failed' ? currentJob.error || 'Failed' : 'Processing on CPU...'} {currentJob.progress}%
)}
); };