// api.jsx — thin browser client for Azoth AI backend const AZOTH_TOKEN_KEY = "azoth.auth.token"; const AzothApi = { token() { return localStorage.getItem(AZOTH_TOKEN_KEY) || ""; }, setToken(token) { if (token) localStorage.setItem(AZOTH_TOKEN_KEY, token); else localStorage.removeItem(AZOTH_TOKEN_KEY); }, async request(path, options = {}) { const headers = { ...(options.headers || {}) }; if (options.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json"; const token = AzothApi.token(); if (token) headers.Authorization = `Bearer ${token}`; const res = await fetch(path, { ...options, headers }); const text = await res.text(); let data = {}; if (text) { try { data = JSON.parse(text); } catch { data = { detail: text }; } } if (!res.ok) { const msg = data.detail || data.message || `Помилка ${res.status}`; throw new Error(Array.isArray(msg) ? msg.map(x => x.msg || x).join("; ") : msg); } return data; }, register({ displayName, email, password }) { return AzothApi.request("/api/register", { method: "POST", body: JSON.stringify({ display_name: displayName, email, password }), }); }, login({ email, password }) { return AzothApi.request("/api/login", { method: "POST", body: JSON.stringify({ email, password }), }); }, logout() { return AzothApi.request("/api/logout", { method: "POST" }).catch(() => ({})) .finally(() => AzothApi.setToken("")); }, me() { return AzothApi.request("/api/me"); }, threads() { return AzothApi.request("/api/threads?limit=60"); }, createThread(title = "Нова бесіда") { return AzothApi.request("/api/threads", { method: "POST", body: JSON.stringify({ title }), }); }, archiveThread(threadId) { return AzothApi.request(`/api/threads/${encodeURIComponent(threadId)}`, { method: "DELETE" }); }, history(threadId) { return AzothApi.request(`/api/history?thread_id=${encodeURIComponent(threadId)}&limit=80`); }, async streamChat({ message, threadId, search }, handlers = {}) { const headers = { "Content-Type": "application/json" }; const token = AzothApi.token(); if (token) headers.Authorization = `Bearer ${token}`; const res = await fetch("/api/chat-stream", { method: "POST", headers, body: JSON.stringify({ message, thread_id: threadId || null, search }), }); if (!res.ok || !res.body) { const text = await res.text(); throw new Error(text || `Помилка ${res.status}`); } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; const dispatch = (raw) => { const lines = raw.split("\n"); const event = (lines.find(l => l.startsWith("event:")) || "event: message").slice(6).trim(); const dataLine = lines.find(l => l.startsWith("data:")); if (!dataLine) return; let data = {}; try { data = JSON.parse(dataLine.slice(5).trim()); } catch { return; } if (handlers[event]) handlers[event](data); if (handlers.any) handlers.any(event, data); }; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split("\n\n"); buffer = parts.pop() || ""; parts.forEach(dispatch); } if (buffer.trim()) dispatch(buffer); }, }; Object.assign(window, { AzothApi });