feat(ui): guardrails/GuardrailTestPlayground to shadcn

- antd Card/List/Empty/Spin/Input/Typography + @ant-design
  ExperimentOutlined/SearchOutlined \u2192 shadcn Card/Input + native
  <ul> with divide-y border + lucide FlaskConical/Loader2/Search.
- Active-list item uses blue-tinted bg + left border (categorical,
  added to overrides).
- List.Item.Meta flattened to plain flex layouts.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 14:32:20 +00:00
parent f4e7aba66a
commit ac6cd602a9
No known key found for this signature in database
2 changed files with 97 additions and 86 deletions

View file

@ -65,6 +65,7 @@
"src/components/common_components/KeyLifecycleSettings.tsx",
"src/components/guardrails/GuardrailTestPanel.tsx",
"src/components/guardrails/GuardrailTestResults.tsx",
"src/components/guardrails/GuardrailTestPlayground.tsx",
"src/components/prompts/prompt_editor_view/PromptEditorHeader.tsx",
"src/components/prompts/prompt_editor_view/VersionHistorySidePanel.tsx",
"src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx",

View file

@ -1,6 +1,8 @@
import React, { useState } from "react";
import { Card, List, Empty, Spin, Input, Typography } from "antd";
import { ExperimentOutlined, SearchOutlined } from "@ant-design/icons";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { FlaskConical, Loader2, Search } from "lucide-react";
import { cn } from "@/lib/utils";
import GuardrailTestPanel from "./GuardrailTestPanel";
import { applyGuardrail } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
@ -13,6 +15,7 @@ interface GuardrailItem {
mode: string;
default_on: boolean;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
guardrail_info: Record<string, any> | null;
created_at?: string;
updated_at?: string;
@ -41,16 +44,19 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
guardrailsList,
isLoading,
accessToken,
onClose,
}) => {
const [selectedGuardrails, setSelectedGuardrails] = useState<Set<string>>(new Set());
const [selectedGuardrails, setSelectedGuardrails] = useState<Set<string>>(
new Set(),
);
const [searchQuery, setSearchQuery] = useState("");
const [testResults, setTestResults] = useState<TestResult[]>([]);
const [testErrors, setTestErrors] = useState<TestError[]>([]);
const [isTesting, setIsTesting] = useState(false);
const filteredGuardrails = guardrailsList.filter((guardrail) =>
guardrail.guardrail_name?.toLowerCase().includes(searchQuery.toLowerCase())
guardrail.guardrail_name
?.toLowerCase()
.includes(searchQuery.toLowerCase()),
);
const toggleGuardrailSelection = (guardrailName: string) => {
@ -79,7 +85,13 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
Array.from(selectedGuardrails).map(async (guardrailName) => {
const startTime = Date.now();
try {
const result = await applyGuardrail(accessToken, guardrailName, text, null, null);
const result = await applyGuardrail(
accessToken,
guardrailName,
text,
null,
null,
);
const latency = Date.now() - startTime;
results.push({
guardrailName,
@ -95,7 +107,7 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
latency,
});
}
})
}),
);
setTestResults(results);
@ -104,122 +116,121 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
if (results.length > 0) {
NotificationsManager.success(
`${results.length} guardrail${results.length > 1 ? "s" : ""} applied successfully`
`${results.length} guardrail${results.length > 1 ? "s" : ""} applied successfully`,
);
}
if (errors.length > 0) {
NotificationsManager.fromBackend(
`${errors.length} guardrail${errors.length > 1 ? "s" : ""} failed`
`${errors.length} guardrail${errors.length > 1 ? "s" : ""} failed`,
);
}
};
return (
<div className="w-full h-[calc(100vh-200px)]">
<Card className="h-full" styles={{ body: { padding: 0, height: "100%" } }}>
<Card className="h-full p-0 overflow-hidden">
<div className="flex h-full">
{/* Left Sidebar - Guardrails List */}
<div className="w-1/4 border-r border-gray-200 flex flex-col overflow-hidden">
<div className="p-4 border-b border-gray-200">
<div className="w-1/4 border-r border-border flex flex-col overflow-hidden">
<div className="p-4 border-b border-border">
<div className="mb-3">
<h3 className="text-lg font-semibold mb-3">Guardrails</h3>
<Input
prefix={<SearchOutlined />}
placeholder="Search guardrails..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
<Input
placeholder="Search guardrails..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8"
/>
</div>
</div>
</div>
<div className="flex-1 overflow-auto">
{isLoading ? (
<div className="flex items-center justify-center h-32">
<Spin />
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : filteredGuardrails.length === 0 ? (
<div className="p-4">
<Empty
description={
searchQuery ? "No guardrails match your search" : "No guardrails available"
}
/>
<div className="p-4 text-center text-sm text-muted-foreground">
{searchQuery
? "No guardrails match your search"
: "No guardrails available"}
</div>
) : (
<List
dataSource={filteredGuardrails}
renderItem={(guardrail) => (
<List.Item
onClick={() => {
if (guardrail.guardrail_name) {
toggleGuardrailSelection(guardrail.guardrail_name);
}
}}
style={{ paddingLeft: 24, paddingRight: 16 }}
className={`cursor-pointer hover:bg-gray-50 transition-colors ${
selectedGuardrails.has(guardrail.guardrail_name || "")
? "bg-blue-50 border-l-4 border-l-blue-500"
: "border-l-4 border-l-transparent"
}`}
>
<List.Item.Meta
title={
<div className="flex items-center space-x-2">
<ExperimentOutlined className="text-gray-400" />
<span className="font-medium text-gray-900">
{guardrail.guardrail_name}
<ul className="divide-y divide-border">
{filteredGuardrails.map((guardrail) => {
const isSelected = selectedGuardrails.has(
guardrail.guardrail_name || "",
);
return (
<li
key={guardrail.guardrail_id || guardrail.guardrail_name}
onClick={() => {
if (guardrail.guardrail_name) {
toggleGuardrailSelection(guardrail.guardrail_name);
}
}}
className={cn(
"pl-6 pr-4 py-3 cursor-pointer hover:bg-muted transition-colors border-l-4",
isSelected
? "bg-blue-50 dark:bg-blue-950/30 border-l-blue-500"
: "border-l-transparent",
)}
>
<div className="flex items-center space-x-2">
<FlaskConical className="h-4 w-4 text-muted-foreground" />
<span className="font-medium text-foreground">
{guardrail.guardrail_name}
</span>
</div>
<div className="text-xs space-y-1 mt-1">
<div>
<span className="font-medium">Type: </span>
<span className="text-muted-foreground">
{guardrail.litellm_params.guardrail}
</span>
</div>
}
description={
<div className="text-xs space-y-1 mt-1">
<div>
<span className="font-medium">Type: </span>
<span className="text-gray-600">
{guardrail.litellm_params.guardrail}
</span>
</div>
<div>
<span className="font-medium">Mode: </span>
<span className="text-gray-600">
{guardrail.litellm_params.mode}
</span>
</div>
<div>
<span className="font-medium">Mode: </span>
<span className="text-muted-foreground">
{guardrail.litellm_params.mode}
</span>
</div>
}
/>
</List.Item>
)}
/>
</div>
</li>
);
})}
</ul>
)}
</div>
<div className="p-3 border-t border-gray-200 bg-gray-50">
<Typography.Text className="text-xs text-gray-600">
{selectedGuardrails.size} of {filteredGuardrails.length} selected
</Typography.Text>
<div className="p-3 border-t border-border bg-muted">
<span className="text-xs text-muted-foreground">
{selectedGuardrails.size} of {filteredGuardrails.length}{" "}
selected
</span>
</div>
</div>
{/* Right Panel - Test Area */}
<div className="w-3/4 flex flex-col bg-white">
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
<Typography.Title level={2} className="text-xl font-semibold mb-0">
<div className="w-3/4 flex flex-col bg-background">
<div className="p-4 border-b border-border flex justify-between items-center">
<h2 className="text-xl font-semibold mb-0">
Guardrail Testing Playground
</Typography.Title>
</h2>
</div>
<div className="flex-1 overflow-auto p-4">
{selectedGuardrails.size === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-gray-400">
<ExperimentOutlined style={{ fontSize: "48px", marginBottom: "16px" }} />
<Typography.Paragraph className="text-lg font-medium text-gray-600 mb-2">
<div className="h-full flex flex-col items-center justify-center text-muted-foreground">
<FlaskConical className="h-12 w-12 mb-4" />
<p className="text-lg font-medium text-foreground mb-2">
Select Guardrails to Test
</Typography.Paragraph>
<Typography.Paragraph className="text-center text-gray-500 max-w-md">
Choose one or more guardrails from the left sidebar to start testing and
comparing results.
</Typography.Paragraph>
</p>
<p className="text-center text-muted-foreground max-w-md">
Choose one or more guardrails from the left sidebar to
start testing and comparing results.
</p>
</div>
) : (
<div className="h-full">
@ -242,4 +253,3 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
};
export default GuardrailTestPlayground;