feat(webui): redesign settings and BYOK configuration
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
Xubin Ren
co-authored by
Cursor
parent
451d740849
commit
2cc32ca07c
@@ -5,6 +5,7 @@ import {
|
||||
fetchSessionMessages,
|
||||
listSessions,
|
||||
listSlashCommands,
|
||||
updateProviderSettings,
|
||||
updateSettings,
|
||||
} from "@/lib/api";
|
||||
|
||||
@@ -55,6 +56,21 @@ describe("webui API helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("serializes provider settings updates without returning secrets", async () => {
|
||||
await updateProviderSettings("tok", {
|
||||
provider: "openrouter",
|
||||
apiKey: "sk-or-test",
|
||||
apiBase: "https://openrouter.ai/api/v1",
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/settings/provider/update?provider=openrouter&api_key=sk-or-test&api_base=https%3A%2F%2Fopenrouter.ai%2Fapi%2Fv1",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer tok" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("maps generated session titles from the sessions list", async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
|
||||
@@ -155,7 +155,7 @@ describe("App layout", () => {
|
||||
expect(document.body.style.pointerEvents).not.toBe("none");
|
||||
}, 15_000);
|
||||
|
||||
it("opens the Cursor-style settings view from the header", async () => {
|
||||
it("opens the settings view from the sidebar footer", async () => {
|
||||
mockSessions = [
|
||||
{
|
||||
key: "websocket:chat-a",
|
||||
@@ -181,8 +181,13 @@ describe("App layout", () => {
|
||||
has_api_key: true,
|
||||
},
|
||||
providers: [
|
||||
{ name: "auto", label: "Auto" },
|
||||
{ name: "openai", label: "OpenAI" },
|
||||
{ name: "openai", label: "OpenAI", configured: true },
|
||||
{
|
||||
name: "openrouter",
|
||||
label: "OpenRouter",
|
||||
configured: false,
|
||||
default_api_base: "https://openrouter.ai/api/v1",
|
||||
},
|
||||
],
|
||||
runtime: {
|
||||
config_path: "/tmp/config.json",
|
||||
@@ -198,11 +203,24 @@ describe("App layout", () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
fireEvent.click(screen.getByRole("button", { name: "Open settings" }));
|
||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
fireEvent.click(within(sidebar).getByRole("button", { name: "Settings" }));
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "General" })).toBeInTheDocument();
|
||||
expect(document.title).toBe("Settings · nanobot");
|
||||
expect(screen.queryByRole("navigation", { name: "Sidebar navigation" })).not.toBeInTheDocument();
|
||||
const settingsNav = screen.getByRole("navigation", { name: "Settings sections" });
|
||||
expect(within(settingsNav).getByRole("button", { name: "General" })).toHaveAttribute(
|
||||
"aria-current",
|
||||
"page",
|
||||
);
|
||||
expect(within(settingsNav).getByRole("button", { name: "BYOK" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Sign out" })).toBeInTheDocument();
|
||||
expect(screen.getByText("AI")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("openai/gpt-4o")).toBeInTheDocument();
|
||||
fireEvent.click(within(settingsNav).getByRole("button", { name: "BYOK" }));
|
||||
expect(screen.getByText("OpenRouter")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Not configured").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("filters sidebar sessions through the lightweight search row", async () => {
|
||||
@@ -285,7 +303,7 @@ describe("App layout", () => {
|
||||
expect(screen.getByText("What can I do for you?")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Start a new chat" })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Toggle theme from header" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Open settings" })).toBeInTheDocument();
|
||||
expect(within(sidebar).getByRole("button", { name: "Settings" })).toBeInTheDocument();
|
||||
|
||||
expect(within(sidebar).getByText("Existing chat")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { resources } from "@/i18n";
|
||||
|
||||
const QUICK_ACTION_KEYS = ["plan", "analyze", "brainstorm", "code", "summarize", "more"];
|
||||
const IMAGE_QUICK_ACTION_KEYS = ["icon", "sticker", "poster", "product", "portrait", "edit"];
|
||||
const SETTINGS_NAV_KEYS = ["general", "byok"];
|
||||
|
||||
describe("webui i18n", () => {
|
||||
it("switches UI copy and document locale through the language switcher", async () => {
|
||||
@@ -62,4 +63,28 @@ describe("webui i18n", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps settings navigation localized for every registered locale", () => {
|
||||
for (const resource of Object.values(resources)) {
|
||||
const common = resource.common;
|
||||
expect(common.app.system.restarting).toBeTruthy();
|
||||
expect(common.sidebar.settings).toBeTruthy();
|
||||
expect(common.settings.sidebar.title).toBeTruthy();
|
||||
expect(common.settings.backToChat).toBeTruthy();
|
||||
for (const key of SETTINGS_NAV_KEYS) {
|
||||
expect(common.settings.nav[key as keyof typeof common.settings.nav]).toBeTruthy();
|
||||
}
|
||||
expect(common.settings.rows.theme).toBeTruthy();
|
||||
expect(common.settings.status.loading).toBeTruthy();
|
||||
expect(common.settings.actions.save).toBeTruthy();
|
||||
expect(common.settings.actions.edit).toBeTruthy();
|
||||
expect(common.settings.byok.configured).toBeTruthy();
|
||||
expect(common.settings.byok.configuredSection).toBeTruthy();
|
||||
expect(common.settings.byok.showMore).toBeTruthy();
|
||||
expect(common.settings.byok.apiKeyRequired).toBeTruthy();
|
||||
expect(common.settings.byok.showApiKey).toBeTruthy();
|
||||
expect(common.settings.byok.hideApiKey).toBeTruthy();
|
||||
expect(common.settings.byok.configuredKeyHint).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user