mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-19 16:12:30 +00:00
Add a session-scoped slash command palette sourced from backend command metadata, and keep welcome-page quick actions localized across all WebUI languages. Co-authored-by: Cursor <cursoragent@cursor.com>
148 lines
3.8 KiB
TypeScript
148 lines
3.8 KiB
TypeScript
import type { ChatSummary, SettingsPayload, SettingsUpdate, SlashCommand } from "./types";
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
constructor(status: number, message: string) {
|
|
super(message);
|
|
this.status = status;
|
|
this.name = "ApiError";
|
|
}
|
|
}
|
|
|
|
async function request<T>(
|
|
url: string,
|
|
token: string,
|
|
init?: RequestInit,
|
|
): Promise<T> {
|
|
const res = await fetch(url, {
|
|
...(init ?? {}),
|
|
headers: {
|
|
...(init?.headers ?? {}),
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
credentials: "same-origin",
|
|
});
|
|
if (!res.ok) {
|
|
throw new ApiError(res.status, `HTTP ${res.status}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
function splitKey(key: string): { channel: string; chatId: string } {
|
|
const idx = key.indexOf(":");
|
|
if (idx === -1) return { channel: "", chatId: key };
|
|
return { channel: key.slice(0, idx), chatId: key.slice(idx + 1) };
|
|
}
|
|
|
|
export async function listSessions(
|
|
token: string,
|
|
base: string = "",
|
|
): Promise<ChatSummary[]> {
|
|
type Row = {
|
|
key: string;
|
|
created_at: string | null;
|
|
updated_at: string | null;
|
|
title?: string;
|
|
preview?: string;
|
|
};
|
|
const body = await request<{ sessions: Row[] }>(
|
|
`${base}/api/sessions`,
|
|
token,
|
|
);
|
|
return body.sessions.map((s) => ({
|
|
key: s.key,
|
|
...splitKey(s.key),
|
|
createdAt: s.created_at,
|
|
updatedAt: s.updated_at,
|
|
title: s.title ?? "",
|
|
preview: s.preview ?? "",
|
|
}));
|
|
}
|
|
|
|
/** Signed image URL attached to a historical user message. The server
|
|
* emits these in place of raw on-disk paths so the client can render
|
|
* previews without learning where media lives on disk. Each URL is a
|
|
* self-authenticating ``/api/media/...`` route (see backend
|
|
* ``_sign_media_path``) safe to drop into an ``<img src>`` attribute. */
|
|
export interface SessionMediaUrl {
|
|
url: string;
|
|
name?: string;
|
|
}
|
|
|
|
export async function fetchSessionMessages(
|
|
token: string,
|
|
key: string,
|
|
base: string = "",
|
|
): Promise<{
|
|
key: string;
|
|
created_at: string | null;
|
|
updated_at: string | null;
|
|
messages: Array<{
|
|
role: string;
|
|
content: string;
|
|
timestamp?: string;
|
|
tool_calls?: unknown;
|
|
tool_call_id?: string;
|
|
name?: string;
|
|
/** Present on ``user`` turns that attached images. Paths have already
|
|
* been stripped server-side; only the signed fetch URLs survive. */
|
|
media_urls?: SessionMediaUrl[];
|
|
}>;
|
|
}> {
|
|
return request(
|
|
`${base}/api/sessions/${encodeURIComponent(key)}/messages`,
|
|
token,
|
|
);
|
|
}
|
|
|
|
export async function deleteSession(
|
|
token: string,
|
|
key: string,
|
|
base: string = "",
|
|
): Promise<boolean> {
|
|
const body = await request<{ deleted: boolean }>(
|
|
`${base}/api/sessions/${encodeURIComponent(key)}/delete`,
|
|
token,
|
|
);
|
|
return body.deleted;
|
|
}
|
|
|
|
export async function fetchSettings(
|
|
token: string,
|
|
base: string = "",
|
|
): Promise<SettingsPayload> {
|
|
return request<SettingsPayload>(`${base}/api/settings`, token);
|
|
}
|
|
|
|
export async function listSlashCommands(
|
|
token: string,
|
|
base: string = "",
|
|
): Promise<SlashCommand[]> {
|
|
type Row = {
|
|
command: string;
|
|
title: string;
|
|
description: string;
|
|
icon: string;
|
|
arg_hint?: string;
|
|
};
|
|
const body = await request<{ commands: Row[] }>(`${base}/api/commands`, token);
|
|
return body.commands.map((command) => ({
|
|
command: command.command,
|
|
title: command.title,
|
|
description: command.description,
|
|
icon: command.icon,
|
|
argHint: command.arg_hint ?? "",
|
|
}));
|
|
}
|
|
|
|
export async function updateSettings(
|
|
token: string,
|
|
update: SettingsUpdate,
|
|
base: string = "",
|
|
): Promise<SettingsPayload> {
|
|
const query = new URLSearchParams();
|
|
if (update.model !== undefined) query.set("model", update.model);
|
|
if (update.provider !== undefined) query.set("provider", update.provider);
|
|
return request<SettingsPayload>(`${base}/api/settings/update?${query}`, token);
|
|
}
|