From 75e172b0e0ae03ed8e86922eb1717b6905e2acb7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Apr 2026 12:13:02 +0000 Subject: [PATCH] feat(ui): migrate chat chrome to shadcn - ChatMessages: chrome-only migration; antd Collapse/Panel -> shadcn Accordion for tool call card; antd Tooltip -> shadcn Tooltip for edit/copy action buttons. Message rendering, streaming state, markdown/syntax-highlighter internals untouched. - ChatPage: antd Tooltip/Skeleton/Popover -> shadcn equivalents for sidebar nav items, model selector, MCP connect picker, and top-bar actions. Sidebar layout, storage banner, and chat input textarea preserved. - ConversationList: antd Button/Input/Modal/Popconfirm/Tooltip/Avatar/Typography -> shadcn equivalents. Modal (Cmd+K search) -> Dialog; Popconfirm -> custom Popover with Cancel/Delete buttons; Avatar -> shadcn Avatar+Fallback. - MCPAppsPanel: antd Spin/Input/Button/Skeleton -> shadcn Button/Input/Skeleton + LoaderCircle spinner shim; Input with prefix icon rebuilt as absolute-positioned icon + padded input. - Add ChatMessages.tsx to no-raw-tailwind-colors override (inline code styling in markdown renderer uses raw palette). Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 1 + .../src/components/chat/ChatMessages.tsx | 163 ++++++---- .../src/components/chat/ChatPage.tsx | 198 +++++++----- .../src/components/chat/ConversationList.tsx | 288 +++++++++++------- .../src/components/chat/MCPAppsPanel.tsx | 91 ++++-- 5 files changed, 470 insertions(+), 271 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index 2a73b0e31b3..cad0840a673 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -152,6 +152,7 @@ "src/components/playground/chat_ui/AgentBuilderView.tsx", "src/components/playground/chat_ui/RealtimePlayground.tsx", "src/components/playground/compareUI/CompareUI.tsx", + "src/components/chat/ChatMessages.tsx", "src/components/DeletedKeysPage/DeletedKeysPage.tsx", "src/components/DeletedTeamsPage/DeletedTeamsPage.tsx", "src/components/molecules/models/columns.tsx", diff --git a/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx b/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx index 5ec77002cb5..0927ca627c9 100644 --- a/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx +++ b/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx @@ -1,7 +1,23 @@ "use client"; -import { Wrench as ToolOutlined, Copy as CopyOutlined, Check as CheckOutlined, Pencil as EditOutlined } from "lucide-react"; -import { Collapse, Tooltip } from "antd"; +import { + Wrench as ToolOutlined, + Copy as CopyOutlined, + Check as CheckOutlined, + Pencil as EditOutlined, +} from "lucide-react"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import React, { useEffect, useRef, useState } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; @@ -11,8 +27,6 @@ import ReasoningContent from "../playground/chat_ui/ReasoningContent"; import MCPEventsDisplay from "../playground/chat_ui/MCPEventsDisplay"; import { ChatMessage } from "./types"; -const { Panel } = Collapse; - // Keys whose values must be redacted in tool args display const REDACTED_KEY_PATTERNS = /token|key|secret|password|auth/i; @@ -194,22 +208,41 @@ function UserBubble({ message, onEdit, isStreaming }: UserBubbleProps) {
{/* Edit button — appears on hover, to the left of the bubble */} {hovered && !isStreaming && onEdit && ( - - - + + + + + + Edit message + + )}
- - - + + + + + + {copied ? "Copied!" : "Copy"} + +
); } @@ -445,25 +486,32 @@ function ToolCard({ message }: ToolCardProps) { return (
- - - + + + + {message.toolName ?? "Tool call"} - } - key="tool" - > + + {redactedArgs !== undefined && (
)} - - + + +
{formatTimestamp(message.timestamp)}
diff --git a/ui/litellm-dashboard/src/components/chat/ChatPage.tsx b/ui/litellm-dashboard/src/components/chat/ChatPage.tsx index 8a21d0d92ae..1ecd40bf729 100644 --- a/ui/litellm-dashboard/src/components/chat/ChatPage.tsx +++ b/ui/litellm-dashboard/src/components/chat/ChatPage.tsx @@ -1,9 +1,20 @@ "use client"; import React, { useCallback, useEffect, useRef, useState, useLayoutEffect } from "react"; -import { Tooltip, Skeleton, Popover } from "antd"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import MessageManager from "@/components/molecules/message_manager"; -import { Settings as SettingOutlined, Plus as PlusOutlined, Pencil as EditOutlined, PanelLeftClose as MenuFoldOutlined, PanelLeft as MenuUnfoldOutlined, Search as SearchOutlined, MessageSquare as MessageOutlined, LayoutGrid as AppstoreOutlined, Key as KeyOutlined, ArrowLeft as ArrowLeftOutlined, ChevronDown as DownOutlined, X as CloseOutlined, Check as CheckOutlined } from "lucide-react"; +import { Settings as SettingOutlined, Plus as PlusOutlined, Pencil as EditOutlined, PanelLeftClose as MenuFoldOutlined, PanelLeft as MenuUnfoldOutlined, Search as SearchOutlined, MessageSquare as MessageOutlined, LayoutGrid as AppstoreOutlined, Key as KeyOutlined, ArrowLeft as ArrowLeftOutlined, ChevronDown as DownOutlined, Check as CheckOutlined } from "lucide-react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { useRouter, useSearchParams } from "next/navigation"; @@ -583,7 +594,9 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user active = false, kbd?: string, ) => ( - + + + - + + {sidebarCollapsed && ( + {label} + )} + + ); // ---- Model selector trigger button ---- const modelSelectorTrigger = isLoadingModels ? ( - + ) : ( { setModelSelectorOpen(open); if (!open) setModelSearchText(""); }} - content={modelSelectorContent} - trigger="click" - placement="bottomLeft" > + + + + {modelSelectorContent} + ); @@ -722,32 +742,29 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user padding: inConversation ? "4px 12px 10px" : "8px 12px 12px", borderTop: "1px solid #f3f4f6", }}> - + + + + - } - trigger="click" - placement="topLeft" - > - +
@@ -834,18 +851,26 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user
)} - - - + + + + + + + {sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} + + +
{/* Sidebar nav buttons */} @@ -861,35 +886,44 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user {sidebarNavItem(, "Chats", () => setSidebarView("chats"), sidebarView === "chats")} {sidebarNavItem(, "Apps", () => setSidebarView("apps"), sidebarView === "apps")} {sidebarNavItem(, "Credentials", () => setSidebarView("credentials"), sidebarView === "credentials")} - - { - (e.currentTarget as HTMLAnchorElement).style.background = "#f5f5f5"; - }} - onMouseLeave={(e) => { - (e.currentTarget as HTMLAnchorElement).style.background = "transparent"; - }} - > - - {!sidebarCollapsed && ( - Back to Developer Console UI + + + + { + (e.currentTarget as HTMLAnchorElement).style.background = "#f5f5f5"; + }} + onMouseLeave={(e) => { + (e.currentTarget as HTMLAnchorElement).style.background = "transparent"; + }} + > + + {!sidebarCollapsed && ( + Back to Developer Console UI + )} + + + {sidebarCollapsed && ( + + Back to Developer Console UI + )} - - + +
@@ -931,15 +965,23 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user {/* Right: settings */}
- - - + + + + + + Settings + +
diff --git a/ui/litellm-dashboard/src/components/chat/ConversationList.tsx b/ui/litellm-dashboard/src/components/chat/ConversationList.tsx index e0c535f20e1..874a7298b38 100644 --- a/ui/litellm-dashboard/src/components/chat/ConversationList.tsx +++ b/ui/litellm-dashboard/src/components/chat/ConversationList.tsx @@ -1,20 +1,52 @@ "use client"; import React, { useState, useEffect, useRef, useCallback } from "react"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { - Button, - Input, - Modal, - Popconfirm, Tooltip, - Avatar, - Typography, -} from "antd"; -import { Pencil as EditOutlined, Trash2 as DeleteOutlined, Plus as PlusOutlined, Search as SearchOutlined, User as UserOutlined, MessageSquare as MessageOutlined } from "lucide-react"; + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + Pencil as EditOutlined, + Trash2 as DeleteOutlined, + Plus as PlusOutlined, + Search as SearchOutlined, + User as UserOutlined, + MessageSquare as MessageOutlined, +} from "lucide-react"; import dayjs from "dayjs"; import { Conversation } from "./types"; -const { Text } = Typography; +const Text = ({ + className, + style, + children, + title, +}: { + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; + title?: string; +}) => ( + + {children} + +); interface Props { conversations: Conversation[]; @@ -61,6 +93,51 @@ const groupConversations = (conversations: Conversation[]): GroupedConversations })); }; +// ---- Delete confirmation popover (replaces antd Popconfirm) ---- + +const DeleteConfirmPopover: React.FC<{ onConfirm: () => void }> = ({ + onConfirm, +}) => { + const [open, setOpen] = useState(false); + return ( + + + + + e.stopPropagation()} + > +

Delete this conversation?

+
+ + +
+
+
+ ); +}; + // ---- Single conversation row ---- interface ConversationRowProps { @@ -149,16 +226,13 @@ const ConversationRow: React.FC = ({ > {editing ? ( { - inputRef.current = node?.input ?? null; - }} - size="small" + ref={inputRef} value={editValue} onChange={(e) => setEditValue(e.target.value)} onKeyDown={handleKeyDown} onBlur={commitRename} onClick={(e) => e.stopPropagation()} - style={{ flex: 1, fontSize: 13 }} + className="flex-1 h-7 text-[13px]" /> ) : ( <> @@ -189,32 +263,23 @@ const ConversationRow: React.FC = ({ }} onClick={(e) => e.stopPropagation()} > - - + + Rename - + + onDelete(conv.id)} />
)} @@ -255,67 +320,51 @@ const SearchModal: React.FC = ({ }; return ( - - } - placeholder="Search conversations…" - value={query} - onChange={(e) => setQuery(e.target.value)} - style={{ marginBottom: 12 }} - allowClear - /> + (!o ? onClose() : undefined)}> + + + Search conversations + +
+ + setQuery(e.target.value)} + className="pl-9" + /> +
-
- {filtered.length === 0 ? ( -
- No conversations found -
- ) : ( - filtered.map((conv) => { - const truncated = - conv.title.length > 55 ? conv.title.slice(0, 55) + "…" : conv.title; - return ( -
handleSelect(conv.id)} - style={{ - display: "flex", - alignItems: "center", - gap: 8, - padding: "8px 10px", - borderRadius: 6, - cursor: "pointer", - transition: "background-color 0.1s", - }} - onMouseEnter={(e) => { - (e.currentTarget as HTMLDivElement).style.backgroundColor = "#f0f5ff"; - }} - onMouseLeave={(e) => { - (e.currentTarget as HTMLDivElement).style.backgroundColor = "transparent"; - }} - > - - {truncated} - + {filtered.length === 0 ? ( +
+ No conversations found +
+ ) : ( + filtered.map((conv) => { + const truncated = + conv.title.length > 55 + ? conv.title.slice(0, 55) + "…" + : conv.title; + return ( +
handleSelect(conv.id)} + className="flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer hover:bg-accent" > - {dayjs(conv.updatedAt).format("MMM D")} - -
- ); - }) - )} -
- + + {truncated} + + {dayjs(conv.updatedAt).format("MMM D")} + +
+ ); + }) + )} + +
+
); }; @@ -366,19 +415,23 @@ const ConversationList: React.FC = ({ > {/* Top: New Chat button */}
- - - + + + + + + + Chats are saved locally in this browser. All requests are + logged in Spend → Logs. + + +
{/* Conversation list (scrollable) */} @@ -443,11 +496,14 @@ const ConversationList: React.FC = ({ gap: 8, }} > - } - style={{ backgroundColor: "#e0e7ff", color: "#4f46e5", flexShrink: 0 }} - /> + + + + + ( + +); import { deleteMCPOAuthUserCredential, fetchMCPServers, getMCPOAuthUserCredentialStatus, listMCPTools } from "../networking"; import { AUTH_TYPE, MCPServer, MCPTool, handleTransport } from "../mcp_tools/types"; import MessageManager from "@/components/molecules/message_manager"; @@ -37,12 +61,18 @@ const OAuth2ConnectButton: React.FC = ({ if (variant === "button") { return ( ); } @@ -310,8 +340,7 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange {detailServer.auth_type === AUTH_TYPE.OAUTH2 ? ( oauthConnected.has(detailServer.server_id) ? ( )} @@ -439,15 +477,28 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange ) : null} - } - placeholder="Search servers..." - value={query} - onChange={(e) => setQuery(e.target.value)} - allowClear - style={{ width: 220, borderRadius: 8, fontSize: 13 }} - size="middle" - /> +
+ + setQuery(e.target.value)} + className="pl-9 pr-8 h-9 text-[13px] rounded-lg" + /> + {query && ( + + )} +
{/* Tabs */} @@ -545,7 +596,7 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange ) : null ) : loadingCounts ? ( - + ) : null}