diff --git a/src/app/(app)/pdf/[id]/page.tsx b/src/app/(app)/pdf/[id]/page.tsx index cd79988..91d36c6 100644 --- a/src/app/(app)/pdf/[id]/page.tsx +++ b/src/app/(app)/pdf/[id]/page.tsx @@ -326,7 +326,13 @@ export default function PDFViewerPage() { {/* header: status badge + model attribution */}
- + {parseUiState === 'failed' ? ( + + + + ) : ( + + )} PDF Layout Parse
@@ -336,33 +342,36 @@ export default function PDFViewerPage() {
- {/* animated layout scanner */} + {/* animated layout scanner — static "halted" view when failed */}
- +
{/* live status + progress */} -
- {parseUiState === 'failed' ? ( -

{statusText}

- ) : null} - -
-

- {hasMeasuredProgress ? `Page ${pagesParsed} / ${totalPages}` : 'Awaiting first page'} + {parseUiState === 'failed' ? ( +

+

{statusText}

+

{stageLabel}

+
+ ) : ( +
+
+

+ {hasMeasuredProgress ? `Page ${pagesParsed} / ${totalPages}` : 'Awaiting first page'} +

+

{stageLabel}

+
+
+
+
+

+ {hasMeasuredProgress ? `${Math.round(progressPercent)}% complete` : 'Calibrating layout pass'}

-

{stageLabel}

-
-
-
-

- {hasMeasuredProgress ? `${Math.round(progressPercent)}% complete` : 'Calibrating layout pass'} -

-
+ )}
{/* attribution footer */} diff --git a/src/components/reader/PdfLayoutScan.module.css b/src/components/reader/PdfLayoutScan.module.css index 5b46a19..0c44b79 100644 --- a/src/components/reader/PdfLayoutScan.module.css +++ b/src/components/reader/PdfLayoutScan.module.css @@ -58,6 +58,28 @@ to { opacity: 1; transform: translateY(0) scale(1); } } +/* ── failed / halted state ────────────────────────────────────────────── */ +.tagFailed { + color: var(--foreground); + background: color-mix(in srgb, var(--foreground) 12%, transparent); + box-shadow: none; + animation: none; +} + +.pageFailed { + display: grid; + place-items: center; + opacity: 0.55; + filter: grayscale(0.4); +} +.alert { + width: 38%; + height: 38%; + stroke: var(--accent); + stroke-width: 1.6; + color: var(--accent); +} + /* ── region boxes (one shown at a time, centered) ─────────────────────── */ .solos { position: absolute; diff --git a/src/components/reader/PdfLayoutScan.tsx b/src/components/reader/PdfLayoutScan.tsx index 9314065..7f46d00 100644 --- a/src/components/reader/PdfLayoutScan.tsx +++ b/src/components/reader/PdfLayoutScan.tsx @@ -14,6 +14,10 @@ import styles from './PdfLayoutScan.module.css'; * (see PARSED_PDF_BLOCK_KINDS in types/parsed-pdf). Purely decorative; honours * prefers-reduced-motion by freezing on a single region. Styles live in the * adjacent CSS module so they stay out of the global stylesheet. + * + * When `failed` is set the animation is replaced by a static "halted" view — + * the beam stops, the page dims, and an alert glyph sits on it — so the loader + * never implies active work after a parse failure. */ // Only a handful of real shapes — most regions are just text lines. @@ -92,18 +96,44 @@ function BlockContent({ shape }: { shape: BlockShape }) { ); } -export function PdfLayoutScan() { +export function PdfLayoutScan({ failed = false }: { failed?: boolean }) { const [active, setActive] = useState(0); useEffect(() => { - if (typeof window === 'undefined') return; + if (typeof window === 'undefined' || failed) return; // no cycling once halted const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; if (reduced) return; // freeze on the first region for reduced-motion users const id = window.setInterval(() => { setActive((i) => (i + 1) % SCAN_BLOCKS.length); }, STEP_MS); return () => window.clearInterval(id); - }, []); + }, [failed]); + + if (failed) { + return ( +
+
+ Parse halted +
+ +
+ + + + + + + + + + +
+
+ ); + } return (