export async function request(url, data, method = "POST") { try { const res = await fetch(url, { method, credentials: "same-origin", headers: {"Content-Type": "application/json"}, body: data ? JSON.stringify(data) : undefined }); return await res.json(); } catch { return {error: "Network error - please try again."}; } } export const get = (url) => request(url, null, "GET"); export const post = (url, data) => request(url, data, "POST"); export const put = (url, data) => request(url, data, "PUT"); export const del = (url) => request(url, null, "DELETE"); /** * @typedef {"skipped"|"failed"|"no_results"|"no_relevant_results"|"succeeded"} SearchState * @typedef {{event: "status", data: {status: "accepted"|"searching"|"generating", sourceCount?: number, searchState?: SearchState}} * | {event: "chunk", data: {chunk: string, fullReply: string}} * | {event: "thinking", data: {chunk: string, fullThinking: string}} * | {event: "done", data: {reply: string, sources?: import("./webSearch.js").WebSource[], searchState?: SearchState}} * | {event: "error", data: {error: string, code?: string, searchState?: SearchState}}} ChatStreamEvent */ export async function stream(url, data, handlers = {}, signal = undefined) { const response = await fetch(url, { method: "POST", credentials: "same-origin", headers: {"Content-Type": "application/json"}, body: JSON.stringify(data || {}), signal }); if (!response.ok || !response.body) { let error = "Streaming request failed"; try { const payload = await response.json(); error = payload.error || error; } catch { // Keep the generic error if the server did not send JSON. } if (handlers.onError) { handlers.onError({error}); return; } throw new Error(error); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; let terminal = false; try { while (true) { const {value, done} = await reader.read(); if (done) break; buffer += decoder.decode(value, {stream: true}); const parts = buffer.split(/\r?\n\r?\n/); buffer = parts.pop() || ""; for (const part of parts) { const lines = part.split("\n"); const eventLine = lines.find((line) => line.startsWith("event:")); const dataLine = lines.find((line) => line.startsWith("data:")); if (!eventLine || !dataLine) continue; const event = eventLine.slice(6).trim(); const payload = JSON.parse(dataLine.slice(5).trim()); if (!payload || typeof payload !== "object") throw new Error("Invalid response stream."); const textFields = {chunk: "fullReply", thinking: "fullThinking", status: "status", done: "reply", error: "error"}; if (textFields[event] && typeof payload[textFields[event]] !== "string") throw new Error("Invalid response stream."); if (terminal) continue; if (event === "chunk" && handlers.onChunk) handlers.onChunk(payload); if (event === "thinking" && handlers.onThinking) handlers.onThinking(payload); if (event === "status" && handlers.onStatus) handlers.onStatus(payload); if (event === "done" && handlers.onDone) handlers.onDone(payload); if (event === "error" && handlers.onError) handlers.onError(payload); if (event === "done" || event === "error") terminal = true; } if (terminal) break; } if (!terminal) throw new Error("Response was interrupted. Please try again."); } finally { await reader.cancel().catch(() => {}); reader.releaseLock(); } }