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}