mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix(ui/chat): resolve chat routes at render time so navigation works under server_root_path
CHAT_ROUTES was built once at module load via migratedHref, capturing an empty server root path before the UI-config bootstrap sets it. Under SERVER_ROOT_PATH every chat route came out unprefixed, so router.push hard-navigated to a 404; during a send that page unload also aborted the streaming request, so the first message of a new conversation never rendered and only a second send appeared to work. Compute the routes at render time via getChatRoutes(), update the send URL with a shallow history.pushState instead of a router navigation, and source the active conversation id from the hook's local state so it propagates without a router round-trip
This commit is contained in:
parent
be658d5d29
commit
2b9b681e12
5 changed files with 77 additions and 29 deletions
|
|
@ -10,7 +10,7 @@ import { Button } from "@/components/ui/button";
|
|||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useChatShell } from "@/contexts/ChatShellContext";
|
||||
import { CHAT_ROUTES } from "@/components/chat/ChatShell";
|
||||
import { getChatRoutes } from "@/components/chat/ChatShell";
|
||||
import ChatMessages from "@/components/chat/ChatMessages";
|
||||
import MCPConnectPicker from "@/components/chat/MCPConnectPicker";
|
||||
import { fetchAvailableModels } from "@/components/llm_calls/fetch_models";
|
||||
|
|
@ -87,7 +87,7 @@ export default function ChatConversationPage() {
|
|||
const streamScrollLock = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (staleId) router.replace(CHAT_ROUTES.chats);
|
||||
if (staleId) router.replace(getChatRoutes().chats);
|
||||
}, [staleId, router]);
|
||||
|
||||
// Load models
|
||||
|
|
@ -140,7 +140,7 @@ export default function ChatConversationPage() {
|
|||
if (!convId) {
|
||||
convId = createConversation(model);
|
||||
setResponsesSessionId(null); // new conversation starts a fresh session
|
||||
router.push(`${CHAT_ROUTES.chats}?id=${convId}`);
|
||||
window.history.pushState(null, "", `${window.location.pathname}?id=${convId}`);
|
||||
}
|
||||
|
||||
appendMessage(convId, { role: "user", content: trimmed });
|
||||
|
|
@ -248,7 +248,6 @@ export default function ChatConversationPage() {
|
|||
createConversation,
|
||||
appendMessage,
|
||||
updateLastAssistantMessage,
|
||||
router,
|
||||
isStreaming,
|
||||
responsesSessionId,
|
||||
],
|
||||
|
|
@ -529,7 +528,7 @@ export default function ChatConversationPage() {
|
|||
Chat with 100+ LLMs + MCP tools; authenticate once, use them here.{" "}
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => router.push(CHAT_ROUTES.integrations)}
|
||||
onClick={() => router.push(getChatRoutes().integrations)}
|
||||
className="h-auto p-0 text-sm font-medium"
|
||||
>
|
||||
Open Integrations ->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
// Regression for the chat sidebar / first-message navigation under SERVER_ROOT_PATH.
|
||||
// getChatRoutes() must read the server root path at call time. The previous
|
||||
// module-level `CHAT_ROUTES` captured it once at import, before the UI-config
|
||||
// bootstrap runs setServerRootPath, so every chat route was permanently
|
||||
// unprefixed and router.push() navigated to a 404 (which, mid-stream, also
|
||||
// aborted the first message). These tests deliberately apply the root path
|
||||
// AFTER importing the module so a frozen-at-import implementation fails.
|
||||
describe("getChatRoutes under server_root_path", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.stubEnv("NODE_ENV", "test");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("reflects a server root path applied after the module is loaded", async () => {
|
||||
const { getChatRoutes } = await import("./ChatShell");
|
||||
const { setServerRootPath } = await import("@/lib/serverRootPath");
|
||||
|
||||
setServerRootPath("/gw");
|
||||
|
||||
const routes = getChatRoutes();
|
||||
expect(routes.chats).toBe("/gw/ui/chat");
|
||||
expect(routes.integrations).toBe("/gw/ui/chat/integrations");
|
||||
expect(routes.credentials).toBe("/gw/ui/chat/credentials");
|
||||
expect(routes.apiKeys).toBe("/gw/ui/chat/api-keys");
|
||||
expect(routes.usage).toBe("/gw/ui/chat/usage");
|
||||
});
|
||||
|
||||
it("builds /ui-rooted paths when no server root path is set", async () => {
|
||||
const { getChatRoutes } = await import("./ChatShell");
|
||||
const { setServerRootPath } = await import("@/lib/serverRootPath");
|
||||
|
||||
setServerRootPath("/");
|
||||
|
||||
expect(getChatRoutes().chats).toBe("/ui/chat");
|
||||
expect(getChatRoutes().integrations).toBe("/ui/chat/integrations");
|
||||
});
|
||||
});
|
||||
|
|
@ -9,14 +9,16 @@ import { migratedHref } from "@/utils/migratedPages";
|
|||
import { useChatShell } from "@/contexts/ChatShellContext";
|
||||
import ConversationList from "./ConversationList";
|
||||
|
||||
const CHAT_BASE = migratedHref("chat");
|
||||
export const CHAT_ROUTES = {
|
||||
chats: CHAT_BASE,
|
||||
integrations: `${CHAT_BASE}/integrations`,
|
||||
credentials: `${CHAT_BASE}/credentials`,
|
||||
apiKeys: `${CHAT_BASE}/api-keys`,
|
||||
usage: `${CHAT_BASE}/usage`,
|
||||
};
|
||||
export function getChatRoutes() {
|
||||
const base = migratedHref("chat");
|
||||
return {
|
||||
chats: base,
|
||||
integrations: `${base}/integrations`,
|
||||
credentials: `${base}/credentials`,
|
||||
apiKeys: `${base}/api-keys`,
|
||||
usage: `${base}/usage`,
|
||||
};
|
||||
}
|
||||
|
||||
function stripTrailingSlash(path: string): string {
|
||||
return path.length > 1 ? path.replace(/\/+$/, "") : path;
|
||||
|
|
@ -54,7 +56,8 @@ const ChatShell: React.FC<ChatShellProps> = ({ children }) => {
|
|||
const pathname = stripTrailingSlash(usePathname() ?? "");
|
||||
const { conversations, activeConversationId, deleteConversation, renameConversation } = useChatShell();
|
||||
|
||||
const isChatsRoute = pathname === CHAT_ROUTES.chats;
|
||||
const routes = getChatRoutes();
|
||||
const isChatsRoute = pathname === routes.chats;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col bg-background overflow-hidden">
|
||||
|
|
@ -73,7 +76,7 @@ const ChatShell: React.FC<ChatShellProps> = ({ children }) => {
|
|||
<div className="flex flex-1 min-h-0 overflow-hidden">
|
||||
<div className="shrink-0 bg-sidebar border-sidebar-border border-r flex flex-col overflow-hidden w-[260px]">
|
||||
<div className="px-2 pt-3 pb-1 shrink-0">
|
||||
<Button onClick={() => router.push(CHAT_ROUTES.chats)} className="w-full justify-start gap-2.5">
|
||||
<Button onClick={() => router.push(routes.chats)} className="w-full justify-start gap-2.5">
|
||||
<Plus className="h-4 w-4" />
|
||||
New Chat
|
||||
</Button>
|
||||
|
|
@ -85,32 +88,32 @@ const ChatShell: React.FC<ChatShellProps> = ({ children }) => {
|
|||
<NavItem
|
||||
icon={<MessageSquare className="h-4 w-4" />}
|
||||
label="Chats"
|
||||
onClick={() => router.push(CHAT_ROUTES.chats)}
|
||||
onClick={() => router.push(routes.chats)}
|
||||
active={isChatsRoute}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<LayoutGrid className="h-4 w-4" />}
|
||||
label="Integrations"
|
||||
onClick={() => router.push(CHAT_ROUTES.integrations)}
|
||||
active={pathname === CHAT_ROUTES.integrations}
|
||||
onClick={() => router.push(routes.integrations)}
|
||||
active={pathname === routes.integrations}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<KeyRound className="h-4 w-4" />}
|
||||
label="Credentials"
|
||||
onClick={() => router.push(CHAT_ROUTES.credentials)}
|
||||
active={pathname === CHAT_ROUTES.credentials}
|
||||
onClick={() => router.push(routes.credentials)}
|
||||
active={pathname === routes.credentials}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<Lock className="h-4 w-4" />}
|
||||
label="API Keys"
|
||||
onClick={() => router.push(CHAT_ROUTES.apiKeys)}
|
||||
active={pathname === CHAT_ROUTES.apiKeys}
|
||||
onClick={() => router.push(routes.apiKeys)}
|
||||
active={pathname === routes.apiKeys}
|
||||
/>
|
||||
<NavItem
|
||||
icon={<BarChart3 className="h-4 w-4" />}
|
||||
label="Usage"
|
||||
onClick={() => router.push(CHAT_ROUTES.usage)}
|
||||
active={pathname === CHAT_ROUTES.usage}
|
||||
onClick={() => router.push(routes.usage)}
|
||||
active={pathname === routes.usage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -120,10 +123,10 @@ const ChatShell: React.FC<ChatShellProps> = ({ children }) => {
|
|||
<ConversationList
|
||||
conversations={conversations}
|
||||
activeConversationId={activeConversationId}
|
||||
onSelect={(id) => router.push(`${CHAT_ROUTES.chats}?id=${id}`)}
|
||||
onSelect={(id) => router.push(`${routes.chats}?id=${id}`)}
|
||||
onDelete={(id) => {
|
||||
deleteConversation(id);
|
||||
if (id === activeConversationId) router.push(CHAT_ROUTES.chats);
|
||||
if (id === activeConversationId) router.push(routes.chats);
|
||||
}}
|
||||
onRename={renameConversation}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export function useChatHistory(
|
|||
): {
|
||||
conversations: Conversation[];
|
||||
activeConversation: Conversation | null;
|
||||
currentActiveId: string | null;
|
||||
storageUnavailable: boolean;
|
||||
staleId: boolean;
|
||||
createConversation: (model: string) => string;
|
||||
|
|
@ -208,6 +209,7 @@ export function useChatHistory(
|
|||
return {
|
||||
conversations,
|
||||
activeConversation,
|
||||
currentActiveId,
|
||||
storageUnavailable,
|
||||
staleId,
|
||||
createConversation,
|
||||
|
|
|
|||
|
|
@ -57,12 +57,13 @@ export function ChatShellProvider({
|
|||
children,
|
||||
}: ChatShellProviderProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const activeConversationId = searchParams.get("id");
|
||||
const urlConversationId = searchParams.get("id");
|
||||
const [selectedMCPServers, setSelectedMCPServers] = useState<string[]>([]);
|
||||
|
||||
const {
|
||||
conversations,
|
||||
activeConversation,
|
||||
currentActiveId,
|
||||
storageUnavailable,
|
||||
staleId,
|
||||
createConversation,
|
||||
|
|
@ -71,7 +72,7 @@ export function ChatShellProvider({
|
|||
truncateFromMessage,
|
||||
deleteConversation,
|
||||
renameConversation,
|
||||
} = useChatHistory(activeConversationId, userId);
|
||||
} = useChatHistory(urlConversationId, userId);
|
||||
|
||||
return (
|
||||
<ChatShellContext.Provider
|
||||
|
|
@ -85,7 +86,7 @@ export function ChatShellProvider({
|
|||
setSelectedMCPServers,
|
||||
conversations,
|
||||
activeConversation,
|
||||
activeConversationId,
|
||||
activeConversationId: currentActiveId,
|
||||
storageUnavailable,
|
||||
staleId,
|
||||
createConversation,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue