Files
papercrate/frontend/src/preview/PreviewWorkspace.jsx
T
2025-10-29 11:52:19 +01:00

221 lines
6.1 KiB
React

import React, { useMemo } from 'react';
import { formatFileSize } from '../utils/format';
import { DownloadIcon, TextScanIcon, AnalyzeIcon, CloseIcon } from '../ui/icons';
import { openOcrTextInNewTab } from '../utils/ocr';
const PreviewWorkspace = ({
document,
previewEntry,
}) => {
if (!document) {
return null;
}
const title = document.title || document.original_name || 'Document';
const mime = previewEntry?.contentType || document.content_type || 'application/pdf';
const sizeBytes = Number(document.current_version?.size_bytes) || 0;
const sizeLabel = sizeBytes > 0 ? formatFileSize(sizeBytes) : null;
const metadata =
document.metadata && Object.keys(document.metadata).length > 0 ? document.metadata : null;
const folderName = document.folder_path || document.folder_name || null;
const issuedAt = document.issued_at || document.current_version?.issued_at || null;
const createdAt = document.created_at || null;
const updatedAt = document.updated_at || null;
const tags = Array.isArray(document.tags) ? document.tags : [];
const correspondents = Array.isArray(document.correspondents) ? document.correspondents : [];
const metadataSummary = useMemo(() => {
const rows = [];
if (mime) rows.push(['Type', mime]);
if (sizeLabel) rows.push(['Size', sizeLabel]);
if (issuedAt) rows.push(['Issued', new Date(issuedAt).toLocaleString()]);
if (createdAt) rows.push(['Created', new Date(createdAt).toLocaleString()]);
if (updatedAt) rows.push(['Updated', new Date(updatedAt).toLocaleString()]);
if (folderName) rows.push(['Folder', folderName]);
if (tags.length) {
rows.push(['Tags', tags.map((tag) => tag.label || tag.name || tag.slug).filter(Boolean).join(', ')]);
}
if (correspondents.length) {
rows.push([
'Correspondents',
correspondents
.map((entry) => entry.name || entry.label || entry.slug)
.filter(Boolean)
.join(', '),
]);
}
return rows;
}, [mime, sizeLabel, issuedAt, createdAt, updatedAt, folderName, tags, correspondents]);
return (
<section className="preview-workspace">
<aside className="preview-workspace__sidebar">
<div className="preview-workspace__info">
{metadataSummary.length ? (
<dl className="preview-workspace__summary">
{metadataSummary.map(([label, value]) => (
<div className="preview-workspace__summary-row" key={label}>
<dt>{label}</dt>
<dd>{value || '—'}</dd>
</div>
))}
</dl>
) : null}
</div>
{metadata ? (
<section className="preview-workspace__metadata">
<h4>Metadata payload</h4>
<pre>{JSON.stringify(metadata, null, 2)}</pre>
</section>
) : null}
</aside>
<div className="preview-workspace__viewer">
{!previewEntry?.url ? (
<div className="preview-workspace__message">Loading preview</div>
) : (
<iframe
src={previewEntry.url}
title={`Preview of ${title}`}
className="preview-workspace__object"
/>
)}
</div>
</section>
);
};
export default PreviewWorkspace;
export const createPreviewWorkspaceHeaderActions = ({
document,
ensureAssetUrl,
getDocumentAsset,
resolveApiPath,
notifyApiError,
onRegenerate,
}) => {
if (!document) {
return null;
}
const downloadHref = document.current_version?.download_path
? resolveApiPath(document.current_version.download_path)
: null;
const ocrAsset = getDocumentAsset(document, 'ocr-text');
const hasOcr = Boolean(ocrAsset);
const handleOcrClick = async () => {
try {
const success = await openOcrTextInNewTab({
document,
getDocumentAsset,
ensureAssetUrl,
});
if (!success) {
throw new Error('OCR text URL unavailable.');
}
} catch (error) {
notifyApiError(error, 'Unable to open OCR text.');
}
};
return (
<>
{downloadHref ? (
<a
className="icon-button"
href={downloadHref}
target="_blank"
rel="noopener noreferrer"
aria-label="Download document"
title="Download document"
>
<DownloadIcon />
</a>
) : null}
{hasOcr ? (
<button
type="button"
className="icon-button ghost"
onClick={handleOcrClick}
aria-label="View OCR text"
title="View OCR text"
>
<TextScanIcon />
</button>
) : null}
<button
type="button"
className="icon-button ghost"
onClick={() => onRegenerate(document.id)}
aria-label="Re-run analysis"
title="Re-run analysis"
>
<AnalyzeIcon />
</button>
</>
);
};
export const createPreviewSurface = ({
document,
previewEntry,
ensureAssetUrl,
getDocumentAsset,
resolveApiPath,
notifyApiError,
onRegenerate,
onClose,
renderSidebarToggle,
}) => {
if (!document) {
return null;
}
const title = document.title || document.original_name || 'Document preview';
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
const closeButton = onClose
? (
<button
type="button"
className="icon-button ghost"
onClick={() => onClose?.()}
aria-label="Close preview"
title="Close preview"
>
<CloseIcon />
</button>
)
: null;
const leading = sidebarToggle || closeButton
? (
<>
{sidebarToggle}
{closeButton}
</>
)
: null;
const header = {
title,
subtitle: null,
leading,
actions: createPreviewWorkspaceHeaderActions({
document,
ensureAssetUrl,
getDocumentAsset,
resolveApiPath,
notifyApiError,
onRegenerate,
}),
};
return {
key: 'preview',
variant: 'preview',
header,
content: <PreviewWorkspace document={document} previewEntry={previewEntry} />,
supportsDetail: false,
};
};