Spaces:
Sleeping
Sleeping
| import type { Message, QueryResult } from "../types"; | |
| const BASE_URL = ""; | |
| /** | |
| * POST /api/auth/login | |
| * | |
| * Sends the shared password to the API and returns a signed JWT on success. | |
| * Throws an error with message "Invalid password" on HTTP 401. | |
| * Throws a generic error for any other non-200 response. | |
| */ | |
| export async function login(password: string): Promise<{ token: string }> { | |
| const response = await fetch(`${BASE_URL}/api/auth/login`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ password }), | |
| }); | |
| if (response.status === 401) { | |
| throw new Error("Invalid password"); | |
| } | |
| if (!response.ok) { | |
| throw new Error(`Login failed: ${response.status} ${response.statusText}`); | |
| } | |
| return response.json() as Promise<{ token: string }>; | |
| } | |
| /** | |
| * POST /api/query/stream | |
| * | |
| * Opens a streaming SSE connection to the query endpoint and dispatches | |
| * events to the provided callbacks: | |
| * - onToken(text) — called for each `token` SSE event | |
| * - onDone(meta) — called once when the `done` SSE event is received | |
| * - onError(msg) — called if an `error` SSE event is received | |
| * | |
| * Uses fetch + ReadableStream so the JWT can be sent in the Authorization | |
| * header (EventSource does not support custom headers). | |
| */ | |
| export async function streamQuery( | |
| payload: { | |
| query: string; | |
| history: Message[]; | |
| profession: string | null; | |
| language: string | null; | |
| }, | |
| token: string, | |
| signal: AbortSignal, | |
| onToken: (text: string) => void, | |
| onDone: (metadata: QueryResult) => void, | |
| onError: (message: string) => void | |
| ): Promise<void> { | |
| const response = await fetch(`${BASE_URL}/api/query/stream`, { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| Authorization: `Bearer ${token}`, | |
| }, | |
| body: JSON.stringify(payload), | |
| signal, | |
| }); | |
| if (response.status === 401) { | |
| // Throw a recognisable message so useStream can skip retries and | |
| // App.tsx can call handle401() to clear the token and show the gate. | |
| throw new Error("Unauthorized"); | |
| } | |
| if (!response.ok) { | |
| throw new Error( | |
| `Stream request failed: ${response.status} ${response.statusText}` | |
| ); | |
| } | |
| if (!response.body) { | |
| throw new Error("Response body is null — streaming not supported"); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder("utf-8"); | |
| // Buffer for incomplete lines that span chunk boundaries | |
| let lineBuffer = ""; | |
| // The most recently seen `event:` line value | |
| let currentEvent = ""; | |
| try { | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| lineBuffer += decoder.decode(value, { stream: true }); | |
| // Process all complete lines (terminated by \n) | |
| const lines = lineBuffer.split("\n"); | |
| // Keep the last (potentially incomplete) segment in the buffer | |
| lineBuffer = lines.pop() ?? ""; | |
| for (const rawLine of lines) { | |
| const line = rawLine.trimEnd(); // strip trailing \r if present | |
| if (line === "") { | |
| // Blank line = end of SSE message; reset event type | |
| currentEvent = ""; | |
| continue; | |
| } | |
| if (line.startsWith("event:")) { | |
| currentEvent = line.slice("event:".length).trim(); | |
| continue; | |
| } | |
| if (line.startsWith("data:")) { | |
| const jsonStr = line.slice("data:".length).trim(); | |
| try { | |
| const parsed = JSON.parse(jsonStr) as Record<string, unknown>; | |
| switch (currentEvent) { | |
| case "token": { | |
| const text = parsed["text"]; | |
| if (typeof text === "string") { | |
| onToken(text); | |
| } | |
| break; | |
| } | |
| case "done": { | |
| onDone(parsed as unknown as QueryResult); | |
| break; | |
| } | |
| case "error": { | |
| const message = parsed["message"]; | |
| onError(typeof message === "string" ? message : jsonStr); | |
| return; // Close immediately on error event | |
| } | |
| default: | |
| // Unknown event type — ignore | |
| break; | |
| } | |
| } catch { | |
| // Malformed JSON in data line — skip silently | |
| } | |
| } | |
| } | |
| } | |
| } finally { | |
| reader.releaseLock(); | |
| } | |
| } | |