Add force-reparse confirmation flow in PDF UI
This commit is contained in:
parent
10748c7fcd
commit
dd494d50aa
4 changed files with 66 additions and 4 deletions
|
|
@ -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<number>(100);
|
||||
const [activeSidebar, setActiveSidebar] = useState<null | 'settings' | 'audiobook' | 'segments'>(null);
|
||||
const [showForceReparseConfirm, setShowForceReparseConfirm] = useState(false);
|
||||
const [containerHeight, setContainerHeight] = useState<string>('auto');
|
||||
const inFlightDocIdRef = useRef<string | null>(null);
|
||||
const loadedDocIdRef = useRef<string | null>(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() {
|
|||
<div className="flex justify-start">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => forceReparseParsedPdf()}
|
||||
onClick={requestForceReparse}
|
||||
className="inline-flex items-center rounded-md border border-offbase bg-offbase px-2.5 py-1 text-xs text-foreground hover:text-accent transition-colors"
|
||||
>
|
||||
Retry Parse
|
||||
|
|
@ -445,9 +464,18 @@ export default function PDFViewerPage() {
|
|||
},
|
||||
});
|
||||
},
|
||||
onForceReparse: () => forceReparseParsedPdf(),
|
||||
onForceReparse: requestForceReparse,
|
||||
}}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isOpen={showForceReparseConfirm}
|
||||
onClose={() => setShowForceReparseConfirm(false)}
|
||||
onConfirm={confirmForceReparse}
|
||||
title={FORCE_REPARSE_CONFIRM_TITLE}
|
||||
message={FORCE_REPARSE_CONFIRM_MESSAGE}
|
||||
confirmText={FORCE_REPARSE_CONFIRM_TEXT}
|
||||
cancelText="Cancel"
|
||||
/>
|
||||
<SegmentsSidebar
|
||||
isOpen={activeSidebar === 'segments'}
|
||||
setIsOpen={(isOpen) => setActiveSidebar((prev) => isOpen ? 'segments' : (prev === 'segments' ? null : prev))}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
import { Section, ToggleRow, CheckItem, buttonClass, segmentedButtonClass, segmentedGroupClass } from '@/components/formPrimitives';
|
||||
import { RefreshIcon, SparkleIcon } from '@/components/icons/Icons';
|
||||
import type { ParsedPdfBlockKind, PdfParseStatus } from '@/types/parsed-pdf';
|
||||
import { isForceReparseDisabled } from '@/lib/client/pdf/force-reparse';
|
||||
|
||||
const PDF_SKIP_KIND_OPTIONS: Array<{ kind: ParsedPdfBlockKind; label: string }> = [
|
||||
{ kind: 'header', label: 'Header' },
|
||||
|
|
@ -221,10 +222,10 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: {
|
|||
type="button"
|
||||
className={buttonClass({ variant: 'ghost', size: 'icon', className: '!h-5 !w-5 hover:scale-[1.1] shrink-0' })}
|
||||
onClick={pdf.onForceReparse}
|
||||
disabled={pdf.parseStatus === 'running' || pdf.parseStatus === 'pending'}
|
||||
disabled={isForceReparseDisabled(pdf.parseStatus)}
|
||||
title="Force reparse"
|
||||
>
|
||||
<RefreshIcon className={`h-3 w-3 ${pdf.parseStatus === 'running' || pdf.parseStatus === 'pending' ? 'animate-spin' : ''}`} />
|
||||
<RefreshIcon className={`h-3 w-3 ${isForceReparseDisabled(pdf.parseStatus) ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
9
src/lib/client/pdf/force-reparse.ts
Normal file
9
src/lib/client/pdf/force-reparse.ts
Normal file
|
|
@ -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';
|
||||
}
|
||||
24
tests/unit/pdf-force-reparse.spec.ts
Normal file
24
tests/unit/pdf-force-reparse.spec.ts
Normal file
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue