view ocr text

This commit is contained in:
2025-10-16 17:10:23 +02:00
parent d7c65ed551
commit 97e800bfd5
2 changed files with 177 additions and 7 deletions
+133 -7
View File
@@ -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
</button>
<button
type="button"
className="secondary"
onClick={() => onRegenerateThumbnails(singleDoc.id)}
>
Re-run analysis
</button>
<button
type="button"
className="secondary"
onClick={() => onRegenerateThumbnails(singleDoc.id)}
>
Re-run analysis
</button>
</div>
{hasOcrAsset ? (
<div className="detail-ocr-trigger">
<button type="button" className="secondary" onClick={openOcr}>
View OCR text
</button>
</div>
) : null}
<TagSection
title="Tags"
tags={tagsForDoc.map((tag) => ({
@@ -728,6 +817,43 @@ const DetailPanel = ({
<pre>{JSON.stringify(metadata, null, 2)}</pre>
</div>
)}
{hasOcrAsset && ocrOpen
? createPortal(
<div className="modal-backdrop" role="presentation" onClick={closeOcr}>
<div
className="modal modal--ocr"
role="dialog"
aria-modal="true"
aria-label="OCR text"
onClick={(event) => event.stopPropagation()}
>
<div className="modal__header">
<h3>OCR Text</h3>
<button
type="button"
className="icon-button ghost"
onClick={closeOcr}
aria-label="Close OCR text"
>
×
</button>
</div>
<div className="modal__body detail-ocr__content">
{ocrLoading ? (
<div className="meta">Loading OCR text</div>
) : ocrError ? (
<div className="status-inline error">{ocrError}</div>
) : ocrText ? (
<pre>{ocrText}</pre>
) : (
<div className="meta">OCR text empty.</div>
)}
</div>
</div>
</div>,
document.body,
)
: null}
</>
);
};
+44
View File
@@ -1516,6 +1516,50 @@ button.icon-button.ghost:hover:not([disabled]) {
margin: 0.6rem 0;
}
.detail-ocr-trigger {
margin: 0.75rem 0;
}
.detail-ocr__content {
max-height: 60vh;
overflow: auto;
padding: 0.75rem;
background: var(--surface-soft);
border: 1px solid var(--border);
border-radius: 6px;
box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.05);
}
.detail-ocr__content pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-family: var(--font-mono, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace);
font-size: 0.85rem;
line-height: 1.4;
}
.modal__body {
width: 100%;
}
.modal--ocr {
width: min(720px, 100%);
max-height: 80vh;
gap: 0.75rem;
}
.modal--ocr .modal__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.modal--ocr .modal__body {
padding: 0;
}
.preview-pane {
margin-top: 0.4rem;
border: none;