dakshtaneja's picture
chat: stop, regenerate, inline prompt editing, copy answer
b3def84
Raw
History Blame Contribute Delete
4.45 kB
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}`);
}