2026-02-13 23:15:12 -08:00
|
|
|
export type ChatSummary = {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string | null;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
updatedAt: string;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-13 23:49:55 -08:00
|
|
|
export type SearchSummary = {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string | null;
|
|
|
|
|
query: string | null;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
updatedAt: string;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-13 23:15:12 -08:00
|
|
|
export type Message = {
|
|
|
|
|
id: string;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
role: "system" | "user" | "assistant" | "tool";
|
|
|
|
|
content: string;
|
|
|
|
|
name: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type ChatDetail = {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string | null;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
updatedAt: string;
|
|
|
|
|
messages: Message[];
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-13 23:49:55 -08:00
|
|
|
export type SearchResultItem = {
|
|
|
|
|
id: string;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
rank: number;
|
|
|
|
|
title: string | null;
|
|
|
|
|
url: string;
|
|
|
|
|
publishedDate: string | null;
|
|
|
|
|
author: string | null;
|
|
|
|
|
text: string | null;
|
|
|
|
|
highlights: string[] | null;
|
|
|
|
|
highlightScores: number[] | null;
|
|
|
|
|
score: number | null;
|
|
|
|
|
favicon: string | null;
|
|
|
|
|
image: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type SearchDetail = {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string | null;
|
|
|
|
|
query: string | null;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
updatedAt: string;
|
|
|
|
|
requestId: string | null;
|
|
|
|
|
latencyMs: number | null;
|
|
|
|
|
error: string | null;
|
2026-02-14 00:14:10 -08:00
|
|
|
answerText: string | null;
|
|
|
|
|
answerRequestId: string | null;
|
|
|
|
|
answerCitations: Array<{
|
|
|
|
|
id?: string;
|
|
|
|
|
url?: string;
|
|
|
|
|
title?: string | null;
|
|
|
|
|
publishedDate?: string | null;
|
|
|
|
|
author?: string | null;
|
|
|
|
|
text?: string | null;
|
|
|
|
|
}> | null;
|
|
|
|
|
answerError: string | null;
|
2026-02-13 23:49:55 -08:00
|
|
|
results: SearchResultItem[];
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-14 01:53:34 -08:00
|
|
|
export type SearchRunRequest = {
|
|
|
|
|
query?: string;
|
|
|
|
|
title?: string;
|
|
|
|
|
type?: "auto" | "fast" | "deep" | "instant";
|
|
|
|
|
numResults?: number;
|
|
|
|
|
includeDomains?: string[];
|
|
|
|
|
excludeDomains?: string[];
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-13 23:15:12 -08:00
|
|
|
export type CompletionRequestMessage = {
|
|
|
|
|
role: "system" | "user" | "assistant" | "tool";
|
|
|
|
|
content: string;
|
|
|
|
|
name?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type CompletionResponse = {
|
|
|
|
|
chatId: string | null;
|
|
|
|
|
message: {
|
|
|
|
|
role: "assistant";
|
|
|
|
|
content: string;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8787";
|
|
|
|
|
const ENV_ADMIN_TOKEN = (import.meta.env.VITE_ADMIN_TOKEN as string | undefined)?.trim() || null;
|
|
|
|
|
let authToken: string | null = ENV_ADMIN_TOKEN;
|
|
|
|
|
|
|
|
|
|
export function getConfiguredToken() {
|
|
|
|
|
return ENV_ADMIN_TOKEN;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function setAuthToken(token: string | null) {
|
|
|
|
|
authToken = token?.trim() || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
|
|
|
|
const headers = new Headers(init?.headers ?? {});
|
2026-02-14 01:10:27 -08:00
|
|
|
const hasBody = init?.body !== undefined && init.body !== null;
|
|
|
|
|
if (hasBody && !headers.has("Content-Type")) {
|
|
|
|
|
headers.set("Content-Type", "application/json");
|
|
|
|
|
}
|
2026-02-13 23:15:12 -08:00
|
|
|
if (authToken) {
|
|
|
|
|
headers.set("Authorization", `Bearer ${authToken}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
|
|
|
...init,
|
|
|
|
|
headers,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const fallback = `${response.status} ${response.statusText}`;
|
|
|
|
|
let message = fallback;
|
|
|
|
|
try {
|
|
|
|
|
const body = (await response.json()) as { message?: string };
|
|
|
|
|
if (body.message) message = body.message;
|
|
|
|
|
} catch {
|
|
|
|
|
// keep fallback message
|
|
|
|
|
}
|
|
|
|
|
throw new Error(message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (await response.json()) as T;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function listChats() {
|
|
|
|
|
const data = await api<{ chats: ChatSummary[] }>("/v1/chats");
|
|
|
|
|
return data.chats;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function verifySession() {
|
|
|
|
|
return api<{ authenticated: true; mode: "open" | "token" }>("/v1/auth/session");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createChat(title?: string) {
|
|
|
|
|
const data = await api<{ chat: ChatSummary }>("/v1/chats", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ title }),
|
|
|
|
|
});
|
|
|
|
|
return data.chat;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getChat(chatId: string) {
|
|
|
|
|
const data = await api<{ chat: ChatDetail }>(`/v1/chats/${chatId}`);
|
|
|
|
|
return data.chat;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 01:10:27 -08:00
|
|
|
export async function deleteChat(chatId: string) {
|
|
|
|
|
await api<{ deleted: true }>(`/v1/chats/${chatId}`, { method: "DELETE" });
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 23:49:55 -08:00
|
|
|
export async function listSearches() {
|
|
|
|
|
const data = await api<{ searches: SearchSummary[] }>("/v1/searches");
|
|
|
|
|
return data.searches;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createSearch(body?: { title?: string; query?: string }) {
|
|
|
|
|
const data = await api<{ search: SearchSummary }>("/v1/searches", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify(body ?? {}),
|
|
|
|
|
});
|
|
|
|
|
return data.search;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getSearch(searchId: string) {
|
|
|
|
|
const data = await api<{ search: SearchDetail }>(`/v1/searches/${searchId}`);
|
|
|
|
|
return data.search;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 01:10:27 -08:00
|
|
|
export async function deleteSearch(searchId: string) {
|
|
|
|
|
await api<{ deleted: true }>(`/v1/searches/${searchId}`, { method: "DELETE" });
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 01:53:34 -08:00
|
|
|
type RunSearchStreamHandlers = {
|
|
|
|
|
onSearchResults?: (payload: { requestId: string | null; results: SearchResultItem[] }) => void;
|
|
|
|
|
onSearchError?: (payload: { error: string }) => void;
|
|
|
|
|
onAnswer?: (payload: { answerText: string | null; answerRequestId: string | null; answerCitations: SearchDetail["answerCitations"] }) => void;
|
|
|
|
|
onAnswerError?: (payload: { error: string }) => void;
|
|
|
|
|
onDone?: (payload: { search: SearchDetail }) => void;
|
|
|
|
|
onError?: (payload: { message: string }) => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export async function runSearchStream(
|
2026-02-13 23:49:55 -08:00
|
|
|
searchId: string,
|
2026-02-14 01:53:34 -08:00
|
|
|
body: SearchRunRequest,
|
|
|
|
|
handlers: RunSearchStreamHandlers,
|
|
|
|
|
options?: { signal?: AbortSignal }
|
2026-02-13 23:49:55 -08:00
|
|
|
) {
|
2026-02-14 01:53:34 -08:00
|
|
|
const headers = new Headers({
|
|
|
|
|
Accept: "text/event-stream",
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
});
|
|
|
|
|
if (authToken) {
|
|
|
|
|
headers.set("Authorization", `Bearer ${authToken}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`${API_BASE_URL}/v1/searches/${searchId}/run/stream`, {
|
2026-02-13 23:49:55 -08:00
|
|
|
method: "POST",
|
2026-02-14 01:53:34 -08:00
|
|
|
headers,
|
2026-02-13 23:49:55 -08:00
|
|
|
body: JSON.stringify(body),
|
2026-02-14 01:53:34 -08:00
|
|
|
signal: options?.signal,
|
2026-02-13 23:49:55 -08:00
|
|
|
});
|
2026-02-14 01:53:34 -08:00
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const fallback = `${response.status} ${response.statusText}`;
|
|
|
|
|
let message = fallback;
|
|
|
|
|
try {
|
|
|
|
|
const body = (await response.json()) as { message?: string };
|
|
|
|
|
if (body.message) message = body.message;
|
|
|
|
|
} catch {
|
|
|
|
|
// keep fallback message
|
|
|
|
|
}
|
|
|
|
|
throw new Error(message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!response.body) {
|
|
|
|
|
throw new Error("No response stream");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reader = response.body.getReader();
|
|
|
|
|
const decoder = new TextDecoder();
|
|
|
|
|
let buffer = "";
|
|
|
|
|
let eventName = "message";
|
|
|
|
|
let dataLines: string[] = [];
|
|
|
|
|
|
|
|
|
|
const flushEvent = () => {
|
|
|
|
|
if (!dataLines.length) {
|
|
|
|
|
eventName = "message";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dataText = dataLines.join("\n");
|
|
|
|
|
let payload: any = null;
|
|
|
|
|
try {
|
|
|
|
|
payload = JSON.parse(dataText);
|
|
|
|
|
} catch {
|
|
|
|
|
payload = { message: dataText };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (eventName === "search_results") handlers.onSearchResults?.(payload);
|
|
|
|
|
else if (eventName === "search_error") handlers.onSearchError?.(payload);
|
|
|
|
|
else if (eventName === "answer") handlers.onAnswer?.(payload);
|
|
|
|
|
else if (eventName === "answer_error") handlers.onAnswerError?.(payload);
|
|
|
|
|
else if (eventName === "done") handlers.onDone?.(payload);
|
|
|
|
|
else if (eventName === "error") handlers.onError?.(payload);
|
|
|
|
|
|
|
|
|
|
dataLines = [];
|
|
|
|
|
eventName = "message";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
const { value, done } = await reader.read();
|
|
|
|
|
if (done) break;
|
|
|
|
|
|
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
|
let newlineIndex = buffer.indexOf("\n");
|
|
|
|
|
|
|
|
|
|
while (newlineIndex >= 0) {
|
|
|
|
|
const rawLine = buffer.slice(0, newlineIndex);
|
|
|
|
|
buffer = buffer.slice(newlineIndex + 1);
|
|
|
|
|
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
|
|
|
|
|
|
|
|
if (!line) {
|
|
|
|
|
flushEvent();
|
|
|
|
|
} else if (line.startsWith("event:")) {
|
|
|
|
|
eventName = line.slice("event:".length).trim();
|
|
|
|
|
} else if (line.startsWith("data:")) {
|
|
|
|
|
dataLines.push(line.slice("data:".length).trimStart());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newlineIndex = buffer.indexOf("\n");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buffer += decoder.decode();
|
|
|
|
|
if (buffer.length) {
|
|
|
|
|
const line = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
|
|
|
|
|
if (line.startsWith("event:")) {
|
|
|
|
|
eventName = line.slice("event:".length).trim();
|
|
|
|
|
} else if (line.startsWith("data:")) {
|
|
|
|
|
dataLines.push(line.slice("data:".length).trimStart());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
flushEvent();
|
2026-02-13 23:49:55 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-13 23:15:12 -08:00
|
|
|
export async function runCompletion(body: {
|
|
|
|
|
chatId: string;
|
|
|
|
|
provider: "openai" | "anthropic" | "xai";
|
|
|
|
|
model: string;
|
|
|
|
|
messages: CompletionRequestMessage[];
|
|
|
|
|
}) {
|
|
|
|
|
return api<CompletionResponse>("/v1/chat-completions", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
|
|
|
|
}
|