import { KnowledgeSource, RetrievedChunk, Citation, Language, LegalQueryResponse, ImageJob } from './types'; const DEFAULT_API_BASE = 'http://127.0.0.1:8000'; const API_BASE = (import.meta as any).env?.VITE_API_URL || DEFAULT_API_BASE; // ── Types ──────────────────────────────────────────────────────────────────── type BackendSource = { id: string; type: 'pdf' | 'url' | 'youtube' | 'statute' | 'judgment' | 'constitution'; title: string; origin?: string; language?: string; status?: KnowledgeSource['status']; chunkCount?: number; createdAt?: string; dateAdded?: string; doc_type?: string; // For legal sources court?: string; // For legal sources judgment_date?: string; // For legal sources progress_percentage?: number; error_message?: string; }; export interface QueryResponse { answer: string; sources: Citation[]; retrievedChunks: RetrievedChunk[]; conversationId?: string; // Added for Phase 1 } export interface Conversation { id: string; title: string; conv_type?: 'general' | 'legal'; created_at: string; updated_at: string; message_count: number; } export interface ConversationMessage { id: string; question: string; answer: string; sourcesUsed: string[]; createdAt: string; } // ── Helper: Normalization ─────────────────────────────────────────────────── function normalizeSource(raw: BackendSource): KnowledgeSource { // Map "url" → "web" for frontend type consistency let type: KnowledgeSource['type'] = raw.type === 'url' ? 'web' : (raw.type as KnowledgeSource['type']); // If it's a legal type from Prompt 4, we might want to map it to 'pdf' // or keep it if types.ts supports it. Assuming pdf for now if not 'web'/'youtube'. if (['statute', 'judgment', 'constitution'].includes(raw.type)) { type = 'pdf'; } const language = ((raw.language || 'en').toUpperCase()) as Language; const dateRaw = raw.createdAt || raw.dateAdded; const dateAdded = dateRaw ? new Date(dateRaw) : new Date(); const metadata: KnowledgeSource['metadata'] = {}; if (raw.origin) { if (type === 'web' || type === 'youtube') metadata.url = raw.origin; if (raw.origin.match(/[?&]v=([^&]+)/)) { const match = raw.origin.match(/[?&]v=([^&]+)/); if (match) metadata.videoId = match[1]; } } // Add legal metadata if present if (raw.doc_type) metadata.docType = raw.doc_type; if (raw.court) metadata.court = raw.court; if (raw.judgment_date) metadata.judgmentDate = raw.judgment_date; return { id: String(raw.id), type: type || 'pdf', title: raw.title, language, status: raw.status || 'completed', chunkCount: raw.chunkCount ?? 0, dateAdded, progress_percentage: raw.progress_percentage ?? 0, error_message: raw.error_message, metadata, }; } // ── Sources API ────────────────────────────────────────────────────────────── export async function fetchSources(): Promise { const res = await fetch(`${API_BASE}/sources`); if (!res.ok) throw new Error(`Failed to fetch sources: ${res.statusText}`); const data = await res.json(); const raw: BackendSource[] = Array.isArray(data) ? data : (data.sources ?? []); return raw.map(normalizeSource); } export async function fetchLegalSources(): Promise { const res = await fetch(`${API_BASE}/legal/legal-sources`); if (!res.ok) throw new Error(`Failed to fetch legal sources: ${res.statusText}`); const data = await res.json(); const raw: BackendSource[] = data.sources ?? []; return raw.map(normalizeSource); } export async function deleteSource(id: string): Promise { const res = await fetch(`${API_BASE}/sources/${encodeURIComponent(id)}`, { method: 'DELETE', }); if (!res.ok) { const detail = await res.text().catch(() => ''); throw new Error(detail || `Delete failed with status ${res.status}`); } } // ── Ingestion API ──────────────────────────────────────────────────────────── export async function uploadPdf(file: File): Promise { const formData = new FormData(); formData.append('file', file); const res = await fetch(`${API_BASE}/upload-pdf`, { method: 'POST', body: formData, }); if (!res.ok) throw new Error(await res.text() || 'PDF Upload failed'); const data = await res.json(); return normalizeSource({ id: data.source_id, type: 'pdf', title: data.title, chunkCount: data.chunk_count, status: data.status || 'UPLOADING', createdAt: new Date().toISOString(), }); } export async function fetchSourceProgress(sourceId: string): Promise { const res = await fetch(`${API_BASE}/sources/${encodeURIComponent(sourceId)}`); if (!res.ok) throw new Error(`Failed to fetch source progress: ${res.statusText}`); const data = await res.json(); return { ...normalizeSource(data), progress_percentage: data.progress_percentage ?? 0, error_message: data.error_message, }; } export async function uploadLegal(file: File, docType: string = "judgment"): Promise { const formData = new FormData(); formData.append('file', file); const res = await fetch(`${API_BASE}/legal/upload-legal?doc_type=${docType}`, { method: 'POST', body: formData, }); if (!res.ok) throw new Error(await res.text() || 'Legal Upload failed'); const data = await res.json(); return normalizeSource({ id: data.source_id, type: data.doc_type as any || 'pdf', title: data.title, chunkCount: data.chunk_count, status: 'completed', createdAt: new Date().toISOString(), doc_type: data.doc_type }); } export async function addWebsite(url: string, language: Language = 'EN'): Promise { const res = await fetch(`${API_BASE}/add-url`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, language: language.toLowerCase() }), }); if (!res.ok) throw new Error(await res.text() || 'URL Ingestion failed'); const data = await res.json(); return normalizeSource({ id: data.source_id, type: 'url', title: data.title, chunkCount: data.chunk_count, status: data.status || 'EXTRACTING', origin: url, }); } export async function addYouTube(url: string, language: Language = 'EN'): Promise { const res = await fetch(`${API_BASE}/add-youtube`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, language: language.toLowerCase() }), }); if (!res.ok) throw new Error(await res.text() || 'YouTube Ingestion failed'); const data = await res.json(); return normalizeSource({ id: data.source_id, type: 'youtube', title: data.title, chunkCount: data.chunk_count, status: data.status || 'EXTRACTING', origin: url, }); } // ── Image API ──────────────────────────────────────────────────────────────── export async function uploadImage(file: File, context: string = ""): Promise<{ image_id: string, status: string }> { const formData = new FormData(); formData.append('file', file); formData.append('context', context); const res = await fetch(`${API_BASE}/images/upload-image`, { method: 'POST', body: formData, }); if (!res.ok) throw new Error(await res.text() || 'Image Upload failed'); return await res.json(); } export async function fetchImageJobs(): Promise { const res = await fetch(`${API_BASE}/images/image-jobs`); if (!res.ok) throw new Error(`Failed to fetch image jobs: ${res.statusText}`); const data = await res.json(); return data.jobs ?? []; } export async function fetchPendingImageCount(): Promise { const res = await fetch(`${API_BASE}/images/image-jobs/pending-count`); if (!res.ok) throw new Error(`Failed to fetch pending count: ${res.statusText}`); const data = await res.json(); return data.count ?? 0; } // ── Query API ─────────────────────────────────────────────────────────────── export async function queryRag( question: string, sourceIds?: string[], imageId?: string, includeImages?: boolean, llmProvider: string = "groq" ): Promise { const res = await fetch(`${API_BASE}/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, source_ids: sourceIds && sourceIds.length > 0 ? sourceIds : null, conversation_id: (window as any).currentConversationId || null, image_id: imageId || null, include_images: includeImages || false, llm_provider: llmProvider, }), }); if (!res.ok) throw new Error(await res.text() || 'Query failed'); const data = await res.json(); return { answer: data.answer, conversationId: data.conversationId, // Added for Phase 1 sources: (data.citations || []).map((c: any) => ({ sourceTitle: c.sourceTitle || c.source_title || '', sourceType: c.sourceType === 'url' ? 'web' : (c.sourceType || 'pdf'), reference: c.reference || '', snippet: c.snippet || '', })), retrievedChunks: (data.retrievedChunks || []).map((c: any) => ({ id: c.chunkId || c.id || '', sourceId: c.sourceId || '', sourceName: c.sourceTitle || c.sourceName || '', sourceType: c.sourceType === 'url' ? 'web' : (c.sourceType || 'pdf'), language: ((c.language || 'en').toUpperCase()) as Language, text: c.text || '', similarityScore: c.score ?? c.similarityScore ?? 0, metadata: { page: c.pageNumber ?? c.metadata?.page, timestamp: c.timestampS != null ? `${Math.floor(c.timestampS / 60)}:${String(c.timestampS % 60).padStart(2, '0')}` : c.metadata?.timestamp, url: c.urlRef || c.metadata?.url, ipcSections: c.metadata?.ipc_sections || c.ipc_sections || [], }, })), }; } // ── Streaming API ─────────────────────────────────────────────────────────── export async function streamQueryRag( question: string, sourceIds: string[] | undefined, history: Array<{ role: string; content: string }>, onToken: (token: string) => void, onMeta: (meta: { chatId: string; conversationId?: string; citations: Citation[]; retrievedChunks: RetrievedChunk[] }) => void, onError: (err: Error) => void, imageId?: string, includeImages?: boolean, llmProvider: string = "groq", isLegalMode: boolean = false, legalFilter: string | null = null, agenticMode: boolean = false, onAgentStatus?: (status: { stage: number; message: string }) => void ): Promise { let response: Response; try { response = await fetch(`${API_BASE}/query-stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, source_ids: sourceIds && sourceIds.length > 0 ? sourceIds : null, history, conversation_id: (window as any).currentConversationId || null, image_id: imageId || null, include_images: includeImages || false, llm_provider: llmProvider, is_legal_mode: isLegalMode, legal_filter: legalFilter, agentic_mode: agenticMode, }), }); } catch (err) { if (err instanceof TypeError && err.message.includes('fetch')) { onError(new Error("Cannot connect to server. Is the backend running on port 8000?")); } else { onError(err instanceof Error ? err : new Error(String(err))); } return; } if (!response.ok) { onError(new Error(`Server returned ${response.status}`)); return; } if (!response.body) { onError(new Error('No response body from server')); return; } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; const jsonStr = line.slice(6).trim(); if (!jsonStr) continue; try { const parsed = JSON.parse(jsonStr); if (parsed.type === 'error') { onError(new Error(parsed.message || "Unknown streaming error")); return; } if (parsed.type === 'agent_status') { if (onAgentStatus) { onAgentStatus({ stage: parsed.stage, message: parsed.message }); } } if (parsed.type === 'meta') { const citations: Citation[] = (parsed.citations || []).map((c: any) => ({ sourceTitle: c.sourceTitle || c.source_title || '', sourceType: (c.sourceType === 'url' ? 'web' : c.sourceType || 'pdf') as any, reference: c.reference || '', snippet: c.snippet || '', })); const retrievedChunks: RetrievedChunk[] = (parsed.retrievedChunks || []).map((c: any, i: number) => ({ id: c.chunkId || String(i), sourceId: c.sourceId || '', sourceName: c.sourceTitle || '', sourceType: (c.sourceType === 'url' ? 'web' : c.sourceType || 'pdf') as any, text: c.text || '', similarityScore: c.score || 0, metadata: { page: c.pageNumber, timestamp: c.timestampS, url: c.urlRef } })); onMeta({ chatId: parsed.chatId || '', conversationId: parsed.conversationId, citations, retrievedChunks }); } if (parsed.type === 'token' && parsed.content) { onToken(parsed.content); } if (parsed.type === 'done') { return; } } catch (e) { // Partial JSON or parse error — skip } } } } catch (err) { onError(err instanceof Error ? err : new Error(String(err))); } } // ── Evaluation API ─────────────────────────────────────────────────────────── export async function startEvaluation(nQuestions: number = 15, agenticMode: boolean = false): Promise { const response = await fetch(`${API_BASE}/eval/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ n_questions: nQuestions, agentic_mode: agenticMode }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Failed to start evaluation'); } const data = await response.json(); return data.job_id; } export async function getEvaluationStatus(jobId: string): Promise { const response = await fetch(`${API_BASE}/eval/status/${jobId}`); if (!response.ok) { throw new Error('Failed to fetch evaluation status'); } return response.json(); } // ── History API ───────────────────────────────────────────────────────────── export interface BackendChatEntry { id: string; question: string; answer: string; sourcesUsed: string[]; createdAt: string; } export async function fetchHistory(): Promise { const res = await fetch(`${API_BASE}/history`); if (!res.ok) throw new Error(`Failed to fetch history: ${res.statusText}`); const data = await res.json(); return data.history ?? []; } export async function clearHistory(): Promise { const res = await fetch(`${API_BASE}/history`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to clear history'); } // ── Events ────────────────────────────────────────────────────────────────── export function notifySidebarRefresh() { window.dispatchEvent(new CustomEvent('sources-updated')); } export async function queryLegal(question: string, sourceFilter?: string, modelType: "finetuned" | "base" = "finetuned"): Promise { const res = await fetch(`${API_BASE}/legal-query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, source_filter: sourceFilter || null, language: "en", model_type: modelType }) }); if (!res.ok) { const text = await res.text(); throw new Error(`Failed to query legal search: ${text}`); } return res.json(); } // ── Conversations API ──────────────────────────────────────────────────────── export async function createConversation(title = 'New Chat'): Promise { const res = await fetch(`${API_BASE}/conversations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }), }); if (!res.ok) throw new Error('Failed to create conversation'); return res.json(); } export async function fetchConversations(): Promise { const res = await fetch(`${API_BASE}/conversations`); if (!res.ok) throw new Error('Failed to fetch conversations'); const data = await res.json(); return data.conversations ?? []; } export async function fetchConversationMessages(convId: string): Promise<{ conversation: { id: string; title: string }; messages: ConversationMessage[]; }> { const res = await fetch(`${API_BASE}/conversations/${convId}/messages`); if (!res.ok) throw new Error('Failed to fetch messages'); return res.json(); } export async function renameConversation(convId: string, title: string): Promise { await fetch(`${API_BASE}/conversations/${convId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }), }); } export async function deleteConversation(convId: string): Promise { await fetch(`${API_BASE}/conversations/${convId}`, { method: 'DELETE' }); } // ── Export API ────────────────────────────────────────────────────────────── export async function exportToPDF(title: string, content: string, citations: any[]): Promise { const response = await fetch(`${API_BASE}/export/pdf`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, content, citations }), }); if (!response.ok) throw new Error('Failed to export PDF'); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `InteleX_Research_${new Date().getTime()}.pdf`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); }