/** * UploadDropzone — drag-drop + file-picker for uploading documents into the * active session (PDF, HTML, Markdown). */ import { useCallback, useRef, useState } from "react"; import { ingestHtmlFromFile, ingestMarkdownFromFile, ingestPdfFromFile, } from "@source/index"; import { useActiveDocumentId, useEngine, usePdfByteStore, } from "./EngineContext"; type UploadFormat = "pdf" | "html" | "markdown" | "unsupported"; 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; } function detectFormat(file: File): UploadFormat { const name = file.name.toLowerCase(); if (file.type === "application/pdf" || name.endsWith(".pdf")) return "pdf"; if (file.type === "text/html" || name.endsWith(".html") || name.endsWith(".htm")) { return "html"; } if ( file.type === "text/markdown" || name.endsWith(".md") || name.endsWith(".markdown") ) { return "markdown"; } return "unsupported"; } 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 format = detectFormat(file); if (format === "unsupported") { return { file, status: "error", error: "Unsupported format (PDF, HTML, or Markdown only)", }; } return { file, status: "queued" }; }); 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 format = detectFormat(entry.file); const ingested = await (format === "html" ? ingestHtmlFromFile(entry.file, byteStore) : format === "markdown" ? ingestMarkdownFromFile(entry.file, byteStore) : ingestPdfFromFile(entry.file, byteStore)); engine.documents.register(ingested); entry.status = "done"; lastDocumentId = ingested.document.id; onUploaded?.(ingested.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); e.target.value = ""; }, [processFiles], ); return (
Drop PDF, HTML, or Markdown files here
or
{entries.length > 0 && (
    {entries.map((entry, i) => (
  • {entry.file.name} — {entry.status} {entry.error ? `: ${entry.error}` : ""}
  • ))}
)}
); }