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 <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-24 12:03:28 +00:00
parent 10c89b4e83
commit a5b2a32b4a
No known key found for this signature in database
7 changed files with 649 additions and 318 deletions

View file

@ -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",

View file

@ -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 `<Form form={form}>` harness. Cannot migrate
independently of the parent forms. Defer with parents.
`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 `<Form form={form}>` harness. Cannot migrate
independently of the parent forms. Defer with parents.
- `src/components/playground/chat_ui/ChatUI.tsx` (2239 LoC): chrome-only
migration blocked by deeply-coupled antd `Select.OptGroup` + `optionLabelProp`
+ `maxTagCount="responsive"` + custom `filterOption` on the MCP servers
multi-select (with "__all__" sentinel, toolset/server option groups, and per-
option custom JSX rendering). The direct-tool / arg selectors inside the MCP
tool picker use the same pattern. shadcn `<Select>` has no `OptGroup` and no
searchable multi-select primitive; a faithful rewrite needs a custom
`Command`+`Popover` combobox plus a chip display, in tandem with the "__all__"
sentinel handling. Migrating the surrounding chrome (Modal, Popover, Tooltip,
Spin, Button, TextArea, Dragger) alone would leave the file in an
inconsistent half-migrated state and exceeds the 2-attempt budget. Defer until
a reusable searchable-multi-select-with-groups primitive exists.

View file

@ -1,20 +1,64 @@
"use client";
import { MessageCircle as CommentOutlined, Trash2 as DeleteOutlined, FlaskConical as ExperimentOutlined, Link as LinkOutlined, Plus as PlusOutlined, Bot as RobotOutlined, Save as SaveOutlined } from "lucide-react";
import { Button, Input, Modal, Select, Spin, Tabs } from "antd";
import React, { useCallback, useEffect, useState } from "react";
import {
MessageCircle as CommentOutlined,
Trash2 as DeleteOutlined,
FlaskConical as ExperimentOutlined,
Link as LinkOutlined,
Plus as PlusOutlined,
Bot as RobotOutlined,
Save as SaveOutlined,
LoaderCircle,
X,
} 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 { Textarea } from "@/components/ui/textarea";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import CodeBlock from "@/app/(dashboard)/api-reference/components/CodeBlock";
import NotificationsManager from "../../molecules/notifications_manager";
import { keyCreateCall, modelCreateCall, modelDeleteCall, modelPatchUpdateCall, proxyBaseUrl } from "../../networking";
import {
keyCreateCall,
modelCreateCall,
modelDeleteCall,
modelPatchUpdateCall,
proxyBaseUrl,
} from "../../networking";
import { fetchMCPServers } from "../../networking";
import { MCPServer } from "../../mcp_tools/types";
import { AgentModel, fetchAvailableAgentModels, MCPToolEntry } from "../llm_calls/fetch_agents";
import {
AgentModel,
fetchAvailableAgentModels,
MCPToolEntry,
} from "../llm_calls/fetch_agents";
import { fetchAvailableModels, ModelGroup } from "../llm_calls/fetch_models";
import ComplianceUI from "../complianceUI/ComplianceUI";
import ChatUI from "./ChatUI";
const { TextArea } = Input;
export interface AgentBuilderViewProps {
accessToken: string | null;
token: string | null;
@ -102,12 +146,17 @@ function ConnectTabContent({
the model <span className="font-mono text-gray-800">{agentName}</span>.
</p>
<Button
type="primary"
onClick={onCreateKey}
loading={creatingKey}
disabled={disabledPersonalKeyCreation}
disabled={disabledPersonalKeyCreation || creatingKey}
>
Create key for this agent
{creatingKey ? (
<>
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
"Create key for this agent"
)}
</Button>
{disabledPersonalKeyCreation && (
<p className="text-xs text-amber-600 mt-2">Key creation is disabled for your account.</p>
@ -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 (
<div className="space-y-2">
<Select
value=""
onValueChange={(v) => {
if (v && !selectedIds.includes(v)) onChange([...selectedIds, v]);
}}
>
<SelectTrigger className="w-full">
<SelectValue
placeholder={
loading
? "Loading MCP servers..."
: "Select MCP servers to attach (same format as chat completions API)"
}
/>
</SelectTrigger>
<SelectContent>
{remaining.length === 0 ? (
<div className="py-2 px-3 text-sm text-muted-foreground">
{options.length === 0
? "No MCP servers available"
: "All available servers selected"}
</div>
) : (
remaining.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))
)}
</SelectContent>
</Select>
{selectedIds.length > 0 && (
<div className="flex flex-wrap gap-1">
{selectedIds.map((id) => {
const opt = options.find((o) => o.value === id);
return (
<Badge
key={id}
variant="secondary"
className="flex items-center gap-1"
>
{opt?.label ?? id}
<button
type="button"
onClick={() =>
onChange(selectedIds.filter((s) => s !== id))
}
className="inline-flex items-center justify-center rounded-full hover:bg-muted-foreground/20"
aria-label={`Remove ${opt?.label ?? id}`}
>
<X size={12} />
</button>
</Badge>
);
})}
</div>
)}
</div>
);
}
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({
<span className="text-sm font-medium text-gray-900">Agent Builder</span>
{isNewAgent ? (
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSaveAgent}
loading={saving}
disabled={!draftName?.trim() || !draftUnderlyingModel}
disabled={saving || !draftName?.trim() || !draftUnderlyingModel}
>
Save Agent
{saving ? (
<>
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
<>
<SaveOutlined className="mr-2 h-4 w-4" />
Save Agent
</>
)}
</Button>
) : (
<span className="text-xs text-gray-500">Build Agents that pass your compliance requirements.</span>
@ -443,12 +586,20 @@ export default function AgentBuilderView({
<div className="w-60 flex-shrink-0 border-r border-gray-200 bg-white flex flex-col">
<div className="flex items-center justify-between border-b border-gray-200 p-3">
<span className="text-xs font-semibold uppercase tracking-wide text-gray-500">Agents</span>
<Button type="text" size="small" icon={<PlusOutlined />} onClick={handleAddAgent} aria-label="Add agent" />
<Button
variant="ghost"
size="sm"
onClick={handleAddAgent}
aria-label="Add agent"
className="h-7 w-7 p-0"
>
<PlusOutlined className="h-4 w-4" />
</Button>
</div>
<div className="flex-1 overflow-y-auto p-2">
{loadingAgents ? (
<div className="flex justify-center py-4">
<Spin size="small" />
<LoaderCircle className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<>
@ -489,220 +640,292 @@ export default function AgentBuilderView({
{(selectedId !== null || isNewAgent) && (
<>
<Tabs
activeKey={activeTab}
onChange={(k) => 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: (
<span>
<RobotOutlined className="mr-1" /> Configure
</span>
),
children: (
<div className="h-full overflow-y-auto p-6">
{(isNewAgent || selectedAgent) ? (
<div className="mx-auto max-w-xl space-y-4">
{!selectedAgentModelId && selectedAgent && (
<div className="rounded border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
This agent cannot be updated or deleted here (missing model id). Manage it from Models &amp; Endpoints.
</div>
)}
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">Agent name</label>
<Input
value={draftName}
onChange={(e) => setDraftName(e.target.value)}
placeholder="My Agent"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">System prompt</label>
<TextArea
value={draftSystemPrompt}
onChange={(e) => setDraftSystemPrompt(e.target.value)}
placeholder="You are a helpful assistant..."
rows={6}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">Underlying LLM</label>
<Select
value={draftUnderlyingModel}
onChange={setDraftUnderlyingModel}
className="w-full"
options={modelGroups.map((m) => ({ value: m.model_group, label: m.model_group }))}
placeholder="Select model"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">Temperature</label>
<Input
type="number"
min={0}
max={2}
step={0.1}
value={draftTemperature}
onChange={(e) => setDraftTemperature(Number(e.target.value))}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">Max tokens</label>
<Input
type="number"
min={1}
value={draftMaxTokens}
onChange={(e) => setDraftMaxTokens(Number(e.target.value))}
/>
</div>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">MCP servers</label>
<Select
mode="multiple"
placeholder="Select MCP servers to attach (same format as chat completions API)"
value={selectedMCPServerIds}
onChange={handleMCPServerChange}
loading={loadingMCPServers}
className="w-full"
allowClear
showSearch
optionFilterProp="label"
options={mcpServers.map((s) => ({
value: s.server_id,
label: s.alias || s.server_name || s.server_id,
}))}
/>
{selectedAgent && draftTools.length > 0 && (
<p className="mt-1 text-xs text-gray-500">
{draftTools.length} MCP server{draftTools.length !== 1 ? "s" : ""} saved. Use the same <code className="rounded bg-gray-100 px-1">tools</code> array in chat completions when calling this agent.
</p>
)}
</div>
{selectedAgent && (
<div className="flex flex-wrap items-center gap-2 pt-2">
{selectedAgentModelId && (
<>
<Button
type="primary"
icon={<SaveOutlined />}
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"
>
<TabsList className="mx-4 mt-2 self-start">
<TabsTrigger value="configure">
<RobotOutlined className="mr-1 h-4 w-4" /> Configure
</TabsTrigger>
<TabsTrigger value="chat" disabled={isNewAgent}>
<CommentOutlined className="mr-1 h-4 w-4" /> Chat
</TabsTrigger>
<TabsTrigger value="test" disabled={isNewAgent}>
<ExperimentOutlined className="mr-1 h-4 w-4" /> Batch Test
</TabsTrigger>
<TabsTrigger value="connect" disabled={isNewAgent}>
<LinkOutlined className="mr-1 h-4 w-4" /> Connect
</TabsTrigger>
</TabsList>
<TabsContent value="configure" className="flex-1 overflow-hidden m-0">
<div className="h-full overflow-y-auto p-6">
{isNewAgent || selectedAgent ? (
<div className="mx-auto max-w-xl space-y-4">
{!selectedAgentModelId && selectedAgent && (
// eslint-disable-next-line litellm-ui/no-raw-tailwind-colors
<div className="rounded border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
This agent cannot be updated or deleted here
(missing model id). Manage it from Models &amp;
Endpoints.
</div>
)}
<div>
<label className="mb-1 block text-sm font-medium">
Agent name
</label>
<Input
value={draftName}
onChange={(e) => setDraftName(e.target.value)}
placeholder="My Agent"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium">
System prompt
</label>
<Textarea
value={draftSystemPrompt}
onChange={(e) =>
setDraftSystemPrompt(e.target.value)
}
placeholder="You are a helpful assistant..."
rows={6}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium">
Underlying LLM
</label>
<Select
value={draftUnderlyingModel ?? ""}
onValueChange={(v) => setDraftUnderlyingModel(v)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select model" />
</SelectTrigger>
<SelectContent>
{modelGroups.map((m) => (
<SelectItem
key={m.model_group}
value={m.model_group}
>
{m.model_group}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 block text-sm font-medium">
Temperature
</label>
<Input
type="number"
min={0}
max={2}
step={0.1}
value={draftTemperature}
onChange={(e) =>
setDraftTemperature(Number(e.target.value))
}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium">
Max tokens
</label>
<Input
type="number"
min={1}
value={draftMaxTokens}
onChange={(e) =>
setDraftMaxTokens(Number(e.target.value))
}
/>
</div>
</div>
<div>
<label className="mb-1 block text-sm font-medium">
MCP servers
</label>
<MCPServerMultiSelect
servers={mcpServers}
selectedIds={selectedMCPServerIds}
onChange={handleMCPServerChange}
loading={loadingMCPServers}
/>
{selectedAgent && draftTools.length > 0 && (
<p className="mt-1 text-xs text-muted-foreground">
{draftTools.length} MCP server
{draftTools.length !== 1 ? "s" : ""} saved. Use
the same{" "}
<code className="rounded bg-muted px-1">
tools
</code>{" "}
array in chat completions when calling this
agent.
</p>
)}
</div>
{selectedAgent && (
<div className="flex flex-wrap items-center gap-2 pt-2">
{selectedAgentModelId && (
<>
<Button
onClick={handleUpdateAgent}
disabled={
saving ||
!draftName?.trim() ||
!draftUnderlyingModel
}
>
{saving ? (
<>
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
Updating...
</>
) : (
<>
<SaveOutlined className="mr-2 h-4 w-4" />
Update Agent
</Button>
<Button
type="default"
danger
icon={<DeleteOutlined />}
onClick={handleDeleteAgent}
loading={deleting}
>
Delete
</Button>
</>
)}
<Button type="primary" icon={<CommentOutlined />} onClick={() => setActiveTab("chat")}>
Test in Chat
</>
)}
</Button>
</div>
<Button
variant="destructive"
onClick={handleDeleteAgent}
disabled={deleting}
>
{deleting ? (
<>
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
Deleting...
</>
) : (
<>
<DeleteOutlined className="mr-2 h-4 w-4" />
Delete
</>
)}
</Button>
</>
)}
</div>
) : null}
</div>
),
},
{
key: "chat",
label: (
<span>
<CommentOutlined className="mr-1" /> Chat
</span>
),
disabled: isNewAgent,
children: (
<div className="flex h-full flex-col min-h-0">
{selectedAgent ? (
<ChatUI
key={selectedAgent.model_name}
simplified
fixedModel={selectedAgent.model_name}
accessToken={accessToken}
token={token}
userRole={userRole}
userID={userID}
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
proxySettings={proxySettings}
/>
) : (
<div className="flex flex-1 items-center justify-center text-gray-500">
Save an agent first to test in Chat.
<Button onClick={() => setActiveTab("chat")}>
<CommentOutlined className="mr-2 h-4 w-4" />
Test in Chat
</Button>
</div>
)}
</div>
),
},
{
key: "test",
label: (
<span>
<ExperimentOutlined className="mr-1" /> Batch Test
</span>
),
disabled: isNewAgent,
children: (
<div className="flex h-full flex-col min-h-0">
{selectedAgent ? (
<ComplianceUI
accessToken={accessToken}
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
backendMode="chat_completions"
fixedModel={selectedAgent.model_name}
proxySettings={proxySettings}
/>
) : (
<div className="flex flex-1 items-center justify-center text-gray-500">
Select an agent to run batch tests.
</div>
)}
) : null}
</div>
</TabsContent>
<TabsContent value="chat" className="flex-1 overflow-hidden m-0">
<div className="flex h-full flex-col min-h-0">
{selectedAgent ? (
<ChatUI
key={selectedAgent.model_name}
simplified
fixedModel={selectedAgent.model_name}
accessToken={accessToken}
token={token}
userRole={userRole}
userID={userID}
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
proxySettings={proxySettings}
/>
) : (
<div className="flex flex-1 items-center justify-center text-muted-foreground">
Save an agent first to test in Chat.
</div>
),
},
{
key: "connect",
label: (
<span>
<LinkOutlined className="mr-1" /> Connect
</span>
),
disabled: isNewAgent,
children: (
<div className="h-full overflow-y-auto p-6">
{selectedAgent ? (
<ConnectTabContent
agentName={selectedAgent.model_name}
proxySettings={proxySettings}
customProxyBaseUrl={customProxyBaseUrl}
accessToken={accessToken}
userID={userID}
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
creatingKey={creatingKey}
createdKeyValue={createdKeyValue}
onCreateKey={handleCreateKeyForAgent}
/>
) : (
<div className="flex flex-1 items-center justify-center text-gray-500">
Select an agent to see how to connect.
</div>
)}
)}
</div>
</TabsContent>
<TabsContent value="test" className="flex-1 overflow-hidden m-0">
<div className="flex h-full flex-col min-h-0">
{selectedAgent ? (
<ComplianceUI
accessToken={accessToken}
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
backendMode="chat_completions"
fixedModel={selectedAgent.model_name}
proxySettings={proxySettings}
/>
) : (
<div className="flex flex-1 items-center justify-center text-muted-foreground">
Select an agent to run batch tests.
</div>
),
},
]}
/>
)}
</div>
</TabsContent>
<TabsContent
value="connect"
className="flex-1 overflow-hidden m-0"
>
<div className="h-full overflow-y-auto p-6">
{selectedAgent ? (
<ConnectTabContent
agentName={selectedAgent.model_name}
proxySettings={proxySettings}
customProxyBaseUrl={customProxyBaseUrl}
accessToken={accessToken}
userID={userID}
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
creatingKey={creatingKey}
createdKeyValue={createdKeyValue}
onCreateKey={handleCreateKeyForAgent}
/>
) : (
<div className="flex flex-1 items-center justify-center text-muted-foreground">
Select an agent to see how to connect.
</div>
)}
</div>
</TabsContent>
</Tabs>
<AlertDialog
open={deleteConfirmOpen}
onOpenChange={setDeleteConfirmOpen}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete agent</AlertDialogTitle>
<AlertDialogDescription>
{`Are you sure you want to delete "${selectedAgent?.model_name ?? ""}"? This cannot be undone.`}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleting}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
confirmDeleteAgent();
}}
disabled={deleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{deleting ? (
<>
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
Deleting...
</>
) : (
"Delete"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)}
</div>

View file

@ -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;
}) => <span className={className}>{children}</span>;
interface RealtimeMessage {
role: "user" | "assistant" | "system" | "status";
@ -356,19 +377,38 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
</div>
<div className="flex items-center gap-2">
<Select
size="small"
value={selectedVoice}
onChange={setSelectedVoice}
options={OPEN_AI_VOICE_SELECT_OPTIONS}
style={{ width: 220 }}
onValueChange={setSelectedVoice}
disabled={isConnected}
/>
>
<SelectTrigger className="w-56 h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{OPEN_AI_VOICE_SELECT_OPTIONS.map((opt) => (
<SelectItem
key={opt.value}
value={opt.value}
>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
{!isConnected ? (
<Button type="primary" onClick={connect} loading={isConnecting} size="small">
Connect
<Button onClick={connect} disabled={isConnecting} size="sm">
{isConnecting ? (
<>
<LoaderCircle className="mr-2 h-3 w-3 animate-spin" />
Connecting...
</>
) : (
"Connect"
)}
</Button>
) : (
<Button danger onClick={disconnect} size="small" icon={<CloseCircleOutlined />}>
<Button variant="destructive" onClick={disconnect} size="sm">
<CloseCircleOutlined className="mr-2 h-3 w-3" />
Disconnect
</Button>
)}
@ -418,30 +458,40 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
<div className="border-t border-gray-200 p-3 bg-white">
<div className="flex items-center gap-2">
<Button
shape="circle"
size="large"
type={isRecording ? "primary" : "default"}
danger={isRecording}
icon={isRecording ? <AudioMutedOutlined /> : <AudioOutlined />}
variant={isRecording ? "destructive" : "outline"}
size="icon"
onClick={isRecording ? stopRecording : startRecording}
title={isRecording ? "Stop recording" : "Start recording"}
className={isRecording ? "animate-pulse" : ""}
/>
className={`rounded-full h-10 w-10 ${isRecording ? "animate-pulse" : ""}`}
aria-label={isRecording ? "Stop recording" : "Start recording"}
>
{isRecording ? (
<AudioMutedOutlined className="h-5 w-5" />
) : (
<AudioOutlined className="h-5 w-5" />
)}
</Button>
<Input
placeholder="Type a message or use the mic..."
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onPressEnter={sendTextMessage}
className="flex-1"
size="large"
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendTextMessage();
}
}}
className="flex-1 h-10"
/>
<Button
type="primary"
icon={<SendOutlined />}
size="icon"
className="h-10 w-10"
onClick={sendTextMessage}
disabled={!inputText.trim()}
size="large"
/>
aria-label="Send message"
>
<SendOutlined className="h-5 w-5" />
</Button>
</div>
{isRecording && (
<div className="mt-2 flex items-center gap-2 text-red-500 text-xs">

View file

@ -1,4 +1,4 @@
import { render, waitFor } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import CompareUI from "./CompareUI";
@ -108,10 +108,11 @@ describe("CompareUI", () => {
let comparisonPanels = container.querySelectorAll('[data-testid^="comparison-panel-"]');
expect(comparisonPanels).toHaveLength(2);
const addButtons = Array.from(container.querySelectorAll('button[class*="ant-btn"]'));
const addComparisonButton = addButtons.find((btn) => btn.textContent?.includes("Add Comparison"));
const addComparisonButton = screen.getByRole("button", {
name: /add comparison/i,
});
expect(addComparisonButton).toBeInTheDocument();
await user.click(addComparisonButton!);
await user.click(addComparisonButton);
// Wait for the new comparison panel to be added (should have 3 total now)
await waitFor(() => {

View file

@ -1,8 +1,27 @@
"use client";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { Eraser as ClearOutlined, Trash2 as DeleteOutlined, FileText as FilePdfOutlined, Plus as PlusOutlined } from "lucide-react";
import { Button, Input, Select, Tooltip } from "antd";
import {
Eraser as ClearOutlined,
Trash2 as DeleteOutlined,
FileText as FilePdfOutlined,
Plus as PlusOutlined,
} 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 {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useEffect, useMemo, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import ChatImageUpload from "../chat_ui/ChatImageUpload";
@ -698,17 +717,22 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
<span className="text-sm font-medium text-gray-600">Virtual Key Source</span>
<Select
value={apiKeySource}
onChange={(value) => setApiKeySource(value as "session" | "custom")}
onValueChange={(value) => setApiKeySource(value as "session" | "custom")}
disabled={disabledPersonalKeyCreation}
className="w-48"
>
<Select.Option value="session" disabled={!canUseSessionKey}>
Current UI Session
</Select.Option>
<Select.Option value="custom">Virtual Key</Select.Option>
<SelectTrigger className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="session" disabled={!canUseSessionKey}>
Current UI Session
</SelectItem>
<SelectItem value="custom">Virtual Key</SelectItem>
</SelectContent>
</Select>
{apiKeySource === "custom" && (
<Input.Password
<Input
type="password"
value={customApiKey}
onChange={(event) => setCustomApiKey(event.target.value)}
placeholder="Enter Virtual Key"
@ -718,34 +742,52 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-600">Endpoint</span>
<Select
value={selectedEndpoint}
onChange={(value) => setSelectedEndpoint(value as EndpointIdType)}
className="w-56"
<Select
value={selectedEndpoint}
onValueChange={(value) => setSelectedEndpoint(value as EndpointIdType)}
>
{getAvailableEndpoints().map((endpoint) => (
<Select.Option
key={endpoint.value}
value={endpoint.value}
>
{endpoint.label}
</Select.Option>
))}
<SelectTrigger className="w-56">
<SelectValue />
</SelectTrigger>
<SelectContent>
{getAvailableEndpoints().map((endpoint) => (
<SelectItem key={endpoint.value} value={endpoint.value}>
{endpoint.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-3">
<Button onClick={clearAllChats} disabled={!hasMessages} icon={<ClearOutlined />}>
<Button
variant="outline"
onClick={clearAllChats}
disabled={!hasMessages}
>
<ClearOutlined className="mr-2 h-4 w-4" />
Clear All Chats
</Button>
<Tooltip
title={
comparisons.length >= maxComparisons ? "Compare up to 3 models at a time" : "Add another comparison"
}
>
<Button onClick={addComparison} disabled={comparisons.length >= maxComparisons} icon={<PlusOutlined />}>
Add Comparison
</Button>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="outline"
onClick={addComparison}
disabled={comparisons.length >= maxComparisons}
>
<PlusOutlined className="mr-2 h-4 w-4" />
Add Comparison
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{comparisons.length >= maxComparisons
? "Compare up to 3 models at a time"
: "Add another comparison"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</div>

View file

@ -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)}
</span>
</div>
<AntSlider
<Slider
min={0}
max={2}
step={0.01}
value={comparison.temperature}
onChange={(value) => {
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}
</span>
</div>
<AntSlider
<Slider
min={1}
max={32768}
step={1}
value={comparison.maxTokens}
onChange={(value) => {
value={[comparison.maxTokens]}
onValueChange={(value) => {
const nextValue = Array.isArray(value) ? value[0] : value;
const clamped = Math.min(
32768,