// ── Token storage ────────────────────────────────────────────────────────────── const TOKEN_KEY = 'julia_token' export const getToken = (): string | null => localStorage.getItem(TOKEN_KEY) export const setToken = (t: string): void => { localStorage.setItem(TOKEN_KEY, t) } export const clearToken = (): void => { localStorage.removeItem(TOKEN_KEY) } // ── Base fetch with auth ─────────────────────────────────────────────────────── async function request(url: string, init: RequestInit = {}): Promise { const token = getToken() const headers: Record = { ...(init.headers as Record ?? {}), } if (token) headers['Authorization'] = `Bearer ${token}` const r = await fetch(url, { ...init, headers }) if (r.status === 401) { clearToken() window.location.reload() throw new Error('Unauthorized') } if (!r.ok) { const text = await r.text() throw new Error(text || `HTTP ${r.status}`) } return r.json() as Promise } // ── Auth ─────────────────────────────────────────────────────────────────────── export function login(email: string, password: string): Promise<{ token: string; email: string }> { return request('/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }) } export function getMe(): Promise<{ email: string }> { return request('/auth/me') } // ── Conversations ───────────────────────────────────────────────────────────── export interface Conversation { id: string title: string created_at: string updated_at: string } export interface ConversationDetail extends Conversation { messages: Array<{ role: string; text: string; meta?: unknown }> } export function listConversations(): Promise { return request('/conversations') } export function getConversation(id: string): Promise { return request(`/conversations/${id}`) } export function createConversation(title: string, messages: unknown[]): Promise { return request('/conversations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, messages }), }) } export function updateConversation(id: string, title: string, messages: unknown[]): Promise { return request(`/conversations/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, messages }), }) } export function deleteConversation(id: string): Promise<{ status: string }> { return request(`/conversations/${id}`, { method: 'DELETE' }) } // ── Ingestion ───────────────────────────────────────────────────────────────── export interface UploadResponse { job_id: string; doc_id: string; status: string } export interface JobStatus { job_id: string; doc_id: string; filename: string status: 'queued' | 'processing' | 'done' | 'failed' chunks_stored: number; error: string | null created_at: string; updated_at: string } export interface Document { doc_id: string; filename: string } export function uploadDocument(file: File, category: string): Promise { const form = new FormData() form.append('file', file) form.append('category', category) return request('/ingest/upload', { method: 'POST', body: form }) } export function getJobStatus(jobId: string): Promise { return request(`/ingest/status/${jobId}`) } export function listDocuments(): Promise<{ total: number; documents: Document[] }> { return request('/ingest/documents') } export function deleteDocument(docId: string): Promise<{ status: string; doc_id: string }> { return request(`/ingest/documents/${docId}`, { method: 'DELETE' }) } export function clearDocuments(): Promise<{ status: string }> { return request('/ingest/documents', { method: 'DELETE' }) } // ── Query ───────────────────────────────────────────────────────────────────── export interface Citation { filename: string; page: number | string; snippet?: string } export interface AskResponse { answer: string citations: Citation[] source_map: Citation[] chunks_used: number path: 'vector' | 'sql' sql: string | null } export interface HistoryMessage { role: 'user' | 'assistant'; content: string } export async function askQuestionStream( question: string, history: HistoryMessage[], topK = 6, category = '', onToken: (token: string) => void, onDone: (meta: AskResponse) => void, onError: (msg: string) => void, onStatus?: (status: string) => void, uiLanguage = '', ): Promise { const token = getToken() const res = await fetch('/query/ask/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify({ question, history, top_k: topK, category, ui_language: uiLanguage }), }) if (!res.ok || !res.body) { onError(`HTTP ${res.status}`); return } const reader = res.body.getReader() const dec = new TextDecoder() let buf = '' while (true) { const { done, value } = await reader.read() if (done) break buf += dec.decode(value, { stream: true }) const lines = buf.split('\n') buf = lines.pop() ?? '' for (const line of lines) { if (!line.startsWith('data: ')) continue const event = JSON.parse(line.slice(6)) if (event.type === 'token') onToken(event.content) else if (event.type === 'status') onStatus?.(event.status) else if (event.type === 'done') onDone({ answer: event.answer ?? '', citations: event.citations ?? [], source_map: event.source_map ?? [], chunks_used: event.chunks_used ?? 0, path: event.path, sql: event.sql ?? null, }) else if (event.type === 'error') onError(event.message) } } }