feat(webui): upgrade settings and sidebar controls (#3906)
* feat(settings): expand settings api payload * feat(webui): build app-style settings center * feat(webui): add centered chat search dialog * fix(webui): shorten chat search label * fix(webui): center dialog entrance animation * fix(webui): simplify chat search results * fix(webui): tighten mobile settings navigation * feat(webui): persist sidebar state * feat(webui): add sidebar organization controls * refactor(webui): organize backend helpers * refactor(webui): remove utils compatibility shims * refactor(session): move shared webui helpers out of webui package * feat(webui): add image generation settings * style(webui): refine settings overview layout * fix(webui): localize settings zh-CN copy * style(webui): add settings status indicators * feat(webui): show sidebar run indicators * fix(webui): persist sidebar run indicators * fix(webui): highlight settings pending status * fix(webui): align settings test with provider update * fix(utils): preserve legacy webui helper imports
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import type {
|
||||
ChatSummary,
|
||||
ImageGenerationSettingsUpdate,
|
||||
ProviderSettingsUpdate,
|
||||
SettingsPayload,
|
||||
SettingsUpdate,
|
||||
SidebarStatePayload,
|
||||
SlashCommand,
|
||||
WebSearchSettingsUpdate,
|
||||
WebuiThreadPersistedPayload,
|
||||
@@ -52,6 +54,7 @@ export async function listSessions(
|
||||
updated_at: string | null;
|
||||
title?: string;
|
||||
preview?: string;
|
||||
run_started_at?: number | null;
|
||||
};
|
||||
const body = await request<{ sessions: Row[] }>(
|
||||
`${base}/api/sessions`,
|
||||
@@ -64,6 +67,7 @@ export async function listSessions(
|
||||
updatedAt: s.updated_at,
|
||||
title: s.title ?? "",
|
||||
preview: s.preview ?? "",
|
||||
runStartedAt: s.run_started_at ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -125,14 +129,43 @@ export async function listSlashCommands(
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchSidebarState(
|
||||
token: string,
|
||||
base: string = "",
|
||||
): Promise<SidebarStatePayload> {
|
||||
return request<SidebarStatePayload>(`${base}/api/webui/sidebar-state`, token);
|
||||
}
|
||||
|
||||
export async function updateSidebarState(
|
||||
token: string,
|
||||
state: SidebarStatePayload,
|
||||
base: string = "",
|
||||
): Promise<SidebarStatePayload> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("state", JSON.stringify(state));
|
||||
return request<SidebarStatePayload>(
|
||||
`${base}/api/webui/sidebar-state/update?${query}`,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateSettings(
|
||||
token: string,
|
||||
update: SettingsUpdate,
|
||||
base: string = "",
|
||||
): Promise<SettingsPayload> {
|
||||
const query = new URLSearchParams();
|
||||
if (update.modelPreset !== undefined) {
|
||||
query.set("model_preset", update.modelPreset ?? "default");
|
||||
}
|
||||
if (update.model !== undefined) query.set("model", update.model);
|
||||
if (update.provider !== undefined) query.set("provider", update.provider);
|
||||
if (update.timezone !== undefined) query.set("timezone", update.timezone);
|
||||
if (update.botName !== undefined) query.set("bot_name", update.botName);
|
||||
if (update.botIcon !== undefined) query.set("bot_icon", update.botIcon);
|
||||
if (update.toolHintMaxLength !== undefined) {
|
||||
query.set("tool_hint_max_length", String(update.toolHintMaxLength));
|
||||
}
|
||||
return request<SettingsPayload>(`${base}/api/settings/update?${query}`, token);
|
||||
}
|
||||
|
||||
@@ -160,8 +193,31 @@ export async function updateWebSearchSettings(
|
||||
query.set("provider", update.provider);
|
||||
if (update.apiKey !== undefined) query.set("api_key", update.apiKey);
|
||||
if (update.baseUrl !== undefined) query.set("base_url", update.baseUrl);
|
||||
if (update.maxResults !== undefined) query.set("max_results", String(update.maxResults));
|
||||
if (update.timeout !== undefined) query.set("timeout", String(update.timeout));
|
||||
if (update.useJinaReader !== undefined) {
|
||||
query.set("use_jina_reader", String(update.useJinaReader));
|
||||
}
|
||||
return request<SettingsPayload>(
|
||||
`${base}/api/settings/web-search/update?${query}`,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateImageGenerationSettings(
|
||||
token: string,
|
||||
update: ImageGenerationSettingsUpdate,
|
||||
base: string = "",
|
||||
): Promise<SettingsPayload> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("enabled", String(update.enabled));
|
||||
query.set("provider", update.provider);
|
||||
query.set("model", update.model);
|
||||
query.set("default_aspect_ratio", update.defaultAspectRatio);
|
||||
query.set("default_image_size", update.defaultImageSize);
|
||||
query.set("max_images_per_turn", String(update.maxImagesPerTurn));
|
||||
return request<SettingsPayload>(
|
||||
`${base}/api/settings/image-generation/update?${query}`,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ type StatusHandler = (status: ConnectionStatus) => void;
|
||||
type RuntimeModelHandler = (modelName: string | null, modelPreset?: string | null) => void;
|
||||
type SessionUpdateScope = "metadata" | "thread" | string;
|
||||
type SessionUpdateHandler = (chatId: string, scope?: SessionUpdateScope) => void;
|
||||
type RunStatusHandler = (chatId: string, startedAt: number | null) => void;
|
||||
|
||||
/** Structured connection-level errors surfaced to the UI.
|
||||
*
|
||||
@@ -102,6 +103,7 @@ export class NanobotClient {
|
||||
private statusHandlers = new Set<StatusHandler>();
|
||||
private runtimeModelHandlers = new Set<RuntimeModelHandler>();
|
||||
private sessionUpdateHandlers = new Set<SessionUpdateHandler>();
|
||||
private runStatusHandlers = new Set<RunStatusHandler>();
|
||||
private errorHandlers = new Set<ErrorHandler>();
|
||||
// chat_id -> handlers listening on it
|
||||
private chatHandlers = new Map<string, Set<EventHandler>>();
|
||||
@@ -172,6 +174,16 @@ export class NanobotClient {
|
||||
};
|
||||
}
|
||||
|
||||
onRunStatus(handler: RunStatusHandler): Unsubscribe {
|
||||
this.runStatusHandlers.add(handler);
|
||||
for (const [chatId, startedAt] of this.runStartedAtByChatId) {
|
||||
handler(chatId, startedAt);
|
||||
}
|
||||
return () => {
|
||||
this.runStatusHandlers.delete(handler);
|
||||
};
|
||||
}
|
||||
|
||||
/** Subscribe to transport-level faults (see :type:`StreamError`). */
|
||||
onError(handler: ErrorHandler): Unsubscribe {
|
||||
this.errorHandlers.add(handler);
|
||||
@@ -194,9 +206,12 @@ export class NanobotClient {
|
||||
private recordGoalStatusForRunStrip(chatId: string, ev: InboundEvent): void {
|
||||
if (ev.event !== "goal_status") return;
|
||||
if (ev.status === "running" && typeof ev.started_at === "number") {
|
||||
const previous = this.runStartedAtByChatId.get(chatId);
|
||||
this.runStartedAtByChatId.set(chatId, ev.started_at);
|
||||
} else {
|
||||
if (previous !== ev.started_at) this.emitRunStatus(chatId, ev.started_at);
|
||||
} else if (this.runStartedAtByChatId.has(chatId)) {
|
||||
this.runStartedAtByChatId.delete(chatId);
|
||||
this.emitRunStatus(chatId, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +404,12 @@ export class NanobotClient {
|
||||
}
|
||||
}
|
||||
|
||||
private emitRunStatus(chatId: string, startedAt: number | null): void {
|
||||
for (const handler of this.runStatusHandlers) {
|
||||
handler(chatId, startedAt);
|
||||
}
|
||||
}
|
||||
|
||||
private dispatch(chatId: string, ev: InboundEvent): void {
|
||||
const handlers = this.chatHandlers.get(chatId);
|
||||
if (handlers !== undefined && handlers.size > 0) {
|
||||
|
||||
@@ -110,6 +110,30 @@ export interface ChatSummary {
|
||||
updatedAt: string | null;
|
||||
title?: string;
|
||||
preview: string;
|
||||
/** Unix epoch seconds when this session currently has a turn in flight. */
|
||||
runStartedAt?: number | null;
|
||||
}
|
||||
|
||||
export type SidebarDensity = "comfortable" | "compact";
|
||||
export type SidebarSortMode = "updated_desc" | "created_desc" | "title_asc";
|
||||
|
||||
export interface SidebarViewState {
|
||||
density: SidebarDensity;
|
||||
show_previews: boolean;
|
||||
show_timestamps: boolean;
|
||||
show_archived: boolean;
|
||||
sort: SidebarSortMode;
|
||||
}
|
||||
|
||||
export interface SidebarStatePayload {
|
||||
schema_version: number;
|
||||
pinned_keys: string[];
|
||||
archived_keys: string[];
|
||||
title_overrides: Record<string, string>;
|
||||
tags_by_key: Record<string, string[]>;
|
||||
collapsed_groups: Record<string, boolean>;
|
||||
view: SidebarViewState;
|
||||
updated_at?: string | null;
|
||||
}
|
||||
|
||||
export interface BootstrapResponse {
|
||||
@@ -125,7 +149,28 @@ export interface SettingsPayload {
|
||||
provider: string;
|
||||
resolved_provider: string | null;
|
||||
has_api_key: boolean;
|
||||
model_preset: string | null;
|
||||
max_tokens: number;
|
||||
context_window_tokens: number;
|
||||
temperature: number;
|
||||
reasoning_effort: string | null;
|
||||
timezone: string;
|
||||
bot_name: string;
|
||||
bot_icon: string;
|
||||
tool_hint_max_length: number;
|
||||
};
|
||||
model_presets: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
active: boolean;
|
||||
is_default: boolean;
|
||||
model: string;
|
||||
provider: string;
|
||||
max_tokens: number;
|
||||
context_window_tokens: number;
|
||||
temperature: number;
|
||||
reasoning_effort: string | null;
|
||||
}>;
|
||||
providers: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
@@ -139,21 +184,82 @@ export interface SettingsPayload {
|
||||
provider: string;
|
||||
api_key_hint?: string | null;
|
||||
base_url?: string | null;
|
||||
max_results: number;
|
||||
timeout: number;
|
||||
providers: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
credential: "none" | "api_key" | "base_url";
|
||||
}>;
|
||||
};
|
||||
web: {
|
||||
enable: boolean;
|
||||
proxy?: string | null;
|
||||
user_agent?: string | null;
|
||||
search: {
|
||||
max_results: number;
|
||||
timeout: number;
|
||||
};
|
||||
fetch: {
|
||||
use_jina_reader: boolean;
|
||||
};
|
||||
};
|
||||
image_generation: {
|
||||
enabled: boolean;
|
||||
provider: string;
|
||||
provider_configured: boolean;
|
||||
model: string;
|
||||
default_aspect_ratio: string;
|
||||
default_image_size: string;
|
||||
max_images_per_turn: number;
|
||||
save_dir: string;
|
||||
providers: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
configured: boolean;
|
||||
api_key_hint?: string | null;
|
||||
api_base?: string | null;
|
||||
default_api_base?: string | null;
|
||||
}>;
|
||||
};
|
||||
runtime: {
|
||||
config_path: string;
|
||||
workspace_path: string;
|
||||
gateway_host: string;
|
||||
gateway_port: number;
|
||||
heartbeat: {
|
||||
enabled: boolean;
|
||||
interval_s: number;
|
||||
keep_recent_messages: number;
|
||||
};
|
||||
dream: {
|
||||
schedule: string;
|
||||
max_batch_size: number;
|
||||
max_iterations: number;
|
||||
annotate_line_ages: boolean;
|
||||
};
|
||||
unified_session: boolean;
|
||||
};
|
||||
advanced: {
|
||||
restrict_to_workspace: boolean;
|
||||
ssrf_whitelist_count: number;
|
||||
mcp_server_count: number;
|
||||
exec_enabled: boolean;
|
||||
exec_sandbox?: string | null;
|
||||
exec_path_append_set: boolean;
|
||||
};
|
||||
requires_restart: boolean;
|
||||
restart_required_sections?: Array<"runtime" | "web" | "image">;
|
||||
}
|
||||
|
||||
export interface SettingsUpdate {
|
||||
model?: string;
|
||||
provider?: string;
|
||||
modelPreset?: string | null;
|
||||
timezone?: string;
|
||||
botName?: string;
|
||||
botIcon?: string;
|
||||
toolHintMaxLength?: number;
|
||||
}
|
||||
|
||||
export interface ProviderSettingsUpdate {
|
||||
@@ -166,6 +272,18 @@ export interface WebSearchSettingsUpdate {
|
||||
provider: string;
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
maxResults?: number;
|
||||
timeout?: number;
|
||||
useJinaReader?: boolean;
|
||||
}
|
||||
|
||||
export interface ImageGenerationSettingsUpdate {
|
||||
enabled: boolean;
|
||||
provider: string;
|
||||
model: string;
|
||||
defaultAspectRatio: string;
|
||||
defaultImageSize: string;
|
||||
maxImagesPerTurn: number;
|
||||
}
|
||||
|
||||
export interface SlashCommand {
|
||||
|
||||
Reference in New Issue
Block a user