/** * FinBot API Client * ================= * Connects the Next.js frontend to the IRIS API proxy. * The proxy forwards to FastAPI when available and serves validated cached * Investor Relations responses when the backend process is warming up. */ export const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '/api/proxy'; export interface Source { id: number; doc_name: string; page: number; support: string; image_url?: string; } export interface KPIRow { metric: string; current: string; previous: string; change: string; interpretation: string; direction: 'positive' | 'negative' | 'neutral'; period?: string; value?: string; } export interface Driver { title: string; detail: string; } export interface VisualItem { id: string; page: number; image_url: string; alt: string; } export interface ChatResponse { response_type: 'ir_response' | 'unsupported' | 'insufficient'; question: string; executive_summary?: string; sources: Source[]; financial_kpis: KPIRow[]; key_drivers_summary?: string; key_drivers: Driver[]; visual_evidence: VisualItem[]; latency_ms: number; model_used: string; } export interface Document { doc_id: string; name: string; doc_type: string; period: string; institution?: string; total_pages: number; status: string; filename: string; chunks_indexed?: number; tables_indexed?: number; colpali_pages?: number; pagemap_file?: string; page_section_map?: Record; page_metadata_map?: Record; retrieval_config?: string; } /** Check if the backend is reachable */ export async function checkBackendHealth(): Promise { try { const res = await fetch(`${BACKEND_URL}/api/health`, { signal: AbortSignal.timeout(2000), }); return res.ok; } catch { return false; } } /** Submit a question to the RAG pipeline */ export async function submitQuestion( question: string, docIds: string[], ): Promise { const res = await fetch(`${BACKEND_URL}/api/chat/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, doc_ids: docIds }), signal: AbortSignal.timeout(120_000), // ColPali can take time }); if (!res.ok) { const err = await res.text(); throw new Error(`Backend error ${res.status}: ${err}`); } return res.json(); } /** Get list of indexed documents */ export async function fetchDocuments(): Promise { const res = await fetch(`${BACKEND_URL}/api/documents/`, { signal: AbortSignal.timeout(5000), }); if (!res.ok) throw new Error('Failed to fetch documents'); return res.json(); } /** Get the URL for a page image (rendered PDF page) */ export function getPageImageUrl(docId: string, pageNumber: number): string { return `${BACKEND_URL}/api/visuals/${docId}/${pageNumber}`; } /** Normalize legacy static page URLs to the backend visual route. */ export function normalizePageImageUrl( imageUrl: string | undefined, fallbackDocId: string, fallbackPage: number, ): string { if (!imageUrl) { return getPageImageUrl(fallbackDocId, fallbackPage); } const legacyPageMatch = imageUrl.match( /\/pages\/([^/]+)\/pages\/page_(\d{4})(?:_[^/.]+)?\.png$/, ); if (legacyPageMatch) { return getPageImageUrl(legacyPageMatch[1], Number.parseInt(legacyPageMatch[2], 10)); } if (imageUrl.startsWith('/api/visuals/')) { return `${BACKEND_URL}${imageUrl}`; } if (imageUrl.startsWith('/')) { return `${BACKEND_URL}${imageUrl}`; } return imageUrl; }