/** * UploadDropzone — drag-drop + file-picker for uploading PDFs into the * active session. * * On every successful drop: * 1. read each File as bytes, * 2. run the source-layer `ingestPdfFromFile` (mints the blob URL * via the session's `PdfByteStore`), * 3. register the resulting `{document, representation}` with the * engine, * 4. activate the most-recently-uploaded document. * * Failures (non-PDFs, ingest errors) are surfaced inline above the * dropzone; the caller doesn't need a separate toast for them. */ import { useCallback, useRef, useState } from "react"; import { ingestPdfFromFile } from "@source/index"; import { useActiveDocumentId, useEngine, usePdfByteStore, } from "@work/index"; interface UploadEntry { readonly file: File; status: "queued" | "uploading" | "done" | "error"; error?: string; } export interface UploadDropzoneProps { /** Optional callback fired after each successful upload. */ readonly onUploaded?: (documentId: import("@shared/ids").DocumentId) => void; } export function UploadDropzone({ onUploaded }: UploadDropzoneProps) { const engine = useEngine(); const byteStore = usePdfByteStore(); const { setId } = useActiveDocumentId(); const [entries, setEntries] = useState([]); const [isOver, setIsOver] = useState(false); const fileInputRef = useRef(null); const processFiles = useCallback( async (files: readonly File[]) => { if (files.length === 0) return; const initial: UploadEntry[] = files.map((file) => { const isPdf = file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf"); if (isPdf) return { file, status: "queued" }; return { file, status: "error", error: "Not a PDF (only application/pdf accepted)", }; }); setEntries((prev) => [...prev, ...initial]); let lastDocumentId: import("@shared/ids").DocumentId | null = null; for (const entry of initial) { if (entry.status === "error") continue; entry.status = "uploading"; setEntries((prev) => [...prev]); try { const { document, representation } = await ingestPdfFromFile( entry.file, byteStore, ); engine.documents.register({ document, representation }); entry.status = "done"; lastDocumentId = document.id; onUploaded?.(document.id); } catch (err) { entry.status = "error"; entry.error = err instanceof Error ? err.message : String(err); } setEntries((prev) => [...prev]); } if (lastDocumentId) setId(lastDocumentId); }, [byteStore, engine, onUploaded, setId], ); const onDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setIsOver(false); const files = Array.from(e.dataTransfer.files); void processFiles(files); }, [processFiles], ); const onDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsOver(true); }, []); const onDragLeave = useCallback(() => { setIsOver(false); }, []); const openPicker = useCallback(() => { fileInputRef.current?.click(); }, []); const onPicked = useCallback( (e: React.ChangeEvent) => { const files = e.target.files ? Array.from(e.target.files) : []; void processFiles(files); // Reset so the same filename can be picked again. e.target.value = ""; }, [processFiles], ); return (
Drop PDF files here
or
{entries.length > 0 && (
    {entries.map((entry, i) => (
  • {entry.file.name} — {entry.status} {entry.error ? `: ${entry.error}` : ""}
  • ))}
)}
); }