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:
Cursor Agent 2026-04-23 07:31:32 +00:00
parent ecaf72f078
commit 3b7b49137d
No known key found for this signature in database
8 changed files with 698 additions and 472 deletions

View file

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

View file

@ -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).

View file

@ -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>
);
}

View file

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

View file

@ -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}

View file

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

View file

@ -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>
);
})}

View file

@ -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>
);
}