export type ChatSummary = { id: string; title: string | null; createdAt: string; updatedAt: string; }; export type SearchSummary = { id: string; title: string | null; query: string | null; createdAt: string; updatedAt: string; }; export type Message = { id: string; createdAt: string; role: "system" | "user" | "assistant" | "tool"; content: string; name: string | null; }; export type ChatDetail = { id: string; title: string | null; createdAt: string; updatedAt: string; messages: Message[]; }; export type SearchResultItem = { id: string; createdAt: string; rank: number; title: string | null; url: string; publishedDate: string | null; author: string | null; text: string | null; highlights: string[] | null; highlightScores: number[] | null; score: number | null; favicon: string | null; image: string | null; }; export type SearchDetail = { id: string; title: string | null; query: string | null; createdAt: string; updatedAt: string; requestId: string | null; latencyMs: number | null; error: string | null; answerText: string | null; answerRequestId: string | null; answerCitations: Array<{ id?: string; url?: string; title?: string | null; publishedDate?: string | null; author?: string | null; text?: string | null; }> | null; answerError: string | null; results: SearchResultItem[]; }; export type SearchRunRequest = { query?: string; title?: string; type?: "auto" | "fast" | "deep" | "instant"; numResults?: number; includeDomains?: string[]; excludeDomains?: string[]; }; export type CompletionRequestMessage = { role: "system" | "user" | "assistant" | "tool"; content: string; name?: string; }; type CompletionResponse = { chatId: string | null; message: { role: "assistant"; content: string; }; }; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "/api"; const ENV_ADMIN_TOKEN = (import.meta.env.VITE_ADMIN_TOKEN as string | undefined)?.trim() || null; let authToken: string | null = ENV_ADMIN_TOKEN; export function getConfiguredToken() { return ENV_ADMIN_TOKEN; } export function setAuthToken(token: string | null) { authToken = token?.trim() || null; } async function api(path: string, init?: RequestInit): Promise { const headers = new Headers(init?.headers ?? {}); const hasBody = init?.body !== undefined && init.body !== null; if (hasBody && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } if (authToken) { headers.set("Authorization", `Bearer ${authToken}`); } const response = await fetch(`${API_BASE_URL}${path}`, { ...init, headers, }); if (!response.ok) { const fallback = `${response.status} ${response.statusText}`; let message = fallback; try { const body = (await response.json()) as { message?: string }; if (body.message) message = body.message; } catch { // keep fallback message } throw new Error(message); } return (await response.json()) as T; } export async function listChats() { const data = await api<{ chats: ChatSummary[] }>("/v1/chats"); return data.chats; } export async function verifySession() { return api<{ authenticated: true; mode: "open" | "token" }>("/v1/auth/session"); } export async function createChat(title?: string) { const data = await api<{ chat: ChatSummary }>("/v1/chats", { method: "POST", body: JSON.stringify({ title }), }); return data.chat; } export async function getChat(chatId: string) { const data = await api<{ chat: ChatDetail }>(`/v1/chats/${chatId}`); return data.chat; } export async function deleteChat(chatId: string) { await api<{ deleted: true }>(`/v1/chats/${chatId}`, { method: "DELETE" }); } export async function listSearches() { const data = await api<{ searches: SearchSummary[] }>("/v1/searches"); return data.searches; } export async function createSearch(body?: { title?: string; query?: string }) { const data = await api<{ search: SearchSummary }>("/v1/searches", { method: "POST", body: JSON.stringify(body ?? {}), }); return data.search; } export async function getSearch(searchId: string) { const data = await api<{ search: SearchDetail }>(`/v1/searches/${searchId}`); return data.search; } export async function deleteSearch(searchId: string) { await api<{ deleted: true }>(`/v1/searches/${searchId}`, { method: "DELETE" }); } type RunSearchStreamHandlers = { onSearchResults?: (payload: { requestId: string | null; results: SearchResultItem[] }) => void; onSearchError?: (payload: { error: string }) => void; onAnswer?: (payload: { answerText: string | null; answerRequestId: string | null; answerCitations: SearchDetail["answerCitations"] }) => void; onAnswerError?: (payload: { error: string }) => void; onDone?: (payload: { search: SearchDetail }) => void; onError?: (payload: { message: string }) => void; }; export async function runSearchStream( searchId: string, body: SearchRunRequest, handlers: RunSearchStreamHandlers, options?: { signal?: AbortSignal } ) { const headers = new Headers({ Accept: "text/event-stream", "Content-Type": "application/json", }); if (authToken) { headers.set("Authorization", `Bearer ${authToken}`); } const response = await fetch(`${API_BASE_URL}/v1/searches/${searchId}/run/stream`, { method: "POST", headers, body: JSON.stringify(body), signal: options?.signal, }); if (!response.ok) { const fallback = `${response.status} ${response.statusText}`; let message = fallback; try { const body = (await response.json()) as { message?: string }; if (body.message) message = body.message; } catch { // keep fallback message } throw new Error(message); } if (!response.body) { throw new Error("No response stream"); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; let eventName = "message"; let dataLines: string[] = []; const flushEvent = () => { if (!dataLines.length) { eventName = "message"; return; } const dataText = dataLines.join("\n"); let payload: any = null; try { payload = JSON.parse(dataText); } catch { payload = { message: dataText }; } if (eventName === "search_results") handlers.onSearchResults?.(payload); else if (eventName === "search_error") handlers.onSearchError?.(payload); else if (eventName === "answer") handlers.onAnswer?.(payload); else if (eventName === "answer_error") handlers.onAnswerError?.(payload); else if (eventName === "done") handlers.onDone?.(payload); else if (eventName === "error") handlers.onError?.(payload); dataLines = []; eventName = "message"; }; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); let newlineIndex = buffer.indexOf("\n"); while (newlineIndex >= 0) { const rawLine = buffer.slice(0, newlineIndex); buffer = buffer.slice(newlineIndex + 1); const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine; if (!line) { flushEvent(); } else if (line.startsWith("event:")) { eventName = line.slice("event:".length).trim(); } else if (line.startsWith("data:")) { dataLines.push(line.slice("data:".length).trimStart()); } newlineIndex = buffer.indexOf("\n"); } } buffer += decoder.decode(); if (buffer.length) { const line = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer; if (line.startsWith("event:")) { eventName = line.slice("event:".length).trim(); } else if (line.startsWith("data:")) { dataLines.push(line.slice("data:".length).trimStart()); } } flushEvent(); } export async function runCompletion(body: { chatId: string; provider: "openai" | "anthropic" | "xai"; model: string; messages: CompletionRequestMessage[]; }) { return api("/v1/chat-completions", { method: "POST", body: JSON.stringify(body), }); }