Decoder2704/python-chatbot-jay
0
1/**2 * FastAPI chat client (JSON chat request with a message field).3 * Response fields used by the UI include answer text and retrieval metadata.4 *5 * Base URL resolution (no chatbot logic here):6 * - If VITE_API_URL is set → use it (cross-origin: separate frontend + API).7 * - Otherwise → "" (relative URLs: same origin as the page — Hugging Face Space, Docker, etc.).8 *9 * Local dev: leave VITE_API_URL unset and run `npm run dev` with Vite proxying API routes to10 * FastAPI (see vite.config.js), or set VITE_API_URL if you prefer direct calls + CORS.11 */12 13function getApiBase() {14 const raw = import.meta.env.VITE_API_URL;15 if (typeof raw === "string" && raw.trim()) {16 return raw.replace(/\/$/, "");17 }18 return "";19}20 21const API_BASE = getApiBase();22const CHAT_URL = `${API_BASE}/chat`;23 24/** Thrown for client validation, HTTP errors, network failures, or malformed responses. */25export class ChatApiError extends Error {26 /**27 * @param {string} message28 * @param {{ status?: number, cause?: unknown }} [opts]29 */30 constructor(message, opts = {}) {31 super(message);32 this.name = "ChatApiError";33 this.status = opts.status;34 this.cause = opts.cause;35 }36}37 38/**39 * @param {unknown} json40 * @returns {string}41 */42function detailFromFastApiError(json) {43 if (!json || typeof json !== "object") return "";44 const d = /** @type {{ detail?: unknown }} */ (json).detail;45 if (typeof d === "string") return d;46 if (Array.isArray(d)) {47 return d48 .map((item) => {49 if (item && typeof item === "object" && "msg" in item) {50 return String(/** @type {{ msg?: string }} */ (item).msg ?? "");51 }52 return String(item);53 })54 .filter(Boolean)55 .join(" ");56 }57 return "";58}59 60/**61 * Ensures JSON matches expected ChatResponse shape (display needs `answer`).62 * @param {unknown} data63 * @returns {{ answer: string, alternate: number, qid: number, aid: number }}64 */65function parseChatResponse(data) {66 if (!data || typeof data !== "object") {67 throw new ChatApiError("Invalid response: expected a JSON object");68 }69 70 const o = /** @type {Record<string, unknown>} */ (data);71 72 if (typeof o.answer !== "string") {73 throw new ChatApiError("Invalid response: missing or invalid `answer`");74 }75 76 const qid = o.qid;77 const aid = o.aid;78 const alt = o.alternate;79 80 if (typeof qid !== "number" || !Number.isFinite(qid)) {81 throw new ChatApiError("Invalid response: missing or invalid `qid`");82 }83 if (typeof aid !== "number" || !Number.isFinite(aid)) {84 throw new ChatApiError("Invalid response: missing or invalid `aid`");85 }86 if (typeof alt !== "number" || !Number.isFinite(alt)) {87 throw new ChatApiError("Invalid response: missing or invalid `alternate`");88 }89 90 return {91 answer: o.answer,92 alternate: alt,93 qid: Math.trunc(qid),94 aid: Math.trunc(aid),95 };96}97 98/**99 * @param {string} message100 * @returns {Promise<{ answer: string, alternate: number, qid: number, aid: number }>}101 */102export async function sendChatMessage(message) {103 const text = typeof message === "string" ? message.trim() : "";104 if (!text) {105 throw new ChatApiError("Message cannot be empty");106 }107 108 let res;109 try {110 res = await fetch(CHAT_URL, {111 method: "POST",112 headers: { "Content-Type": "application/json", Accept: "application/json" },113 body: JSON.stringify({ message: text }),114 });115 } catch (err) {116 const hint =117 API_BASE === ""118 ? "same-origin API (is the backend running, or Vite dev proxy configured?)"119 : `API at ${API_BASE}`;120 const msg =121 err instanceof TypeError && err.message === "Failed to fetch"122 ? `Could not reach the ${hint}`123 : err instanceof Error124 ? `Network error: ${err.message}`125 : "Network error";126 throw new ChatApiError(msg, { cause: err });127 }128 129 const raw = await res.text();130 const ct = res.headers.get("content-type") || "";131 132 if (!res.ok) {133 let detail = res.statusText || `HTTP ${res.status}`;134 if (ct.includes("application/json") && raw) {135 try {136 const j = JSON.parse(raw);137 const d = detailFromFastApiError(j);138 if (d) detail = d;139 } catch {140 /* keep statusText */141 }142 }143 throw new ChatApiError(detail || "Request failed", { status: res.status });144 }145 146 if (!ct.includes("application/json")) {147 throw new ChatApiError("Invalid response: expected application/json");148 }149 150 let parsed;151 try {152 parsed = raw ? JSON.parse(raw) : null;153 } catch {154 throw new ChatApiError("Invalid response: body is not valid JSON");155 }156 157 return parseChatResponse(parsed);158}159 