import { FileText } from "lucide-react"; import * as pdfjs from "pdfjs-dist"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "@/contexts/i18n-context"; import { useFileStore } from "@/stores/file-store"; pdfjs.GlobalWorkerOptions.workerSrc = new URL( "pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url, ).href; /** pdf.js canvas viewer for the document display mode (spec 4.6). */ export function DocumentView({ inputOnly = false }: { inputOnly?: boolean } = {}) { const { t } = useTranslation(); const entry = useFileStore((s) => s.entries[s.selectedIndex]); const canvasRef = useRef(null); const [page, setPage] = useState(1); const [pageCount, setPageCount] = useState(0); const [error, setError] = useState(null); // inputOnly keeps the original input on screen for tools whose output is not // a previewable PDF (e.g. ocr-pdf, whose output is a .txt transcript). const hasProcessedUrl = !inputOnly && !!entry?.processedUrl; const src = inputOnly ? entry?.blobUrl : (entry?.processedUrl ?? entry?.blobUrl); // F6: detect non-PDF input files (word, excel, html, markdown, etc.) const isPdfInput = entry?.file?.name?.toLowerCase().endsWith(".pdf") ?? false; const showInputFallback = !isPdfInput && !hasProcessedUrl; /* A+B: reset pagination and clear stale errors when the document changes. src is intentionally a trigger-only dep (not read inside the callback). */ // biome-ignore lint/correctness/useExhaustiveDependencies: src is the trigger useEffect(() => { setPage(1); setPageCount(0); setError(null); }, [src]); /* C+D: cancel in-flight renders and destroy the doc proxy on cleanup. */ useEffect(() => { if (!canvasRef.current || showInputFallback) return; // F22: when processedUrl is available, use URL-based loading instead of // entry.file (which is the original non-PDF input and would fail pdf.js) const file = hasProcessedUrl ? undefined : entry?.file; const url = src; if (!file && !url) return; let cancelled = false; let doc: pdfjs.PDFDocumentProxy | undefined; let renderTask: pdfjs.RenderTask | undefined; (async () => { try { const source = file ? { data: new Uint8Array(await file.arrayBuffer()) } : { url }; doc = await pdfjs.getDocument(source).promise; if (cancelled) return; setPageCount(doc.numPages); const pdfPage = await doc.getPage(Math.min(page, doc.numPages)); if (cancelled) return; const viewport = pdfPage.getViewport({ scale: 1.2 }); const canvas = canvasRef.current; if (!canvas) return; canvas.width = viewport.width; canvas.height = viewport.height; renderTask = pdfPage.render({ canvas, viewport }); await renderTask.promise; } catch (err) { if (!cancelled) { setError(err instanceof Error ? err.message : String(err)); } } })(); return () => { cancelled = true; renderTask?.cancel(); doc?.loadingTask.destroy(); }; }, [src, page, entry?.file, showInputFallback, hasProcessedUrl]); if (!entry) return null; // F6: graceful fallback for non-PDF input files if (showInputFallback) { const ext = entry.file?.name?.split(".").pop()?.toUpperCase() ?? ""; return (
{ext &&

{ext}

}

{t.tools.documentView.inputNotPreviewable}

); } return (
{error &&

{t.tools.documentView.loadFailed}

} {!error && pageCount > 1 && (
{page} / {pageCount}
)}
); }