Files
pdf/frontend/src/lib/gatewayService.ts
T

373 lines
13 KiB
TypeScript

/**
* 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;
height: number;
}
export interface DocumentInfo {
id: string;
filename: string;
sizeBytes: number;
totalPages: number;
uploadedAt: string;
status: 'processing' | 'ready' | 'error';
pages?: PageInfo[];
}
export interface RenderParams {
documentId: string;
pageIndex: number;
zoom: number;
rotation: number;
}
import type { Point, Rect } from './coordinateMapping';
export interface TextOverlayData {
text: string;
x: number;
y: number;
width: number;
height: number;
fontSize: number;
fontFamily: string;
color: string;
}
export interface RedactionData {
x: number;
y: number;
width: number;
height: number;
fillColor: string;
}
export interface ImageOverlayData {
x: number;
y: number;
width: number;
height: number;
imageData: string;
}
export interface HighlightQuadPoint {
x1: number;
y1: number;
x2: number;
y2: number;
x3: number;
y3: number;
x4: number;
y4: number;
}
export interface HighlightData {
quadPoints: HighlightQuadPoint[];
color: string;
opacity: number;
author: string;
content?: string;
}
export interface FreeTextData {
x: number;
y: number;
width: number;
height: number;
text: string;
fontSize: number;
color: string;
}
export interface StickyNoteData {
x: number;
y: number;
author: string;
content: string;
}
export interface FreehandData {
paths: Point[][];
color: string;
thickness: number;
}
export interface PageRotationData {
rotation: 0 | 90 | 180 | 270;
}
export interface PageDeletionData {}
export interface PageReorderData {
destPageIndex: number;
}
export type EditOperationDataMap = {
text_overlay: TextOverlayData;
redaction: RedactionData;
image_overlay: ImageOverlayData;
highlight: HighlightData;
free_text: FreeTextData;
comment: StickyNoteData;
freehand: FreehandData;
page_rotation: PageRotationData;
page_deletion: PageDeletionData;
page_reorder: PageReorderData;
};
export type EditOperationType = keyof EditOperationDataMap;
export interface EditOperationBase<T extends EditOperationType> {
id: string;
type: T;
pageIndex: number;
data: EditOperationDataMap[T];
}
export type EditOperation = {
[T in EditOperationType]: EditOperationBase<T>;
}[EditOperationType];
export interface EditOperationEnvelope {
version: '1.0';
operations: EditOperation[];
}
class GatewayService {
private baseUrl: string;
constructor() {
// In dev environment, FastAPI gateway runs on port 8000 by default.
this.baseUrl = import.meta.env.VITE_GATEWAY_URL || 'http://127.0.0.1:8000';
}
async getHealth(): Promise<{ status: string; version: string; engine_available: boolean }> {
const response = await fetch(`${this.baseUrl}/health`);
if (!response.ok) throw new Error(`Health check failed: ${response.statusText}`);
return response.json();
}
async listDocuments(): Promise<DocumentInfo[]> {
try {
const response = await fetch(`${this.baseUrl}/documents`);
if (response.status === 501) {
return this.getMockDocuments();
}
if (!response.ok) throw new Error(`Failed to list documents: ${response.statusText}`);
return response.json();
} catch (err) {
// Fallback if backend is not running at all
return this.getMockDocuments();
}
}
async uploadDocument(file: File): Promise<DocumentInfo> {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${this.baseUrl}/documents`, {
method: 'POST',
body: formData,
});
if (response.status === 501) {
// Simulate upload for Phase 0 scaffolding
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: `doc_${Math.random().toString(36).substr(2, 9)}`,
filename: file.name,
sizeBytes: file.size,
totalPages: 5, // Mocked total pages
uploadedAt: new Date().toISOString(),
status: 'ready',
pages: Array.from({ length: 5 }, (_, i) => ({ index: i, width: 612, height: 792 })),
});
}, 1000);
});
}
if (!response.ok) throw new Error(`Upload failed: ${response.statusText}`);
return response.json();
}
async getDocument(id: string): Promise<DocumentInfo> {
try {
const response = await fetch(`${this.baseUrl}/documents/${id}`);
if (response.status === 501) {
const mock = this.getMockDocuments().find(d => d.id === id);
if (!mock) throw new Error('Document not found');
return mock;
}
if (!response.ok) throw new Error(`Failed to fetch document metadata: ${response.statusText}`);
return response.json();
} catch (err) {
const mock = this.getMockDocuments().find(d => d.id === id);
if (!mock) throw new Error('Document not found');
return mock;
}
}
async deleteDocument(id: string): Promise<{ success: boolean }> {
const response = await fetch(`${this.baseUrl}/documents/${id}`, {
method: 'DELETE',
});
if (response.status === 501) {
return { success: true };
}
if (!response.ok) throw new Error(`Failed to delete document: ${response.statusText}`);
return response.json();
}
async renderPage(params: RenderParams): Promise<string> {
try {
const query = new URLSearchParams({
page: params.pageIndex.toString(),
zoom: params.zoom.toString(),
rotation: params.rotation.toString(),
}).toString();
const url = `${this.baseUrl}/render/${params.documentId}?${query}`;
const response = await fetch(url);
if (response.status === 501) {
return this.generateMockPage(params.pageIndex);
}
if (!response.ok) throw new Error(`Page render failed: ${response.statusText}`);
const blob = await response.blob();
return URL.createObjectURL(blob);
} catch (err) {
return this.generateMockPage(params.pageIndex);
}
}
async applyEdits(documentId: string, operations: EditOperation[]): Promise<{ success: boolean; newDocumentId: string }> {
const response = await fetch(`${this.baseUrl}/edits/${documentId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ version: '1.0', operations } as EditOperationEnvelope),
});
if (response.status === 501) {
return { success: true, newDocumentId: `${documentId}_edited` };
}
if (!response.ok) throw new Error(`Failed to apply edits: ${response.statusText}`);
return response.json();
}
private getMockDocuments(): DocumentInfo[] {
return [
{
id: 'sample-doc-1',
filename: 'Quarterly_Financial_Report.pdf',
sizeBytes: 1024 * 1024 * 3.4, // 3.4MB
totalPages: 12,
uploadedAt: new Date(Date.now() - 1000 * 60 * 60 * 3).toISOString(),
status: 'ready',
pages: Array.from({ length: 12 }, (_, i) => ({ index: i, width: 612, height: 792 })),
},
{
id: 'sample-doc-2',
filename: 'Engineering_Specification_v4.pdf',
sizeBytes: 1024 * 1024 * 18.2, // 18.2MB
totalPages: 54,
uploadedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2).toISOString(),
status: 'ready',
pages: Array.from({ length: 54 }, (_, i) => ({ index: i, width: 612, height: 792 })),
},
{
id: 'sample-doc-3',
filename: 'Tenant_Lease_Agreement_Final.pdf',
sizeBytes: 1024 * 245, // 245KB
totalPages: 4,
uploadedAt: new Date(Date.now() - 1000 * 60 * 45).toISOString(),
status: 'ready',
pages: Array.from({ length: 4 }, (_, i) => ({ index: i, width: 612, height: 792 })),
}
];
}
private generateMockPage(pageIndex: number): string {
// Generate a premium vector representation of a mock document page
const width = 800;
const height = 1100;
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
<rect width="${width}" height="${height}" fill="#fafbfc" />
<!-- Shadow border effect -->
<rect x="5" y="5" width="${width - 10}" height="${height - 10}" fill="white" rx="8" filter="drop-shadow(0 4px 6px rgba(0,0,0,0.05))" />
<!-- Header -->
<text x="60" y="80" font-family="system-ui, sans-serif" font-size="28" font-weight="800" fill="#1e293b">DocQube PDF Engine</text>
<text x="60" y="110" font-family="system-ui, sans-serif" font-size="12" font-weight="500" fill="#64748b" letter-spacing="1">PAGE ${pageIndex + 1} OF THE SPECIFICATION</text>
<line x1="60" y1="130" x2="${width - 60}" y2="130" stroke="#f1f5f9" stroke-width="2" />
<!-- Document Content Simulation -->
<rect x="60" y="160" width="120" height="24" fill="#eff6ff" rx="4" />
<text x="70" y="176" font-family="system-ui, sans-serif" font-size="12" font-weight="700" fill="#3b82f6">SECTION ${pageIndex * 2 + 1}.1</text>
<text x="60" y="210" font-family="system-ui, sans-serif" font-size="20" font-weight="700" fill="#0f172a">High-Performance Rendering Architecture</text>
<!-- Multiline mock text -->
<rect x="60" y="235" width="${width - 120}" height="10" fill="#e2e8f0" rx="3" />
<rect x="60" y="255" width="${width - 160}" height="10" fill="#e2e8f0" rx="3" />
<rect x="60" y="275" width="${width - 120}" height="10" fill="#e2e8f0" rx="3" />
<rect x="60" y="295" width="${width - 240}" height="10" fill="#e2e8f0" rx="3" />
<!-- Draw some abstract blueprints / charts on even pages to make them look distinct -->
${pageIndex % 2 === 0 ? `
<!-- Mock Chart -->
<rect x="60" y="340" width="${width - 120}" height="280" fill="#f8fafc" rx="12" stroke="#e2e8f0" stroke-width="1.5" />
<path d="M 100 580 Q 200 420 300 480 T 500 380 T 700 420" fill="none" stroke="url(#chartGrad)" stroke-width="4" stroke-linecap="round" />
<circle cx="300" cy="480" r="6" fill="#3b82f6" stroke="white" stroke-width="2" />
<circle cx="500" cy="380" r="6" fill="#10b981" stroke="white" stroke-width="2" />
<text x="80" y="375" font-family="system-ui, sans-serif" font-size="14" font-weight="700" fill="#334155">Engine Performance Vectors (Mock Data)</text>
<defs>
<linearGradient id="chartGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3b82f6" />
<stop offset="100%" stop-color="#10b981" />
</linearGradient>
</defs>
` : `
<!-- Structured List -->
<circle cx="70" cy="360" r="4" fill="#3b82f6" />
<rect x="90" y="355" width="${width - 160}" height="10" fill="#cbd5e1" rx="3" />
<circle cx="70" cy="390" r="4" fill="#3b82f6" />
<rect x="90" y="385" width="${width - 200}" height="10" fill="#cbd5e1" rx="3" />
<circle cx="70" cy="420" r="4" fill="#3b82f6" />
<rect x="90" y="415" width="${width - 140}" height="10" fill="#cbd5e1" rx="3" />
<!-- A block of code/preformatted text -->
<rect x="60" y="470" width="${width - 120}" height="180" fill="#0f172a" rx="12" />
<text x="90" y="510" font-family="monospace" font-size="13" fill="#38bdf8">#include &lt;pdfengine/core.hpp&gt;</text>
<text x="90" y="535" font-family="monospace" font-size="13" fill="#f87171">int main() {</text>
<text x="120" y="560" font-family="monospace" font-size="13" fill="#a7f3d0"> pdf::Engine engine;</text>
<text x="120" y="585" font-family="monospace" font-size="13" fill="#a7f3d0"> engine.load_document("quarterly.pdf");</text>
<text x="120" y="610" font-family="monospace" font-size="13" fill="#fbbf24"> return engine.render_page(1, 2.0);</text>
<text x="90" y="635" font-family="monospace" font-size="13" fill="#f87171">}</text>
`}
<!-- Footer -->
<line x1="60" y1="1020" x2="${width - 60}" y2="1020" stroke="#f1f5f9" stroke-width="1.5" />
<text x="60" y="1045" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#94a3b8">DocQube Core Engine (Gate G0a scaffolding)</text>
<text x="${width - 100}" y="1045" font-family="system-ui, sans-serif" font-size="11" font-weight="700" fill="#94a3b8">PAGE ${pageIndex + 1}</text>
</svg>
`;
return `data:image/svg+xml;utf8,${encodeURIComponent(svg.trim())}`;
}
}
export const gatewayService = new GatewayService();