import type { ChatMessage, Citation, Conversation, LegalDocument, LocalModelStatus, ModelPullHandlers, StreamHandlers, } from "@/lib/types"; export const API_BASE = (process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8001/api").replace( /\/$/, "", ); function apiUrl(path: string) { const href = `${API_BASE}${path}`; if (/^https?:\/\//i.test(href)) return href; if (typeof window === "undefined") return href; return new URL(href, window.location.origin).toString(); } function apiUrlObject(path: string) { const href = `${API_BASE}${path}`; const base = typeof window === "undefined" ? "http://localhost" : window.location.origin; return new URL(href, base); } export function supportsLocalModelSetupClient() { return /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])/i.test(API_BASE); } async function readJson(response: Response): Promise { if (!response.ok) { const text = await response.text(); let apiMessage: string | undefined; try { const payload = JSON.parse(text) as { detail?: string }; apiMessage = payload.detail; } catch { apiMessage = undefined; } throw new Error(apiMessage || text || response.statusText); } return response.json() as Promise; } export async function listDocuments(search?: string) { const url = apiUrlObject("/documents"); if (search) url.searchParams.set("search", search); return readJson(await fetch(url)); } export async function uploadDocument(file: File) { const form = new FormData(); form.append("file", file); return readJson( await fetch(apiUrl("/documents/upload"), { method: "POST", body: form, }), ); } export async function listConversations() { return readJson(await fetch(apiUrl("/conversations"))); } export async function listMessages(conversationId: string) { return readJson(await fetch(apiUrl(`/conversations/${conversationId}/messages`))); } export async function getLocalModelStatus(model = "qwen3:1.7b") { const url = apiUrlObject("/model/status"); url.searchParams.set("model", model); return readJson(await fetch(url)); } export async function enableLocalModel(model = "qwen3:1.7b") { return readJson( await fetch(apiUrl("/model/enable-local"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model }), }), ); } export function documentFileUrl(documentId: string) { return apiUrl(`/documents/${documentId}/file`); } export async function streamChat( request: { message: string; conversation_id?: string | null; top_k?: number }, handlers: StreamHandlers, ) { const response = await fetch(apiUrl("/chat"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), }); if (!response.ok || !response.body) { throw new Error((await response.text()) || "Unable to start chat stream."); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; const handlePayload = (line: string) => { if (!line.startsWith("data:")) return; const raw = line.slice(5).trim(); if (!raw) return; const event = JSON.parse(raw) as { type: string; conversation?: Conversation; label?: string; citations?: Citation[]; delta?: string; conversation_id?: string; error?: string; }; if (event.type === "conversation" && event.conversation) handlers.onConversation?.(event.conversation); if (event.type === "stage" && event.label) handlers.onStage?.(event.label); if (event.type === "citations" && event.citations) handlers.onCitations?.(event.citations); if (event.type === "answer_delta" && event.delta) handlers.onDelta?.(event.delta); if (event.type === "done" && event.conversation_id) handlers.onDone?.(event.conversation_id); if (event.type === "error" && event.error) handlers.onError?.(event.error); }; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const events = buffer.split("\n\n"); buffer = events.pop() || ""; for (const event of events) { for (const line of event.split("\n")) { handlePayload(line); } } } if (buffer.trim()) { for (const line of buffer.split("\n")) { handlePayload(line); } } } export async function streamLocalModelPull(model: string, handlers: ModelPullHandlers) { const response = await fetch(apiUrl("/model/pull"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model }), }); if (!response.ok || !response.body) { throw new Error((await response.text()) || "Unable to start model download."); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; const handlePayload = (line: string) => { if (!line.startsWith("data:")) return; const raw = line.slice(5).trim(); if (!raw) return; const event = JSON.parse(raw) as { type: string; status?: string; completed?: number | null; total?: number | null; model?: string; error?: string; }; if (event.type === "progress" && event.status) { handlers.onProgress?.({ status: event.status, completed: event.completed, total: event.total, }); } if (event.type === "done" && event.model) handlers.onDone?.(event.model); if (event.type === "error" && event.error) handlers.onError?.(event.error); }; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const events = buffer.split("\n\n"); buffer = events.pop() || ""; for (const event of events) { for (const line of event.split("\n")) { handlePayload(line); } } } if (buffer.trim()) { for (const line of buffer.split("\n")) { handlePayload(line); } } }