diff --git a/src/app/(app)/pdf/[id]/page.tsx b/src/app/(app)/pdf/[id]/page.tsx index 32abacf..1cbb1f1 100644 --- a/src/app/(app)/pdf/[id]/page.tsx +++ b/src/app/(app)/pdf/[id]/page.tsx @@ -37,6 +37,8 @@ const PDFViewer = dynamic( } ); +const PARSE_LOADER_EXPAND_DELAY_MS = 320; + export default function PDFViewerPage() { const canExportAudiobook = useFeatureFlag('enableAudiobookExport'); const { id } = useParams(); @@ -66,6 +68,7 @@ export default function PDFViewerPage() { const [zoomLevel, setZoomLevel] = useState(100); const [activeSidebar, setActiveSidebar] = useState(null); const [showForceReparseConfirm, setShowForceReparseConfirm] = useState(false); + const [showDetailedParseLoader, setShowDetailedParseLoader] = useState(false); const [containerHeight, setContainerHeight] = useState('auto'); const inFlightDocIdRef = useRef(null); const loadedDocIdRef = useRef(null); @@ -75,6 +78,8 @@ export default function PDFViewerPage() { const parseState = parseStatus ?? 'pending'; const isParseReady = parseState === 'ready'; const forceReparseDisabled = isForceReparseDisabled(parseStatus); + const shouldShowExpandedParseLoader = !isLoading + && (parseState === 'pending' || parseState === 'running' || parseState === 'failed'); useEffect(() => { setIsLoading(true); @@ -163,6 +168,22 @@ export default function PDFViewerPage() { stop(); }, [isLoading, isParseReady, stop]); + useEffect(() => { + if (!shouldShowExpandedParseLoader) { + // Keep the current loader variant stable during the final + // parse-ready -> first-frame handoff to avoid a visual flash. + if (!isLoading && isParseReady && !isPdfViewerReady) { + return; + } + setShowDetailedParseLoader(false); + return; + } + const timeout = window.setTimeout(() => { + setShowDetailedParseLoader(true); + }, PARSE_LOADER_EXPAND_DELAY_MS); + return () => window.clearTimeout(timeout); + }, [shouldShowExpandedParseLoader, id, isLoading, isParseReady, isPdfViewerReady]); + // Compute available height = viewport - (header height + tts bar height) useEffect(() => { const compute = () => { @@ -260,6 +281,13 @@ export default function PDFViewerPage() { } const renderPdfStatusLoader = () => { + const compactLabel = isLoading + ? 'Opening PDF...' + : (parseState === 'ready' ? 'Rendering pages...' : 'Checking parse state...'); + const compactSubLabel = isLoading + ? 'Loading document data' + : (parseState === 'ready' ? 'Preparing first frame' : 'Preparing layout parser'); + const totalPages = parseProgress?.totalPages ?? 0; const pagesParsed = parseProgress?.pagesParsed ?? 0; const progressPercent = totalPages > 0 @@ -272,16 +300,12 @@ export default function PDFViewerPage() { let statusSubText = 'Initializing document renderer'; if (!isLoading) { if (parseState === 'pending') { - statusText = parseProgress - ? `Page ${Math.max(0, parseProgress.pagesParsed)} / ${parseProgress.totalPages} parsed` - : 'Preparing PDF layout...'; + statusText = 'Preparing PDF layout...'; statusSubText = parseProgress?.phase === 'merge' ? 'Finalizing stitched block structure' : 'Queueing parser and preparing page extraction'; } else if (parseState === 'running') { - statusText = parseProgress - ? `Page ${Math.max(0, parseProgress.pagesParsed)} / ${parseProgress.totalPages} parsed` - : 'Parsing PDF layout blocks...'; + statusText = 'Parsing PDF layout blocks...'; statusSubText = parseProgress?.phase === 'merge' ? 'Merging cross-page sections' : 'Inferring reading order and text regions'; @@ -291,81 +315,77 @@ export default function PDFViewerPage() { } } - const stageOneComplete = !isLoading; - const stageTwoActive = parseState === 'running' && !isMerging; - const stageTwoComplete = (parseState === 'running' && isMerging) || parseState === 'ready'; - const stageThreeActive = parseState === 'running' && isMerging; - const stageThreeComplete = parseState === 'ready'; + const stageLabel = parseState === 'failed' + ? 'Stage: blocked' + : (parseState === 'pending' + ? 'Stage: prepare' + : (isMerging ? 'Stage: merge' : 'Stage: infer')); return (
-
-
-
+
+ {showDetailedParseLoader ? ( +
+
+
+
+
+ + PDF Layout Parse +
+

{statusText}

+
+ +
+
+

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

+

{stageLabel}

+
+
+
+
+

+ {hasMeasuredProgress ? `${Math.round(progressPercent)}% complete` : statusSubText} +

+
+ + {!isLoading && parseState === 'failed' ? ( +
+ +
+ ) : null} +
+
+ ) : ( +
+
-

PDF Layout Parse

-

{statusText}

-

{statusSubText}

+

{compactLabel}

+

{compactSubLabel}

-
+ - - {parseState === 'failed' ? 'blocked' : (isMerging ? 'merge' : 'infer')} - -
+
- -
-
- Progress - - {hasMeasuredProgress ? `${Math.round(progressPercent)}%` : 'Starting'} - -
-
-
-
-
- {hasMeasuredProgress ? `Page ${pagesParsed}/${totalPages}` : 'Awaiting first page'} - - {isMerging ? 'Cross-page merge' : 'Page inference'} -
+
+ + +
- -
-
- - - Prepare - - - - Infer - - - - Merge - -
-
- - {!isLoading && parseState === 'failed' ? ( -
- -
- ) : null} -
+ )}
); diff --git a/src/components/views/PDFViewer.tsx b/src/components/views/PDFViewer.tsx index 947f9c7..6ac12de 100644 --- a/src/components/views/PDFViewer.tsx +++ b/src/components/views/PDFViewer.tsx @@ -39,6 +39,7 @@ interface PDFOnLinkClickArgs { export function PDFViewer({ zoomLevel, onDocumentReady, pdfState }: PDFViewerProps) { const containerRef = useRef(null); const [isPageRendering, setIsPageRendering] = useState(false); + const hasSignaledReadyRef = useRef(false); const scaleRef = useRef(1); const { containerWidth, containerHeight } = usePDFResize(containerRef); const sentenceHighlightSeqRef = useRef(0); @@ -99,6 +100,16 @@ export function PDFViewer({ zoomLevel, onDocumentReady, pdfState }: PDFViewerPro } }, [layoutKey]); + const markViewerReady = useCallback(() => { + if (hasSignaledReadyRef.current) return; + hasSignaledReadyRef.current = true; + onDocumentReady?.(); + }, [onDocumentReady]); + + useEffect(() => { + hasSignaledReadyRef.current = false; + }, [currDocId, currDocData]); + const clearSentenceHighlightTimeouts = useCallback(() => { for (const t of sentenceHighlightTimeoutsRef.current) clearTimeout(t); sentenceHighlightTimeoutsRef.current = []; @@ -500,7 +511,6 @@ export function PDFViewer({ zoomLevel, onDocumentReady, pdfState }: PDFViewerPro file={documentFile} onLoadSuccess={(pdf) => { onDocumentLoadSuccess(pdf); - onDocumentReady?.(); }} onItemClick={(args: PDFOnLinkClickArgs) => { if (args?.pageNumber) { @@ -531,6 +541,7 @@ export function PDFViewer({ zoomLevel, onDocumentReady, pdfState }: PDFViewerPro onRenderSuccess={() => { lastRenderedLayoutKeyRef.current = layoutKey; setIsPageRendering(false); + markViewerReady(); }} onLoadSuccess={(page) => { setPageWidth(page.originalWidth); @@ -556,6 +567,7 @@ export function PDFViewer({ zoomLevel, onDocumentReady, pdfState }: PDFViewerPro onRenderSuccess={() => { lastRenderedLayoutKeyRef.current = layoutKey; setIsPageRendering(false); + markViewerReady(); }} onLoadSuccess={(page) => { setPageWidth(page.originalWidth); @@ -577,6 +589,7 @@ export function PDFViewer({ zoomLevel, onDocumentReady, pdfState }: PDFViewerPro onRenderSuccess={() => { lastRenderedLayoutKeyRef.current = layoutKey; setIsPageRendering(false); + markViewerReady(); }} onLoadSuccess={(page) => { setPageWidth(page.originalWidth);