diff --git a/web/frontend/src/components/collection-viewer/collection-viewer.tsx b/web/frontend/src/components/collection-viewer/collection-viewer.tsx index ee3a1641f..cec3ad2ba 100644 --- a/web/frontend/src/components/collection-viewer/collection-viewer.tsx +++ b/web/frontend/src/components/collection-viewer/collection-viewer.tsx @@ -6,6 +6,7 @@ import { useDocumentPages } from '@/shared/api/hooks/use-document-workspace' import { Skeleton } from '@/components/ui/skeleton' import { useExtractionHighlights } from '@/components/collection-viewer/use-extraction-highlights.ts' import { useCurrentPageSync } from '@/components/collection-viewer/use-current-page-sync' +import { ViewerProcessingState } from '@/components/collection-viewer/viewer-processing-state' import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react' import { cn, extractFilenameFromUrl } from '@/helpers/utils' import { badgerDocService } from '@/shared/api/badgerdoc/service' @@ -13,6 +14,7 @@ import { toast } from 'sonner' interface CollectionViewerProps { documentId: string + expectedPageCount?: number | null currentPage: number onPageChange: Dispatch> onHighlightClick: (termId: string) => void @@ -35,6 +37,7 @@ interface CollectionViewerProps { export function CollectionViewer({ documentId, + expectedPageCount, highlights, activeHighlightId, onHighlightClick, @@ -47,9 +50,19 @@ export function CollectionViewer({ onHighlightCreate, createdHighlightIds, }: CollectionViewerProps) { - const { data: pages, isLoading } = useDocumentPages(documentId) + const { + data: pages, + isLoading: isLoading, + refetch, + } = useDocumentPages(documentId, expectedPageCount) + const actualPageCount = pages?.length ?? 0 + const pagesReadyByMetadata = + expectedPageCount != null && expectedPageCount > 0 && actualPageCount >= expectedPageCount + const isProcessing = !isLoading && !pagesReadyByMetadata + const isReady = !isLoading && pagesReadyByMetadata + const { containerRef, viewer } = useOsdViewer(isReady ? pages : undefined) + const [isDownloading, setIsDownloading] = useState(false) - const { containerRef, viewer } = useOsdViewer(pages) useExtractionHighlights({ viewer, overlayItems: highlights, @@ -122,6 +135,18 @@ export function CollectionViewer({ [viewer, isEditMode] ) + if (isProcessing) { + return ( +
+ refetch()} + readyPagesCount={actualPageCount} + expectedPagesCount={expectedPageCount} + /> +
+ ) + } + return (
void + readyPagesCount: number + expectedPagesCount?: number | null +} + +export function ViewerProcessingState({ + onRefresh, + readyPagesCount, + expectedPagesCount, +}: ViewerProcessingStateProps) { + const showProgress = expectedPagesCount != null && expectedPagesCount > 0 + + const progress = showProgress ? Math.round((readyPagesCount / expectedPagesCount!) * 100) : 0 + + return ( +
+
+ {/* Spinner */} +
+
+
+ + {/* Title */} +

Processing document…

+ + {/* Description */} +

+ We’re extracting pages and analyzing the content +

+ + {/* Progress */} + {showProgress && ( +
+
+ + {readyPagesCount} of {expectedPagesCount} pages ready + + {progress}% +
+ +
+
+
+
+ )} + + {/* Hint */} +

This usually takes a few seconds

+ + {/* Action */} + +
+
+ ) +} diff --git a/web/frontend/src/features/workspace/page.tsx b/web/frontend/src/features/workspace/page.tsx index 8dbb671bf..ec87e855f 100644 --- a/web/frontend/src/features/workspace/page.tsx +++ b/web/frontend/src/features/workspace/page.tsx @@ -25,6 +25,7 @@ import { taskFiltersFromSearch, taskFiltersToSearch, } from '@/helpers/task-filters-search' +import { parsePositiveNumber } from '@/helpers/utils' import { ExtractionResultsTab } from '@/features/workspace/components/extraction-results-tab.tsx' export function WorkspacePage() { @@ -245,6 +246,7 @@ export function WorkspacePage() { publicationDate: document.publicationDate, } const viewerHighlights = activeTab === 'overview' ? {} : highlights + const expectedPageCount = parsePositiveNumber(document.metadata?.total_pages) const renderTabContent = () => { // Special case: Overview tab (hardcoded) @@ -323,6 +325,7 @@ export function WorkspacePage() { left={ setActiveBlockId(highlightId)} diff --git a/web/frontend/src/helpers/utils.ts b/web/frontend/src/helpers/utils.ts index e3739f730..6896fd336 100644 --- a/web/frontend/src/helpers/utils.ts +++ b/web/frontend/src/helpers/utils.ts @@ -79,3 +79,14 @@ export function getFileExtensionFromFileName(fileName: string): string { if (index === -1) return '' return fileName.slice(index + 1).toLowerCase() } + +export function parsePositiveNumber(value: unknown): number | null { + const numericValue = + typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : null + + if (numericValue == null || !Number.isFinite(numericValue) || numericValue <= 0) { + return null + } + + return numericValue +} diff --git a/web/frontend/src/shared/api/hooks/use-document-workspace.ts b/web/frontend/src/shared/api/hooks/use-document-workspace.ts index 516c195c4..e353edfce 100644 --- a/web/frontend/src/shared/api/hooks/use-document-workspace.ts +++ b/web/frontend/src/shared/api/hooks/use-document-workspace.ts @@ -8,6 +8,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { getApiAdapter } from '../adapters/factory' import type { Document } from '@/shared/types/api' +import { parsePositiveNumber } from '@/helpers/utils' import { toast } from 'sonner' // ============================================================================= @@ -43,16 +44,31 @@ export function useWorkspaceDocument(documentId: string) { queryKey: workspaceKeys.document(documentId), queryFn: (): Promise => adapter.documents.getById(documentId), enabled: !!documentId, + refetchInterval: (query) => { + const totalPages = parsePositiveNumber(query.state.data?.metadata?.total_pages) + return totalPages != null ? false : 2000 + }, }) } -export function useDocumentPages(documentId: string) { +export function useDocumentPages(documentId: string, expectedPageCount?: number | null) { const adapter = getApiAdapter() return useQuery({ queryKey: workspaceKeys.pages(documentId), queryFn: (): Promise => adapter.documents.getPagesById(documentId), enabled: !!documentId, + // Keep polling while expected page count is missing or incomplete. + refetchInterval: (query) => { + const data = query.state.data + const actualPageCount = data?.length ?? 0 + + if (expectedPageCount == null) { + return 5000 + } + + return actualPageCount !== expectedPageCount ? 5000 : false + }, }) } interface UpdateDocumentMeta {