From 7109b2f61c151c8dc2dd08ee17e7f8dac56fb333 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 3 Jul 2026 15:54:44 -0700 Subject: [PATCH] fix(ui): view-switcher navigation from chat route, add beta banner "AI Gateway" in the topnav view switcher only called setMode(), which is meaningful inside the dashboard SPA shell but a no-op on /chat, which lives outside it (only "Chat" had a real navigation). Now switching modes from the chat route does a real navigation back to the dashboard root. Also adds a persistent banner across all chat routes flagging it as a pre-v0 feature not for production use, with a feedback link. --- .../components/Navbar/ViewSwitcher.test.tsx | 23 +- .../src/components/Navbar/ViewSwitcher.tsx | 5 + .../src/components/chat/ChatShell.tsx | 204 ++++++++++-------- 3 files changed, 136 insertions(+), 96 deletions(-) diff --git a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.test.tsx b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.test.tsx index a13af8b2bef..bdbf74cdf7d 100644 --- a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.test.tsx +++ b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.test.tsx @@ -2,13 +2,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import ViewSwitcher from "./ViewSwitcher"; -const { mockUsePluginMode, mockUseUISettings, state } = vi.hoisted(() => { +const { mockUsePluginMode, mockUseUISettings, mockUsePathname, state } = vi.hoisted(() => { const state = { mode: "ai-gateway" as string, setMode: vi.fn(), plugins: [] as { name: string; display_name: string; url: string }[], activePlugin: null as { name: string; display_name: string; url: string } | null, enableChatUI: false, + pathname: "/ui/", }; return { state, @@ -19,11 +20,13 @@ const { mockUsePluginMode, mockUseUISettings, state } = vi.hoisted(() => { activePlugin: state.activePlugin, })), mockUseUISettings: vi.fn(() => ({ data: { values: { enable_chat_ui: state.enableChatUI } } })), + mockUsePathname: vi.fn(() => state.pathname), }; }); vi.mock("@/contexts/PluginModeContext", () => ({ usePluginMode: mockUsePluginMode })); vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({ useUISettings: mockUseUISettings })); +vi.mock("next/navigation", () => ({ usePathname: mockUsePathname })); // Deterministic hrefs so navigation assertions don't depend on server_root_path. vi.mock("@/utils/migratedPages", () => ({ migratedHref: (seg: string) => `/ui/${seg}` })); @@ -42,6 +45,7 @@ describe("ViewSwitcher", () => { state.mode = "ai-gateway"; state.plugins = []; state.enableChatUI = false; + state.pathname = "/ui/"; state.setMode.mockClear(); }); @@ -100,6 +104,23 @@ describe("ViewSwitcher", () => { expect(state.setMode).not.toHaveBeenCalled(); }); + it("navigates back to the dashboard when a mode entry is picked from the chat route", async () => { + state.enableChatUI = true; + state.pathname = "/ui/chat"; + state.plugins = [{ name: "litellm-platform-plugin", display_name: "Chat UI", url: "http://localhost:3300" }]; + render(); + + act(() => { + fireEvent.click(screen.getByRole("button")); + }); + await waitFor(() => expect(screen.getByText("AI Gateway")).toBeInTheDocument()); + act(() => { + fireEvent.click(screen.getByText("AI Gateway")); + }); + expect(state.setMode).toHaveBeenCalledWith("ai-gateway"); + expect(assignSpy).toHaveBeenCalledWith("/ui/"); + }); + it("hides the Chat entry from everyone when disabled", async () => { state.enableChatUI = false; state.plugins = [{ name: "obs", display_name: "Observability", url: "http://localhost:9000" }]; diff --git a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx index fa910049da4..834966aca35 100644 --- a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx +++ b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx @@ -61,6 +61,11 @@ export default function ViewSwitcher() { return; } setMode(key); + // The chat route lives outside the dashboard SPA shell that reacts to `mode`, + // so switching modes from there needs a real navigation, not just state. + if (isChatRoute) { + window.location.assign(migratedHref("")); + } }; return ( diff --git a/ui/litellm-dashboard/src/components/chat/ChatShell.tsx b/ui/litellm-dashboard/src/components/chat/ChatShell.tsx index d78a08faa24..09af0abdf2f 100644 --- a/ui/litellm-dashboard/src/components/chat/ChatShell.tsx +++ b/ui/litellm-dashboard/src/components/chat/ChatShell.tsx @@ -80,105 +80,119 @@ const ChatShell: React.FC = ({ children }) => { const isChatsRoute = pathname === CHAT_ROUTES.chats; return ( -
-
-
- - - - - - -

{sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}

-
-
-
-
+
+
+ This is a pre-v0 feature. Do not use in production, it may change unexpectedly. Please share feedback{" "} + + here + + . +
+
+
+
+ + + + + + +

{sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}

+
+
+
+
-
- } - label="New chat" - onClick={() => router.push(CHAT_ROUTES.chats)} - collapsed={sidebarCollapsed} - /> - } - label="Search chats" - onClick={() => router.push(CHAT_ROUTES.chats)} - collapsed={sidebarCollapsed} - /> -
- - - -
- } - label="Chats" - onClick={() => router.push(CHAT_ROUTES.chats)} - active={isChatsRoute} - collapsed={sidebarCollapsed} - /> - } - label="Integrations" - onClick={() => router.push(CHAT_ROUTES.integrations)} - active={pathname === CHAT_ROUTES.integrations} - collapsed={sidebarCollapsed} - /> - } - label="Credentials" - onClick={() => router.push(CHAT_ROUTES.credentials)} - active={pathname === CHAT_ROUTES.credentials} - collapsed={sidebarCollapsed} - /> - } - label="API Keys" - onClick={() => router.push(CHAT_ROUTES.apiKeys)} - active={pathname === CHAT_ROUTES.apiKeys} - collapsed={sidebarCollapsed} - /> - } - label="Usage" - onClick={() => router.push(CHAT_ROUTES.usage)} - active={pathname === CHAT_ROUTES.usage} - collapsed={sidebarCollapsed} - /> -
- - - - {!sidebarCollapsed && ( -
- router.push(`${CHAT_ROUTES.chats}?id=${id}`)} - onDelete={(id) => { - deleteConversation(id); - if (id === activeConversationId) router.push(CHAT_ROUTES.chats); - }} - onNewChat={() => router.push(CHAT_ROUTES.chats)} - onRename={renameConversation} +
+ } + label="New chat" + onClick={() => router.push(CHAT_ROUTES.chats)} + collapsed={sidebarCollapsed} + /> + } + label="Search chats" + onClick={() => router.push(CHAT_ROUTES.chats)} + collapsed={sidebarCollapsed} />
- )} -
-
{children}
+ + +
+ } + label="Chats" + onClick={() => router.push(CHAT_ROUTES.chats)} + active={isChatsRoute} + collapsed={sidebarCollapsed} + /> + } + label="Integrations" + onClick={() => router.push(CHAT_ROUTES.integrations)} + active={pathname === CHAT_ROUTES.integrations} + collapsed={sidebarCollapsed} + /> + } + label="Credentials" + onClick={() => router.push(CHAT_ROUTES.credentials)} + active={pathname === CHAT_ROUTES.credentials} + collapsed={sidebarCollapsed} + /> + } + label="API Keys" + onClick={() => router.push(CHAT_ROUTES.apiKeys)} + active={pathname === CHAT_ROUTES.apiKeys} + collapsed={sidebarCollapsed} + /> + } + label="Usage" + onClick={() => router.push(CHAT_ROUTES.usage)} + active={pathname === CHAT_ROUTES.usage} + collapsed={sidebarCollapsed} + /> +
+ + + + {!sidebarCollapsed && ( +
+ router.push(`${CHAT_ROUTES.chats}?id=${id}`)} + onDelete={(id) => { + deleteConversation(id); + if (id === activeConversationId) router.push(CHAT_ROUTES.chats); + }} + onNewChat={() => router.push(CHAT_ROUTES.chats)} + onRename={renameConversation} + /> +
+ )} +
+ +
{children}
+
); };