373 lines
13 KiB
TypeScript
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 <pdfengine/core.hpp></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();
|