view ocr text
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { DownloadIcon, EditIcon } from '../ui/icons';
|
import { DownloadIcon, EditIcon } from '../ui/icons';
|
||||||
import { getTagColorStyle } from '../utils/colors';
|
import { getTagColorStyle } from '../utils/colors';
|
||||||
import { formatFileSize } from '../utils/format';
|
import { formatFileSize } from '../utils/format';
|
||||||
@@ -294,6 +295,11 @@ const DetailPanel = ({
|
|||||||
const [titleDraft, setTitleDraft] = useState('');
|
const [titleDraft, setTitleDraft] = useState('');
|
||||||
const [titleSaving, setTitleSaving] = useState(false);
|
const [titleSaving, setTitleSaving] = useState(false);
|
||||||
const [titleError, setTitleError] = useState(null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!singleDoc) {
|
if (!singleDoc) {
|
||||||
@@ -301,6 +307,11 @@ const DetailPanel = ({
|
|||||||
setTitleDraft('');
|
setTitleDraft('');
|
||||||
setTitleError(null);
|
setTitleError(null);
|
||||||
setTitleSaving(false);
|
setTitleSaving(false);
|
||||||
|
setOcrOpen(false);
|
||||||
|
setOcrLoading(false);
|
||||||
|
setOcrError(null);
|
||||||
|
setOcrText('');
|
||||||
|
setOcrLoadedDocId(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +323,14 @@ const DetailPanel = ({
|
|||||||
}
|
}
|
||||||
}, [singleDoc, titleEditDocId]);
|
}, [singleDoc, titleEditDocId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOcrOpen(false);
|
||||||
|
setOcrLoading(false);
|
||||||
|
setOcrError(null);
|
||||||
|
setOcrText('');
|
||||||
|
setOcrLoadedDocId(null);
|
||||||
|
}, [singleDoc?.id]);
|
||||||
|
|
||||||
const startTitleEdit = useCallback(() => {
|
const startTitleEdit = useCallback(() => {
|
||||||
if (!singleDoc) return;
|
if (!singleDoc) return;
|
||||||
setTitleEditDocId(singleDoc.id);
|
setTitleEditDocId(singleDoc.id);
|
||||||
@@ -360,6 +379,69 @@ const DetailPanel = ({
|
|||||||
[onPromoteSelection],
|
[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(
|
const makePreviewItem = useCallback(
|
||||||
(doc) => {
|
(doc) => {
|
||||||
if (!doc) return null;
|
if (!doc) return null;
|
||||||
@@ -681,14 +763,21 @@ const DetailPanel = ({
|
|||||||
>
|
>
|
||||||
Open preview
|
Open preview
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="secondary"
|
className="secondary"
|
||||||
onClick={() => onRegenerateThumbnails(singleDoc.id)}
|
onClick={() => onRegenerateThumbnails(singleDoc.id)}
|
||||||
>
|
>
|
||||||
Re-run analysis
|
Re-run analysis
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{hasOcrAsset ? (
|
||||||
|
<div className="detail-ocr-trigger">
|
||||||
|
<button type="button" className="secondary" onClick={openOcr}>
|
||||||
|
View OCR text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<TagSection
|
<TagSection
|
||||||
title="Tags"
|
title="Tags"
|
||||||
tags={tagsForDoc.map((tag) => ({
|
tags={tagsForDoc.map((tag) => ({
|
||||||
@@ -728,6 +817,43 @@ const DetailPanel = ({
|
|||||||
<pre>{JSON.stringify(metadata, null, 2)}</pre>
|
<pre>{JSON.stringify(metadata, null, 2)}</pre>
|
||||||
</div>
|
</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;
|
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 {
|
.preview-pane {
|
||||||
margin-top: 0.4rem;
|
margin-top: 0.4rem;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user