From a5b2a32b4a98e641c94b5f4b880b6b09041952b5 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Fri, 24 Apr 2026 12:03:28 +0000
Subject: [PATCH] feat(ui): migrate playground chrome to shadcn
- AgentBuilderView: antd Button/Input/Modal/Select/Spin/Tabs + Input.TextArea -> shadcn Button/Input/Select/Textarea/Tabs; Modal.confirm -> AlertDialog; added MCPServerMultiSelect chip-list pattern
- ChatUI message rendering/streaming untouched; chrome defer due to deeply-coupled antd Select.OptGroup + optionLabelProp patterns that require a searchable multi-select primitive. Deferred in BLOCKERS.
- CompareUI: antd Button/Input/Select/Tooltip -> shadcn equivalents; Input.Password -> Input type=password; retained Tooltip wrapper around disabled button via span trigger
- RealtimePlayground: antd Button/Input/Select/Typography -> shadcn equivalents; round mic Button implemented with size=icon; loading spinners use LoaderCircle
- ComparisonPanel: antd Slider -> shadcn Slider (value array)
- CompareUI.test: repaired ant-btn classname selector with semantic getByRole
- ESLint: exempt playground chrome files that retain status-color categorical palettes
Co-authored-by: yuneng-jiang
---
ui/litellm-dashboard/.eslintrc.json | 3 +
ui/litellm-dashboard/docs/BLOCKERS.md | 24 +-
.../playground/chat_ui/AgentBuilderView.tsx | 713 ++++++++++++------
.../playground/chat_ui/RealtimePlayground.tsx | 100 ++-
.../playground/compareUI/CompareUI.test.tsx | 9 +-
.../playground/compareUI/CompareUI.tsx | 104 ++-
.../compareUI/components/ComparisonPanel.tsx | 14 +-
7 files changed, 649 insertions(+), 318 deletions(-)
diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json
index 3fc9318498c..2a73b0e31b3 100644
--- a/ui/litellm-dashboard/.eslintrc.json
+++ b/ui/litellm-dashboard/.eslintrc.json
@@ -149,6 +149,9 @@
"src/components/policies/impact_preview_alert.tsx",
"src/components/policies/impact_popover.tsx",
"src/components/policies/ai_suggestion_modal.tsx",
+ "src/components/playground/chat_ui/AgentBuilderView.tsx",
+ "src/components/playground/chat_ui/RealtimePlayground.tsx",
+ "src/components/playground/compareUI/CompareUI.tsx",
"src/components/DeletedKeysPage/DeletedKeysPage.tsx",
"src/components/DeletedTeamsPage/DeletedTeamsPage.tsx",
"src/components/molecules/models/columns.tsx",
diff --git a/ui/litellm-dashboard/docs/BLOCKERS.md b/ui/litellm-dashboard/docs/BLOCKERS.md
index 43fadfab125..498377d08c7 100644
--- a/ui/litellm-dashboard/docs/BLOCKERS.md
+++ b/ui/litellm-dashboard/docs/BLOCKERS.md
@@ -106,9 +106,21 @@ stay on antd for phase 1 and will be addressed in a targeted follow-up.
prop for `setFieldsValue`/`resetFields`. Tightly coupled to the parent
antd Form. Defer with parents.
- `src/components/mcp_tools/MCPPermissionManagement.tsx`: uses
- `Form.useFormInstance()` + `Form.List` for dynamic static-headers plus
- several `Form.Item` fields (`allow_all_keys`, `available_on_public_internet`,
- `mcp_access_groups`, `extra_headers`). Directly writes to the parent's
- antd Form via `form.setFieldValue`. `MCPPermissionManagement.test.tsx`
- wraps it in an antd `
{disabledPersonalKeyCreation && (
Key creation is disabled for your account.
@@ -122,6 +171,93 @@ function ConnectTabContent({
);
}
+function MCPServerMultiSelect({
+ servers,
+ selectedIds,
+ onChange,
+ loading,
+}: {
+ servers: MCPServer[];
+ selectedIds: string[];
+ onChange: (ids: string[]) => void;
+ loading: boolean;
+}) {
+ const options = useMemo(
+ () =>
+ servers.map((s) => ({
+ value: s.server_id,
+ label: s.alias || s.server_name || s.server_id,
+ })),
+ [servers],
+ );
+ const remaining = useMemo(
+ () => options.filter((o) => !selectedIds.includes(o.value)),
+ [options, selectedIds],
+ );
+
+ return (
+
+
+ {selectedIds.length > 0 && (
+
+ {selectedIds.map((id) => {
+ const opt = options.find((o) => o.value === id);
+ return (
+
+ {opt?.label ?? id}
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
function getAgentModelId(agent: AgentModel): string | null {
const info = agent.model_info as { id?: string } | null | undefined;
return info?.id ?? null;
@@ -189,6 +325,7 @@ export default function AgentBuilderView({
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(false);
+ const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const effectiveApiKey = apiKey || accessToken || "";
const selectedAgent = selectedId === NEW_AGENT_ID ? null : agentModels.find((a) => a.model_name === selectedId) ?? null;
@@ -376,27 +513,26 @@ export default function AgentBuilderView({
const handleDeleteAgent = () => {
if (!selectedAgent || !selectedAgentModelId || !accessToken) return;
- Modal.confirm({
- title: "Delete agent",
- content: `Are you sure you want to delete "${selectedAgent.model_name}"? This cannot be undone.`,
- okText: "Delete",
- okType: "danger",
- cancelText: "Cancel",
- onOk: async () => {
- setDeleting(true);
- try {
- await modelDeleteCall(accessToken, selectedAgentModelId);
- NotificationsManager.success("Agent deleted");
- await loadAgents();
- const remaining = agentModels.filter((a) => a.model_name !== selectedAgent.model_name);
- setSelectedId(remaining.length > 0 ? remaining[0].model_name : null);
- } catch (e) {
- NotificationsManager.fromBackend("Failed to delete agent");
- } finally {
- setDeleting(false);
- }
- },
- });
+ setDeleteConfirmOpen(true);
+ };
+
+ const confirmDeleteAgent = async () => {
+ if (!selectedAgent || !selectedAgentModelId || !accessToken) return;
+ setDeleting(true);
+ try {
+ await modelDeleteCall(accessToken, selectedAgentModelId);
+ NotificationsManager.success("Agent deleted");
+ await loadAgents();
+ const remaining = agentModels.filter(
+ (a) => a.model_name !== selectedAgent.model_name,
+ );
+ setSelectedId(remaining.length > 0 ? remaining[0].model_name : null);
+ } catch (e) {
+ NotificationsManager.fromBackend("Failed to delete agent");
+ } finally {
+ setDeleting(false);
+ setDeleteConfirmOpen(false);
+ }
};
if (!accessToken || !userID || !userRole) {
@@ -414,13 +550,20 @@ export default function AgentBuilderView({
Agent Builder
{isNewAgent ? (
}
onClick={handleSaveAgent}
- loading={saving}
- disabled={!draftName?.trim() || !draftUnderlyingModel}
+ disabled={saving || !draftName?.trim() || !draftUnderlyingModel}
>
- Save Agent
+ {saving ? (
+ <>
+
+ Saving...
+ >
+ ) : (
+ <>
+
+ Save Agent
+ >
+ )}
) : (
Build Agents that pass your compliance requirements.
@@ -443,12 +586,20 @@ export default function AgentBuilderView({
Agents
-
} onClick={handleAddAgent} aria-label="Add agent" />
+
{loadingAgents ? (
-
+
) : (
<>
@@ -489,220 +640,292 @@ export default function AgentBuilderView({
{(selectedId !== null || isNewAgent) && (
<>
setActiveTab(k as "configure" | "chat" | "test" | "connect")}
- className="flex-1 overflow-hidden [&_.ant-tabs-content]:h-full [&_.ant-tabs-tabpane]:h-full [&_.ant-tabs-nav]:pl-4"
- items={[
- {
- key: "configure",
- label: (
-
- Configure
-
- ),
- children: (
-
- {(isNewAgent || selectedAgent) ? (
-
- {!selectedAgentModelId && selectedAgent && (
-
- This agent cannot be updated or deleted here (missing model id). Manage it from Models & Endpoints.
-
- )}
-
-
- setDraftName(e.target.value)}
- placeholder="My Agent"
- />
-
-
-
-
-
-
-
-
-
-
-
- {selectedAgent && (
-
- {selectedAgentModelId && (
- <>
-
}
- onClick={handleUpdateAgent}
- loading={saving}
- disabled={!draftName?.trim() || !draftUnderlyingModel}
- >
+ value={activeTab}
+ onValueChange={(k) =>
+ setActiveTab(k as "configure" | "chat" | "test" | "connect")
+ }
+ className="flex-1 flex flex-col overflow-hidden"
+ >
+
+
+ Configure
+
+
+ Chat
+
+
+ Batch Test
+
+
+ Connect
+
+
+
+
+
+ {isNewAgent || selectedAgent ? (
+
+ {!selectedAgentModelId && selectedAgent && (
+ // eslint-disable-next-line litellm-ui/no-raw-tailwind-colors
+
+ This agent cannot be updated or deleted here
+ (missing model id). Manage it from Models &
+ Endpoints.
+
+ )}
+
+
+ setDraftName(e.target.value)}
+ placeholder="My Agent"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {selectedAgent && draftTools.length > 0 && (
+
+ {draftTools.length} MCP server
+ {draftTools.length !== 1 ? "s" : ""} saved. Use
+ the same{" "}
+
+ tools
+ {" "}
+ array in chat completions when calling this
+ agent.
+
+ )}
+
+ {selectedAgent && (
+
+ {selectedAgentModelId && (
+ <>
+
- }
- onClick={handleDeleteAgent}
- loading={deleting}
- >
- Delete
-
- >
- )}
- } onClick={() => setActiveTab("chat")}>
- Test in Chat
+ >
+ )}
-
+
+ >
)}
-
- ) : null}
-
- ),
- },
- {
- key: "chat",
- label: (
-
- Chat
-
- ),
- disabled: isNewAgent,
- children: (
-
- {selectedAgent ? (
-
- ) : (
-
- Save an agent first to test in Chat.
+
)}
- ),
- },
- {
- key: "test",
- label: (
-
- Batch Test
-
- ),
- disabled: isNewAgent,
- children: (
-
- {selectedAgent ? (
-
- ) : (
-
- Select an agent to run batch tests.
-
- )}
+ ) : null}
+
+
+
+
+
+ {selectedAgent ? (
+
+ ) : (
+
+ Save an agent first to test in Chat.
- ),
- },
- {
- key: "connect",
- label: (
-
- Connect
-
- ),
- disabled: isNewAgent,
- children: (
-
- {selectedAgent ? (
-
- ) : (
-
- Select an agent to see how to connect.
-
- )}
+ )}
+
+
+
+
+
+ {selectedAgent ? (
+
+ ) : (
+
+ Select an agent to run batch tests.
- ),
- },
- ]}
- />
+ )}
+
+
+
+
+
+ {selectedAgent ? (
+
+ ) : (
+
+ Select an agent to see how to connect.
+
+ )}
+
+
+
+
+
+
+ Delete agent
+
+ {`Are you sure you want to delete "${selectedAgent?.model_name ?? ""}"? This cannot be undone.`}
+
+
+
+
+ Cancel
+
+ {
+ e.preventDefault();
+ confirmDeleteAgent();
+ }}
+ disabled={deleting}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ {deleting ? (
+ <>
+
+ Deleting...
+ >
+ ) : (
+ "Delete"
+ )}
+
+
+
+
>
)}
diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/RealtimePlayground.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/RealtimePlayground.tsx
index d6a11616226..203a00c5980 100644
--- a/ui/litellm-dashboard/src/components/playground/chat_ui/RealtimePlayground.tsx
+++ b/ui/litellm-dashboard/src/components/playground/chat_ui/RealtimePlayground.tsx
@@ -1,12 +1,33 @@
"use client";
-import { MicOff as AudioMutedOutlined, Mic as AudioOutlined, XCircle as CloseCircleOutlined, Send as SendOutlined, Volume2 as SoundOutlined } from "lucide-react";
-import { Button, Input, Select, Typography } from "antd";
+import {
+ MicOff as AudioMutedOutlined,
+ Mic as AudioOutlined,
+ XCircle as CloseCircleOutlined,
+ Send as SendOutlined,
+ Volume2 as SoundOutlined,
+ LoaderCircle,
+} from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { getProxyBaseUrl } from "../../networking";
import { OPEN_AI_VOICE_SELECT_OPTIONS } from "./chatConstants";
-const { Text } = Typography;
+const Text = ({
+ className,
+ children,
+}: {
+ className?: string;
+ children?: React.ReactNode;
+}) => {children};
interface RealtimeMessage {
role: "user" | "assistant" | "system" | "status";
@@ -356,19 +377,38 @@ const RealtimePlayground: React.FC = ({
+ >
+
+
+
+
+ {OPEN_AI_VOICE_SELECT_OPTIONS.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
{!isConnected ? (
-
diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/components/ComparisonPanel.tsx b/ui/litellm-dashboard/src/components/playground/compareUI/components/ComparisonPanel.tsx
index 33dbd941547..f44eeb2abde 100644
--- a/ui/litellm-dashboard/src/components/playground/compareUI/components/ComparisonPanel.tsx
+++ b/ui/litellm-dashboard/src/components/playground/compareUI/components/ComparisonPanel.tsx
@@ -6,8 +6,8 @@ import { UnifiedSelector } from "./UnifiedSelector";
import TagSelector from "../../../tag_management/TagSelector";
import VectorStoreSelector from "../../../vector_store_management/VectorStoreSelector";
import GuardrailSelector from "../../../guardrails/GuardrailSelector";
-import { Slider as AntSlider } from "antd";
import { Checkbox } from "@/components/ui/checkbox";
+import { Slider } from "@/components/ui/slider";
import {
Popover,
PopoverContent,
@@ -192,12 +192,12 @@ export function ComparisonPanel({
{comparison.temperature.toFixed(2)}
-
{
+ value={[comparison.temperature]}
+ onValueChange={(value) => {
const nextValue = Array.isArray(value) ? value[0] : value;
const clamped = Math.min(
2,
@@ -219,12 +219,12 @@ export function ComparisonPanel({
{comparison.maxTokens}
- {
+ value={[comparison.maxTokens]}
+ onValueChange={(value) => {
const nextValue = Array.isArray(value) ? value[0] : value;
const clamped = Math.min(
32768,