mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): migrate guardrails subcomponents (GuardrailSelector, guardrail_garden_card) to shadcn
- guardrail_garden_card: @ant-design/icons CheckCircleFilled \u2192 lucide CheckCircle2. Replace inline-style hover state with Tailwind hover: classes. Replace raw color tokens (#e5e7eb / #6b7280 / #16a34a) with semantic tokens; the F1 success badge keeps emerald palette (categorical \u2014 success-only color). - GuardrailSelector: antd Select mode=multiple \u2192 chip-style multi-select via shadcn Popover/Input/Badge (mirrors VectorStoreSelector pattern). Search-filter the suggestions and surface the disabled premium-feature message inline. Gates: TS \u2713 | Vitest 7/7 (guardrail_garden_card) \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
8ae6fed101
commit
6c315e702f
2 changed files with 137 additions and 87 deletions
|
|
@ -1,5 +1,13 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Select } from "antd";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Guardrail } from "./types";
|
||||
import { getGuardrailsList } from "../networking";
|
||||
|
||||
|
|
@ -11,60 +19,123 @@ interface GuardrailSelectorProps {
|
|||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const GuardrailSelector: React.FC<GuardrailSelectorProps> = ({ onChange, value, className, accessToken, disabled }) => {
|
||||
/**
|
||||
* Multi-select for guardrails. Mirrors the VectorStoreSelector pattern
|
||||
* (chip-style trigger + popover suggestion list with search) since shadcn
|
||||
* lacks a native multi-select primitive.
|
||||
*/
|
||||
const GuardrailSelector: React.FC<GuardrailSelectorProps> = ({
|
||||
onChange,
|
||||
value,
|
||||
className,
|
||||
accessToken,
|
||||
disabled,
|
||||
}) => {
|
||||
const [guardrails, setGuardrails] = useState<Guardrail[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const selected = useMemo(() => value ?? [], [value]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchGuardrails = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getGuardrailsList(accessToken);
|
||||
console.log("Guardrails response:", response);
|
||||
if (response.guardrails) {
|
||||
console.log("Guardrails data:", response.guardrails);
|
||||
setGuardrails(response.guardrails);
|
||||
}
|
||||
if (response.guardrails) setGuardrails(response.guardrails);
|
||||
} catch (error) {
|
||||
console.error("Error fetching guardrails:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchGuardrails();
|
||||
}, [accessToken]);
|
||||
|
||||
const handleGuardrailChange = (selectedValues: string[]) => {
|
||||
console.log("Selected guardrails:", selectedValues);
|
||||
onChange(selectedValues);
|
||||
};
|
||||
const remaining = useMemo(
|
||||
() =>
|
||||
guardrails
|
||||
.filter((g) => g.guardrail_name && !selected.includes(g.guardrail_name))
|
||||
.filter((g) =>
|
||||
query
|
||||
? (g.guardrail_name ?? "")
|
||||
.toLowerCase()
|
||||
.includes(query.toLowerCase())
|
||||
: true,
|
||||
),
|
||||
[guardrails, selected, query],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
mode="multiple"
|
||||
disabled={disabled}
|
||||
placeholder={disabled ? "Setting guardrails is a premium feature." : "Select guardrails"}
|
||||
onChange={handleGuardrailChange}
|
||||
value={value}
|
||||
loading={loading}
|
||||
className={className}
|
||||
allowClear
|
||||
options={guardrails.map((guardrail) => {
|
||||
console.log("Mapping guardrail:", guardrail);
|
||||
return {
|
||||
label: `${guardrail.guardrail_name}`,
|
||||
value: guardrail.guardrail_name,
|
||||
};
|
||||
})}
|
||||
optionFilterProp="label"
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"min-h-9 w-full flex flex-wrap items-center gap-1 rounded-md border border-input bg-background px-2 py-1 text-sm text-left disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{selected.length === 0 ? (
|
||||
<span className="text-muted-foreground px-1">
|
||||
{disabled
|
||||
? "Setting guardrails is a premium feature."
|
||||
: "Select guardrails"}
|
||||
</span>
|
||||
) : (
|
||||
selected.map((name) => (
|
||||
<Badge key={name} variant="secondary" className="gap-1">
|
||||
{name}
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onChange(selected.filter((s) => s !== name));
|
||||
}}
|
||||
className="inline-flex items-center"
|
||||
aria-label={`Remove ${name}`}
|
||||
>
|
||||
<X size={12} />
|
||||
</span>
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-[var(--radix-popover-trigger-width)] p-2"
|
||||
>
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="Search guardrails…"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="h-8 mb-2"
|
||||
/>
|
||||
<div className="max-h-60 overflow-y-auto">
|
||||
{remaining.length === 0 ? (
|
||||
<div className="py-2 px-3 text-sm text-muted-foreground">
|
||||
No matches
|
||||
</div>
|
||||
) : (
|
||||
remaining.map((g) => {
|
||||
const name = g.guardrail_name as string;
|
||||
return (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
className="w-full text-left px-2 py-1.5 text-sm rounded hover:bg-accent"
|
||||
onClick={() => onChange([...selected, name])}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,17 @@
|
|||
import React, { useState } from "react";
|
||||
import { CheckCircleFilled } from "@ant-design/icons";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
import { GuardrailCardInfo } from "./guardrail_garden_data";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const LogoWithFallback: React.FC<{ src: string; name: string }> = ({ src, name }) => {
|
||||
const LogoWithFallback: React.FC<{ src: string; name: string }> = ({
|
||||
src,
|
||||
name,
|
||||
}) => {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
if (hasError || !src) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 6,
|
||||
backgroundColor: "#e5e7eb",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "#6b7280",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div className="w-7 h-7 rounded-md bg-muted flex items-center justify-center text-sm font-semibold text-muted-foreground shrink-0">
|
||||
{name?.charAt(0) || "?"}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -31,52 +21,41 @@ const LogoWithFallback: React.FC<{ src: string; name: string }> = ({ src, name }
|
|||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
style={{ width: 28, height: 28, borderRadius: 6, objectFit: "contain", flexShrink: 0 }}
|
||||
className="w-7 h-7 rounded-md object-contain shrink-0"
|
||||
onError={() => setHasError(true)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const GuardrailCard: React.FC<{ card: GuardrailCardInfo; onClick: () => void }> = ({ card, onClick }) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const GuardrailCard: React.FC<{
|
||||
card: GuardrailCardInfo;
|
||||
onClick: () => void;
|
||||
}> = ({ card, onClick }) => {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
border: hovered ? "1px solid #93c5fd" : "1px solid #e5e7eb",
|
||||
backgroundColor: "#ffffff",
|
||||
padding: "20px 20px 16px 20px",
|
||||
cursor: "pointer",
|
||||
transition: "border-color 0.15s, box-shadow 0.15s",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 170,
|
||||
boxShadow: hovered ? "0 1px 6px rgba(59,130,246,0.08)" : "none",
|
||||
}}
|
||||
className={cn(
|
||||
"rounded-xl border border-border bg-background px-5 pt-5 pb-4",
|
||||
"cursor-pointer transition-colors transition-shadow",
|
||||
"hover:border-primary/50 hover:shadow-sm",
|
||||
"flex flex-col min-h-[170px]",
|
||||
)}
|
||||
>
|
||||
{/* Icon + Name row */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }}>
|
||||
<div className="flex items-center gap-2.5 mb-2.5">
|
||||
<LogoWithFallback src={card.logo} name={card.name} />
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: "#111827", lineHeight: 1.3 }}>{card.name}</span>
|
||||
<span className="text-sm font-semibold text-foreground leading-tight">
|
||||
{card.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p
|
||||
className="line-clamp-3"
|
||||
style={{ fontSize: 12, color: "#6b7280", lineHeight: 1.6, margin: 0, flex: 1 }}
|
||||
>
|
||||
<p className="line-clamp-3 text-xs text-muted-foreground leading-relaxed m-0 flex-1">
|
||||
{card.description}
|
||||
</p>
|
||||
|
||||
{/* Eval badge */}
|
||||
{card.eval && (
|
||||
<div style={{ marginTop: 10, display: "flex", alignItems: "center", gap: 4 }}>
|
||||
<CheckCircleFilled style={{ color: "#16a34a", fontSize: 12 }} />
|
||||
<span style={{ fontSize: 11, color: "#16a34a", fontWeight: 500 }}>
|
||||
<div className="mt-2.5 flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3 w-3 text-emerald-600 dark:text-emerald-400" />
|
||||
<span className="text-[11px] text-emerald-600 dark:text-emerald-400 font-medium">
|
||||
F1: {card.eval.f1}% · {card.eval.testCases} test cases
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue