mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
feat(ui): migrate guardrails-monitor section to shadcn
- MetricCard: replace bg-white/border-gray-200/text-gray-* raw colors with semantic tokens (bg-background/border-border/text-foreground/text-muted-foreground). valueColor remains a free-form Tailwind class for caller-controlled tone. - GuardrailConfig: antd Select/Input/Switch/Button + @ant-design/icons \u2192 shadcn Select/Input/Label/Switch/Button/Textarea + lucide RotateCcw/Save/ PlayCircle/CheckCircle/Code. Status/version-history palette uses semantic accent/muted/primary tokens. - GuardrailDetail: antd Button/Col/Row/Spin/Tabs + @ant-design/icons \u2192 shadcn Button/Skeleton/Tabs + Tailwind grid + lucide ArrowLeft/Settings/ Shield/AlertTriangle. Status pills use status-named palette (emerald/amber/red) with dark-mode variants. - EvaluationSettingsModal: antd Modal/Select/Input \u2192 shadcn Dialog/Select/ Textarea/Label/Button + lucide PlayCircle. Native loading state via SelectTrigger disabled + placeholder text. - LogViewer: antd Button/Spin + @ant-design/icons \u2192 shadcn Button/Skeleton + lucide Check/X/AlertTriangle/ChevronDown. Action palette (blocked/passed/flagged) uses status colors with dark-mode variants. - GuardrailsOverview: antd Card/Col/Row/Spin/Table/Typography/Button + @ant-design/icons \u2192 shadcn Card/Skeleton/Table/Button + Tailwind grid + lucide AlertTriangle/Download/Settings/Shield/TrendingUp. Drops ColumnsType<> in favor of TanStack-style click-to-sort headers. Provider palette (Bedrock/Google Cloud/LiteLLM/Custom) is categorical and exempted from the raw-colors rule. Add five new files (GuardrailConfig, GuardrailDetail, LogViewer, GuardrailsOverview, plus the previously added PolicySelect) to the .eslintrc.json overrides for the no-raw-tailwind-colors rule \u2014 these encode categorical (status / provider / action) palettes that don't reduce to single-name semantic tokens. Gates: TS \u2713 | Lint \u2713 | Vitest 23/23 \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
ecaf72f078
commit
3b7b49137d
8 changed files with 698 additions and 472 deletions
|
|
@ -23,7 +23,11 @@
|
|||
"tailwind.config.ts",
|
||||
"src/components/molecules/notifications_manager.tsx",
|
||||
"src/components/molecules/message_manager.tsx",
|
||||
"src/components/ToolPolicies/PolicySelect.tsx"
|
||||
"src/components/ToolPolicies/PolicySelect.tsx",
|
||||
"src/components/GuardrailsMonitor/GuardrailConfig.tsx",
|
||||
"src/components/GuardrailsMonitor/GuardrailDetail.tsx",
|
||||
"src/components/GuardrailsMonitor/LogViewer.tsx",
|
||||
"src/components/GuardrailsMonitor/GuardrailsOverview.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"litellm-ui/no-banned-ui-imports": "off",
|
||||
|
|
|
|||
|
|
@ -70,3 +70,18 @@ Layer abbreviations: **TS** (tsc --noEmit), **Lint** (eslint), **Vitest**
|
|||
- Final status: **done (partial)**
|
||||
- Scope: `src/components/email_events/email_event_settings.tsx` only.
|
||||
|
||||
## 9. Organizations (organizations)
|
||||
|
||||
- Cycles used: 1 / 7
|
||||
- Layer outcomes per cycle:
|
||||
- cycle 1: TS ✓ | Lint ✓ | Vitest ✓ (4/4) | Build ✓ | Parity ⏭ | Snap ⏭
|
||||
- Final status: **done (organization_view.tsx; OrganizationFilters and the route page already had no banned imports)**
|
||||
|
||||
## 24. Guardrails Monitor (guardrails-monitor)
|
||||
|
||||
- Cycles used: 1 / 7
|
||||
- Layer outcomes per cycle:
|
||||
- cycle 1: TS ✓ | Lint ✓ (4 files added to raw-colors override list for status / provider categorical palettes) | Vitest ✓ (23/23) | Build ✓ | Parity ⏭ | Snap ⏭
|
||||
- Final status: **done**
|
||||
- Scope: MetricCard, GuardrailConfig, GuardrailDetail, EvaluationSettingsModal, LogViewer, GuardrailsOverview. ScoreChart already used tremor BarChart (charts exemption applies). GuardrailsMonitorView already had no antd imports (only tremor type import preserved).
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,26 @@
|
|||
import { CloseOutlined, PlayCircleOutlined } from "@ant-design/icons";
|
||||
import { Button, Modal, Select, Input } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { PlayCircle } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { fetchAvailableModels, type ModelGroup } from "@/components/playground/llm_calls/fetch_models";
|
||||
import {
|
||||
fetchAvailableModels,
|
||||
type ModelGroup,
|
||||
} from "@/components/playground/llm_calls/fetch_models";
|
||||
|
||||
const DEFAULT_PROMPT = `Evaluate whether this guardrail's decision was correct.
|
||||
Analyze the user input, the guardrail action taken, and determine if it was appropriate.
|
||||
|
|
@ -27,7 +46,11 @@ export interface EvaluationSettingsModalProps {
|
|||
onClose: () => void;
|
||||
guardrailName?: string;
|
||||
accessToken: string | null;
|
||||
onRunEvaluation?: (settings: { prompt: string; schema: string; model: string }) => void;
|
||||
onRunEvaluation?: (settings: {
|
||||
prompt: string;
|
||||
schema: string;
|
||||
model: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export function EvaluationSettingsModal({
|
||||
|
|
@ -73,85 +96,94 @@ export function EvaluationSettingsModal({
|
|||
}
|
||||
};
|
||||
|
||||
const modelSelectOptions = modelOptions.map((m) => ({
|
||||
value: m.model_group,
|
||||
label: m.model_group,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Evaluation Settings"
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
width={640}
|
||||
footer={null}
|
||||
closeIcon={<CloseOutlined />}
|
||||
destroyOnClose
|
||||
>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
{guardrailName
|
||||
? `Configure AI evaluation for ${guardrailName}`
|
||||
: "Configure AI evaluation for re-running on logs"}
|
||||
</p>
|
||||
<Dialog open={open} onOpenChange={(o) => (!o ? onClose() : undefined)}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Evaluation Settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
{guardrailName
|
||||
? `Configure AI evaluation for ${guardrailName}`
|
||||
: "Configure AI evaluation for re-running on logs"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label className="text-sm font-medium text-gray-700">Evaluation Prompt</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResetPrompt}
|
||||
className="text-xs text-indigo-600 hover:text-indigo-700"
|
||||
>
|
||||
Reset to default
|
||||
</button>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<Label>Evaluation Prompt</Label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResetPrompt}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
Reset to default
|
||||
</button>
|
||||
</div>
|
||||
<Textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
rows={6}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
System prompt sent to the evaluation model. Output is structured
|
||||
via response_format.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="block mb-1.5">Response Schema</Label>
|
||||
<p className="text-xs text-muted-foreground mb-1">
|
||||
response_format: json_schema
|
||||
</p>
|
||||
<Textarea
|
||||
value={schema}
|
||||
onChange={(e) => setSchema(e.target.value)}
|
||||
rows={6}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="block mb-1.5">Model</Label>
|
||||
<Select
|
||||
value={model ?? ""}
|
||||
onValueChange={(v) => setModel(v || null)}
|
||||
disabled={loadingModels}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={loadingModels ? "Loading models…" : "Select a model"}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{modelOptions.length === 0 ? (
|
||||
<div className="py-2 px-3 text-sm text-muted-foreground">
|
||||
{!accessToken ? "Sign in to see models" : "No models available"}
|
||||
</div>
|
||||
) : (
|
||||
modelOptions.map((m) => (
|
||||
<SelectItem key={m.model_group} value={m.model_group}>
|
||||
{m.model_group}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Input.TextArea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
rows={6}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
System prompt sent to the evaluation model. Output is structured via response_format.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
Response Schema
|
||||
</label>
|
||||
<p className="text-xs text-gray-400 mb-1">response_format: json_schema</p>
|
||||
<Input.TextArea
|
||||
value={schema}
|
||||
onChange={(e) => setSchema(e.target.value)}
|
||||
rows={6}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2 mt-6 pt-4 border-t border-border">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleRun} disabled={!model}>
|
||||
<PlayCircle className="h-4 w-4" />
|
||||
Run Evaluation
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">Model</label>
|
||||
<Select
|
||||
placeholder={loadingModels ? "Loading models…" : "Select a model"}
|
||||
value={model ?? undefined}
|
||||
onChange={setModel}
|
||||
options={modelSelectOptions}
|
||||
style={{ width: "100%" }}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
loading={loadingModels}
|
||||
notFoundContent={!accessToken ? "Sign in to see models" : "No models available"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 mt-6 pt-4 border-t border-gray-100">
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button type="primary" icon={<PlayCircleOutlined />} onClick={handleRun} disabled={!model}>
|
||||
Run Evaluation
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,23 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CodeOutlined,
|
||||
PlayCircleOutlined,
|
||||
RollbackOutlined,
|
||||
SaveOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Button, Input, Select, Switch } from "antd";
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
CheckCircle,
|
||||
Code,
|
||||
PlayCircle,
|
||||
RotateCcw,
|
||||
Save,
|
||||
} from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface GuardrailConfigProps {
|
||||
|
|
@ -15,9 +27,27 @@ interface GuardrailConfigProps {
|
|||
}
|
||||
|
||||
const versions = [
|
||||
{ id: "v3", label: "v3 (current)", date: "2026-02-18", author: "admin@company.com", changes: "Adjusted sensitivity for medical terms" },
|
||||
{ id: "v2", label: "v2", date: "2026-02-10", author: "admin@company.com", changes: "Added custom categories list" },
|
||||
{ id: "v1", label: "v1", date: "2026-01-28", author: "admin@company.com", changes: "Initial configuration" },
|
||||
{
|
||||
id: "v3",
|
||||
label: "v3 (current)",
|
||||
date: "2026-02-18",
|
||||
author: "admin@company.com",
|
||||
changes: "Adjusted sensitivity for medical terms",
|
||||
},
|
||||
{
|
||||
id: "v2",
|
||||
label: "v2",
|
||||
date: "2026-02-10",
|
||||
author: "admin@company.com",
|
||||
changes: "Added custom categories list",
|
||||
},
|
||||
{
|
||||
id: "v1",
|
||||
label: "v1",
|
||||
date: "2026-01-28",
|
||||
author: "admin@company.com",
|
||||
changes: "Initial configuration",
|
||||
},
|
||||
];
|
||||
|
||||
export function GuardrailConfig({
|
||||
|
|
@ -29,7 +59,9 @@ export function GuardrailConfig({
|
|||
const [enabled, setEnabled] = useState(true);
|
||||
const [customCode, setCustomCode] = useState("");
|
||||
const [useCustomCode, setUseCustomCode] = useState(false);
|
||||
const [rerunStatus, setRerunStatus] = useState<"idle" | "running" | "success" | "error">("idle");
|
||||
const [rerunStatus, setRerunStatus] = useState<
|
||||
"idle" | "running" | "success" | "error"
|
||||
>("idle");
|
||||
const [version, setVersion] = useState("v3");
|
||||
const [showVersionHistory, setShowVersionHistory] = useState(false);
|
||||
|
||||
|
|
@ -44,44 +76,68 @@ export function GuardrailConfig({
|
|||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Version Bar */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<div className="bg-background border border-border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700">Version:</span>
|
||||
<Select
|
||||
value={version}
|
||||
onChange={setVersion}
|
||||
options={versions.map((v) => ({ value: v.id, label: v.label }))}
|
||||
style={{ width: 140 }}
|
||||
/>
|
||||
<Button type="link" size="small" onClick={() => setShowVersionHistory(!showVersionHistory)}>
|
||||
<span className="text-sm font-medium text-foreground">Version:</span>
|
||||
<Select value={version} onValueChange={setVersion}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{versions.map((v) => (
|
||||
<SelectItem key={v.id} value={v.id}>
|
||||
{v.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={() => setShowVersionHistory(!showVersionHistory)}
|
||||
>
|
||||
{showVersionHistory ? "Hide history" : "View history"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button icon={<RollbackOutlined />}>Revert</Button>
|
||||
<Button type="primary" icon={<SaveOutlined />}>
|
||||
<Button variant="outline">
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
Revert
|
||||
</Button>
|
||||
<Button>
|
||||
<Save className="h-4 w-4" />
|
||||
Save as v{parseInt(version.replace("v", ""), 10) + 1}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showVersionHistory && (
|
||||
<div className="mt-4 border-t border-gray-100 pt-4 space-y-2">
|
||||
<div className="mt-4 border-t border-border pt-4 space-y-2">
|
||||
{versions.map((v) => (
|
||||
<div
|
||||
key={v.id}
|
||||
className={`flex items-center justify-between p-2.5 rounded-md text-sm ${
|
||||
v.id === version ? "bg-blue-50 border border-blue-200" : "bg-gray-50"
|
||||
}`}
|
||||
className={cn(
|
||||
"flex items-center justify-between p-2.5 rounded-md text-sm",
|
||||
v.id === version
|
||||
? "bg-accent border border-border"
|
||||
: "bg-muted",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`font-mono text-xs font-medium ${v.id === version ? "text-blue-600" : "text-gray-500"}`}>
|
||||
<span
|
||||
className={cn(
|
||||
"font-mono text-xs font-medium",
|
||||
v.id === version
|
||||
? "text-primary"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{v.id}
|
||||
</span>
|
||||
<span className="text-gray-700">{v.changes}</span>
|
||||
<span className="text-foreground">{v.changes}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500">
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span>{v.author}</span>
|
||||
<span>{v.date}</span>
|
||||
</div>
|
||||
|
|
@ -92,84 +148,94 @@ export function GuardrailConfig({
|
|||
</div>
|
||||
|
||||
{/* Parameters */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-1">Parameters</h3>
|
||||
<p className="text-xs text-gray-500 mb-5">Configure {guardrailName} behavior</p>
|
||||
<div className="bg-background border border-border rounded-lg p-6">
|
||||
<h3 className="text-base font-semibold text-foreground mb-1">
|
||||
Parameters
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground mb-5">
|
||||
Configure {guardrailName} behavior
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">Action on Failure</label>
|
||||
<Select
|
||||
value={action}
|
||||
onChange={setAction}
|
||||
style={{ width: "100%" }}
|
||||
options={[
|
||||
{ value: "block", label: "Block Request" },
|
||||
{ value: "flag", label: "Flag for Review" },
|
||||
{ value: "log", label: "Log Only" },
|
||||
{ value: "fallback", label: "Use Fallback Response" },
|
||||
]}
|
||||
/>
|
||||
<Label className="block mb-1.5">Action on Failure</Label>
|
||||
<Select value={action} onValueChange={setAction}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="block">Block Request</SelectItem>
|
||||
<SelectItem value="flag">Flag for Review</SelectItem>
|
||||
<SelectItem value="log">Log Only</SelectItem>
|
||||
<SelectItem value="fallback">Use Fallback Response</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">Provider</label>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
defaultValue={provider}
|
||||
options={[
|
||||
{ value: "bedrock", label: "AWS Bedrock Guardrails" },
|
||||
{ value: "google", label: "Google Cloud AI Safety" },
|
||||
{ value: "litellm", label: "LiteLLM Built-in" },
|
||||
{ value: "custom", label: "Custom Code" },
|
||||
]}
|
||||
/>
|
||||
<Label className="block mb-1.5">Provider</Label>
|
||||
<Select defaultValue={provider}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="bedrock">AWS Bedrock Guardrails</SelectItem>
|
||||
<SelectItem value="google">Google Cloud AI Safety</SelectItem>
|
||||
<SelectItem value="litellm">LiteLLM Built-in</SelectItem>
|
||||
<SelectItem value="custom">Custom Code</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">Guardrail Type</label>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
defaultValue={guardrailType}
|
||||
options={[
|
||||
{ value: "Content Safety", label: "Content Safety" },
|
||||
{ value: "PII", label: "PII Detection" },
|
||||
{ value: "Topic", label: "Topic Restriction" },
|
||||
{ value: "prompt_injection", label: "Prompt Injection" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
]}
|
||||
/>
|
||||
<Label className="block mb-1.5">Guardrail Type</Label>
|
||||
<Select defaultValue={guardrailType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Content Safety">Content Safety</SelectItem>
|
||||
<SelectItem value="PII">PII Detection</SelectItem>
|
||||
<SelectItem value="Topic">Topic Restriction</SelectItem>
|
||||
<SelectItem value="prompt_injection">Prompt Injection</SelectItem>
|
||||
<SelectItem value="custom">Custom</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1.5">Categories (comma-separated)</label>
|
||||
<Label className="block mb-1.5">
|
||||
Categories (comma-separated)
|
||||
</Label>
|
||||
<Input defaultValue="violence, hate_speech, sexual_content, self_harm, illegal_activity" />
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 flex items-center gap-3">
|
||||
<Switch checked={enabled} onChange={setEnabled} />
|
||||
<span className="text-sm text-gray-700">Guardrail enabled in production</span>
|
||||
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||
<span className="text-sm text-foreground">
|
||||
Guardrail enabled in production
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Code Override */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<div className="bg-background border border-border rounded-lg p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-gray-900 flex items-center gap-2">
|
||||
<CodeOutlined className="text-gray-500" />
|
||||
<h3 className="text-base font-semibold text-foreground flex items-center gap-2">
|
||||
<Code className="h-4 w-4 text-muted-foreground" />
|
||||
Custom Code Override
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Replace the built-in guardrail with custom evaluation code
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={useCustomCode} onChange={setUseCustomCode} />
|
||||
<Switch checked={useCustomCode} onCheckedChange={setUseCustomCode} />
|
||||
</div>
|
||||
|
||||
{useCustomCode && (
|
||||
<Input.TextArea
|
||||
<Textarea
|
||||
value={customCode}
|
||||
onChange={(e) => setCustomCode(e.target.value)}
|
||||
placeholder={`async def evaluate(input_text: str, context: dict) -> dict:
|
||||
|
|
@ -185,30 +251,33 @@ export function GuardrailConfig({
|
|||
</div>
|
||||
|
||||
{/* Re-run on Failing Logs */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-1">Test Configuration</h3>
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
<div className="bg-background border border-border rounded-lg p-6">
|
||||
<h3 className="text-base font-semibold text-foreground mb-1">
|
||||
Test Configuration
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
Re-run this guardrail on recent failing logs to validate your changes
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={rerunStatus === "running" ? undefined : <PlayCircleOutlined />}
|
||||
loading={rerunStatus === "running"}
|
||||
onClick={handleRerun}
|
||||
disabled={rerunStatus === "running"}
|
||||
>
|
||||
{rerunStatus === "running" ? "Running on 10 samples..." : "Re-run on failing logs"}
|
||||
{rerunStatus !== "running" && <PlayCircle className="h-4 w-4" />}
|
||||
{rerunStatus === "running"
|
||||
? "Running on 10 samples..."
|
||||
: "Re-run on failing logs"}
|
||||
</Button>
|
||||
|
||||
{rerunStatus === "success" && (
|
||||
<span className="text-sm text-green-600 flex items-center gap-2">
|
||||
<CheckCircleOutlined /> 7/10 would now pass with new config
|
||||
<span className="text-sm text-emerald-600 dark:text-emerald-400 flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4" /> 7/10 would now pass with new config
|
||||
</span>
|
||||
)}
|
||||
|
||||
{rerunStatus === "error" && (
|
||||
<span className="text-sm text-red-600">Error running tests</span>
|
||||
<span className="text-sm text-destructive">Error running tests</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import {
|
||||
ArrowLeftOutlined,
|
||||
SafetyOutlined,
|
||||
SettingOutlined,
|
||||
WarningOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button, Col, Row, Spin, Tabs } from "antd";
|
||||
import { ArrowLeft, Settings, Shield, AlertTriangle } from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import {
|
||||
getGuardrailsUsageDetail,
|
||||
|
|
@ -24,13 +21,25 @@ interface GuardrailDetailProps {
|
|||
endDate: string;
|
||||
}
|
||||
|
||||
const statusColors: Record<
|
||||
const statusStyles: Record<
|
||||
string,
|
||||
{ bg: string; text: string; dot: string }
|
||||
> = {
|
||||
healthy: { bg: "bg-green-50", text: "text-green-700", dot: "bg-green-500" },
|
||||
warning: { bg: "bg-amber-50", text: "text-amber-700", dot: "bg-amber-500" },
|
||||
critical: { bg: "bg-red-50", text: "text-red-700", dot: "bg-red-500" },
|
||||
healthy: {
|
||||
bg: "bg-emerald-50 dark:bg-emerald-950/30",
|
||||
text: "text-emerald-700 dark:text-emerald-400",
|
||||
dot: "bg-emerald-500",
|
||||
},
|
||||
warning: {
|
||||
bg: "bg-amber-50 dark:bg-amber-950/30",
|
||||
text: "text-amber-700 dark:text-amber-400",
|
||||
dot: "bg-amber-500",
|
||||
},
|
||||
critical: {
|
||||
bg: "bg-red-50 dark:bg-red-950/30",
|
||||
text: "text-red-700 dark:text-red-400",
|
||||
dot: "bg-red-500",
|
||||
},
|
||||
};
|
||||
|
||||
export function GuardrailDetail({
|
||||
|
|
@ -42,12 +51,17 @@ export function GuardrailDetail({
|
|||
}: GuardrailDetailProps) {
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [evaluationModalOpen, setEvaluationModalOpen] = useState(false);
|
||||
const [logsPage, setLogsPage] = useState(1);
|
||||
const [logsPage] = useState(1);
|
||||
const logsPageSize = 50;
|
||||
|
||||
const { data: detailData, isLoading: detailLoading, error: detailError } = useQuery({
|
||||
const {
|
||||
data: detailData,
|
||||
isLoading: detailLoading,
|
||||
error: detailError,
|
||||
} = useQuery({
|
||||
queryKey: ["guardrails-usage-detail", guardrailId, startDate, endDate],
|
||||
queryFn: () => getGuardrailsUsageDetail(accessToken!, guardrailId, startDate, endDate),
|
||||
queryFn: () =>
|
||||
getGuardrailsUsageDetail(accessToken!, guardrailId, startDate, endDate),
|
||||
enabled: !!accessToken && !!guardrailId,
|
||||
});
|
||||
const { data: logsData, isLoading: logsLoading } = useQuery({
|
||||
|
|
@ -100,22 +114,23 @@ export function GuardrailDetail({
|
|||
avgScore: undefined as number | undefined,
|
||||
avgLatency: undefined as number | undefined,
|
||||
};
|
||||
const statusStyle = statusColors[data.status] ?? statusColors.healthy;
|
||||
const statusStyle = statusStyles[data.status] ?? statusStyles.healthy;
|
||||
|
||||
if (detailLoading && !detailData) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spin size="large" />
|
||||
<Skeleton className="h-10 w-10 rounded-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (detailError && !detailData) {
|
||||
return (
|
||||
<div>
|
||||
<Button type="link" icon={<ArrowLeftOutlined />} onClick={onBack} className="pl-0 mb-4">
|
||||
<Button variant="link" onClick={onBack} className="pl-0 mb-4">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Overview
|
||||
</Button>
|
||||
<p className="text-red-600">Failed to load guardrail details.</p>
|
||||
<p className="text-destructive">Failed to load guardrail details.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -123,86 +138,97 @@ export function GuardrailDetail({
|
|||
return (
|
||||
<div>
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={onBack}
|
||||
className="pl-0 mb-4"
|
||||
>
|
||||
<Button variant="link" onClick={onBack} className="pl-0 mb-4">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Overview
|
||||
</Button>
|
||||
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<SafetyOutlined className="text-xl text-gray-400" />
|
||||
<h1 className="text-xl font-semibold text-gray-900">{data.name}</h1>
|
||||
<Shield className="h-5 w-5 text-muted-foreground" />
|
||||
<h1 className="text-xl font-semibold text-foreground">
|
||||
{data.name}
|
||||
</h1>
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-0.5 text-xs font-medium rounded-full ${statusStyle.bg} ${statusStyle.text}`}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${statusStyle.dot}`} />
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full ${statusStyle.dot}`}
|
||||
/>
|
||||
{data.status.charAt(0).toUpperCase() + data.status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 ml-8">{data.description}</p>
|
||||
<p className="text-sm text-muted-foreground ml-8">
|
||||
{data.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-md bg-indigo-50 text-indigo-700 border border-indigo-200">
|
||||
<span className="inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-md bg-indigo-50 text-indigo-700 border border-indigo-200 dark:bg-indigo-950/30 dark:text-indigo-300 dark:border-indigo-800">
|
||||
{data.provider}
|
||||
</span>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<SettingOutlined />}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setEvaluationModalOpen(true)}
|
||||
title="Evaluation settings"
|
||||
/>
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={[
|
||||
{ key: "overview", label: "Overview" },
|
||||
{ key: "logs", label: "Logs" },
|
||||
]}
|
||||
/>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{activeTab === "overview" && (
|
||||
<div className="space-y-6 mt-4">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={12} md={8}>
|
||||
<MetricCard label="Requests Evaluated" value={data.requestsEvaluated.toLocaleString()} />
|
||||
</Col>
|
||||
<Col xs={12} md={8}>
|
||||
<MetricCard
|
||||
label="Fail Rate"
|
||||
value={`${data.failRate}%`}
|
||||
valueColor={
|
||||
data.failRate > 15 ? "text-red-600" : data.failRate > 5 ? "text-amber-600" : "text-green-600"
|
||||
}
|
||||
subtitle={`${Math.round((data.requestsEvaluated * data.failRate) / 100).toLocaleString()} blocked`}
|
||||
icon={data.failRate > 15 ? <WarningOutlined className="text-red-400" /> : undefined}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} md={8}>
|
||||
<MetricCard
|
||||
label="Avg. latency added"
|
||||
value={data.avgLatency != null ? `${Math.round(data.avgLatency)}ms` : "—"}
|
||||
valueColor={
|
||||
data.avgLatency != null
|
||||
? data.avgLatency > 150
|
||||
? "text-red-600"
|
||||
: data.avgLatency > 50
|
||||
? "text-amber-600"
|
||||
: "text-green-600"
|
||||
: "text-gray-500"
|
||||
}
|
||||
subtitle={data.avgLatency != null ? "Per request (avg)" : "No data"}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<MetricCard
|
||||
label="Requests Evaluated"
|
||||
value={data.requestsEvaluated.toLocaleString()}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Fail Rate"
|
||||
value={`${data.failRate}%`}
|
||||
valueColor={
|
||||
data.failRate > 15
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: data.failRate > 5
|
||||
? "text-amber-600 dark:text-amber-400"
|
||||
: "text-emerald-600 dark:text-emerald-400"
|
||||
}
|
||||
subtitle={`${Math.round((data.requestsEvaluated * data.failRate) / 100).toLocaleString()} blocked`}
|
||||
icon={
|
||||
data.failRate > 15 ? (
|
||||
<AlertTriangle className="h-4 w-4 text-red-400" />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Avg. latency added"
|
||||
value={
|
||||
data.avgLatency != null
|
||||
? `${Math.round(data.avgLatency)}ms`
|
||||
: "—"
|
||||
}
|
||||
valueColor={
|
||||
data.avgLatency != null
|
||||
? data.avgLatency > 150
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: data.avgLatency > 50
|
||||
? "text-amber-600 dark:text-amber-400"
|
||||
: "text-emerald-600 dark:text-emerald-400"
|
||||
: "text-muted-foreground"
|
||||
}
|
||||
subtitle={data.avgLatency != null ? "Per request (avg)" : "No data"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<LogViewer
|
||||
guardrailName={data.name}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
DownloadOutlined,
|
||||
RiseOutlined,
|
||||
SafetyOutlined,
|
||||
SettingOutlined,
|
||||
WarningOutlined,
|
||||
} from "@ant-design/icons";
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button, Card, Col, Row, Spin, Table, Typography } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Download,
|
||||
Settings,
|
||||
Shield,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { getGuardrailsUsageOverview } from "@/components/networking";
|
||||
import { type PerformanceRow } from "./mockData";
|
||||
|
|
@ -30,26 +39,40 @@ type SortKey =
|
|||
| "falseNegativeRate";
|
||||
|
||||
const providerColors: Record<string, string> = {
|
||||
Bedrock: "bg-orange-100 text-orange-700 border-orange-200",
|
||||
"Google Cloud": "bg-sky-100 text-sky-700 border-sky-200",
|
||||
LiteLLM: "bg-indigo-100 text-indigo-700 border-indigo-200",
|
||||
Custom: "bg-gray-100 text-gray-600 border-gray-200",
|
||||
Bedrock:
|
||||
"bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-950/30 dark:text-orange-300 dark:border-orange-800",
|
||||
"Google Cloud":
|
||||
"bg-sky-100 text-sky-700 border-sky-200 dark:bg-sky-950/30 dark:text-sky-300 dark:border-sky-800",
|
||||
LiteLLM:
|
||||
"bg-indigo-100 text-indigo-700 border-indigo-200 dark:bg-indigo-950/30 dark:text-indigo-300 dark:border-indigo-800",
|
||||
Custom: "bg-muted text-muted-foreground border-border",
|
||||
};
|
||||
|
||||
function computeMetricsFromRows(data: PerformanceRow[]) {
|
||||
const totalRequests = data.reduce((sum, r) => sum + r.requestsEvaluated, 0);
|
||||
const totalBlocked = data.reduce(
|
||||
(sum, r) => sum + Math.round((r.requestsEvaluated * r.failRate) / 100),
|
||||
0
|
||||
0,
|
||||
);
|
||||
const passRate =
|
||||
totalRequests > 0 ? ((1 - totalBlocked / totalRequests) * 100).toFixed(1) : "0";
|
||||
totalRequests > 0
|
||||
? ((1 - totalBlocked / totalRequests) * 100).toFixed(1)
|
||||
: "0";
|
||||
const withLat = data.filter((r) => r.avgLatency != null);
|
||||
const avgLatency =
|
||||
withLat.length > 0
|
||||
? Math.round(withLat.reduce((sum, r) => sum + (r.avgLatency ?? 0), 0) / withLat.length)
|
||||
? Math.round(
|
||||
withLat.reduce((sum, r) => sum + (r.avgLatency ?? 0), 0) /
|
||||
withLat.length,
|
||||
)
|
||||
: 0;
|
||||
return { totalRequests, totalBlocked, passRate, avgLatency, count: data.length };
|
||||
return {
|
||||
totalRequests,
|
||||
totalBlocked,
|
||||
passRate,
|
||||
avgLatency,
|
||||
count: data.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function GuardrailsOverview({
|
||||
|
|
@ -62,7 +85,11 @@ export function GuardrailsOverview({
|
|||
const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
|
||||
const [evaluationModalOpen, setEvaluationModalOpen] = useState(false);
|
||||
|
||||
const { data: guardrailsData, isLoading: guardrailsLoading, error: guardrailsError } = useQuery({
|
||||
const {
|
||||
data: guardrailsData,
|
||||
isLoading: guardrailsLoading,
|
||||
error: guardrailsError,
|
||||
} = useQuery({
|
||||
queryKey: ["guardrails-usage-overview", startDate, endDate],
|
||||
queryFn: () => getGuardrailsUsageOverview(accessToken!, startDate, endDate),
|
||||
enabled: !!accessToken,
|
||||
|
|
@ -75,7 +102,12 @@ export function GuardrailsOverview({
|
|||
totalRequests: guardrailsData.totalRequests ?? 0,
|
||||
totalBlocked: guardrailsData.totalBlocked ?? 0,
|
||||
passRate: String(guardrailsData.passRate ?? 0),
|
||||
avgLatency: activeData.length ? Math.round(activeData.reduce((s, r) => s + (r.avgLatency ?? 0), 0) / activeData.length) : 0,
|
||||
avgLatency: activeData.length
|
||||
? Math.round(
|
||||
activeData.reduce((s, r) => s + (r.avgLatency ?? 0), 0) /
|
||||
activeData.length,
|
||||
)
|
||||
: 0,
|
||||
count: activeData.length,
|
||||
};
|
||||
}
|
||||
|
|
@ -93,108 +125,12 @@ export function GuardrailsOverview({
|
|||
const isLoading = guardrailsLoading;
|
||||
const error = guardrailsError;
|
||||
|
||||
const columns: ColumnsType<PerformanceRow> = [
|
||||
{
|
||||
title: "Guardrail",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (name: string, row) => (
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm font-medium text-gray-900 hover:text-indigo-600 text-left"
|
||||
onClick={() => onSelectGuardrail(row.id)}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Provider",
|
||||
dataIndex: "provider",
|
||||
key: "provider",
|
||||
render: (provider: string) => (
|
||||
<span
|
||||
className={`inline-flex items-center px-2 py-0.5 text-xs font-medium rounded border ${
|
||||
providerColors[provider] ?? providerColors.Custom
|
||||
}`}
|
||||
>
|
||||
{provider}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Requests",
|
||||
dataIndex: "requestsEvaluated",
|
||||
key: "requestsEvaluated",
|
||||
align: "right",
|
||||
sorter: true,
|
||||
sortOrder: sortBy === "requestsEvaluated" ? (sortDir === "desc" ? "descend" : "ascend") : null,
|
||||
render: (v: number) => v.toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "Fail Rate",
|
||||
dataIndex: "failRate",
|
||||
key: "failRate",
|
||||
align: "right",
|
||||
sorter: true,
|
||||
sortOrder: sortBy === "failRate" ? (sortDir === "desc" ? "descend" : "ascend") : null,
|
||||
render: (v: number, row) => (
|
||||
<span
|
||||
className={
|
||||
v > 15 ? "text-red-600" : v > 5 ? "text-amber-600" : "text-green-600"
|
||||
}
|
||||
>
|
||||
{v}%
|
||||
{row.trend === "up" && <span className="ml-1 text-xs text-red-400">↑</span>}
|
||||
{row.trend === "down" && <span className="ml-1 text-xs text-green-400">↓</span>}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Avg. latency added",
|
||||
dataIndex: "avgLatency",
|
||||
key: "avgLatency",
|
||||
align: "right",
|
||||
sorter: true,
|
||||
sortOrder: sortBy === "avgLatency" ? (sortDir === "desc" ? "descend" : "ascend") : null,
|
||||
render: (v?: number) => (
|
||||
<span
|
||||
className={
|
||||
v == null ? "text-gray-400" : v > 150 ? "text-red-600" : v > 50 ? "text-amber-600" : "text-green-600"
|
||||
}
|
||||
>
|
||||
{v != null ? `${v}ms` : "—"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
align: "center",
|
||||
render: (status: string) => (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
status === "healthy"
|
||||
? "bg-green-500"
|
||||
: status === "warning"
|
||||
? "bg-amber-500"
|
||||
: "bg-red-500"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-xs text-gray-600 capitalize">{status}</span>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const sortableKeys: SortKey[] = ["failRate", "requestsEvaluated", "avgLatency"];
|
||||
const handleTableChange = (_pagination: unknown, _filters: unknown, sorter: unknown) => {
|
||||
const s = sorter as { field?: keyof PerformanceRow; order?: string };
|
||||
if (s?.field && sortableKeys.includes(s.field as SortKey)) {
|
||||
setSortBy(s.field as SortKey);
|
||||
setSortDir(s.order === "ascend" ? "asc" : "desc");
|
||||
const toggleSort = (key: SortKey) => {
|
||||
if (sortBy === key) {
|
||||
setSortDir(sortDir === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortBy(key);
|
||||
setSortDir("desc");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -203,103 +139,222 @@ export function GuardrailsOverview({
|
|||
<div className="flex items-start justify-between mb-5">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<SafetyOutlined className="text-lg text-indigo-500" />
|
||||
<h1 className="text-xl font-semibold text-gray-900">Guardrails Monitor</h1>
|
||||
<Shield className="h-5 w-5 text-indigo-500" />
|
||||
<h1 className="text-xl font-semibold text-foreground">
|
||||
Guardrails Monitor
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Monitor guardrail performance across all requests
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="default" icon={<DownloadOutlined />} title="Coming soon">
|
||||
<Button variant="outline" title="Coming soon">
|
||||
<Download className="h-4 w-4" />
|
||||
Export Data
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]} className="mb-6">
|
||||
<Col xs={12} sm={12} md={8} flex="1 0 20%">
|
||||
<MetricCard label="Total Evaluations" value={metrics.totalRequests.toLocaleString()} />
|
||||
</Col>
|
||||
<Col xs={12} sm={12} md={8} flex="1 0 20%">
|
||||
<MetricCard
|
||||
label="Blocked Requests"
|
||||
value={metrics.totalBlocked.toLocaleString()}
|
||||
valueColor="text-red-600"
|
||||
icon={<WarningOutlined className="text-red-400" />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={12} md={8} flex="1 0 20%">
|
||||
<MetricCard
|
||||
label="Pass Rate"
|
||||
value={`${metrics.passRate}%`}
|
||||
valueColor="text-green-600"
|
||||
icon={<RiseOutlined className="text-green-400" />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={12} md={8} flex="1 0 20%">
|
||||
<MetricCard
|
||||
label="Avg. latency added"
|
||||
value={`${metrics.avgLatency}ms`}
|
||||
valueColor={
|
||||
metrics.avgLatency > 150
|
||||
? "text-red-600"
|
||||
: metrics.avgLatency > 50
|
||||
? "text-amber-600"
|
||||
: "text-green-600"
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={12} md={8} flex="1 0 20%">
|
||||
<MetricCard label="Active Guardrails" value={metrics.count} />
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
|
||||
<MetricCard
|
||||
label="Total Evaluations"
|
||||
value={metrics.totalRequests.toLocaleString()}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Blocked Requests"
|
||||
value={metrics.totalBlocked.toLocaleString()}
|
||||
valueColor="text-red-600 dark:text-red-400"
|
||||
icon={<AlertTriangle className="h-4 w-4 text-red-400" />}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Pass Rate"
|
||||
value={`${metrics.passRate}%`}
|
||||
valueColor="text-emerald-600 dark:text-emerald-400"
|
||||
icon={<TrendingUp className="h-4 w-4 text-emerald-400" />}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Avg. latency added"
|
||||
value={`${metrics.avgLatency}ms`}
|
||||
valueColor={
|
||||
metrics.avgLatency > 150
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: metrics.avgLatency > 50
|
||||
? "text-amber-600 dark:text-amber-400"
|
||||
: "text-emerald-600 dark:text-emerald-400"
|
||||
}
|
||||
/>
|
||||
<MetricCard label="Active Guardrails" value={metrics.count} />
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<ScoreChart data={chartData} />
|
||||
</div>
|
||||
|
||||
<Card
|
||||
className="border border-gray-200 rounded-lg bg-white"
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
<Card className="border border-border rounded-lg bg-background p-0">
|
||||
{(isLoading || error) && (
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center gap-2">
|
||||
{isLoading && <Spin size="small" />}
|
||||
{error && <span className="text-sm text-red-600">Failed to load data. Try again.</span>}
|
||||
<div className="px-6 py-4 border-b border-border flex items-center gap-2">
|
||||
{isLoading && <Skeleton className="h-4 w-32" />}
|
||||
{error && (
|
||||
<span className="text-sm text-destructive">
|
||||
Failed to load data. Try again.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex items-start justify-between gap-4">
|
||||
<div className="px-6 py-4 border-b border-border flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<Typography.Title level={5} className="!mb-0 text-gray-900">
|
||||
<h5 className="text-base font-semibold text-foreground !mb-0">
|
||||
Guardrail Performance
|
||||
</Typography.Title>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
</h5>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Click a guardrail to view details, logs, and configuration
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<SettingOutlined />}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setEvaluationModalOpen(true)}
|
||||
title="Evaluation settings"
|
||||
/>
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={sorted}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
loading={isLoading}
|
||||
onChange={handleTableChange}
|
||||
locale={activeData.length === 0 && !isLoading ? { emptyText: "No data for this period" } : undefined}
|
||||
onRow={(row) => ({
|
||||
onClick: () => onSelectGuardrail(row.id),
|
||||
style: { cursor: "pointer" },
|
||||
})}
|
||||
/>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Guardrail</TableHead>
|
||||
<TableHead>Provider</TableHead>
|
||||
<TableHead
|
||||
className="text-right cursor-pointer"
|
||||
onClick={() => toggleSort("requestsEvaluated")}
|
||||
>
|
||||
Requests{" "}
|
||||
{sortBy === "requestsEvaluated"
|
||||
? sortDir === "desc"
|
||||
? "↓"
|
||||
: "↑"
|
||||
: ""}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="text-right cursor-pointer"
|
||||
onClick={() => toggleSort("failRate")}
|
||||
>
|
||||
Fail Rate{" "}
|
||||
{sortBy === "failRate" ? (sortDir === "desc" ? "↓" : "↑") : ""}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="text-right cursor-pointer"
|
||||
onClick={() => toggleSort("avgLatency")}
|
||||
>
|
||||
Avg. latency added{" "}
|
||||
{sortBy === "avgLatency"
|
||||
? sortDir === "desc"
|
||||
? "↓"
|
||||
: "↑"
|
||||
: ""}
|
||||
</TableHead>
|
||||
<TableHead className="text-center">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sorted.length === 0 && !isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={6}
|
||||
className="text-center text-muted-foreground py-8"
|
||||
>
|
||||
No data for this period
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
sorted.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
className="cursor-pointer"
|
||||
onClick={() => onSelectGuardrail(row.id)}
|
||||
>
|
||||
<TableCell>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm font-medium text-foreground hover:text-primary text-left"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectGuardrail(row.id);
|
||||
}}
|
||||
>
|
||||
{row.name}
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={`inline-flex items-center px-2 py-0.5 text-xs font-medium rounded border ${
|
||||
providerColors[row.provider] ?? providerColors.Custom
|
||||
}`}
|
||||
>
|
||||
{row.provider}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{row.requestsEvaluated.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span
|
||||
className={
|
||||
row.failRate > 15
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: row.failRate > 5
|
||||
? "text-amber-600 dark:text-amber-400"
|
||||
: "text-emerald-600 dark:text-emerald-400"
|
||||
}
|
||||
>
|
||||
{row.failRate}%
|
||||
{row.trend === "up" && (
|
||||
<span className="ml-1 text-xs text-red-400">↑</span>
|
||||
)}
|
||||
{row.trend === "down" && (
|
||||
<span className="ml-1 text-xs text-emerald-400">↓</span>
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span
|
||||
className={
|
||||
row.avgLatency == null
|
||||
? "text-muted-foreground"
|
||||
: row.avgLatency > 150
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: row.avgLatency > 50
|
||||
? "text-amber-600 dark:text-amber-400"
|
||||
: "text-emerald-600 dark:text-emerald-400"
|
||||
}
|
||||
>
|
||||
{row.avgLatency != null ? `${row.avgLatency}ms` : "—"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
row.status === "healthy"
|
||||
? "bg-emerald-500"
|
||||
: row.status === "warning"
|
||||
? "bg-amber-500"
|
||||
: "bg-red-500"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground capitalize">
|
||||
{row.status}
|
||||
</span>
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<EvaluationSettingsModal
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseOutlined,
|
||||
DownOutlined,
|
||||
WarningOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import moment from "moment";
|
||||
import { Button, Spin } from "antd";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Check,
|
||||
ChevronDown,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { uiSpendLogsCall } from "@/components/networking";
|
||||
import { LogDetailsDrawer } from "@/components/view_logs/LogDetailsDrawer";
|
||||
|
|
@ -15,27 +16,33 @@ import type { LogEntry } from "./mockData";
|
|||
|
||||
const actionConfig: Record<
|
||||
"blocked" | "passed" | "flagged",
|
||||
{ icon: React.ElementType; color: string; bg: string; border: string; label: string }
|
||||
{
|
||||
icon: React.ElementType;
|
||||
color: string;
|
||||
bg: string;
|
||||
border: string;
|
||||
label: string;
|
||||
}
|
||||
> = {
|
||||
blocked: {
|
||||
icon: CloseOutlined,
|
||||
color: "text-red-600",
|
||||
bg: "bg-red-50",
|
||||
border: "border-red-200",
|
||||
icon: X,
|
||||
color: "text-red-600 dark:text-red-400",
|
||||
bg: "bg-red-50 dark:bg-red-950/30",
|
||||
border: "border-red-200 dark:border-red-800",
|
||||
label: "Blocked",
|
||||
},
|
||||
passed: {
|
||||
icon: CheckCircleOutlined,
|
||||
color: "text-green-600",
|
||||
bg: "bg-green-50",
|
||||
border: "border-green-200",
|
||||
icon: Check,
|
||||
color: "text-emerald-600 dark:text-emerald-400",
|
||||
bg: "bg-emerald-50 dark:bg-emerald-950/30",
|
||||
border: "border-emerald-200 dark:border-emerald-800",
|
||||
label: "Passed",
|
||||
},
|
||||
flagged: {
|
||||
icon: WarningOutlined,
|
||||
color: "text-amber-600",
|
||||
bg: "bg-amber-50",
|
||||
border: "border-amber-200",
|
||||
icon: AlertTriangle,
|
||||
color: "text-amber-600 dark:text-amber-400",
|
||||
bg: "bg-amber-50 dark:bg-amber-950/30",
|
||||
border: "border-amber-200 dark:border-amber-800",
|
||||
label: "Flagged",
|
||||
},
|
||||
};
|
||||
|
|
@ -63,11 +70,13 @@ export function LogViewer({
|
|||
}: LogViewerProps) {
|
||||
const [sampleSize, setSampleSize] = useState(10);
|
||||
const [activeFilter, setActiveFilter] = useState<string>(filterAction);
|
||||
const [selectedRequestId, setSelectedRequestId] = useState<string | null>(null);
|
||||
const [selectedRequestId, setSelectedRequestId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
const filteredLogs = logs.filter(
|
||||
(log) => activeFilter === "all" || log.action === activeFilter
|
||||
(log) => activeFilter === "all" || log.action === activeFilter,
|
||||
);
|
||||
const displayLogs = filteredLogs.slice(0, sampleSize);
|
||||
const total = totalLogs ?? logs.length;
|
||||
|
|
@ -117,14 +126,14 @@ export function LogViewer({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="bg-background border border-border rounded-lg">
|
||||
<div className="p-4 border-b border-border">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-gray-900">
|
||||
<h3 className="text-base font-semibold text-foreground">
|
||||
{guardrailName ? `Logs — ${guardrailName}` : "Request Logs"}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{logsLoading
|
||||
? "Loading…"
|
||||
: logs.length > 0
|
||||
|
|
@ -138,22 +147,24 @@ export function LogViewer({
|
|||
{filters.map((f) => (
|
||||
<Button
|
||||
key={f}
|
||||
type={activeFilter === f ? "primary" : "default"}
|
||||
size="small"
|
||||
variant={activeFilter === f ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setActiveFilter(f)}
|
||||
>
|
||||
{f.charAt(0).toUpperCase() + f.slice(1)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="h-4 w-px bg-gray-200" />
|
||||
<div className="h-4 w-px bg-border" />
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-gray-500 mr-1">Sample:</span>
|
||||
<span className="text-xs text-muted-foreground mr-1">
|
||||
Sample:
|
||||
</span>
|
||||
{sampleSizes.map((size) => (
|
||||
<Button
|
||||
key={size}
|
||||
type={sampleSize === size ? "primary" : "default"}
|
||||
size="small"
|
||||
variant={sampleSize === size ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSampleSize(size)}
|
||||
>
|
||||
{size}
|
||||
|
|
@ -167,16 +178,16 @@ export function LogViewer({
|
|||
|
||||
{logsLoading && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spin />
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
{!logsLoading && displayLogs.length === 0 && (
|
||||
<div className="py-12 text-center text-sm text-gray-500">
|
||||
<div className="py-12 text-center text-sm text-muted-foreground">
|
||||
No logs to display. Adjust filters or date range.
|
||||
</div>
|
||||
)}
|
||||
{!logsLoading && displayLogs.length > 0 && (
|
||||
<div className="divide-y divide-gray-100">
|
||||
<div className="divide-y divide-border">
|
||||
{displayLogs.map((log) => {
|
||||
const config = actionConfig[log.action];
|
||||
const ActionIcon = config.icon;
|
||||
|
|
@ -185,7 +196,7 @@ export function LogViewer({
|
|||
key={log.id}
|
||||
type="button"
|
||||
onClick={() => handleLogClick(log)}
|
||||
className="w-full text-left px-4 py-3 hover:bg-gray-50 transition-colors flex items-start gap-3"
|
||||
className="w-full text-left px-4 py-3 hover:bg-muted transition-colors flex items-start gap-3"
|
||||
>
|
||||
<ActionIcon
|
||||
className={`w-4 h-4 mt-0.5 flex-shrink-0 ${config.color}`}
|
||||
|
|
@ -197,17 +208,21 @@ export function LogViewer({
|
|||
>
|
||||
{config.label}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">{log.timestamp}</span>
|
||||
<span className="text-xs text-gray-400">·</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{log.timestamp}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">·</span>
|
||||
{log.model && (
|
||||
<span className="text-xs text-gray-500">{log.model}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{log.model}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-800 truncate">
|
||||
<p className="text-sm text-foreground truncate">
|
||||
{log.input_snippet ?? log.input ?? "—"}
|
||||
</p>
|
||||
</div>
|
||||
<DownOutlined className="w-4 h-4 text-gray-400 flex-shrink-0 mt-1" />
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground flex-shrink-0 mt-1" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import React, { type ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MetricCardProps {
|
||||
label: string;
|
||||
value: string | number;
|
||||
/**
|
||||
* Tailwind text-color class. Defaults to a foreground-tone token.
|
||||
* Callers can pass any tone here including non-semantic ones (the
|
||||
* label/value usage is decorative, not a theme state).
|
||||
*/
|
||||
valueColor?: string;
|
||||
icon?: ReactNode;
|
||||
subtitle?: string;
|
||||
|
|
@ -11,20 +17,24 @@ interface MetricCardProps {
|
|||
export function MetricCard({
|
||||
label,
|
||||
value,
|
||||
valueColor = "text-gray-900",
|
||||
valueColor = "text-foreground",
|
||||
icon,
|
||||
subtitle,
|
||||
}: MetricCardProps) {
|
||||
return (
|
||||
<div className="h-full bg-white border border-gray-200 rounded-lg p-5 flex flex-col">
|
||||
<div className="h-full bg-background border border-border rounded-lg p-5 flex flex-col">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-gray-600">{label}</span>
|
||||
{icon && <span className="text-gray-400">{icon}</span>}
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
{icon && <span className="text-muted-foreground">{icon}</span>}
|
||||
</div>
|
||||
<div className={`text-3xl font-semibold ${valueColor} tracking-tight`}>
|
||||
<div className={cn("text-3xl font-semibold tracking-tight", valueColor)}>
|
||||
{value}
|
||||
</div>
|
||||
{subtitle && <p className="text-xs text-gray-500 mt-1">{subtitle}</p>}
|
||||
{subtitle && (
|
||||
<p className="text-xs text-muted-foreground mt-1">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue