search fuunction done
This commit is contained in:
+30
-8
@@ -5,7 +5,7 @@ import { PDFViewer } from './viewer/PDFViewer';
|
|||||||
import type { PDFViewerRef } from './viewer/PDFViewer';
|
import type { PDFViewerRef } from './viewer/PDFViewer';
|
||||||
import type { Annotation } from './viewer/AnnotationLayer';
|
import type { Annotation } from './viewer/AnnotationLayer';
|
||||||
import { gatewayService } from './lib/gatewayService';
|
import { gatewayService } from './lib/gatewayService';
|
||||||
import type { DocumentInfo } from './lib/gatewayService';
|
import type { DocumentInfo, SearchResult } from './lib/gatewayService';
|
||||||
import { wasmLoader } from './lib/wasmLoader';
|
import { wasmLoader } from './lib/wasmLoader';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
@@ -29,16 +29,30 @@ function App() {
|
|||||||
|
|
||||||
// Search State
|
// Search State
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
||||||
const [searchResultCount, setSearchResultCount] = useState(0);
|
const [searchResultCount, setSearchResultCount] = useState(0);
|
||||||
const [searchCurrentMatch, setSearchCurrentMatch] = useState(0);
|
const [searchCurrentMatch, setSearchCurrentMatch] = useState(0);
|
||||||
|
|
||||||
const handleSearch = (query: string) => {
|
const handleSearch = async (query: string) => {
|
||||||
setSearchQuery(query);
|
setSearchQuery(query);
|
||||||
// Mock search results for Phase 0
|
if (!query || !selectedDocId) {
|
||||||
if (query) {
|
setSearchResults([]);
|
||||||
setSearchResultCount(5);
|
setSearchResultCount(0);
|
||||||
setSearchCurrentMatch(0);
|
setSearchCurrentMatch(0);
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await gatewayService.searchDocument(selectedDocId, query);
|
||||||
|
setSearchResults(results);
|
||||||
|
setSearchResultCount(results.length);
|
||||||
|
setSearchCurrentMatch(0);
|
||||||
|
if (results.length > 0) {
|
||||||
|
viewerRef.current?.scrollToPage(results[0].pageIndex);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Search failed", err);
|
||||||
|
setSearchResults([]);
|
||||||
setSearchResultCount(0);
|
setSearchResultCount(0);
|
||||||
setSearchCurrentMatch(0);
|
setSearchCurrentMatch(0);
|
||||||
}
|
}
|
||||||
@@ -46,13 +60,17 @@ function App() {
|
|||||||
|
|
||||||
const handleSearchNext = () => {
|
const handleSearchNext = () => {
|
||||||
if (searchResultCount > 0) {
|
if (searchResultCount > 0) {
|
||||||
setSearchCurrentMatch((prev) => (prev + 1) % searchResultCount);
|
const nextMatch = (searchCurrentMatch + 1) % searchResultCount;
|
||||||
|
setSearchCurrentMatch(nextMatch);
|
||||||
|
viewerRef.current?.scrollToPage(searchResults[nextMatch].pageIndex);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchPrev = () => {
|
const handleSearchPrev = () => {
|
||||||
if (searchResultCount > 0) {
|
if (searchResultCount > 0) {
|
||||||
setSearchCurrentMatch((prev) => (prev - 1 + searchResultCount) % searchResultCount);
|
const prevMatch = (searchCurrentMatch - 1 + searchResultCount) % searchResultCount;
|
||||||
|
setSearchCurrentMatch(prevMatch);
|
||||||
|
viewerRef.current?.scrollToPage(searchResults[prevMatch].pageIndex);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,11 +216,15 @@ function App() {
|
|||||||
ref={viewerRef}
|
ref={viewerRef}
|
||||||
documentId={activeDoc.id}
|
documentId={activeDoc.id}
|
||||||
totalPages={activeDoc.totalPages}
|
totalPages={activeDoc.totalPages}
|
||||||
|
pageWidth={activeDoc.pageWidth}
|
||||||
|
pageHeight={activeDoc.pageHeight}
|
||||||
zoom={zoom}
|
zoom={zoom}
|
||||||
rotation={rotation}
|
rotation={rotation}
|
||||||
activeTool={activeTool}
|
activeTool={activeTool}
|
||||||
annotations={annotations}
|
annotations={annotations}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
|
searchResults={searchResults}
|
||||||
|
searchCurrentMatch={searchCurrentMatch}
|
||||||
onAnnotationAdded={handleAnnotationAdded}
|
onAnnotationAdded={handleAnnotationAdded}
|
||||||
onPageVisible={setCurrentPage}
|
onPageVisible={setCurrentPage}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -160,6 +160,14 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
Highlight
|
Highlight
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onActiveToolChange('draw')}
|
||||||
|
className={`tool-btn draw ${activeTool === 'draw' ? 'active' : ''}`}
|
||||||
|
title="Freehand Ink Pen"
|
||||||
|
>
|
||||||
|
Draw
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => onActiveToolChange('signature')}
|
onClick={() => onActiveToolChange('signature')}
|
||||||
className={`tool-btn signature ${activeTool === 'signature' ? 'active' : ''}`}
|
className={`tool-btn signature ${activeTool === 'signature' ? 'active' : ''}`}
|
||||||
|
|||||||
@@ -313,6 +313,12 @@ body {
|
|||||||
border: 1px solid rgba(245, 158, 11, 0.4);
|
border: 1px solid rgba(245, 158, 11, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-btn.draw.active {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #60a5fa;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
.tool-btn.signature.active {
|
.tool-btn.signature.active {
|
||||||
background: rgba(79, 70, 229, 0.2);
|
background: rgba(79, 70, 229, 0.2);
|
||||||
color: #c7d2fe;
|
color: #c7d2fe;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface DocumentInfo {
|
|||||||
filename: string;
|
filename: string;
|
||||||
sizeBytes: number;
|
sizeBytes: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
|
pageWidth: number;
|
||||||
|
pageHeight: number;
|
||||||
uploadedAt: string;
|
uploadedAt: string;
|
||||||
status: 'processing' | 'ready' | 'error';
|
status: 'processing' | 'ready' | 'error';
|
||||||
}
|
}
|
||||||
@@ -21,7 +23,20 @@ export interface RenderParams {
|
|||||||
rotation: number;
|
rotation: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
import type { Point, Rect } from './coordinateMapping';
|
import type { Point } from './coordinateMapping';
|
||||||
|
|
||||||
|
export interface SearchRect {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
pageIndex: number;
|
||||||
|
rects: SearchRect[];
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TextOverlayData {
|
export interface TextOverlayData {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -171,6 +186,8 @@ class GatewayService {
|
|||||||
filename: file.name,
|
filename: file.name,
|
||||||
sizeBytes: file.size,
|
sizeBytes: file.size,
|
||||||
totalPages: 5, // Mocked total pages
|
totalPages: 5, // Mocked total pages
|
||||||
|
pageWidth: 612,
|
||||||
|
pageHeight: 792,
|
||||||
uploadedAt: new Date().toISOString(),
|
uploadedAt: new Date().toISOString(),
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
});
|
});
|
||||||
@@ -248,6 +265,21 @@ class GatewayService {
|
|||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async searchDocument(documentId: string, query: string): Promise<SearchResult[]> {
|
||||||
|
if (!query) return [];
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams({ q: query });
|
||||||
|
const response = await fetch(`${this.baseUrl}/documents/${documentId}/search?${urlParams.toString()}`);
|
||||||
|
|
||||||
|
if (response.status === 501) {
|
||||||
|
// Return mock empty results if backend isn't available
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`Failed to search document: ${response.statusText}`);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
private getMockDocuments(): DocumentInfo[] {
|
private getMockDocuments(): DocumentInfo[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -255,6 +287,8 @@ class GatewayService {
|
|||||||
filename: 'Quarterly_Financial_Report.pdf',
|
filename: 'Quarterly_Financial_Report.pdf',
|
||||||
sizeBytes: 1024 * 1024 * 3.4, // 3.4MB
|
sizeBytes: 1024 * 1024 * 3.4, // 3.4MB
|
||||||
totalPages: 12,
|
totalPages: 12,
|
||||||
|
pageWidth: 612,
|
||||||
|
pageHeight: 792,
|
||||||
uploadedAt: new Date(Date.now() - 1000 * 60 * 60 * 3).toISOString(),
|
uploadedAt: new Date(Date.now() - 1000 * 60 * 60 * 3).toISOString(),
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
},
|
},
|
||||||
@@ -263,6 +297,8 @@ class GatewayService {
|
|||||||
filename: 'Engineering_Specification_v4.pdf',
|
filename: 'Engineering_Specification_v4.pdf',
|
||||||
sizeBytes: 1024 * 1024 * 18.2, // 18.2MB
|
sizeBytes: 1024 * 1024 * 18.2, // 18.2MB
|
||||||
totalPages: 54,
|
totalPages: 54,
|
||||||
|
pageWidth: 612,
|
||||||
|
pageHeight: 792,
|
||||||
uploadedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2).toISOString(),
|
uploadedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2).toISOString(),
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
},
|
},
|
||||||
@@ -271,6 +307,8 @@ class GatewayService {
|
|||||||
filename: 'Tenant_Lease_Agreement_Final.pdf',
|
filename: 'Tenant_Lease_Agreement_Final.pdf',
|
||||||
sizeBytes: 1024 * 245, // 245KB
|
sizeBytes: 1024 * 245, // 245KB
|
||||||
totalPages: 4,
|
totalPages: 4,
|
||||||
|
pageWidth: 612,
|
||||||
|
pageHeight: 792,
|
||||||
uploadedAt: new Date(Date.now() - 1000 * 60 * 45).toISOString(),
|
uploadedAt: new Date(Date.now() - 1000 * 60 * 45).toISOString(),
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ class WasmLoader {
|
|||||||
try {
|
try {
|
||||||
// Dynamic import from the public folder / static route
|
// Dynamic import from the public folder / static route
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const createModule = (await import(/* @vite-ignore */ '/pdfengine.mjs')).default;
|
const moduleUrl = (await import('/pdfengine.mjs?url')).default;
|
||||||
|
const createModule = (await import(/* @vite-ignore */ moduleUrl)).default;
|
||||||
const Module = await createModule({
|
const Module = await createModule({
|
||||||
locateFile: (path: string) => {
|
locateFile: (path: string) => {
|
||||||
if (path.endsWith('.wasm')) {
|
if (path.endsWith('.wasm')) {
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import type { Rect } from '../lib/coordinateMapping';
|
|||||||
|
|
||||||
export interface Annotation {
|
export interface Annotation {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'highlight' | 'signature' | 'strikeout' | 'comment';
|
type: 'highlight' | 'signature' | 'strikeout' | 'comment' | 'ink';
|
||||||
bbox: Rect;
|
bbox: Rect;
|
||||||
color?: string;
|
color?: string;
|
||||||
author: string;
|
author: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
|
paths?: { x: number; y: number }[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AnnotationLayerProps {
|
interface AnnotationLayerProps {
|
||||||
@@ -59,6 +60,23 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* Ink Annotations */}
|
||||||
|
<svg
|
||||||
|
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}
|
||||||
|
>
|
||||||
|
{annotations
|
||||||
|
.filter((anno) => anno.type === 'ink' && anno.paths)
|
||||||
|
.map((anno) => (
|
||||||
|
<g key={anno.id} stroke={anno.color || '#3b82f6'} strokeWidth={2 * zoom} fill="none" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
{anno.paths!.map((path, i) => {
|
||||||
|
if (path.length === 0) return null;
|
||||||
|
const d = path.map((pt, j) => `${j === 0 ? 'M' : 'L'} ${pt.x * zoom} ${pt.y * zoom}`).join(' ');
|
||||||
|
return <path key={i} d={d} />;
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import React from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
|
import type { Annotation } from './AnnotationLayer';
|
||||||
|
|
||||||
interface OverlayLayerProps {
|
interface OverlayLayerProps {
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
activeTool: string;
|
activeTool: string;
|
||||||
onDrawStroke?: (path: string) => void;
|
zoom: number;
|
||||||
|
onAnnotationAdded?: (anno: Annotation) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OverlayLayer: React.FC<OverlayLayerProps> = ({
|
export const OverlayLayer: React.FC<OverlayLayerProps> = ({
|
||||||
@@ -13,11 +15,70 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
activeTool,
|
activeTool,
|
||||||
|
zoom,
|
||||||
|
onAnnotationAdded,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isDrawing, setIsDrawing] = useState(false);
|
||||||
|
const [currentPath, setCurrentPath] = useState<{ x: number; y: number }[]>([]);
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
|
const getCoordinates = (e: React.MouseEvent | MouseEvent) => {
|
||||||
|
if (!svgRef.current) return { x: 0, y: 0 };
|
||||||
|
const rect = svgRef.current.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: (e.clientX - rect.left) / zoom,
|
||||||
|
y: (e.clientY - rect.top) / zoom,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerDown = (e: React.PointerEvent) => {
|
||||||
|
if (activeTool !== 'draw') return;
|
||||||
|
setIsDrawing(true);
|
||||||
|
e.target.setPointerCapture?.(e.pointerId);
|
||||||
|
setCurrentPath([getCoordinates(e)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerMove = (e: React.PointerEvent) => {
|
||||||
|
if (!isDrawing || activeTool !== 'draw') return;
|
||||||
|
setCurrentPath((prev) => [...prev, getCoordinates(e)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerUp = (e: React.PointerEvent) => {
|
||||||
|
if (!isDrawing || activeTool !== 'draw') return;
|
||||||
|
setIsDrawing(false);
|
||||||
|
e.target.releasePointerCapture?.(e.pointerId);
|
||||||
|
|
||||||
|
if (currentPath.length > 1) {
|
||||||
|
// Calculate bounding box
|
||||||
|
const xs = currentPath.map(p => p.x);
|
||||||
|
const ys = currentPath.map(p => p.y);
|
||||||
|
const minX = Math.min(...xs);
|
||||||
|
const maxX = Math.max(...xs);
|
||||||
|
const minY = Math.min(...ys);
|
||||||
|
const maxY = Math.max(...ys);
|
||||||
|
|
||||||
|
const newAnno: Annotation = {
|
||||||
|
id: `anno_${Math.random().toString(36).substring(2, 11)}`,
|
||||||
|
type: 'ink',
|
||||||
|
bbox: {
|
||||||
|
x: minX,
|
||||||
|
y: minY,
|
||||||
|
width: maxX - minX,
|
||||||
|
height: maxY - minY,
|
||||||
|
},
|
||||||
|
author: 'Current User',
|
||||||
|
paths: [currentPath],
|
||||||
|
color: '#3b82f6', // Default blue color for now
|
||||||
|
};
|
||||||
|
onAnnotationAdded?.(newAnno);
|
||||||
|
}
|
||||||
|
setCurrentPath([]);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="overlay-layer"
|
className="overlay-layer"
|
||||||
style={{ width: `${width}px`, height: `${height}px` }}
|
style={{ width: `${width}px`, height: `${height}px`, pointerEvents: activeTool === 'draw' ? 'auto' : 'none' }}
|
||||||
>
|
>
|
||||||
{/* Signature overlay state indicator */}
|
{/* Signature overlay state indicator */}
|
||||||
{activeTool === 'signature' && (
|
{activeTool === 'signature' && (
|
||||||
@@ -29,9 +90,30 @@ export const OverlayLayer: React.FC<OverlayLayerProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTool === 'draw' && (
|
{activeTool === 'draw' && (
|
||||||
<div className="overlay-toast animate-pulse">
|
<>
|
||||||
|
<div className="overlay-toast animate-pulse" style={{ pointerEvents: 'none' }}>
|
||||||
Freehand Ink Pen Enabled
|
Freehand Ink Pen Enabled
|
||||||
</div>
|
</div>
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
style={{ width: '100%', height: '100%', position: 'absolute', top: 0, left: 0, cursor: 'crosshair', touchAction: 'none' }}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerMove={handlePointerMove}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onPointerCancel={handlePointerUp}
|
||||||
|
>
|
||||||
|
{currentPath.length > 0 && (
|
||||||
|
<path
|
||||||
|
d={currentPath.map((pt, i) => `${i === 0 ? 'M' : 'L'} ${pt.x * zoom} ${pt.y * zoom}`).join(' ')}
|
||||||
|
stroke="#3b82f6"
|
||||||
|
strokeWidth={2 * zoom}
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,15 +7,20 @@ import { OverlayLayer } from './OverlayLayer';
|
|||||||
import { SearchOverlayLayer } from './SearchOverlayLayer';
|
import { SearchOverlayLayer } from './SearchOverlayLayer';
|
||||||
import type { Rect } from '../lib/coordinateMapping';
|
import type { Rect } from '../lib/coordinateMapping';
|
||||||
import { gatewayService } from '../lib/gatewayService';
|
import { gatewayService } from '../lib/gatewayService';
|
||||||
|
import type { SearchResult } from '../lib/gatewayService';
|
||||||
|
|
||||||
interface PDFViewerProps {
|
interface PDFViewerProps {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
|
pageWidth: number;
|
||||||
|
pageHeight: number;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
rotation: number;
|
rotation: number;
|
||||||
activeTool: string;
|
activeTool: string;
|
||||||
annotations: Annotation[];
|
annotations: Annotation[];
|
||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
|
searchResults?: SearchResult[];
|
||||||
|
searchCurrentMatch?: number;
|
||||||
onAnnotationAdded?: (anno: Annotation) => void;
|
onAnnotationAdded?: (anno: Annotation) => void;
|
||||||
onPageVisible?: (pageIndex: number) => void;
|
onPageVisible?: (pageIndex: number) => void;
|
||||||
}
|
}
|
||||||
@@ -36,11 +41,15 @@ export interface PDFViewerRef {
|
|||||||
export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
||||||
documentId,
|
documentId,
|
||||||
totalPages,
|
totalPages,
|
||||||
|
pageWidth,
|
||||||
|
pageHeight,
|
||||||
zoom,
|
zoom,
|
||||||
rotation,
|
rotation,
|
||||||
activeTool,
|
activeTool,
|
||||||
annotations,
|
annotations,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
searchResults,
|
||||||
|
searchCurrentMatch,
|
||||||
onAnnotationAdded,
|
onAnnotationAdded,
|
||||||
onPageVisible,
|
onPageVisible,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
@@ -49,9 +58,9 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
|||||||
const [renderedPages, setRenderedPages] = useState<string[]>([]);
|
const [renderedPages, setRenderedPages] = useState<string[]>([]);
|
||||||
const [containerHeight, setContainerHeight] = useState(800);
|
const [containerHeight, setContainerHeight] = useState(800);
|
||||||
|
|
||||||
// Standard Page Dimensions: Letter size is 612x792 pt
|
// Use provided dimensions or fallback to Letter size
|
||||||
const basePageWidth = 612;
|
const basePageWidth = pageWidth || 612;
|
||||||
const basePageHeight = 792;
|
const basePageHeight = pageHeight || 792;
|
||||||
const pageGap = 24; // space between pages
|
const pageGap = 24; // space between pages
|
||||||
|
|
||||||
// Calculate layout coordinates for all pages sequentially
|
// Calculate layout coordinates for all pages sequentially
|
||||||
@@ -298,6 +307,8 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
|||||||
width={page.width}
|
width={page.width}
|
||||||
height={page.height}
|
height={page.height}
|
||||||
activeTool={activeTool}
|
activeTool={activeTool}
|
||||||
|
zoom={zoom}
|
||||||
|
onAnnotationAdded={onAnnotationAdded}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Search highlights overlay */}
|
{/* Search highlights overlay */}
|
||||||
@@ -307,6 +318,8 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
|||||||
height={page.height}
|
height={page.height}
|
||||||
zoom={zoom}
|
zoom={zoom}
|
||||||
searchQuery={searchQuery || ''}
|
searchQuery={searchQuery || ''}
|
||||||
|
searchResults={searchResults}
|
||||||
|
searchCurrentMatch={searchCurrentMatch}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import type { SearchResult } from '../lib/gatewayService';
|
||||||
|
|
||||||
interface SearchOverlayLayerProps {
|
interface SearchOverlayLayerProps {
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
@@ -6,6 +7,8 @@ interface SearchOverlayLayerProps {
|
|||||||
height: number;
|
height: number;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
|
searchResults?: SearchResult[];
|
||||||
|
searchCurrentMatch?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchOverlayLayer: React.FC<SearchOverlayLayerProps> = ({
|
export const SearchOverlayLayer: React.FC<SearchOverlayLayerProps> = ({
|
||||||
@@ -14,47 +17,45 @@ export const SearchOverlayLayer: React.FC<SearchOverlayLayerProps> = ({
|
|||||||
height,
|
height,
|
||||||
zoom,
|
zoom,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
searchResults = [],
|
||||||
|
searchCurrentMatch = 0,
|
||||||
}) => {
|
}) => {
|
||||||
if (!searchQuery) return null;
|
if (!searchQuery || searchResults.length === 0) return null;
|
||||||
|
|
||||||
// Mock some search results based on the query for Phase 0/1
|
// Filter results for this specific page and keep track of global index
|
||||||
// We'll just generate deterministic-looking boxes so it looks like it found something
|
const pageMatches = searchResults
|
||||||
const mockResults = [];
|
.map((result, globalIndex) => ({ ...result, globalIndex }))
|
||||||
const hash = searchQuery.length + pageIndex;
|
.filter((result) => result.pageIndex === pageIndex);
|
||||||
|
|
||||||
if (hash % 3 !== 0) {
|
if (pageMatches.length === 0) return null;
|
||||||
mockResults.push({
|
|
||||||
x: 100 * zoom,
|
|
||||||
y: (150 + hash * 10) * zoom,
|
|
||||||
width: 120 * zoom,
|
|
||||||
height: 18 * zoom,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (hash % 2 === 0) {
|
|
||||||
mockResults.push({
|
|
||||||
x: 300 * zoom,
|
|
||||||
y: (250 + hash * 5) * zoom,
|
|
||||||
width: 80 * zoom,
|
|
||||||
height: 18 * zoom,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute top-0 left-0 pointer-events-none z-20"
|
className="absolute top-0 left-0 pointer-events-none z-20"
|
||||||
style={{ width: `${width}px`, height: `${height}px` }}
|
style={{ width: `${width}px`, height: `${height}px` }}
|
||||||
>
|
>
|
||||||
{mockResults.map((rect, idx) => (
|
{pageMatches.map((match) => (
|
||||||
|
<React.Fragment key={match.globalIndex}>
|
||||||
|
{match.rects.map((rect, rectIdx) => {
|
||||||
|
const isActive = match.globalIndex === searchCurrentMatch;
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={rectIdx}
|
||||||
className="absolute bg-yellow-400/40 border border-yellow-500/60 rounded-sm"
|
className={`absolute rounded-sm border ${
|
||||||
|
isActive
|
||||||
|
? 'bg-orange-500/50 border-orange-600/80 shadow-[0_0_8px_rgba(249,115,22,0.6)] z-30'
|
||||||
|
: 'bg-yellow-400/40 border-yellow-500/60'
|
||||||
|
}`}
|
||||||
style={{
|
style={{
|
||||||
left: `${rect.x}px`,
|
left: `${rect.x * zoom}px`,
|
||||||
top: `${rect.y}px`,
|
top: `${rect.y * zoom}px`,
|
||||||
width: `${rect.width}px`,
|
width: `${rect.w * zoom}px`,
|
||||||
height: `${rect.height}px`,
|
height: `${rect.h * zoom}px`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class DocumentInfoResponse(BaseModel):
|
|||||||
filename: str
|
filename: str
|
||||||
sizeBytes: int
|
sizeBytes: int
|
||||||
totalPages: int
|
totalPages: int
|
||||||
|
pageWidth: float
|
||||||
|
pageHeight: float
|
||||||
uploadedAt: str
|
uploadedAt: str
|
||||||
status: str
|
status: str
|
||||||
|
|
||||||
@@ -33,6 +35,8 @@ async def upload_document(file: UploadFile = File(...), password: str = "") -> D
|
|||||||
filename=info["filename"],
|
filename=info["filename"],
|
||||||
sizeBytes=info["sizeBytes"],
|
sizeBytes=info["sizeBytes"],
|
||||||
totalPages=info["totalPages"],
|
totalPages=info["totalPages"],
|
||||||
|
pageWidth=info["pageWidth"],
|
||||||
|
pageHeight=info["pageHeight"],
|
||||||
uploadedAt=info["uploadedAt"],
|
uploadedAt=info["uploadedAt"],
|
||||||
status=info["status"]
|
status=info["status"]
|
||||||
)
|
)
|
||||||
@@ -62,6 +66,8 @@ def list_documents() -> List[DocumentInfoResponse]:
|
|||||||
filename=d["filename"],
|
filename=d["filename"],
|
||||||
sizeBytes=d["sizeBytes"],
|
sizeBytes=d["sizeBytes"],
|
||||||
totalPages=d["totalPages"],
|
totalPages=d["totalPages"],
|
||||||
|
pageWidth=d.get("pageWidth", 612.0),
|
||||||
|
pageHeight=d.get("pageHeight", 792.0),
|
||||||
uploadedAt=d["uploadedAt"],
|
uploadedAt=d["uploadedAt"],
|
||||||
status=d["status"]
|
status=d["status"]
|
||||||
)
|
)
|
||||||
@@ -85,6 +91,8 @@ def get_document(document_id: str) -> DocumentInfoResponse:
|
|||||||
filename=d["filename"],
|
filename=d["filename"],
|
||||||
sizeBytes=d["sizeBytes"],
|
sizeBytes=d["sizeBytes"],
|
||||||
totalPages=d["totalPages"],
|
totalPages=d["totalPages"],
|
||||||
|
pageWidth=d.get("pageWidth", 612.0),
|
||||||
|
pageHeight=d.get("pageHeight", 792.0),
|
||||||
uploadedAt=d["uploadedAt"],
|
uploadedAt=d["uploadedAt"],
|
||||||
status=d["status"]
|
status=d["status"]
|
||||||
)
|
)
|
||||||
@@ -203,3 +211,95 @@ def get_document_fonts(document_id: str, start_page: int = 0, end_page: int = -1
|
|||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||||
|
|
||||||
|
class SearchRect(BaseModel):
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
w: float
|
||||||
|
h: float
|
||||||
|
|
||||||
|
class SearchMatch(BaseModel):
|
||||||
|
pageIndex: int
|
||||||
|
rects: List[SearchRect]
|
||||||
|
text: str
|
||||||
|
|
||||||
|
@router.get("/{document_id}/search", response_model=List[SearchMatch])
|
||||||
|
def search_document(document_id: str, q: str) -> List[SearchMatch]:
|
||||||
|
if not engine.is_available():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
|
detail="Engine bridge (bindings/python) not yet available."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not q:
|
||||||
|
return []
|
||||||
|
|
||||||
|
doc_info = document_store.get_document(document_id)
|
||||||
|
if not doc_info:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc = doc_info["doc_instance"]
|
||||||
|
matches = []
|
||||||
|
lower_query = q.lower()
|
||||||
|
query_len = len(lower_query)
|
||||||
|
|
||||||
|
for page_idx in range(doc.page_count):
|
||||||
|
page = doc.get_page(page_idx)
|
||||||
|
glyphs = page.extract_text_with_bounds()
|
||||||
|
if not glyphs:
|
||||||
|
continue
|
||||||
|
|
||||||
|
text_str = ""
|
||||||
|
char_to_glyph = []
|
||||||
|
for i, g in enumerate(glyphs):
|
||||||
|
s = g.get("text", "")
|
||||||
|
start_len = len(text_str)
|
||||||
|
text_str += s
|
||||||
|
for _ in range(len(text_str) - start_len):
|
||||||
|
char_to_glyph.append(i)
|
||||||
|
|
||||||
|
lower_text = text_str.lower()
|
||||||
|
idx = 0
|
||||||
|
while True:
|
||||||
|
idx = lower_text.find(lower_query, idx)
|
||||||
|
if idx == -1:
|
||||||
|
break
|
||||||
|
|
||||||
|
start_glyph_idx = char_to_glyph[idx]
|
||||||
|
end_glyph_idx = char_to_glyph[idx + query_len - 1]
|
||||||
|
|
||||||
|
rects = []
|
||||||
|
current_rect = None
|
||||||
|
|
||||||
|
for g_idx in range(start_glyph_idx, end_glyph_idx + 1):
|
||||||
|
g = glyphs[g_idx]
|
||||||
|
dom_y = page.height - (g["y"] + g["h"])
|
||||||
|
|
||||||
|
if current_rect is None:
|
||||||
|
current_rect = {"x": g["x"], "y": dom_y, "w": g["w"], "h": g["h"]}
|
||||||
|
else:
|
||||||
|
if abs(dom_y - current_rect["y"]) < g.get("fontSize", 12) * 0.5:
|
||||||
|
max_x = max(current_rect["x"] + current_rect["w"], g["x"] + g["w"])
|
||||||
|
current_rect["w"] = max_x - current_rect["x"]
|
||||||
|
current_rect["y"] = min(current_rect["y"], dom_y)
|
||||||
|
current_rect["h"] = max(current_rect["h"], g["h"])
|
||||||
|
else:
|
||||||
|
rects.append(SearchRect(**current_rect))
|
||||||
|
current_rect = {"x": g["x"], "y": dom_y, "w": g["w"], "h": g["h"]}
|
||||||
|
|
||||||
|
if current_rect:
|
||||||
|
rects.append(SearchRect(**current_rect))
|
||||||
|
|
||||||
|
matches.append(SearchMatch(
|
||||||
|
pageIndex=page_idx,
|
||||||
|
rects=rects,
|
||||||
|
text=text_str[idx:idx + query_len]
|
||||||
|
))
|
||||||
|
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
return matches
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
||||||
@@ -11,12 +11,23 @@ class DocumentStore:
|
|||||||
def add_document(self, filename: str, bytes_data: bytes, doc_instance: Any) -> Dict[str, Any]:
|
def add_document(self, filename: str, bytes_data: bytes, doc_instance: Any) -> Dict[str, Any]:
|
||||||
doc_id = str(uuid.uuid4())
|
doc_id = str(uuid.uuid4())
|
||||||
uploaded_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
uploaded_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||||
|
page_width = 612.0
|
||||||
|
page_height = 792.0
|
||||||
|
if doc_instance.page_count > 0:
|
||||||
|
try:
|
||||||
|
page_0 = doc_instance.get_page(0)
|
||||||
|
page_width = page_0.width
|
||||||
|
page_height = page_0.height
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
info = {
|
info = {
|
||||||
"id": doc_id,
|
"id": doc_id,
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"sizeBytes": len(bytes_data),
|
"sizeBytes": len(bytes_data),
|
||||||
"totalPages": doc_instance.page_count,
|
"totalPages": doc_instance.page_count,
|
||||||
|
"pageWidth": page_width,
|
||||||
|
"pageHeight": page_height,
|
||||||
"uploadedAt": uploaded_at,
|
"uploadedAt": uploaded_at,
|
||||||
"status": "ready",
|
"status": "ready",
|
||||||
"doc_instance": doc_instance,
|
"doc_instance": doc_instance,
|
||||||
|
|||||||
+10
-9
@@ -43,19 +43,20 @@ if (-not $vcpkgRoot) {
|
|||||||
Write-Host "VCPKG_ROOT defaulted to: $vcpkgRoot" -ForegroundColor Yellow
|
Write-Host "VCPKG_ROOT defaulted to: $vcpkgRoot" -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
$vcvars = "C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
|
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
||||||
if (-not (Test-Path $vcvars)) {
|
if (Test-Path $vswhere) {
|
||||||
$vcvars = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $vcvars)) {
|
|
||||||
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
|
||||||
if (Test-Path $vswhere) {
|
|
||||||
$vsPath = (& $vswhere -latest -prerelease -products * -property installationPath | Select-Object -First 1)
|
$vsPath = (& $vswhere -latest -prerelease -products * -property installationPath | Select-Object -First 1)
|
||||||
if ($vsPath) {
|
if ($vsPath) {
|
||||||
$vcvars = Join-Path $vsPath "VC\Auxiliary\Build\vcvars64.bat"
|
$vcvars = Join-Path $vsPath "VC\Auxiliary\Build\vcvars64.bat"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (-not $vcvars -or -not (Test-Path $vcvars)) {
|
||||||
|
$vcvars = "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $vcvars)) {
|
||||||
|
$vcvars = "C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvars64.bat"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $vcvars)) {
|
if (-not (Test-Path $vcvars)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user