diff --git a/frontend/src/detail/DetailPanel.jsx b/frontend/src/detail/DetailPanel.jsx index 07d2260..47f2e26 100644 --- a/frontend/src/detail/DetailPanel.jsx +++ b/frontend/src/detail/DetailPanel.jsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; import { DownloadIcon, EditIcon } from '../ui/icons'; import { getTagColorStyle } from '../utils/colors'; import { formatFileSize } from '../utils/format'; @@ -294,6 +295,11 @@ const DetailPanel = ({ const [titleDraft, setTitleDraft] = useState(''); const [titleSaving, setTitleSaving] = useState(false); const [titleError, setTitleError] = useState(null); + const [ocrOpen, setOcrOpen] = useState(false); + const [ocrLoading, setOcrLoading] = useState(false); + const [ocrError, setOcrError] = useState(null); + const [ocrText, setOcrText] = useState(''); + const [ocrLoadedDocId, setOcrLoadedDocId] = useState(null); useEffect(() => { if (!singleDoc) { @@ -301,6 +307,11 @@ const DetailPanel = ({ setTitleDraft(''); setTitleError(null); setTitleSaving(false); + setOcrOpen(false); + setOcrLoading(false); + setOcrError(null); + setOcrText(''); + setOcrLoadedDocId(null); return; } @@ -312,6 +323,14 @@ const DetailPanel = ({ } }, [singleDoc, titleEditDocId]); + useEffect(() => { + setOcrOpen(false); + setOcrLoading(false); + setOcrError(null); + setOcrText(''); + setOcrLoadedDocId(null); + }, [singleDoc?.id]); + const startTitleEdit = useCallback(() => { if (!singleDoc) return; setTitleEditDocId(singleDoc.id); @@ -360,6 +379,69 @@ const DetailPanel = ({ [onPromoteSelection], ); + const hasOcrAsset = useMemo( + () => Boolean(singleDoc && getDocumentAsset(singleDoc, 'ocr-text')), + [singleDoc, getDocumentAsset], + ); + + const loadOcrText = useCallback(async () => { + if (!singleDoc) { + return; + } + if (ocrLoadedDocId === singleDoc.id && ocrText) { + return; + } + const asset = getDocumentAsset(singleDoc, 'ocr-text'); + if (!asset) { + setOcrError('No OCR text available for this document.'); + setOcrText(''); + setOcrLoadedDocId(singleDoc.id); + return; + } + setOcrLoading(true); + setOcrError(null); + try { + let entry = asset; + if (ensureAssetUrl) { + entry = (await ensureAssetUrl(singleDoc.id, asset, { force: false })) || asset; + } + let url = entry?.url || null; + if (!url) { + url = resolveDocumentAssetUrl(singleDoc, 'ocr-text', { + ensureAssetUrl, + getAsset: getDocumentAsset, + }); + } + if (!url) { + throw new Error('OCR text URL is unavailable.'); + } + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch OCR text (status ${response.status}).`); + } + const text = await response.text(); + setOcrText(text); + setOcrLoadedDocId(singleDoc.id); + } catch (error) { + setOcrError(error.message || 'Failed to load OCR text.'); + setOcrText(''); + } finally { + setOcrLoading(false); + } + }, [singleDoc, ensureAssetUrl, getDocumentAsset, ocrLoadedDocId, ocrText]); + + const openOcr = useCallback(() => { + if (!singleDoc) { + return; + } + setOcrOpen(true); + loadOcrText(); + }, [singleDoc, loadOcrText]); + + const closeOcr = useCallback(() => { + setOcrOpen(false); + }, []); + const makePreviewItem = useCallback( (doc) => { if (!doc) return null; @@ -681,14 +763,21 @@ const DetailPanel = ({ > Open preview - + + {hasOcrAsset ? ( +
{JSON.stringify(metadata, null, 2)}
)}
+ {hasOcrAsset && ocrOpen
+ ? createPortal(
+ {ocrText}
+ ) : (
+