import { apiBaseUrl, apiFetch, buildAuthHeaders, refreshAccessToken, notifyAuthError, extractErrorMessage } from "./client.js"; export async function sendChatStream(message, { conversationId, mode, onChunk, onSources, onStatus, onDone, onError, signal } = {}) { const body = JSON.stringify({ message, ...(conversationId ? { conversationId } : {}), ...(mode ? { mode } : {}) }); const doFetch = () => fetch(`${apiBaseUrl}/api/chat/stream`, { method: "POST", headers: buildAuthHeaders(), body, signal }); let res; try { res = await doFetch(); if (res.status === 401) { let newToken = null; try { newToken = await refreshAccessToken(); } catch { notifyAuthError(); onError?.(new Error("session_expired")); return; } if (newToken) res = await doFetch(); } } catch (e) { if (e?.name === "AbortError") return; onError?.(e); return; } if (res.status === 401) { notifyAuthError(); onError?.(new Error("session_expired")); return; } if (!res.ok) { const text = await res.text().catch(() => ""); onError?.(new Error(text || `http_error:${res.status}`)); return; } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; const STREAM_INACTIVITY_TIMEOUT_MS = 45_000; function readWithTimeout() { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error("stream_inactivity_timeout")), STREAM_INACTIVITY_TIMEOUT_MS); reader.read().then( (result) => { clearTimeout(timer); resolve(result); }, (err) => { clearTimeout(timer); reject(err); } ); }); } try { while (true) { const { done, value } = await readWithTimeout(); 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; const payload = line.slice(6); if (payload === "[DONE]") { onDone?.(); return; } try { const parsed = JSON.parse(payload); if (parsed.type === "delta") onChunk?.(parsed.delta); else if (parsed.type === "sources") onSources?.(parsed.sources); else if (parsed.type === "status") onStatus?.(parsed); else if (parsed.type === "error") onError?.(new Error(parsed.error)); } catch {} } } } catch (e) { if (e?.name === "AbortError") return; if (e?.message === "stream_inactivity_timeout") { reader.cancel().catch(() => {}); onError?.(new Error("A resposta demorou demais e foi interrompida.")); return; } onError?.(e); } onDone?.(); } export async function ingestDocuments(documents) { return apiFetch("/api/ingest", { method: "POST", body: { documents } }); } function sendIngestFile(file, { source, onProgress } = {}) { return new Promise((resolve, reject) => { const form = new FormData(); form.append("file", file); if (source) form.append("source", source); const xhr = new XMLHttpRequest(); xhr.upload.onprogress = (e) => { if (e.lengthComputable) onProgress?.(Math.round((e.loaded / e.total) * 100)); }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { try { resolve(JSON.parse(xhr.responseText)); } catch { resolve(xhr.responseText); } return; } let payload = null; try { payload = JSON.parse(xhr.responseText); } catch {} const err = new Error(extractErrorMessage(payload, xhr.status)); err.status = xhr.status; reject(err); }; xhr.onerror = () => reject(new Error("network_error")); xhr.ontimeout = () => reject(new Error("timeout")); xhr.open("POST", `${apiBaseUrl}/api/ingest/file`); const authHeaders = buildAuthHeaders(); if (authHeaders["Authorization"]) xhr.setRequestHeader("Authorization", authHeaders["Authorization"]); xhr.send(form); }); } export async function ingestFile(file, { source, onProgress } = {}) { try { return await sendIngestFile(file, { source, onProgress }); } catch (err) { if (err.status !== 401) throw err; try { await refreshAccessToken(); } catch { notifyAuthError(); throw new Error("session_expired"); } return sendIngestFile(file, { source, onProgress }); } } export async function ingestUrl(url, { source } = {}) { return apiFetch("/api/ingest/url", { method: "POST", body: { url, ...(source ? { source } : {}) } }); } export async function search(query) { return apiFetch("/api/search", { method: "POST", body: { query } }); } export async function listDocuments({ limit = 50, offset = 0 } = {}) { const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }).toString(); return apiFetch(`/api/documents?${params}`); } export async function deleteDocumentsBySource(source) { return apiFetch(`/api/documents/source/${encodeURIComponent(source)}`, { method: "DELETE" }); }