/** * lib/api.ts — API client for the RAG backend. */ const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; export interface SearchResult { document_id: string; chunk_id: string; title: string; path: string; content: string; score: number; tags: string[]; highlight?: string; } export interface SearchResponse { results: SearchResult[]; total: number; query_time_ms: number; } export interface Document { id: string; path: string; title: string; content: string; frontmatter: Record; tags: string[]; aliases: string[]; word_count: number | null; created_at: string; updated_at: string; indexed_at: string | null; } export interface StatsResponse { total_documents: number; total_chunks: number; total_relations: number; total_tags: number; last_indexed: string | null; embedding_model: string; chat_model: string; } export interface TagCount { tag: string; count: number; } export interface GraphData { nodes: { id: string; title: string; path: string; tags: string[]; word_count: number | null }[]; edges: { source: string; target: string; relation_type: string; label: string | null }[]; } export async function search(query: string, tags?: string[], limit = 10): Promise { const res = await fetch(`${API_BASE}/api/v1/search`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, tags, limit, hybrid: true }), }); if (!res.ok) throw new Error(`Search failed: ${res.status}`); return res.json(); } export async function getDocument(id: string): Promise { const res = await fetch(`${API_BASE}/api/v1/document/${id}`); if (!res.ok) throw new Error(`Document not found: ${res.status}`); return res.json(); } export async function getStats(): Promise { const res = await fetch(`${API_BASE}/api/v1/stats`); if (!res.ok) throw new Error('Stats fetch failed'); return res.json(); } export async function getTags(): Promise { const res = await fetch(`${API_BASE}/api/v1/tags`); if (!res.ok) throw new Error('Tags fetch failed'); return res.json(); } export async function getGraph(limit = 150): Promise { const res = await fetch(`${API_BASE}/api/v1/graph?limit=${limit}`); if (!res.ok) throw new Error('Graph fetch failed'); return res.json(); } export function streamChat( message: string, contextLimit = 5, onToken: (token: string) => void, onSources: (sources: { title: string; path: string; score: number }[]) => void, onDone: () => void, ): () => void { const controller = new AbortController(); (async () => { try { const res = await fetch(`${API_BASE}/api/v1/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, context_limit: contextLimit, stream: true }), signal: controller.signal, }); if (!res.ok || !res.body) return; const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; 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; try { const data = JSON.parse(line.slice(6)); if (data.type === 'token') onToken(data.token); else if (data.type === 'sources') onSources(data.sources); else if (data.type === 'done') onDone(); } catch {} } } } catch (err: unknown) { if ((err as Error)?.name !== 'AbortError') console.error('Stream error:', err); } })(); return () => controller.abort(); }