diff --git a/src/app/(app)/pdf/[id]/page.tsx b/src/app/(app)/pdf/[id]/page.tsx index 52b6f9c..32abacf 100644 --- a/src/app/(app)/pdf/[id]/page.tsx +++ b/src/app/(app)/pdf/[id]/page.tsx @@ -10,6 +10,7 @@ import { DocumentHeaderMenu } from '@/components/documents/DocumentHeaderMenu'; import { SegmentsSidebar } from '@/components/reader/SegmentsSidebar'; import { Header } from '@/components/Header'; import { AudiobookExportModal } from '@/components/AudiobookExportModal'; +import { ConfirmDialog } from '@/components/ConfirmDialog'; import type { TTSAudiobookChapter } from '@/types/tts'; import type { AudiobookGenerationSettings } from '@/types/client'; import TTSPlayer from '@/components/player/TTSPlayer'; @@ -19,6 +20,12 @@ import { RateLimitBanner } from '@/components/auth/RateLimitBanner'; import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext'; import { useFeatureFlag } from '@/contexts/RuntimeConfigContext'; import { LoadingSpinner } from '@/components/Spinner'; +import { + FORCE_REPARSE_CONFIRM_MESSAGE, + FORCE_REPARSE_CONFIRM_TEXT, + FORCE_REPARSE_CONFIRM_TITLE, + isForceReparseDisabled, +} from '@/lib/client/pdf/force-reparse'; import { usePdfDocument } from './usePdfDocument'; // Dynamic import for client-side rendering only @@ -58,6 +65,7 @@ export default function PDFViewerPage() { const [isPdfViewerReady, setIsPdfViewerReady] = useState(false); const [zoomLevel, setZoomLevel] = useState(100); const [activeSidebar, setActiveSidebar] = useState(null); + const [showForceReparseConfirm, setShowForceReparseConfirm] = useState(false); const [containerHeight, setContainerHeight] = useState('auto'); const inFlightDocIdRef = useRef(null); const loadedDocIdRef = useRef(null); @@ -66,6 +74,7 @@ export default function PDFViewerPage() { const [isNavigatingBack, setIsNavigatingBack] = useState(false); const parseState = parseStatus ?? 'pending'; const isParseReady = parseState === 'ready'; + const forceReparseDisabled = isForceReparseDisabled(parseStatus); useEffect(() => { setIsLoading(true); @@ -198,6 +207,16 @@ export default function PDFViewerPage() { }, delayMs); }, [isNavigatingBack, stop, activeSidebar, router]); + const requestForceReparse = useCallback(() => { + if (forceReparseDisabled) return; + setShowForceReparseConfirm(true); + }, [forceReparseDisabled]); + + const confirmForceReparse = useCallback(() => { + setShowForceReparseConfirm(false); + void forceReparseParsedPdf(); + }, [forceReparseParsedPdf]); + const handleGenerateAudiobook = useCallback(async ( onProgress: (progress: number) => void, signal: AbortSignal, @@ -339,7 +358,7 @@ export default function PDFViewerPage() {
diff --git a/src/lib/client/pdf/force-reparse.ts b/src/lib/client/pdf/force-reparse.ts new file mode 100644 index 0000000..425976d --- /dev/null +++ b/src/lib/client/pdf/force-reparse.ts @@ -0,0 +1,9 @@ +import type { PdfParseStatus } from '@/types/parsed-pdf'; + +export const FORCE_REPARSE_CONFIRM_TITLE = 'Reparse PDF Layout?'; +export const FORCE_REPARSE_CONFIRM_MESSAGE = 'This reruns page layout parsing from scratch and can take a while on large documents.'; +export const FORCE_REPARSE_CONFIRM_TEXT = 'Reparse Now'; + +export function isForceReparseDisabled(status: PdfParseStatus | null): boolean { + return status === 'pending' || status === 'running'; +} diff --git a/tests/unit/pdf-force-reparse.spec.ts b/tests/unit/pdf-force-reparse.spec.ts new file mode 100644 index 0000000..95c2ce4 --- /dev/null +++ b/tests/unit/pdf-force-reparse.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test'; +import { + FORCE_REPARSE_CONFIRM_MESSAGE, + FORCE_REPARSE_CONFIRM_TEXT, + FORCE_REPARSE_CONFIRM_TITLE, + isForceReparseDisabled, +} from '../../src/lib/client/pdf/force-reparse'; + +test.describe('pdf force reparse controls', () => { + test('disables action while parse is pending or running', () => { + expect(isForceReparseDisabled('pending')).toBeTruthy(); + expect(isForceReparseDisabled('running')).toBeTruthy(); + expect(isForceReparseDisabled('ready')).toBeFalsy(); + expect(isForceReparseDisabled('failed')).toBeFalsy(); + expect(isForceReparseDisabled(null)).toBeFalsy(); + }); + + test('confirmation copy warns about expensive rerun', () => { + expect(FORCE_REPARSE_CONFIRM_TITLE).toContain('Reparse'); + expect(FORCE_REPARSE_CONFIRM_TEXT).toContain('Reparse'); + expect(FORCE_REPARSE_CONFIRM_MESSAGE.toLowerCase()).toContain('from scratch'); + expect(FORCE_REPARSE_CONFIRM_MESSAGE.toLowerCase()).toContain('take a while'); + }); +});