feat: Add OCR script for PDFs using Ollama and refine document preview navigation and zoom behavior.

This commit is contained in:
2025-12-03 17:07:51 +01:00
parent 186743ee37
commit e46318aee5
6 changed files with 126 additions and 27 deletions
+8 -1
View File
@@ -35,6 +35,10 @@ const DocumentsRouteContent: React.FC = () => {
navigate(target);
}, [navigate]);
const handleDocumentNavigate = useCallback((documentId: string) => {
navigate(`/documents/${documentId}`);
}, [navigate]);
const documentsTablePropsWithNav = useMemo(() => (
surfaceConfig.documentsTableProps
? { ...surfaceConfig.documentsTableProps, onBreadcrumbNavigate: handleHeaderBreadcrumbClick }
@@ -76,7 +80,10 @@ const DocumentsRouteContent: React.FC = () => {
const content = renderSurface();
return (
<DocumentsFilterProvider value={documentsFilter}>
<PreviewProvider ensureDownloadUrl={surfaceConfig.documentsTableProps?.ensureDownloadUrl}>
<PreviewProvider
ensureDownloadUrl={surfaceConfig.documentsTableProps?.ensureDownloadUrl}
onNavigate={handleDocumentNavigate}
>
{content}
</PreviewProvider>
</DocumentsFilterProvider>
+53 -21
View File
@@ -3,12 +3,15 @@ import { createPortal } from 'react-dom';
import { clamp } from '../utils/math';
import PdfViewer from '../preview/PdfViewer';
import MediaViewer from '../preview/MediaViewer';
import PanelHeader from '../ui/PanelHeader';
import { IconX, DownloadIcon, FileInfoIcon } from '../ui/icons';
import type { Document } from '../types/documents';
interface PreviewZoomOverlayProps {
open?: boolean;
onClose?: () => void;
onMaximize?: () => void;
document?: Document | null;
}
@@ -44,6 +47,7 @@ const noop = () => { };
const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
open = false,
onClose = noop,
onMaximize,
document: overlayDocument = null,
}) => {
const portalTarget = document.body;
@@ -235,25 +239,10 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
};
const toggleZoomAtPoint = (clientX: number, clientY: number) => {
if (isPdfDisplay) {
return;
}
const media = mediaRef.current;
setIsNativeScale((current) => {
if (!current && media) {
const rect = media.getBoundingClientRect();
const xRatio = rect.width > 0 ? (clientX - rect.left) / rect.width : 0.5;
const yRatio = rect.height > 0 ? (clientY - rect.top) / rect.height : 0.5;
focusRef.current = {
xRatio: clamp(xRatio, 0, 1),
yRatio: clamp(yRatio, 0, 1),
};
} else {
focusRef.current = null;
}
return !current;
});
const toggleZoomAtPoint = (_clientX: number, _clientY: number) => {
// Zooming is disabled for images as per user request.
// PDFs handle their own zooming via the PdfViewer.
return;
};
const handleContentClick = (event: React.MouseEvent<HTMLElement>) => {
@@ -287,7 +276,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
const contentStyle: CSSProperties = isNativeScale
? {
cursor: 'zoom-out',
cursor: 'default',
width: naturalSize.width ? `${naturalSize.width}px` : 'auto',
height: naturalSize.height ? `${naturalSize.height}px` : 'auto',
maxWidth: 'none',
@@ -295,7 +284,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
touchAction: 'manipulation',
}
: {
cursor: 'zoom-in',
cursor: 'default',
maxWidth: 'calc(100vw - 2 * var(--preview-padding, 1.5vmin))',
maxHeight: 'calc(100vh - 2 * var(--preview-padding, 1.5vmin))',
touchAction: 'manipulation',
@@ -307,6 +296,8 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
const effectiveDisplay = documentLink;
const downloadUrl = activeDocument?.current_version?.download?.url;
return createPortal(
(
<div
@@ -317,6 +308,47 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
onClick={onClose}
onKeyDown={handleKeyDown}
>
<PanelHeader
className="panel-header--dark"
title={documentTitle}
leading={
<button
onClick={onClose}
className="icon-button"
aria-label="Close preview"
type="button"
>
<IconX />
</button>
}
actions={
<>
{downloadUrl && (
<a
href={downloadUrl}
target="_blank"
rel="noopener noreferrer"
className="icon-button"
aria-label="Download document"
title="Download document"
>
<DownloadIcon />
</a>
)}
{onMaximize && (
<button
onClick={onMaximize}
className="icon-button"
aria-label="Open document info"
title="Open document info"
type="button"
>
<FileInfoIcon />
</button>
)}
</>
}
/>
<div
className={stageClassName}
onClick={(event) => {
+2 -1
View File
@@ -379,7 +379,8 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
const stackStyle = useMemo<CSSProperties>(() => ({
'--pdf-viewer-viewport-width': `${viewportWidth}px`,
'--pdf-viewer-viewport-height': `${viewportHeight}px`,
}), [viewportHeight, viewportWidth]);
cursor: viewMode === 'fit-width' ? 'zoom-out' : 'zoom-in',
}), [viewportHeight, viewportWidth, viewMode]);
const toggleViewMode = useCallback(() => {
setViewMode((prev) => (prev === 'fit-width' ? 'contain' : 'fit-width'));
+10 -1
View File
@@ -21,9 +21,10 @@ export const usePreviewContext = () => {
interface PreviewProviderProps {
children: React.ReactNode;
ensureDownloadUrl?: (docId: Identifier) => Promise<any>;
onNavigate?: (documentId: Identifier) => void;
}
export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensureDownloadUrl }) => {
export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensureDownloadUrl, onNavigate }) => {
const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
@@ -36,6 +37,13 @@ export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensu
setPreviewUrl(null);
}, []);
const handleMaximize = useCallback(() => {
if (previewDoc && onNavigate) {
onNavigate(previewDoc.id);
closePreview();
}
}, [previewDoc, onNavigate, closePreview]);
useEffect(() => {
if (!previewDoc) {
setPreviewUrl(null);
@@ -87,6 +95,7 @@ export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensu
<PreviewZoomOverlay
open={Boolean(previewUrl)}
onClose={closePreview}
onMaximize={onNavigate ? handleMaximize : undefined}
document={overlayDocument}
/>
</PreviewContext.Provider>
@@ -75,6 +75,28 @@
margin-left: auto;
}
/* Dark theme variant for preview zoom overlay */
.panel-header--dark {
background: var(--overlay-dark);
color: var(--text-on-dark);
pointer-events: auto;
}
.panel-header--dark .icon-button,
.panel-header--dark button,
.panel-header--dark a.icon-button {
color: var(--text-on-dark);
opacity: 0.75;
pointer-events: auto;
}
.panel-header--dark .icon-button:hover:not([disabled]),
.panel-header--dark button:hover:not([disabled]),
.panel-header--dark a.icon-button:hover {
background: var(--surface-hover);
opacity: 1;
}
.panel-body {
padding: 0.5rem 1rem;
}
+31 -3
View File
@@ -6,10 +6,12 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 2000000;
cursor: zoom-out;
z-index: 6000000;
cursor: default;
opacity: 0;
pointer-events: none;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.preview-zoom-backdrop--visible {
@@ -23,8 +25,9 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 3000000;
z-index: 6000001;
--preview-padding: 1.5vmin;
--header-clearance: calc(2.5rem + 0.5rem);
}
.preview-zoom__stage--pdf {
@@ -42,6 +45,8 @@
display: flex;
align-items: center;
justify-content: center;
padding-top: var(--header-clearance);
box-sizing: border-box;
}
.preview-zoom__scroll--pdf {
@@ -60,6 +65,8 @@
}
.preview-zoom__scroll--native {
width: 100%;
height: 100%;
overflow: auto;
cursor: zoom-out;
justify-content: flex-start;
@@ -73,6 +80,8 @@
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: var(--header-clearance);
box-sizing: border-box;
}
.preview-zoom__pdf-viewer {
@@ -93,4 +102,23 @@
z-index: -1;
pointer-events: none;
border-radius: inherit;
}
/* Header positioning for preview zoom overlay */
.preview-zoom-backdrop .panel-header--dark {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 6000002;
cursor: default;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
pointer-events: auto;
box-shadow: 0 1rem 0.5rem var(--shadow-subtle);
}
/* Prevent header clicks from closing backdrop */
.preview-zoom-backdrop .panel-header--dark * {
pointer-events: auto;
}