view ocr text
This commit is contained in:
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user