import React, { useCallback, useEffect, useState } from 'react'; import Layout from './components/Layout'; import FileLoader from './components/FileLoader'; import ReorderPanel from './components/ReorderPanel'; import ActionsPanel from './components/ActionsPanel'; import PagePreviewModal from './components/PagePreviewModal'; import WorkspacePanel from './components/WorkspacePanel'; import ActionDialog, { type ActionDialogAction, } from './components/ActionDialog'; import HelpDialog from './components/HelpDialog'; import { PDFDocument } from 'pdf-lib'; import type { StoredWorkspace, WorkspaceSummary, } from './workspace/workspaceTypes'; import { createInitialPageRefs, createPageRefId, createPdfId, createWorkspaceId, defaultWorkspaceNameFromPdfName, normalizeRotation, useWorkspaceState, } from './workspace/useWorkspaceState'; import { deleteWorkspaceFromIndexedDb, listWorkspaces, loadWorkspaceFromIndexedDb, saveWorkspaceToIndexedDb, } from './workspace/workspaceDb'; import type { PageRef, PdfFile } from './pdf/pdfTypes'; import { loadPdfFromFile, mergePdfFiles, splitIntoSinglePages, exportPages, } from './pdf/pdfService'; import { usePdfThumbnails } from './pdf/usePdfThumbnails'; import { usePdfGeneratedOutputs } from './hooks/usePdfGeneratedOutputs'; import { createSplitResultsZip, createSplitZipFilename, } from './pdf/pdfZipService'; import { createSelectionPdfName, createSelectionWorkspaceName, getSelectedPagesInVisualOrder, } from './workspace/workspaceSelection'; function isEditableKeyboardTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) return false; const tagName = target.tagName.toLowerCase(); return ( target.isContentEditable || tagName === 'input' || tagName === 'textarea' || tagName === 'select' ); } const App: React.FC = () => { const [actionDialog, setActionDialog] = useState<{ title: string; content: React.ReactNode; actions: ActionDialogAction[]; } | null>(null); const [helpOpen, setHelpOpen] = useState(false); const [pdf, setPdf] = useState(null); const [isBusy, setIsBusy] = useState(false); const [error, setError] = useState(null); const [workspaces, setWorkspaces] = useState([]); const [activeWorkspaceId, setActiveWorkspaceId] = useState( null ); const [workspaceName, setWorkspaceName] = useState(''); const [previewPageId, setPreviewPageId] = useState(null); const [pendingFile, setPendingFile] = useState(null); const [showMergeOptions, setShowMergeOptions] = useState(false); const [mergeMode, setMergeMode] = useState< 'overwrite' | 'append' | 'insertAt' >('append'); const [mergeInsertAt, setMergeInsertAt] = useState(''); const { splitDownloads, splitZipDownload, subsetDownload, exportDownload, replaceSplitResults, replaceSubsetResult, replaceExportResult, clearAllResults: clearGeneratedOutputs, } = usePdfGeneratedOutputs(); const handleWorkspaceContentChanged = useCallback(() => { clearGeneratedOutputs(); }, [clearGeneratedOutputs]); const { pages, selectedPageIds, setSelectedPageIds, lastSelectedVisualIndex, setLastSelectedVisualIndex, workspaceDirty, setWorkspaceDirty, workspaceMessage, setWorkspaceMessage, workspaceHistory, redoHistory, getCurrentCommandState, createWorkspaceCommand, executeWorkspaceCommand, handleUndo, handleRedo, replaceWorkspaceState, resetWorkspaceState: resetWorkspaceCommandState, } = useWorkspaceState({ onContentChanged: handleWorkspaceContentChanged }); const handleThumbnailError = useCallback( (message: string, thrown: unknown) => { console.error(thrown); setError(message); }, [] ); const { thumbnails: reorderThumbnails, clearThumbnailCache } = usePdfThumbnails({ pdf, pages, onError: handleThumbnailError, }); const closeActionDialog = useCallback(() => { setActionDialog(null); }, []); const openActionDialog = useCallback( (dialog: { title: string; content: React.ReactNode; actions: ActionDialogAction[]; }) => { setActionDialog(dialog); }, [] ); const refreshWorkspaces = async () => { try { const summaries = await listWorkspaces(); setWorkspaces(summaries); } catch (e) { console.error(e); setError('Failed to read saved workspaces from browser storage.'); } }; useEffect(() => { void refreshWorkspaces(); }, []); const resetWorkspaceState = () => { setPdf(null); setActiveWorkspaceId(null); setWorkspaceName(''); resetWorkspaceCommandState(); clearGeneratedOutputs(); clearThumbnailCache(); setPreviewPageId(null); }; const handleSaveWorkspace = async (): Promise => { if (!pdf || pages.length === 0) return false; setError(null); const now = new Date().toISOString(); const name = workspaceName.trim() || defaultWorkspaceNameFromPdfName(pdf.name); const workspaceId = activeWorkspaceId ?? createWorkspaceId(); const existing = workspaces.find( (workspace) => workspace.id === workspaceId ); const workspace: StoredWorkspace = { schemaVersion: 1, id: workspaceId, name, createdAt: existing?.createdAt ?? now, updatedAt: now, pdfId: pdf.id, pdfName: pdf.name, sourcePageCount: pdf.pageCount, pages, selectedPageIds, history: workspaceHistory, redoHistory, }; setIsBusy(true); try { await saveWorkspaceToIndexedDb({ workspace, pdfArrayBuffer: pdf.arrayBuffer, }); setActiveWorkspaceId(workspaceId); setWorkspaceName(name); setWorkspaceDirty(false); setWorkspaceMessage(`Workspace "${name}" saved.`); await refreshWorkspaces(); return true; } catch (e) { console.error(e); setError( 'Failed to save workspace. The browser storage quota may be full.' ); return false; } finally { setIsBusy(false); } }; const performResetWorkspace = () => { resetWorkspaceState(); }; const handleResetWorkspace = () => { if (!pdf) return; if (!workspaceDirty) { performResetWorkspace(); return; } openActionDialog({ title: 'Reset workspace?', content: ( <>

This workspace has unsaved changes.

Do you want to save it before resetting?

), actions: [ { label: 'Cancel', variant: 'secondary', onClick: closeActionDialog, }, { label: 'Reset without saving', variant: 'danger', onClick: () => { closeActionDialog(); performResetWorkspace(); }, }, { label: 'Save and reset', variant: 'primary', autoFocus: true, onClick: async () => { closeActionDialog(); const saved = await handleSaveWorkspace(); if (saved) { performResetWorkspace(); } }, }, ], }); }; const handleLoadWorkspace = async (workspaceId: string) => { setError(null); setIsBusy(true); try { const loaded = await loadWorkspaceFromIndexedDb(workspaceId); if (!loaded) { setError('Workspace not found.'); await refreshWorkspaces(); return; } clearGeneratedOutputs(); const doc = await PDFDocument.load(loaded.pdfArrayBuffer); const loadedPdf: PdfFile = { id: loaded.workspace.pdfId, name: loaded.workspace.pdfName, pageCount: doc.getPageCount(), arrayBuffer: loaded.pdfArrayBuffer, doc, }; setPdf(loadedPdf); replaceWorkspaceState({ pages: loaded.workspace.pages, selectedPageIds: loaded.workspace.selectedPageIds ?? [], lastSelectedVisualIndex: null, history: loaded.workspace.history ?? [], redoHistory: loaded.workspace.redoHistory ?? [], dirty: false, message: `Workspace "${loaded.workspace.name}" loaded.`, }); setPreviewPageId(null); clearThumbnailCache(); setActiveWorkspaceId(loaded.workspace.id); setWorkspaceName(loaded.workspace.name); } catch (e) { console.error(e); setError('Failed to load workspace from browser storage.'); } finally { setIsBusy(false); } }; const handleDeleteWorkspace = (workspaceId: string) => { const workspace = workspaces.find((item) => item.id === workspaceId); const name = workspace?.name ?? 'this workspace'; openActionDialog({ title: 'Delete workspace?', content: ( <>

Delete the saved workspace {name} from this browser?

The currently open in-memory document will not be closed, but the saved workspace entry will be removed.

), actions: [ { label: 'Cancel', variant: 'secondary', onClick: closeActionDialog, }, { label: 'Delete workspace', variant: 'danger', autoFocus: true, onClick: () => { closeActionDialog(); void performDeleteWorkspace(workspaceId); }, }, ], }); }; const performDeleteWorkspace = async (workspaceId: string) => { setError(null); try { await deleteWorkspaceFromIndexedDb(workspaceId); if (activeWorkspaceId === workspaceId) { setActiveWorkspaceId(null); setWorkspaceDirty(true); setWorkspaceMessage( 'Saved workspace deleted. Current in-memory document remains open.' ); } await refreshWorkspaces(); } catch (e) { console.error(e); setError('Failed to delete workspace.'); } }; const loadFileAsNew = async (file: File) => { setError(null); resetWorkspaceState(); setIsBusy(true); try { const loaded = await loadPdfFromFile(file); const initialPages = createInitialPageRefs(loaded.pageCount); setPdf(loaded); replaceWorkspaceState({ pages: initialPages, selectedPageIds: [], lastSelectedVisualIndex: null, history: [], redoHistory: [], dirty: true, message: null, }); setWorkspaceName(defaultWorkspaceNameFromPdfName(loaded.name)); } catch (e) { console.error(e); setError('Failed to load PDF (see console).'); } finally { setIsBusy(false); } }; const handleFileLoaded = (file: File) => { if (!pdf || pages.length === 0) { void loadFileAsNew(file); } else { setPendingFile(file); setShowMergeOptions(true); setMergeMode('append'); setMergeInsertAt(String(pages.length + 1)); } }; const handleMergeCancel = () => { setPendingFile(null); setShowMergeOptions(false); }; const handleMergeConfirm = async () => { if (!pendingFile) return; if (!pdf || mergeMode === 'overwrite') { await loadFileAsNew(pendingFile); setPendingFile(null); setShowMergeOptions(false); return; } setError(null); setIsBusy(true); try { // 1) Materialize the current in-memory workspace (page refs + rotations) const currentBlob = await exportPages(pdf, pages); const currentArrayBuffer = await currentBlob.arrayBuffer(); const currentDoc = await PDFDocument.load(currentArrayBuffer); const currentPdf: PdfFile = { id: pdf.id, name: pdf.name, doc: currentDoc, arrayBuffer: currentArrayBuffer, pageCount: pages.length, }; // 2) Load the new PDF const newPdf = await loadPdfFromFile(pendingFile); // 3) Determine insert position (0-based) let insertAt = pages.length; // default: append at end if (mergeMode === 'insertAt') { const parsed = parseInt(mergeInsertAt, 10); if (Number.isFinite(parsed)) { insertAt = Math.min(Math.max(parsed - 1, 0), pages.length); } } else if (mergeMode === 'append') { insertAt = pages.length; } // 4) Merge const mergedPdf = await mergePdfFiles(currentPdf, newPdf, insertAt); const mergedPages = createInitialPageRefs(mergedPdf.pageCount); // 5) Reset state to the merged document setPdf(mergedPdf); replaceWorkspaceState({ pages: mergedPages, selectedPageIds: [], lastSelectedVisualIndex: null, history: [], redoHistory: [], dirty: true, message: null, }); clearGeneratedOutputs(); clearThumbnailCache(); setPreviewPageId(null); setWorkspaceName(defaultWorkspaceNameFromPdfName(mergedPdf.name)); setActiveWorkspaceId(null); } catch (e) { console.error(e); setError('Failed to merge PDF (see console).'); } finally { setIsBusy(false); setPendingFile(null); setShowMergeOptions(false); } }; useEffect(() => { if ( previewPageId != null && !pages.some((page) => page.id === previewPageId) ) { setPreviewPageId(null); } }, [previewPageId, pages]); const hasPdf = !!pdf; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (isEditableKeyboardTarget(e.target)) return; if (e.key === 'F1' || e.key === '?') { e.preventDefault(); setHelpOpen(true); } }; window.addEventListener('keydown', handleKeyDown); return () => { window.removeEventListener('keydown', handleKeyDown); }; }, []); // === UI interactions === const handleRotatePageClockwise = (pageId: string) => { const before = getCurrentCommandState(); const afterPages = pages.map((page) => page.id === pageId ? { ...page, rotation: (normalizeRotation(page.rotation) + 90) % 360 } : page ); executeWorkspaceCommand( createWorkspaceCommand({ type: 'page.rotate', label: 'Rotated page clockwise', before, after: { ...before, pages: afterPages, }, details: { pageId, degrees: 90, }, }) ); }; const handleRotatePageCounterclockwise = (pageId: string) => { const before = getCurrentCommandState(); const afterPages = pages.map((page) => page.id === pageId ? { ...page, rotation: (normalizeRotation(page.rotation) + 270) % 360 } : page ); executeWorkspaceCommand( createWorkspaceCommand({ type: 'page.rotate', label: 'Rotated page counterclockwise', before, after: { ...before, pages: afterPages, }, details: { pageId, degrees: -90, }, }) ); }; const handleDeletePage = (pageId: string) => { const page = pages.find((item) => item.id === pageId); const visualIndex = page ? pages.indexOf(page) : -1; const pageLabel = visualIndex >= 0 ? `page at position ${visualIndex + 1}` : 'this page'; openActionDialog({ title: 'Delete page?', content: (

Delete {pageLabel} from the current workspace?

), actions: [ { label: 'Cancel', variant: 'secondary', onClick: closeActionDialog, }, { label: 'Delete page', variant: 'danger', autoFocus: true, onClick: () => { closeActionDialog(); performDeletePage(pageId); }, }, ], }); }; const performDeletePage = (pageId: string) => { const before = getCurrentCommandState(); executeWorkspaceCommand( createWorkspaceCommand({ type: 'page.delete', label: 'Deleted page', before, after: { pages: pages.filter((page) => page.id !== pageId), selectedPageIds: selectedPageIds.filter((id) => id !== pageId), lastSelectedVisualIndex: null, }, details: { pageId, }, }) ); }; const handleReorder = (newPages: PageRef[]) => { const before = getCurrentCommandState(); executeWorkspaceCommand( createWorkspaceCommand({ type: 'pages.reorder', label: 'Reordered pages', before, after: { ...before, pages: newPages, }, details: { pageCount: newPages.length, }, }) ); }; const handleToggleSelect = ( pageId: string, visualIndex: number, e: React.MouseEvent ) => { setSelectedPageIds((prev) => { if (e.shiftKey && lastSelectedVisualIndex !== null && pages.length > 0) { const from = Math.min(lastSelectedVisualIndex, visualIndex); const to = Math.max(lastSelectedVisualIndex, visualIndex); const rangeIds = pages.slice(from, to + 1).map((page) => page.id); const set = new Set(prev); rangeIds.forEach((id) => set.add(id)); return Array.from(set); } if (prev.includes(pageId)) { return prev.filter((id) => id !== pageId); } return [...prev, pageId]; }); setLastSelectedVisualIndex(visualIndex); }; const handleSelectAll = () => { setSelectedPageIds(pages.map((page) => page.id)); setLastSelectedVisualIndex(null); }; const handleClearSelection = () => { setSelectedPageIds([]); setLastSelectedVisualIndex(null); }; const performDeleteSelected = useCallback( (pageIdsToDelete: string[]) => { if (pageIdsToDelete.length === 0) return; const before = getCurrentCommandState(); const selectedSet = new Set(pageIdsToDelete); executeWorkspaceCommand( createWorkspaceCommand({ type: 'pages.delete', label: pageIdsToDelete.length === 1 ? 'Deleted selected page' : `Deleted ${pageIdsToDelete.length} selected pages`, before, after: { pages: pages.filter((page) => !selectedSet.has(page.id)), selectedPageIds: [], lastSelectedVisualIndex: null, }, details: { count: pageIdsToDelete.length, }, }) ); }, [ createWorkspaceCommand, executeWorkspaceCommand, getCurrentCommandState, pages, ] ); const handleDeleteSelected = useCallback(() => { if (selectedPageIds.length === 0) return; const idsToDelete = [...selectedPageIds]; openActionDialog({ title: idsToDelete.length === 1 ? 'Delete selected page?' : 'Delete selected pages?', content: (

Delete{' '} {idsToDelete.length === 1 ? '1 selected page' : `${idsToDelete.length} selected pages`} {' '} from the current workspace?

), actions: [ { label: 'Cancel', variant: 'secondary', onClick: closeActionDialog, }, { label: idsToDelete.length === 1 ? 'Delete page' : 'Delete pages', variant: 'danger', autoFocus: true, onClick: () => { closeActionDialog(); performDeleteSelected(idsToDelete); }, }, ], }); }, [ closeActionDialog, openActionDialog, performDeleteSelected, selectedPageIds, ]); const handleCopyPagesToSlot = (pageIds: string[], insertSlot: number) => { if (!pdf || pageIds.length === 0) return; const pageIdSet = new Set(pageIds); // Copy in current visual order, not in arbitrary selectedPageIds order. const sourcePages = pages.filter((page) => pageIdSet.has(page.id)); if (sourcePages.length === 0) return; const copiedPages: PageRef[] = sourcePages.map((page) => ({ ...page, id: createPageRefId(), })); const clampedSlot = Math.min(Math.max(insertSlot, 0), pages.length); const afterPages = [ ...pages.slice(0, clampedSlot), ...copiedPages, ...pages.slice(clampedSlot), ]; const before = getCurrentCommandState(); executeWorkspaceCommand( createWorkspaceCommand({ type: 'pages.copy', label: copiedPages.length === 1 ? 'Copied page' : `Copied ${copiedPages.length} pages`, before, after: { pages: afterPages, selectedPageIds: copiedPages.map((page) => page.id), lastSelectedVisualIndex: null, }, details: { count: copiedPages.length, insertSlot: clampedSlot, }, }) ); }; const handleOpenPreview = (pageId: string) => { setPreviewPageId(pageId); }; const handleClosePreview = () => { setPreviewPageId(null); }; const handlePreviewPrevious = () => { setPreviewPageId((current) => { if (current == null || pages.length === 0) return current; const visualIndex = pages.findIndex((page) => page.id === current); if (visualIndex <= 0) return current; return pages[visualIndex - 1].id; }); }; const handlePreviewNext = () => { setPreviewPageId((current) => { if (current == null || pages.length === 0) return current; const visualIndex = pages.findIndex((page) => page.id === current); if (visualIndex < 0 || visualIndex >= pages.length - 1) return current; return pages[visualIndex + 1].id; }); }; useEffect(() => { if (!hasPdf || previewPageId !== null) return; const handleKeyDown = (e: KeyboardEvent) => { if (isEditableKeyboardTarget(e.target)) return; const key = e.key.toLowerCase(); if ((e.ctrlKey || e.metaKey) && key === 'z') { e.preventDefault(); if (e.shiftKey) { handleRedo(); } else { handleUndo(); } return; } if ((e.ctrlKey || e.metaKey) && key === 'y') { e.preventDefault(); handleRedo(); return; } if ((e.ctrlKey || e.metaKey) && key === 'a') { e.preventDefault(); setSelectedPageIds(pages.map((page) => page.id)); setLastSelectedVisualIndex(null); return; } if ( (e.key === 'Delete' || e.key === 'Backspace') && selectedPageIds.length > 0 ) { e.preventDefault(); handleDeleteSelected(); return; } if (e.key === 'Escape' && selectedPageIds.length > 0) { e.preventDefault(); setSelectedPageIds([]); setLastSelectedVisualIndex(null); } }; window.addEventListener('keydown', handleKeyDown); return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [ hasPdf, previewPageId, pages, selectedPageIds, workspaceHistory, redoHistory, handleUndo, handleRedo, handleDeleteSelected, setSelectedPageIds, setLastSelectedVisualIndex, ]); const handleSplit = async () => { if (!pdf) return; setError(null); setIsBusy(true); try { const result = await splitIntoSinglePages(pdf); const zipBlob = await createSplitResultsZip(result); replaceSplitResults(result, { blob: zipBlob, filename: createSplitZipFilename(pdf.name), }); } catch (e) { console.error(e); setError('Error while splitting PDF (see console).'); } finally { setIsBusy(false); } }; const handleExtractSelected = async () => { if (!pdf || selectedPageIds.length === 0) return; setError(null); setIsBusy(true); try { const selectedPages = getSelectedPagesInVisualOrder( pages, selectedPageIds ); if (selectedPages.length === 0) return; const blob = await exportPages(pdf, selectedPages); const base = pdf.name.replace(/\.pdf$/i, ''); const filename = `${base}_selected.pdf`; replaceSubsetResult(blob, filename); } catch (e) { console.error(e); setError('Error while extracting selected pages (see console).'); } finally { setIsBusy(false); } }; const performOpenSelectionAsWorkspace = async () => { if (!pdf || selectedPageIds.length === 0) return; const selectedPages = getSelectedPagesInVisualOrder(pages, selectedPageIds); if (selectedPages.length === 0) return; setError(null); setIsBusy(true); try { const selectedPageCount = selectedPages.length; const blob = await exportPages(pdf, selectedPages); const arrayBuffer = await blob.arrayBuffer(); const doc = await PDFDocument.load(arrayBuffer); const pdfName = createSelectionPdfName(pdf.name, selectedPageCount); const workspaceName = createSelectionWorkspaceName( pdf.name, selectedPageCount ); const extractedPdf: PdfFile = { id: createPdfId(), name: pdfName, doc, pageCount: doc.getPageCount(), arrayBuffer, }; setPdf(extractedPdf); replaceWorkspaceState({ pages: createInitialPageRefs(extractedPdf.pageCount), selectedPageIds: [], lastSelectedVisualIndex: null, history: [], redoHistory: [], dirty: true, message: `Created a new workspace from ${selectedPageCount} selected ${ selectedPageCount === 1 ? 'page' : 'pages' }.`, }); setActiveWorkspaceId(null); setWorkspaceName(workspaceName); setPreviewPageId(null); clearGeneratedOutputs(); clearThumbnailCache(); } catch (e) { console.error(e); setError('Error while opening selection as a new workspace.'); } finally { setIsBusy(false); } }; const handleOpenSelectionAsWorkspace = () => { if (!pdf || selectedPageIds.length === 0) return; const selectedPages = getSelectedPagesInVisualOrder(pages, selectedPageIds); if (selectedPages.length === 0) return; if (!workspaceDirty) { void performOpenSelectionAsWorkspace(); return; } openActionDialog({ title: 'Open selection as new workspace?', content: ( <>

This will replace the current in-memory workspace with a new workspace built from {selectedPages.length}{' '} {selectedPages.length === 1 ? 'selected page' : 'selected pages'}.

The current workspace has unsaved changes. Do you want to save it before opening the selection as a new workspace?

), actions: [ { label: 'Cancel', variant: 'secondary', onClick: closeActionDialog, }, { label: 'Open without saving', variant: 'danger', onClick: () => { closeActionDialog(); void performOpenSelectionAsWorkspace(); }, }, { label: 'Save and open', variant: 'primary', autoFocus: true, onClick: async () => { closeActionDialog(); const saved = await handleSaveWorkspace(); if (saved) { await performOpenSelectionAsWorkspace(); } }, }, ], }); }; const handleExportReordered = async () => { if (!pdf || pages.length === 0) return; setError(null); setIsBusy(true); try { const blob = await exportPages(pdf, pages); const base = pdf.name.replace(/\.pdf$/i, ''); const filename = `${base}_reordered.pdf`; replaceExportResult(blob, filename); } catch (e) { console.error(e); setError('Error while exporting reordered PDF (see console).'); } finally { setIsBusy(false); } }; const previewVisualIndex = previewPageId != null ? pages.findIndex((page) => page.id === previewPageId) : -1; const previewPage = previewVisualIndex >= 0 ? pages[previewVisualIndex] : null; const canPreviewPrevious = previewVisualIndex > 0; const canPreviewNext = previewVisualIndex >= 0 && previewVisualIndex < pages.length - 1; return ( setHelpOpen(true)}> { setWorkspaceName(value); setWorkspaceDirty(true); }} onSaveWorkspace={handleSaveWorkspace} onLoadWorkspace={handleLoadWorkspace} onDeleteWorkspace={handleDeleteWorkspace} onRefreshWorkspaces={refreshWorkspaces} onResetWorkspace={handleResetWorkspace} onUndo={handleUndo} onRedo={handleRedo} /> {showMergeOptions && pendingFile && pdf && pages.length > 0 && (

Open file: merge or replace?

You already have {pdf.name} with {pages.length}{' '} pages open. What should happen with{' '} {pendingFile.name}?

)} {error && (
Error: {error}
)} = 0 ? previewVisualIndex : null} totalPages={pages.length} canGoPrevious={canPreviewPrevious} canGoNext={canPreviewNext} onPrevious={handlePreviewPrevious} onNext={handlePreviewNext} onClose={handleClosePreview} /> {actionDialog?.content} setHelpOpen(false)} />
); }; export default App;