feat: implemented export and wasm rendering
This commit is contained in:
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -7,6 +7,7 @@ import type { Annotation } from './viewer/AnnotationLayer';
|
||||
import { gatewayService } from './lib/gatewayService';
|
||||
import type { DocumentInfo, SearchResult } from './lib/gatewayService';
|
||||
import { wasmLoader } from './lib/wasmLoader';
|
||||
import { WasmInspector } from './components/WasmInspector';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
@@ -25,6 +26,7 @@ function App() {
|
||||
const [backendHealthy, setBackendHealthy] = useState<boolean | null>(null);
|
||||
const [sidebarTab, setSidebarTab] = useState<'documents' | 'annotations' | 'outline'>('documents');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [wasmInspectorOpen, setWasmInspectorOpen] = useState<boolean>(false);
|
||||
|
||||
// Search State
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -328,6 +330,19 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!selectedDocId || !activeDoc) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await gatewayService.exportDocument(selectedDocId, activeDoc.filename);
|
||||
} catch (err) {
|
||||
console.error('Failed to export document:', err);
|
||||
alert('Failed to export document. Make sure the gateway is connected.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen flex flex-col overflow-hidden bg-slate-950 font-sans text-slate-100 antialiased">
|
||||
{/* Top Navigation / Toolbar */}
|
||||
@@ -347,6 +362,9 @@ function App() {
|
||||
searchCurrentMatch={searchCurrentMatch}
|
||||
onSearchNext={handleSearchNext}
|
||||
onSearchPrev={handleSearchPrev}
|
||||
onExport={handleExport}
|
||||
onWasmInspectToggle={() => setWasmInspectorOpen(!wasmInspectorOpen)}
|
||||
wasmInspectorOpen={wasmInspectorOpen}
|
||||
/>
|
||||
|
||||
<div className="flex-1 w-full flex overflow-hidden">
|
||||
@@ -401,6 +419,14 @@ function App() {
|
||||
<p className="text-sm font-bold">No active document. Please upload a PDF file.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{wasmInspectorOpen && activeDoc && (
|
||||
<WasmInspector
|
||||
documentId={activeDoc.id}
|
||||
currentPage={currentPage}
|
||||
onClose={() => setWasmInspectorOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,9 @@ interface ToolbarProps {
|
||||
searchCurrentMatch: number;
|
||||
onSearchNext: () => void;
|
||||
onSearchPrev: () => void;
|
||||
onExport?: () => void;
|
||||
onWasmInspectToggle?: () => void;
|
||||
wasmInspectorOpen?: boolean;
|
||||
}
|
||||
|
||||
export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
@@ -37,6 +40,9 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
searchCurrentMatch,
|
||||
onSearchNext,
|
||||
onSearchPrev,
|
||||
onExport,
|
||||
onWasmInspectToggle,
|
||||
wasmInspectorOpen = false,
|
||||
}) => {
|
||||
|
||||
const handleZoomPercentSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
@@ -194,6 +200,30 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
onPrev={onSearchPrev}
|
||||
/>
|
||||
|
||||
{onWasmInspectToggle && (
|
||||
<button
|
||||
onClick={onWasmInspectToggle}
|
||||
className={`wasm-badge cursor-pointer hover:bg-indigo-500/25 hover:border-indigo-400/40 transition-all ${wasmInspectorOpen ? 'border-indigo-400 bg-indigo-500/25 text-indigo-200' : ''}`}
|
||||
title="Inspect client-side WebAssembly execution"
|
||||
>
|
||||
<span className="wasm-dot" />
|
||||
WASM Inspect
|
||||
</button>
|
||||
)}
|
||||
|
||||
{onExport && (
|
||||
<button
|
||||
onClick={onExport}
|
||||
className="export-btn"
|
||||
title="Export clean PDF (Stripping incremental history)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
Export PDF
|
||||
</button>
|
||||
)}
|
||||
|
||||
<label className="upload-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { wasmLoader } from '../lib/wasmLoader';
|
||||
import { gatewayService } from '../lib/gatewayService';
|
||||
|
||||
interface WasmInspectorProps {
|
||||
documentId: string;
|
||||
currentPage: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const WasmInspector: React.FC<WasmInspectorProps> = ({
|
||||
documentId,
|
||||
currentPage,
|
||||
onClose,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const [status, setStatus] = useState<string>('Initializing...');
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [engineInfo, setEngineInfo] = useState<string>('');
|
||||
const [textJson, setTextJson] = useState<string>('');
|
||||
const [isCompiling, setIsCompiling] = useState<boolean>(true);
|
||||
const [stats, setStats] = useState<{
|
||||
loadTimeMs: number;
|
||||
renderTimeMs: number;
|
||||
textTimeMs: number;
|
||||
docHandle: number;
|
||||
fileSize: number;
|
||||
}>({ loadTimeMs: 0, renderTimeMs: 0, textTimeMs: 0, docHandle: 0, fileSize: 0 });
|
||||
|
||||
const addLog = (msg: string) => {
|
||||
setLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
let docHandle = 0;
|
||||
let engine: any = null;
|
||||
|
||||
const runWasmPipeline = async () => {
|
||||
try {
|
||||
setIsCompiling(true);
|
||||
setStatus('Loading WASM module...');
|
||||
addLog('Initializing Emscripten compiler context in browser...');
|
||||
|
||||
engine = await wasmLoader.loadEngine();
|
||||
if (!active) return;
|
||||
|
||||
const info = engine.engineBuildInfo();
|
||||
setEngineInfo(info);
|
||||
addLog(`C++ Core Loaded: ${info}`);
|
||||
|
||||
setStatus('Downloading PDF bytes from Gateway...');
|
||||
addLog(`Fetching document ID: ${documentId} ...`);
|
||||
|
||||
const startTimeFetch = performance.now();
|
||||
const buffer = await gatewayService.fetchDocumentBytes(documentId);
|
||||
const fetchTime = performance.now() - startTimeFetch;
|
||||
if (!active) return;
|
||||
addLog(`Downloaded ${buffer.byteLength} bytes in ${fetchTime.toFixed(1)}ms`);
|
||||
|
||||
setStatus('Allocating heap and loading document...');
|
||||
addLog('Calling loadDocument() on WASM engine...');
|
||||
|
||||
const startTimeLoad = performance.now();
|
||||
docHandle = engine.loadDocument(buffer);
|
||||
const loadTime = performance.now() - startTimeLoad;
|
||||
if (!active) return;
|
||||
|
||||
if (docHandle <= 0) {
|
||||
throw new Error('loadDocument failed to return a valid handle');
|
||||
}
|
||||
addLog(`Document loaded successfully. Allocated handle ID: ${docHandle} in ${loadTime.toFixed(1)}ms`);
|
||||
|
||||
// Update stats
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
docHandle,
|
||||
fileSize: buffer.byteLength,
|
||||
loadTimeMs: loadTime,
|
||||
}));
|
||||
|
||||
// Render Page
|
||||
setStatus('Rendering page client-side...');
|
||||
addLog(`Calling renderPage(handle: ${docHandle}, pageIndex: ${currentPage}, scale: 1.0) ...`);
|
||||
|
||||
const startTimeRender = performance.now();
|
||||
const scale = 1.0;
|
||||
const imageData = engine.renderPage(docHandle, currentPage, scale);
|
||||
const renderTime = performance.now() - startTimeRender;
|
||||
if (!active) return;
|
||||
|
||||
addLog(`Page rendered to raw pixel RGBA buffer in ${renderTime.toFixed(1)}ms`);
|
||||
|
||||
// Draw onto Canvas
|
||||
if (canvasRef.current) {
|
||||
const canvas = canvasRef.current;
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
addLog(`Rendered pixel bytes displayed on HTML5 canvas (${imageData.width}x${imageData.height})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get Text JSON
|
||||
setStatus('Extracting page text...');
|
||||
addLog(`Calling getTextJson(handle: ${docHandle}, pageIndex: ${currentPage}) ...`);
|
||||
|
||||
const startTimeText = performance.now();
|
||||
const textData = engine.getTextJson(docHandle, currentPage);
|
||||
const textTime = performance.now() - startTimeText;
|
||||
if (!active) return;
|
||||
|
||||
addLog(`Text query complete in ${textTime.toFixed(1)}ms`);
|
||||
|
||||
// Pretty print JSON
|
||||
try {
|
||||
const parsed = JSON.parse(textData);
|
||||
setTextJson(JSON.stringify(parsed, null, 2));
|
||||
} catch {
|
||||
setTextJson(textData);
|
||||
}
|
||||
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
renderTimeMs: renderTime,
|
||||
textTimeMs: textTime,
|
||||
}));
|
||||
setStatus('Complete');
|
||||
setIsCompiling(false);
|
||||
} catch (err: any) {
|
||||
addLog(`ERROR: ${err.message}`);
|
||||
setStatus('Pipeline Failed');
|
||||
setIsCompiling(false);
|
||||
}
|
||||
};
|
||||
|
||||
runWasmPipeline();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
if (docHandle > 0 && engine) {
|
||||
try {
|
||||
engine.freeDocument(docHandle);
|
||||
console.log(`[WasmInspector] Freed doc handle ${docHandle}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [documentId, currentPage]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-[400px] border-l border-slate-800 bg-slate-900/90 backdrop-blur-md h-full text-slate-200 overflow-hidden shadow-2xl z-40 animate-in slide-in-from-right duration-300">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-slate-800 bg-slate-950/40">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-indigo-500 animate-pulse shadow-[0_0_8px_#6366f1]" />
|
||||
<h3 className="font-bold text-sm tracking-wider uppercase text-indigo-400">WASM Compiler Inspect</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-slate-400 hover:text-white p-1.5 rounded-lg hover:bg-slate-800 transition-colors"
|
||||
title="Close Inspector"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{/* Status Card */}
|
||||
<div className="p-3.5 rounded-xl border border-slate-800 bg-slate-950/20">
|
||||
<div className="text-xs text-slate-400 font-medium">Pipeline Status</div>
|
||||
<div className="text-sm font-bold text-slate-100 flex items-center gap-2 mt-1">
|
||||
{isCompiling && <span className="w-3.5 h-3.5 border-2 border-indigo-400 border-t-transparent rounded-full animate-spin" />}
|
||||
{status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<div className="grid grid-cols-2 gap-2.5">
|
||||
<div className="p-3 rounded-lg border border-slate-800 bg-slate-950/10">
|
||||
<div className="text-[10px] text-slate-400 font-semibold uppercase tracking-wider">Doc Handle ID</div>
|
||||
<div className="text-sm font-bold mt-0.5 text-indigo-300">{stats.docHandle || 'N/A'}</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-slate-800 bg-slate-950/10">
|
||||
<div className="text-[10px] text-slate-400 font-semibold uppercase tracking-wider">File Size</div>
|
||||
<div className="text-sm font-bold mt-0.5 text-slate-200">
|
||||
{stats.fileSize ? `${(stats.fileSize / 1024).toFixed(1)} KB` : '0 KB'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-slate-800 bg-slate-950/10">
|
||||
<div className="text-[10px] text-slate-400 font-semibold uppercase tracking-wider">WASM Load</div>
|
||||
<div className="text-sm font-bold mt-0.5 text-emerald-400">
|
||||
{stats.loadTimeMs ? `${stats.loadTimeMs.toFixed(1)}ms` : '0ms'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-slate-800 bg-slate-950/10">
|
||||
<div className="text-[10px] text-slate-400 font-semibold uppercase tracking-wider">Render Time</div>
|
||||
<div className="text-sm font-bold mt-0.5 text-emerald-400">
|
||||
{stats.renderTimeMs ? `${stats.renderTimeMs.toFixed(1)}ms` : '0ms'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Client-side Canvas Preview */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[11px] font-bold tracking-wider uppercase text-slate-400">Client Canvas Output</div>
|
||||
<div className="border border-slate-800 rounded-xl bg-slate-950 flex items-center justify-center p-4 overflow-hidden relative min-h-[160px]">
|
||||
<canvas ref={canvasRef} className="max-w-full max-h-[220px] rounded shadow-lg object-contain bg-slate-900 border border-slate-800/40" />
|
||||
{!stats.renderTimeMs && (
|
||||
<div className="absolute text-slate-500 text-xs font-bold uppercase tracking-widest">Awaiting Render...</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text Bounds JSON Output */}
|
||||
<div className="flex-1 flex flex-col space-y-1.5 min-h-[220px]">
|
||||
<div className="text-[11px] font-bold tracking-wider uppercase text-slate-400">extractTextWithBounds() JSON</div>
|
||||
<div className="flex-1 min-h-[150px] max-h-[300px] border border-slate-800 rounded-xl bg-slate-950 p-3 overflow-y-auto font-mono text-xs text-indigo-300">
|
||||
{textJson ? (
|
||||
<pre className="whitespace-pre-wrap">{textJson}</pre>
|
||||
) : (
|
||||
<div className="text-slate-650 italic text-center py-8">No text extracted yet</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Console Logs */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[11px] font-bold tracking-wider uppercase text-slate-400">Execution Console Logs</div>
|
||||
<div className="border border-slate-800 rounded-xl bg-slate-950 p-3 max-h-[200px] overflow-y-auto font-mono text-[10px] space-y-1 text-slate-400">
|
||||
{logs.map((log, i) => (
|
||||
<div key={i} className={log.includes('ERROR') ? 'text-rose-400' : log.includes('Success') || log.includes('successfully') ? 'text-emerald-400' : ''}>
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer / Engine Tag */}
|
||||
<div className="p-3 border-t border-slate-800 bg-slate-950/60 text-[10px] text-center text-slate-500 font-bold uppercase tracking-widest">
|
||||
{engineInfo || 'WASM Engine Offline'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -364,6 +364,26 @@ body {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-main);
|
||||
transition: transform 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.hidden-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
/**
|
||||
* Gateway API Service Client
|
||||
*
|
||||
* Handles all network requests to the FastAPI gateway backend.
|
||||
* Provides endpoints for document CRUD, rendering, metadata retrieval, and edits.
|
||||
*/
|
||||
|
||||
export interface PageInfo {
|
||||
index: number;
|
||||
width: number;
|
||||
@@ -411,6 +404,26 @@ class GatewayService {
|
||||
`;
|
||||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg.trim())}`;
|
||||
}
|
||||
|
||||
async exportDocument(documentId: string, filename: string): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/documents/${documentId}/export`);
|
||||
if (!response.ok) throw new Error(`Export failed: ${response.statusText}`);
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async fetchDocumentBytes(documentId: string): Promise<ArrayBuffer> {
|
||||
const response = await fetch(`${this.baseUrl}/documents/${documentId}/export`);
|
||||
if (!response.ok) throw new Error(`Failed to fetch document bytes: ${response.statusText}`);
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
export const gatewayService = new GatewayService();
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface WasmEngineInstance {
|
||||
freeDocument: (handle: number) => void;
|
||||
engineBuildInfo: () => string;
|
||||
engineHasSkia: () => boolean;
|
||||
getTextJson: (handle: number, page: number) => string;
|
||||
}
|
||||
|
||||
class WasmLoader {
|
||||
@@ -85,6 +86,9 @@ class WasmLoader {
|
||||
},
|
||||
engineHasSkia: () => {
|
||||
return Module.ccall('engineHasSkia', 'number', [], []) !== 0;
|
||||
},
|
||||
getTextJson: (handle: number, page: number) => {
|
||||
return Module.ccall('getPageTextJson', 'string', ['number', 'number'], [handle, page]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -131,6 +135,17 @@ class WasmLoader {
|
||||
},
|
||||
engineHasSkia: () => {
|
||||
return true;
|
||||
},
|
||||
getTextJson: (handle: number, page: number) => {
|
||||
void handle;
|
||||
return JSON.stringify([
|
||||
{ text: "P", x: 60.0, y: 80.0, w: 6.0, h: 8.0, fontSize: 12.0 },
|
||||
{ text: "a", x: 66.0, y: 80.0, w: 6.0, h: 8.0, fontSize: 12.0 },
|
||||
{ text: "g", x: 72.0, y: 80.0, w: 6.0, h: 8.0, fontSize: 12.0 },
|
||||
{ text: "e", x: 78.0, y: 80.0, w: 6.0, h: 8.0, fontSize: 12.0 },
|
||||
{ text: " ", x: 84.0, y: 80.0, w: 6.0, h: 8.0, fontSize: 12.0 },
|
||||
{ text: String(page + 1), x: 90.0, y: 80.0, w: 6.0, h: 8.0, fontSize: 12.0 }
|
||||
]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
|
||||
const handlePointerDown = (e: React.PointerEvent) => {
|
||||
if (activeTool !== 'draw') return;
|
||||
setIsDrawing(true);
|
||||
e.target.setPointerCapture?.(e.pointerId);
|
||||
if (e.target instanceof Element) {
|
||||
e.target.setPointerCapture(e.pointerId);
|
||||
}
|
||||
setCurrentPath([getCoordinates(e)]);
|
||||
};
|
||||
|
||||
@@ -46,7 +48,9 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
|
||||
const handlePointerUp = (e: React.PointerEvent) => {
|
||||
if (!isDrawing || activeTool !== 'draw') return;
|
||||
setIsDrawing(false);
|
||||
e.target.releasePointerCapture?.(e.pointerId);
|
||||
if (e.target instanceof Element) {
|
||||
e.target.releasePointerCapture(e.pointerId);
|
||||
}
|
||||
|
||||
if (currentPath.length > 1) {
|
||||
// Calculate bounding box
|
||||
|
||||
@@ -59,11 +59,13 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [scrollPosition, setScrollPosition] = useState({ scrollLeft: 0, scrollTop: 0 });
|
||||
const [renderedPages, setRenderedPages] = useState<string[]>([]);
|
||||
const [verifiedPages, setVerifiedPages] = useState<Record<number, boolean>>({});
|
||||
const [containerHeight, setContainerHeight] = useState(800);
|
||||
|
||||
// Reset cached page renders when switching documents
|
||||
useEffect(() => {
|
||||
setRenderedPages([]);
|
||||
setVerifiedPages({});
|
||||
}, [documentId]);
|
||||
|
||||
// Use provided dimensions or fallback to Letter size
|
||||
@@ -206,7 +208,7 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
|
||||
useEffect(() => {
|
||||
const verifyPageModel = async () => {
|
||||
if (visiblePages.length > 0 && documentId && !renderedPages[visiblePages[0].index + '_verified']) {
|
||||
if (visiblePages.length > 0 && documentId && !verifiedPages[visiblePages[0].index]) {
|
||||
const pageIndex = visiblePages[0].index;
|
||||
try {
|
||||
const model = await gatewayService.getPageModel(documentId, pageIndex);
|
||||
@@ -220,18 +222,17 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||
});
|
||||
});
|
||||
});
|
||||
setRenderedPages((prev) => {
|
||||
const next = [...prev];
|
||||
next[pageIndex + '_verified' as any] = 'true';
|
||||
return next;
|
||||
});
|
||||
setVerifiedPages((prev) => ({
|
||||
...prev,
|
||||
[pageIndex]: true,
|
||||
}));
|
||||
} catch (e) {
|
||||
// ignore or log
|
||||
}
|
||||
}
|
||||
};
|
||||
verifyPageModel();
|
||||
}, [visiblePages, documentId]);
|
||||
}, [visiblePages, documentId, verifiedPages]);
|
||||
|
||||
const handleTextSelection = (text: string, bbox: Rect) => {
|
||||
if (activeTool === 'highlight') {
|
||||
|
||||
Reference in New Issue
Block a user