Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ 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'
import { toast } from 'sonner'

interface CollectionViewerProps {
documentId: string
expectedPageCount?: number | null
currentPage: number
onPageChange: Dispatch<SetStateAction<number>>
onHighlightClick: (termId: string) => void
Expand All @@ -35,6 +37,7 @@ interface CollectionViewerProps {

export function CollectionViewer({
documentId,
expectedPageCount,
highlights,
activeHighlightId,
onHighlightClick,
Expand All @@ -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,
Expand Down Expand Up @@ -122,6 +135,18 @@ export function CollectionViewer({
[viewer, isEditMode]
)

if (isProcessing) {
return (
<section className="flex h-full w-full flex-col">
<ViewerProcessingState
onRefresh={() => refetch()}
readyPagesCount={actualPageCount}
expectedPagesCount={expectedPageCount}
/>
</section>
)
}

return (
<section className="flex h-full w-full flex-col">
<ViewerToolbar
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Button } from '@/components/ui/button'

interface ViewerProcessingStateProps {
onRefresh: () => 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 (
<div className="flex h-full w-full items-center justify-center">
<div className="text-center max-w-sm w-full px-4">
{/* Spinner */}
<div className="flex justify-center mb-4">
<div className="h-8 w-8 rounded-full border-2 border-gray-300 border-t-black animate-spin" />
</div>

{/* Title */}
<h3 className="text-lg font-medium mb-2">Processing document…</h3>

{/* Description */}
<p className="text-sm text-gray-500 mb-4">
We’re extracting pages and analyzing the content
</p>

{/* Progress */}
{showProgress && (
<div className="mb-4">
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span>
{readyPagesCount} of {expectedPagesCount} pages ready
</span>
<span>{progress}%</span>
</div>

<div className="h-2 w-full bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-black transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
</div>
)}

{/* Hint */}
<p className="text-xs text-gray-400 mb-4">This usually takes a few seconds</p>

{/* Action */}
<Button variant="outline" onClick={onRefresh}>
Refresh
</Button>
</div>
</div>
)
}
3 changes: 3 additions & 0 deletions web/frontend/src/features/workspace/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -323,6 +325,7 @@ export function WorkspacePage() {
left={
<CollectionViewer
documentId={documentId?.toString() || ''}
expectedPageCount={expectedPageCount}
currentPage={currentPage}
onPageChange={setCurrentPage}
onHighlightClick={(highlightId) => setActiveBlockId(highlightId)}
Expand Down
11 changes: 11 additions & 0 deletions web/frontend/src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
18 changes: 17 additions & 1 deletion web/frontend/src/shared/api/hooks/use-document-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

// =============================================================================
Expand Down Expand Up @@ -43,16 +44,31 @@ export function useWorkspaceDocument(documentId: string) {
queryKey: workspaceKeys.document(documentId),
queryFn: (): Promise<Document> => 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<string[]> => 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 {
Expand Down