Spaces:
Running
Running
File size: 4,448 Bytes
b3def84 6b3d40b 98d4633 6b3d40b 98d4633 6b3d40b 98d4633 6b3d40b 001f1df 6b3d40b 001f1df 6b3d40b 001f1df b3def84 b03e13f 001f1df e719504 001f1df b3def84 001f1df f3a0f20 9eeb459 001f1df 9eeb459 001f1df b3def84 001f1df 98d4633 9eeb459 b3def84 001f1df 98d4633 001f1df 98d4633 6b3d40b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | import type { ChatTurn, MetricsSummary, RunResult, SourceImage } from "./types";
// Same origin in production (FastAPI serves the static export);
// the local backend during `next dev`. On Vercel this MUST be set to the
// backend (HF Space) URL, since the frontend and API are different origins.
const API_BASE =
process.env.NEXT_PUBLIC_API_BASE ??
(process.env.NODE_ENV === "development" ? "http://localhost:8000" : "");
// --- Access code (shared demo gate) -----------------------------------------
// Entered at runtime, kept in sessionStorage — never baked into the bundle.
const CODE_KEY = "ar_access_code";
export function getAccessCode(): string {
if (typeof window === "undefined") return "";
return window.sessionStorage.getItem(CODE_KEY) ?? "";
}
export function setAccessCode(code: string): void {
window.sessionStorage.setItem(CODE_KEY, code);
}
export function clearAccessCode(): void {
window.sessionStorage.removeItem(CODE_KEY);
}
let authFailureHandler: (() => void) | null = null;
export function onAuthFailure(fn: () => void): void {
authFailureHandler = fn;
}
function handle401(): void {
clearAccessCode();
authFailureHandler?.();
}
function authHeaders(extra: Record<string, string> = {}): Record<string, string> {
const code = getAccessCode();
return code ? { ...extra, "X-Access-Code": code } : extra;
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
...init,
headers: authHeaders(init?.headers as Record<string, string>),
});
if (res.status === 401) {
handle401();
throw new Error("access code rejected");
}
if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(`${res.status}: ${body.slice(0, 300)}`);
}
return res.json();
}
export function submitQuery(
query: string,
history: ChatTurn[] = [],
): Promise<RunResult> {
return request<RunResult>("/api/query", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, history }),
});
}
export interface StreamEvent {
type: "stage" | "auction" | "token" | "reset" | "reasoning" | "verification" | "images" | "frontier_failed" | "error" | "done";
stage?: "bidding" | "drafting" | "searching" | "verifying" | "delivering" | "escalating";
model?: string;
text?: string;
reason?: string | null;
message?: string;
score?: number;
passed?: boolean;
feedback?: string;
winner?: string | null;
verified?: boolean;
escalated?: boolean;
images?: SourceImage[];
run?: RunResult;
}
export type QueryHint = "general" | "coding" | "reasoning";
// POST + NDJSON reader (EventSource is GET-only)
export async function streamQuery(
query: string,
history: ChatTurn[],
hint: QueryHint,
onEvent: (ev: StreamEvent) => void,
// Aborting rejects the in-flight reader.read() below, which surfaces to the
// caller as an AbortError — that's how "stop generating" unwinds
signal?: AbortSignal,
): Promise<void> {
const res = await fetch(`${API_BASE}/api/query/stream`, {
method: "POST",
headers: authHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ query, history, hint }),
signal,
});
if (res.status === 401) {
handle401();
throw new Error("access code rejected");
}
if (!res.ok || !res.body) {
const body = await res.text().catch(() => "");
throw new Error(`${res.status}: ${body.slice(0, 300)}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let nl;
while ((nl = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, nl).trim();
buffer = buffer.slice(nl + 1);
if (line) onEvent(JSON.parse(line) as StreamEvent);
}
}
}
// Open endpoint (no code) — tells the frontend whether to show the gate
export async function fetchHealth(): Promise<{ access_required: boolean }> {
const res = await fetch(`${API_BASE}/health`);
if (!res.ok) throw new Error(`${res.status}`);
return res.json();
}
export function fetchMetrics(): Promise<MetricsSummary> {
return request<MetricsSummary>("/api/metrics");
}
export function fetchRuns(limit = 50): Promise<RunResult[]> {
return request<RunResult[]>(`/api/runs?limit=${limit}`);
}
|