mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
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.
This commit is contained in:
parent
a58930e94e
commit
7109b2f61c
3 changed files with 136 additions and 96 deletions
|
|
@ -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(<ViewSwitcher />);
|
||||
|
||||
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" }];
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -80,105 +80,119 @@ const ChatShell: React.FC<ChatShellProps> = ({ children }) => {
|
|||
const isChatsRoute = pathname === CHAT_ROUTES.chats;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full bg-background overflow-hidden">
|
||||
<div
|
||||
className="shrink-0 bg-secondary border-r flex flex-col overflow-hidden"
|
||||
style={{ width: sidebarCollapsed ? 56 : 260, transition: "width 0.2s cubic-bezier(0.4, 0, 0.2, 1)" }}
|
||||
>
|
||||
<div className="flex items-center justify-start px-2.5 py-3 shrink-0">
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => setSidebarCollapsed((v) => !v)}
|
||||
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground flex items-center cursor-pointer transition-colors"
|
||||
style={{ background: "none", border: "none" }}
|
||||
>
|
||||
{sidebarCollapsed ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>{sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex h-full w-full flex-col bg-background overflow-hidden">
|
||||
<div className="shrink-0 border-b border-amber-200 bg-amber-50 px-4 py-1.5 text-center text-[13px] text-amber-800">
|
||||
This is a pre-v0 feature. Do not use in production, it may change unexpectedly. Please share feedback{" "}
|
||||
<a
|
||||
href="https://github.com/BerriAI/litellm/discussions/32085"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-medium underline"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
<div className="flex flex-1 min-h-0 overflow-hidden">
|
||||
<div
|
||||
className="shrink-0 bg-secondary border-r flex flex-col overflow-hidden"
|
||||
style={{ width: sidebarCollapsed ? 56 : 260, transition: "width 0.2s cubic-bezier(0.4, 0, 0.2, 1)" }}
|
||||
>
|
||||
<div className="flex items-center justify-start px-2.5 py-3 shrink-0">
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => setSidebarCollapsed((v) => !v)}
|
||||
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground flex items-center cursor-pointer transition-colors"
|
||||
style={{ background: "none", border: "none" }}
|
||||
>
|
||||
{sidebarCollapsed ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>{sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="px-2 pb-1 shrink-0">
|
||||
<NavItem
|
||||
icon={<Pencil className="h-4 w-4" />}
|
||||
label="New chat"
|
||||
onClick={() => router.push(CHAT_ROUTES.chats)}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<Search className="h-4 w-4" />}
|
||||
label="Search chats"
|
||||
onClick={() => router.push(CHAT_ROUTES.chats)}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="mx-2 shrink-0" />
|
||||
|
||||
<div className="px-2 py-1 shrink-0">
|
||||
<NavItem
|
||||
icon={<MessageSquare className="h-4 w-4" />}
|
||||
label="Chats"
|
||||
onClick={() => router.push(CHAT_ROUTES.chats)}
|
||||
active={isChatsRoute}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<LayoutGrid className="h-4 w-4" />}
|
||||
label="Integrations"
|
||||
onClick={() => router.push(CHAT_ROUTES.integrations)}
|
||||
active={pathname === CHAT_ROUTES.integrations}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<KeyRound className="h-4 w-4" />}
|
||||
label="Credentials"
|
||||
onClick={() => router.push(CHAT_ROUTES.credentials)}
|
||||
active={pathname === CHAT_ROUTES.credentials}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<Lock className="h-4 w-4" />}
|
||||
label="API Keys"
|
||||
onClick={() => router.push(CHAT_ROUTES.apiKeys)}
|
||||
active={pathname === CHAT_ROUTES.apiKeys}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<BarChart3 className="h-4 w-4" />}
|
||||
label="Usage"
|
||||
onClick={() => router.push(CHAT_ROUTES.usage)}
|
||||
active={pathname === CHAT_ROUTES.usage}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="mx-2 shrink-0" />
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
<ConversationList
|
||||
conversations={conversations}
|
||||
activeConversationId={activeConversationId}
|
||||
onSelect={(id) => 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}
|
||||
<div className="px-2 pb-1 shrink-0">
|
||||
<NavItem
|
||||
icon={<Pencil className="h-4 w-4" />}
|
||||
label="New chat"
|
||||
onClick={() => router.push(CHAT_ROUTES.chats)}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<Search className="h-4 w-4" />}
|
||||
label="Search chats"
|
||||
onClick={() => router.push(CHAT_ROUTES.chats)}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden min-w-0">{children}</div>
|
||||
<Separator className="mx-2 shrink-0" />
|
||||
|
||||
<div className="px-2 py-1 shrink-0">
|
||||
<NavItem
|
||||
icon={<MessageSquare className="h-4 w-4" />}
|
||||
label="Chats"
|
||||
onClick={() => router.push(CHAT_ROUTES.chats)}
|
||||
active={isChatsRoute}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<LayoutGrid className="h-4 w-4" />}
|
||||
label="Integrations"
|
||||
onClick={() => router.push(CHAT_ROUTES.integrations)}
|
||||
active={pathname === CHAT_ROUTES.integrations}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<KeyRound className="h-4 w-4" />}
|
||||
label="Credentials"
|
||||
onClick={() => router.push(CHAT_ROUTES.credentials)}
|
||||
active={pathname === CHAT_ROUTES.credentials}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<Lock className="h-4 w-4" />}
|
||||
label="API Keys"
|
||||
onClick={() => router.push(CHAT_ROUTES.apiKeys)}
|
||||
active={pathname === CHAT_ROUTES.apiKeys}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<BarChart3 className="h-4 w-4" />}
|
||||
label="Usage"
|
||||
onClick={() => router.push(CHAT_ROUTES.usage)}
|
||||
active={pathname === CHAT_ROUTES.usage}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="mx-2 shrink-0" />
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
<ConversationList
|
||||
conversations={conversations}
|
||||
activeConversationId={activeConversationId}
|
||||
onSelect={(id) => 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}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden min-w-0">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue