feat(ui): view_logs/GuardrailViewer/CompliancePanel to shadcn

- antd Tooltip + raw SVG check/cross/spinner + chevron \u2192 shadcn
  Tooltip + lucide CheckCircle2/XCircle/Loader2/ChevronDown.
- Inline hex gray palette replaced with semantic tokens; compliant /
  non-compliant status pills use categorical emerald / red tones
  (file added to .eslintrc.json overrides).

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 14:56:51 +00:00
parent fe16b30108
commit d51e049dd9
No known key found for this signature in database
2 changed files with 71 additions and 63 deletions

View file

@ -68,6 +68,7 @@
"src/components/guardrails/GuardrailTestPlayground.tsx",
"src/components/vector_store_management/S3VectorsConfig.tsx",
"src/components/mcp_tools/mcp_server_cost_config.tsx",
"src/components/view_logs/GuardrailViewer/CompliancePanel.tsx",
"src/components/prompts/prompt_editor_view/PromptEditorHeader.tsx",
"src/components/prompts/prompt_editor_view/VersionHistorySidePanel.tsx",
"src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx",

View file

@ -1,5 +1,12 @@
import React, { useState, useEffect } from "react";
import { Tooltip } from "antd";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { CheckCircle2, ChevronDown, Loader2, XCircle } from "lucide-react";
import { cn } from "@/lib/utils";
import {
checkEuAiActCompliance,
checkGdprCompliance,
@ -14,35 +21,11 @@ interface CompliancePanelProps {
user?: string;
model?: string;
startTime?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
metadata?: Record<string, any>;
};
}
// -- Icons --
const CheckIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="#16A34A" strokeWidth="1.5" fill="#F0FDF4" />
<path d="M5 8l2 2 4-4" stroke="#16A34A" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
const CrossIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="#DC2626" strokeWidth="1.5" fill="#FEF2F2" />
<path d="M6 6l4 4M10 6l-4 4" stroke="#DC2626" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
const SpinnerIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="animate-spin">
<circle cx="8" cy="8" r="6" stroke="#D1D5DB" strokeWidth="2" />
<path d="M8 2a6 6 0 0 1 6 6" stroke="#6366F1" strokeWidth="2" strokeLinecap="round" />
</svg>
);
// -- Sub-components --
const ComplianceCard = ({
title,
data,
@ -57,71 +40,88 @@ const ComplianceCard = ({
const [expanded, setExpanded] = useState(false);
return (
<div className="border border-gray-200 rounded-lg bg-white">
<div className="border border-border rounded-lg bg-background">
<div
className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-gray-50 transition-colors"
className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-muted transition-colors"
onClick={() => setExpanded(!expanded)}
>
<div className="flex items-center gap-2">
{loading ? (
<SpinnerIcon />
<Loader2 className="h-4 w-4 animate-spin text-indigo-500" />
) : error ? (
<Tooltip title={error}>
<span className="text-gray-400 text-sm">--</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-muted-foreground text-sm">--</span>
</TooltipTrigger>
<TooltipContent>{error}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : data?.compliant ? (
<CheckIcon />
<CheckCircle2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
) : (
<CrossIcon />
<XCircle className="h-4 w-4 text-destructive" />
)}
<span className="font-medium text-sm text-gray-900">{title}</span>
<span className="font-medium text-sm text-foreground">{title}</span>
</div>
<div className="flex items-center gap-2">
{!loading && !error && data && (
<span
className={`px-2 py-0.5 rounded text-[11px] font-semibold uppercase ${
className={cn(
"px-2 py-0.5 rounded text-[11px] font-semibold uppercase border",
data.compliant
? "bg-green-100 text-green-700 border border-green-200"
: "bg-red-100 text-red-700 border border-red-200"
}`}
? "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-950 dark:text-emerald-300 dark:border-emerald-900"
: "bg-red-100 text-red-700 border-red-200 dark:bg-red-950 dark:text-red-300 dark:border-red-900",
)}
>
{data.compliant ? "COMPLIANT" : "NON-COMPLIANT"}
</span>
)}
{error && (
<span className="px-2 py-0.5 rounded text-[11px] font-medium bg-gray-100 text-gray-500 border border-gray-200">
<span className="px-2 py-0.5 rounded text-[11px] font-medium bg-muted text-muted-foreground border border-border">
UNAVAILABLE
</span>
)}
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
className={`transition-transform ${expanded ? "rotate-180" : ""}`}
>
<path d="M6 8l4 4 4-4" stroke="#6B7280" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<ChevronDown
className={cn(
"h-4 w-4 text-muted-foreground transition-transform",
expanded && "rotate-180",
)}
/>
</div>
</div>
{expanded && (
<div className="border-t border-gray-100 px-4 py-3">
{loading && <p className="text-sm text-gray-500">Checking compliance...</p>}
{error && <p className="text-sm text-red-600">{error}</p>}
<div className="border-t border-border px-4 py-3">
{loading && (
<p className="text-sm text-muted-foreground">
Checking compliance...
</p>
)}
{error && <p className="text-sm text-destructive">{error}</p>}
{data && (
<div className="space-y-2">
{data.checks.map((check, idx) => (
<div key={idx} className="flex items-start gap-2">
<div className="flex-shrink-0 mt-0.5">
{check.passed ? <CheckIcon /> : <CrossIcon />}
{check.passed ? (
<CheckCircle2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
) : (
<XCircle className="h-4 w-4 text-destructive" />
)}
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900">{check.check_name}</span>
<span className="text-[10px] font-mono text-gray-400">{check.article}</span>
<span className="text-sm font-medium text-foreground">
{check.check_name}
</span>
<span className="text-[10px] font-mono text-muted-foreground">
{check.article}
</span>
</div>
<p className="text-xs text-gray-500 mt-0.5">{check.detail}</p>
<p className="text-xs text-muted-foreground mt-0.5">
{check.detail}
</p>
</div>
</div>
))}
@ -133,10 +133,13 @@ const ComplianceCard = ({
);
};
// -- Main Component --
const CompliancePanel: React.FC<CompliancePanelProps> = ({ accessToken, logEntry }) => {
const [euAiActData, setEuAiActData] = useState<ComplianceResponse | null>(null);
const CompliancePanel: React.FC<CompliancePanelProps> = ({
accessToken,
logEntry,
}) => {
const [euAiActData, setEuAiActData] = useState<ComplianceResponse | null>(
null,
);
const [gdprData, setGdprData] = useState<ComplianceResponse | null>(null);
const [euAiActLoading, setEuAiActLoading] = useState(false);
const [gdprLoading, setGdprLoading] = useState(false);
@ -158,20 +161,24 @@ const CompliancePanel: React.FC<CompliancePanelProps> = ({ accessToken, logEntry
setEuAiActError(null);
checkEuAiActCompliance(accessToken, payload)
.then(setEuAiActData)
.catch((err) => setEuAiActError(err.message || "Failed to check EU AI Act compliance"))
.catch((err) =>
setEuAiActError(err.message || "Failed to check EU AI Act compliance"),
)
.finally(() => setEuAiActLoading(false));
setGdprLoading(true);
setGdprError(null);
checkGdprCompliance(accessToken, payload)
.then(setGdprData)
.catch((err) => setGdprError(err.message || "Failed to check GDPR compliance"))
.catch((err) =>
setGdprError(err.message || "Failed to check GDPR compliance"),
)
.finally(() => setGdprLoading(false));
}, [accessToken, logEntry]);
return (
<div>
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-4">
Regulatory Compliance
</h4>
<div className="space-y-3">