feat(ui/): guardrails - team submissions via UI

This commit is contained in:
Krrish Dholakia 2026-02-28 15:41:08 -08:00
parent b232e2f564
commit a4d9c44191
48 changed files with 1294 additions and 298 deletions

View file

@ -0,0 +1,6 @@
-- DropIndex
DROP INDEX "LiteLLM_GuardrailsTable_guardrail_name_team_id_key";
-- CreateIndex
CREATE UNIQUE INDEX "LiteLLM_GuardrailsTable_guardrail_name_key" ON "LiteLLM_GuardrailsTable"("guardrail_name");

View file

@ -0,0 +1,10 @@
-- AlterTable: add submission lifecycle columns to LiteLLM_GuardrailsTable
-- status: pending_review (team-registered), active (approved), rejected. Default active for existing rows.
ALTER TABLE "LiteLLM_GuardrailsTable" ADD COLUMN "status" TEXT NOT NULL DEFAULT 'active';
ALTER TABLE "LiteLLM_GuardrailsTable" ADD COLUMN "submitted_by_user_id" TEXT;
ALTER TABLE "LiteLLM_GuardrailsTable" ADD COLUMN "submitted_by_email" TEXT;
ALTER TABLE "LiteLLM_GuardrailsTable" ADD COLUMN "submitted_at" TIMESTAMP(3);
ALTER TABLE "LiteLLM_GuardrailsTable" ADD COLUMN "reviewed_at" TIMESTAMP(3);
-- CreateIndex
CREATE INDEX "LiteLLM_GuardrailsTable_status_idx" ON "LiteLLM_GuardrailsTable"("status");

View file

@ -871,6 +871,14 @@ model LiteLLM_GuardrailsTable {
team_id String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
// Submission lifecycle: pending_review (team-registered), active (approved), rejected
status String @default("active")
submitted_by_user_id String?
submitted_by_email String?
submitted_at DateTime?
reviewed_at DateTime?
@@index([status])
}
// Daily guardrail metrics for usage dashboard (one row per guardrail per day)

View file

@ -4,6 +4,7 @@ CRUD ENDPOINTS FOR GUARDRAILS
import concurrent.futures
import inspect
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast
from fastapi import APIRouter, Depends, HTTPException
@ -12,37 +13,31 @@ from pydantic import BaseModel
from litellm._logging import verbose_proxy_logger
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.guardrails.guardrail_hooks.custom_code.code_validator import (
CustomCodeValidationError,
validate_custom_code,
)
from litellm.proxy.guardrails.guardrail_hooks.custom_code.primitives import (
get_custom_code_primitives,
)
CustomCodeValidationError, validate_custom_code)
from litellm.proxy.guardrails.guardrail_hooks.custom_code.primitives import \
get_custom_code_primitives
from litellm.proxy.guardrails.guardrail_registry import GuardrailRegistry
from litellm.proxy.guardrails.usage_endpoints import router as guardrails_usage_router
from litellm.types.guardrails import (
PII_ENTITY_CATEGORIES_MAP,
ApplyGuardrailRequest,
ApplyGuardrailResponse,
BaseLitellmParams,
BedrockGuardrailConfigModel,
Guardrail,
GuardrailEventHooks,
GuardrailInfoResponse,
GuardrailUIAddGuardrailSettings,
LakeraV2GuardrailConfigModel,
ListGuardrailsResponse,
LitellmParams,
PatchGuardrailRequest,
PiiAction,
PiiEntityType,
PresidioPresidioConfigModelUserInterface,
SupportedGuardrailIntegrations,
ToolPermissionGuardrailConfigModel,
)
from litellm.proxy.guardrails.usage_endpoints import \
router as guardrails_usage_router
from litellm.types.guardrails import (PII_ENTITY_CATEGORIES_MAP,
ApplyGuardrailRequest,
ApplyGuardrailResponse,
BaseLitellmParams,
BedrockGuardrailConfigModel, Guardrail,
GuardrailEventHooks,
GuardrailInfoResponse,
GuardrailUIAddGuardrailSettings,
LakeraV2GuardrailConfigModel,
ListGuardrailsResponse, LitellmParams,
PatchGuardrailRequest, PiiAction,
PiiEntityType,
PresidioPresidioConfigModelUserInterface,
SupportedGuardrailIntegrations,
ToolPermissionGuardrailConfigModel)
#### GUARDRAILS ENDPOINTS ####
@ -161,7 +156,8 @@ async def list_guardrails_v2():
```
"""
from litellm.litellm_core_utils.litellm_logging import _get_masked_values
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.guardrails.guardrail_registry import \
IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
if prisma_client is None:
@ -303,7 +299,8 @@ async def create_guardrail(
}
```
"""
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.guardrails.guardrail_registry import \
IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
@ -401,7 +398,8 @@ async def update_guardrail(
}
```
"""
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.guardrails.guardrail_registry import \
IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
@ -477,7 +475,8 @@ async def delete_guardrail(
}
```
"""
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.guardrails.guardrail_registry import \
IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
@ -579,7 +578,8 @@ async def patch_guardrail(
}
```
"""
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.guardrails.guardrail_registry import \
IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
@ -707,7 +707,8 @@ async def get_guardrail_info(guardrail_id: str):
"""
from litellm.litellm_core_utils.litellm_logging import _get_masked_values
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.guardrails.guardrail_registry import \
IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
from litellm.types.guardrails import GUARDRAIL_DEFINITION_LOCATION
@ -782,10 +783,8 @@ async def get_guardrail_ui_settings():
- Content filter settings (patterns and categories)
"""
from litellm.proxy.guardrails.guardrail_hooks.litellm_content_filter.patterns import (
PATTERN_CATEGORIES,
get_available_content_categories,
get_pattern_metadata,
)
PATTERN_CATEGORIES, get_available_content_categories,
get_pattern_metadata)
# Convert the PII_ENTITY_CATEGORIES_MAP to the format expected by the UI
category_maps = []
@ -1369,7 +1368,8 @@ async def get_provider_specific_params():
}
### get the config model for the guardrail - go through the registry and get the config model for the guardrail
from litellm.proxy.guardrails.guardrail_registry import guardrail_class_registry
from litellm.proxy.guardrails.guardrail_registry import \
guardrail_class_registry
for guardrail_name, guardrail_class in guardrail_class_registry.items():
guardrail_config_model = guardrail_class.get_config_model()

View file

@ -11,29 +11,21 @@ from litellm._logging import verbose_proxy_logger
from litellm._uuid import uuid
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.proxy.utils import PrismaClient
from litellm.proxy.guardrails.guardrail_hooks.grayswan import GraySwanGuardrail
from litellm.proxy.guardrails.guardrail_hooks.grayswan import \
initialize_guardrail as initialize_grayswan
from litellm.proxy.types_utils.utils import get_instance_fn
from litellm.proxy.utils import PrismaClient
from litellm.secret_managers.main import get_secret
from litellm.types.guardrails import (
Guardrail,
GuardrailEventHooks,
LakeraCategoryThresholds,
LitellmParams,
SupportedGuardrailIntegrations,
)
from litellm.proxy.guardrails.guardrail_hooks.grayswan import (
GraySwanGuardrail,
initialize_guardrail as initialize_grayswan,
)
from litellm.types.guardrails import (Guardrail, GuardrailEventHooks,
LakeraCategoryThresholds, LitellmParams,
SupportedGuardrailIntegrations)
from .guardrail_initializers import (
initialize_bedrock,
initialize_hide_secrets,
initialize_lakera,
initialize_lakera_v2,
initialize_presidio,
initialize_tool_permission,
)
from .guardrail_initializers import (initialize_bedrock,
initialize_hide_secrets,
initialize_lakera, initialize_lakera_v2,
initialize_presidio,
initialize_tool_permission)
guardrail_initializer_registry = {
SupportedGuardrailIntegrations.BEDROCK.value: initialize_bedrock,
@ -327,11 +319,13 @@ class GuardrailRegistry:
prisma_client: PrismaClient,
) -> List[Guardrail]:
"""
Get all guardrails from the database
Get all active guardrails from the database.
Only rows with status == "active" are returned (pending_review and rejected are excluded).
"""
try:
guardrails_from_db = (
await prisma_client.db.litellm_guardrailstable.find_many(
where={"status": "active"},
order={"created_at": "desc"},
)
)

View file

@ -871,6 +871,14 @@ model LiteLLM_GuardrailsTable {
team_id String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
// Submission lifecycle: pending_review (team-registered), active (approved), rejected
status String @default("active")
submitted_by_user_id String?
submitted_by_email String?
submitted_at DateTime?
reviewed_at DateTime?
@@index([status])
}
// Daily guardrail metrics for usage dashboard (one row per guardrail per day)

View file

@ -871,6 +871,14 @@ model LiteLLM_GuardrailsTable {
team_id String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
// Submission lifecycle: pending_review (team-registered), active (approved), rejected
status String @default("active")
submitted_by_user_id String?
submitted_by_email String?
submitted_at DateTime?
reviewed_at DateTime?
@@index([status])
}
// Daily guardrail metrics for usage dashboard (one row per guardrail per day)

View file

@ -12984,6 +12984,21 @@
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
"integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
}
}

View file

@ -1,249 +1,15 @@
import React, { useState, useEffect } from "react";
import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
import { Dropdown } from "antd";
import { DownOutlined, PlusOutlined, CodeOutlined } from "@ant-design/icons";
import { getGuardrailsList, deleteGuardrailCall } from "./networking";
import AddGuardrailForm from "./guardrails/add_guardrail_form";
import GuardrailTable from "./guardrails/guardrail_table";
import { isAdminRole } from "@/utils/roles";
import GuardrailInfoView from "./guardrails/guardrail_info";
import GuardrailTestPlayground from "./guardrails/GuardrailTestPlayground";
import NotificationsManager from "./molecules/notifications_manager";
import { Guardrail, GuardrailDefinitionLocation } from "./guardrails/types";
import DeleteResourceModal from "./common_components/DeleteResourceModal";
import { getGuardrailLogoAndName } from "./guardrails/guardrail_info_helpers";
import { CustomCodeModal } from "./guardrails/custom_code";
import GuardrailGarden from "./guardrails/guardrail_garden";
import React from "react";
import { GuardrailsPage } from "./guardrails/GuardrailsPage";
interface GuardrailsPanelProps {
accessToken: string | null;
userRole?: string;
}
interface GuardrailItem {
guardrail_id?: string;
guardrail_name: string | null;
litellm_params: {
guardrail: string;
mode: string;
default_on: boolean;
};
guardrail_info: Record<string, any> | null;
created_at?: string;
updated_at?: string;
guardrail_definition_location: GuardrailDefinitionLocation;
}
interface GuardrailsResponse {
guardrails: Guardrail[];
}
const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole }) => {
const [guardrailsList, setGuardrailsList] = useState<Guardrail[]>([]);
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
const [isCustomCodeModalVisible, setIsCustomCodeModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [guardrailToDelete, setGuardrailToDelete] = useState<Guardrail | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedGuardrailId, setSelectedGuardrailId] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<number>(0);
const isAdmin = userRole ? isAdminRole(userRole) : false;
const fetchGuardrails = async () => {
if (!accessToken) {
return;
}
setIsLoading(true);
try {
const response: GuardrailsResponse = await getGuardrailsList(accessToken);
console.log(`guardrails: ${JSON.stringify(response)}`);
setGuardrailsList(response.guardrails);
} catch (error) {
console.error("Error fetching guardrails:", error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchGuardrails();
}, [accessToken]);
const handleAddGuardrail = () => {
if (selectedGuardrailId) {
setSelectedGuardrailId(null);
}
setIsAddModalVisible(true);
};
const handleAddCustomCodeGuardrail = () => {
if (selectedGuardrailId) {
setSelectedGuardrailId(null);
}
setIsCustomCodeModalVisible(true);
};
const handleCloseModal = () => {
setIsAddModalVisible(false);
};
const handleCloseCustomCodeModal = () => {
setIsCustomCodeModalVisible(false);
};
const handleSuccess = () => {
fetchGuardrails();
};
const handleDeleteClick = (guardrailId: string, guardrailName: string) => {
const guardrail = guardrailsList.find((g) => g.guardrail_id === guardrailId) || null;
setGuardrailToDelete(guardrail);
setIsDeleteModalOpen(true);
};
const handleDeleteConfirm = async () => {
if (!guardrailToDelete || !accessToken) return;
setIsDeleting(true);
try {
await deleteGuardrailCall(accessToken, guardrailToDelete.guardrail_id);
NotificationsManager.success(`Guardrail "${guardrailToDelete.guardrail_name}" deleted successfully`);
await fetchGuardrails();
} catch (error) {
console.error("Error deleting guardrail:", error);
NotificationsManager.fromBackend("Failed to delete guardrail");
} finally {
setIsDeleting(false);
setIsDeleteModalOpen(false);
setGuardrailToDelete(null);
}
};
const handleDeleteCancel = () => {
setIsDeleteModalOpen(false);
setGuardrailToDelete(null);
};
const providerDisplayName =
guardrailToDelete && guardrailToDelete.litellm_params
? getGuardrailLogoAndName(guardrailToDelete.litellm_params.guardrail).displayName
: undefined;
const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken }) => {
return (
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
<TabGroup index={activeTab} onIndexChange={setActiveTab}>
<TabList className="mb-4">
<Tab>Guardrail Garden</Tab>
<Tab>Guardrails</Tab>
<Tab disabled={!accessToken || guardrailsList.length === 0}>Test Playground</Tab>
</TabList>
<TabPanels>
{/* Guardrail Garden Tab */}
<TabPanel>
<GuardrailGarden
accessToken={accessToken}
onGuardrailCreated={handleSuccess}
/>
</TabPanel>
{/* Existing Guardrails Tab */}
<TabPanel>
<div className="flex justify-between items-center mb-4">
<Dropdown
menu={{
items: [
{
key: "provider",
icon: <PlusOutlined />,
label: "Add Provider Guardrail",
onClick: handleAddGuardrail,
},
{
key: "custom_code",
icon: <CodeOutlined />,
label: "Create Custom Code Guardrail",
onClick: handleAddCustomCodeGuardrail,
},
],
}}
trigger={["click"]}
disabled={!accessToken}
>
<Button disabled={!accessToken}>
+ Add New Guardrail <DownOutlined className="ml-2" />
</Button>
</Dropdown>
</div>
{selectedGuardrailId ? (
<GuardrailInfoView
guardrailId={selectedGuardrailId}
onClose={() => setSelectedGuardrailId(null)}
accessToken={accessToken}
isAdmin={isAdmin}
/>
) : (
<GuardrailTable
guardrailsList={guardrailsList}
isLoading={isLoading}
onDeleteClick={handleDeleteClick}
accessToken={accessToken}
onGuardrailUpdated={fetchGuardrails}
isAdmin={isAdmin}
onGuardrailClick={(id) => setSelectedGuardrailId(id)}
/>
)}
<AddGuardrailForm
visible={isAddModalVisible}
onClose={handleCloseModal}
accessToken={accessToken}
onSuccess={handleSuccess}
/>
<CustomCodeModal
visible={isCustomCodeModalVisible}
onClose={handleCloseCustomCodeModal}
accessToken={accessToken}
onSuccess={handleSuccess}
/>
<DeleteResourceModal
isOpen={isDeleteModalOpen}
title="Delete Guardrail"
message={`Are you sure you want to delete guardrail: ${guardrailToDelete?.guardrail_name}? This action cannot be undone.`}
resourceInformationTitle="Guardrail Information"
resourceInformation={[
{ label: "Name", value: guardrailToDelete?.guardrail_name },
{ label: "ID", value: guardrailToDelete?.guardrail_id, code: true },
{ label: "Provider", value: providerDisplayName },
{ label: "Mode", value: guardrailToDelete?.litellm_params.mode },
{
label: "Default On",
value: guardrailToDelete?.litellm_params.default_on ? "Yes" : "No",
},
]}
onCancel={handleDeleteCancel}
onOk={handleDeleteConfirm}
confirmLoading={isDeleting}
/>
</TabPanel>
{/* Test Playground Tab */}
<TabPanel>
<GuardrailTestPlayground
guardrailsList={guardrailsList}
isLoading={isLoading}
accessToken={accessToken}
onClose={() => setActiveTab(0)}
/>
</TabPanel>
</TabPanels>
</TabGroup>
<GuardrailsPage accessToken={accessToken} />
</div>
);
};

View file

@ -0,0 +1,177 @@
"use client";
import React, { useState } from "react";
import {
SearchIcon,
ArrowRightIcon,
CheckCircleIcon,
ShieldIcon,
} from "lucide-react";
const MOCK_GARDEN_CARDS = [
{
title: "Denied Financial Advice",
description:
"Detects requests for personalized financial advice, investment recommendations, or financial...",
f1Score: "100%",
testCases: 207,
},
{
title: "Insults & Personal Attacks",
description:
"Detects insults, name-calling, and personal attacks directed at the chatbot, staff, or other people.",
f1Score: "100%",
testCases: 299,
},
{
title: "Denied Legal Advice",
description:
"Detects requests for unauthorized legal advice, case analysis, or legal recommendations.",
},
{
title: "Denied Medical Advice",
description:
"Detects requests for medical diagnosis, treatment recommendations, or health advice.",
},
{
title: "Harmful Violence",
description:
"Detects content related to violence, criminal planning, attacks, and violent threats.",
},
{
title: "Harmful Self-Harm",
description:
"Detects content related to self-harm, suicide, and dangerous self-destructive behavior.",
},
{
title: "Harmful Child Safety",
description:
"Detects content that could endanger child safety or exploit minors.",
},
{
title: "Harmful Illegal Weapons",
description:
"Detects content related to illegal weapons manufacturing, distribution, or acquisition.",
},
{
title: "Bias: Gender",
description:
"Detects gender-based discrimination, stereotypes, and biased language.",
},
{
title: "Bias: Racial",
description:
"Detects racial discrimination, stereotypes, and racially biased content.",
},
];
type GuardrailCardProps = {
title: string;
description: string;
f1Score?: string;
testCases?: number;
};
function GuardrailCard({
title,
description,
f1Score,
testCases,
}: GuardrailCardProps) {
return (
<div className="border border-gray-200 rounded-lg p-4 bg-white hover:border-gray-300 transition-colors cursor-pointer">
<div className="flex items-start gap-3 mb-2">
<div className="flex-shrink-0 w-9 h-9 rounded-full bg-blue-100 flex items-center justify-center">
<ShieldIcon className="h-4 w-4 text-blue-500" />
</div>
<h3 className="text-sm font-semibold text-gray-900 leading-tight">
{title}
</h3>
</div>
<p className="text-xs text-gray-500 leading-relaxed mb-2">
{description}
</p>
{f1Score && testCases !== undefined && (
<div className="flex items-center gap-1 text-xs text-green-600">
<CheckCircleIcon className="h-3.5 w-3.5" />
<span>
F1: {f1Score} · {testCases} test cases
</span>
</div>
)}
</div>
);
}
export function GuardrailGardenTab() {
const [search, setSearch] = useState("");
const filteredCards = MOCK_GARDEN_CARDS.filter(
(card) =>
!search ||
card.title.toLowerCase().includes(search.toLowerCase()) ||
card.description.toLowerCase().includes(search.toLowerCase())
);
return (
<div className="p-6">
{/* Search */}
<div className="relative mb-8">
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Search guardrails"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-9 pr-4 py-2 border border-gray-200 rounded-md text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
{/* LiteLLM Content Filter Section */}
<div className="mb-6">
<div className="flex items-center justify-between mb-1">
<h2 className="text-lg font-semibold text-gray-900">
LiteLLM Content Filter
</h2>
<button
type="button"
className="text-sm text-blue-500 hover:text-blue-600 flex items-center gap-1"
>
<ArrowRightIcon className="h-3.5 w-3.5" />
Show all ({MOCK_GARDEN_CARDS.length})
</button>
</div>
<p className="text-sm text-gray-500 mb-5">
Built-in guardrails powered by LiteLLM. Zero latency, no external
dependencies, no additional cost.
</p>
{/* Row 1 */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 mb-3">
{filteredCards.slice(0, 6).map((card) => (
<GuardrailCard
key={card.title}
title={card.title}
description={card.description}
f1Score={card.f1Score}
testCases={card.testCases}
/>
))}
</div>
{/* Row 2 */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
{filteredCards.slice(6, 10).map((card) => (
<GuardrailCard
key={card.title}
title={card.title}
description={card.description}
f1Score={card.f1Score}
testCases={card.testCases}
/>
))}
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,81 @@
"use client";
import React from "react";
const MOCK_GUARDRAILS = [
{
name: "Denied Financial Advice",
type: "LiteLLM Built-in",
status: "Active" as const,
appliedTo: "All routes",
},
{
name: "Insults & Personal Attacks",
type: "LiteLLM Built-in",
status: "Active" as const,
appliedTo: "Customer-facing",
},
{
name: "Prompt Injection Detector",
type: "Team Custom",
status: "Active" as const,
appliedTo: "ML Platform team",
},
];
export function GuardrailsListTab() {
return (
<div className="p-6">
<div className="mb-4">
<h2 className="text-lg font-semibold text-gray-900 mb-1">Guardrails</h2>
<p className="text-sm text-gray-500">
Configure and manage active guardrails for your AI gateway.
</p>
</div>
<div className="border border-gray-200 rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
<th className="text-left px-4 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wide">
Name
</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wide">
Type
</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wide">
Status
</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wide">
Applied To
</th>
</tr>
</thead>
<tbody>
{MOCK_GUARDRAILS.map((row, i) => (
<tr
key={row.name}
className={
i < MOCK_GUARDRAILS.length - 1
? "border-b border-gray-100"
: ""
}
>
<td className="px-4 py-3 font-medium text-gray-900">
{row.name}
</td>
<td className="px-4 py-3 text-gray-500">{row.type}</td>
<td className="px-4 py-3">
<span className="inline-flex items-center gap-1.5 text-xs font-medium px-2 py-0.5 rounded-full bg-green-50 text-green-700">
<span className="w-1.5 h-1.5 rounded-full bg-green-500" />
{row.status}
</span>
</td>
<td className="px-4 py-3 text-gray-500">{row.appliedTo}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

View file

@ -0,0 +1,62 @@
"use client";
import React, { useState } from "react";
import { GuardrailGardenTab } from "./GuardrailGardenTab";
import { GuardrailsListTab } from "./GuardrailsListTab";
import { PlaygroundTab } from "./PlaygroundTab";
import { TeamGuardrailsTab } from "./TeamGuardrailsTab";
type Tab = "garden" | "guardrails" | "playground" | "team";
const TABS: { id: Tab; label: string }[] = [
{ id: "garden", label: "Guardrail Garden" },
{ id: "guardrails", label: "Guardrails" },
{ id: "playground", label: "Test Playground" },
{ id: "team", label: "Team Guardrails" },
];
interface GuardrailsPageProps {
accessToken?: string | null;
}
export function GuardrailsPage({ accessToken }: GuardrailsPageProps) {
const [activeTab, setActiveTab] = useState<Tab>("garden");
return (
<div className="flex flex-col w-full min-h-0 flex-1">
{/* Tab bar */}
<div className="border-b border-gray-200 px-6 flex-shrink-0 bg-white">
<div className="flex items-center gap-0">
{TABS.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
className={`relative px-4 py-3.5 text-sm font-medium transition-colors focus:outline-none ${
activeTab === tab.id
? "text-blue-500"
: "text-gray-500 hover:text-gray-700"
} ${tab.id === "team" ? "flex items-center gap-1.5" : ""}`}
>
{tab.id === "team" && (
<span className="inline-flex items-center justify-center w-1.5 h-1.5 rounded-full bg-blue-500" />
)}
{tab.label}
{activeTab === tab.id && (
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500 rounded-t-full" />
)}
</button>
))}
</div>
</div>
{/* Tab content */}
<div className="flex-1 overflow-auto bg-white">
{activeTab === "garden" && <GuardrailGardenTab />}
{activeTab === "guardrails" && <GuardrailsListTab />}
{activeTab === "playground" && <PlaygroundTab />}
{activeTab === "team" && <TeamGuardrailsTab />}
</div>
</div>
);
}

View file

@ -0,0 +1,63 @@
"use client";
import React, { useState } from "react";
export function PlaygroundTab() {
const [prompt, setPrompt] = useState("");
const [result, setResult] = useState<string | null>(null);
function handleTest() {
setResult(
prompt.toLowerCase().includes("financial")
? "🚫 Blocked by: Denied Financial Advice guardrail (confidence: 97%)"
: "✅ Passed all guardrails. Safe to proceed."
);
}
return (
<div className="p-6 max-w-2xl">
<div className="mb-4">
<h2 className="text-lg font-semibold text-gray-900 mb-1">
Test Playground
</h2>
<p className="text-sm text-gray-500">
Test your guardrails against sample prompts to verify they work as
expected.
</p>
</div>
<div className="space-y-4">
<div>
<label className="block text-xs font-semibold text-gray-700 mb-1.5">
Test Prompt
</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Enter a prompt to test against your guardrails..."
rows={4}
className="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 resize-none"
/>
</div>
<button
type="button"
onClick={handleTest}
disabled={!prompt.trim()}
className="bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-medium px-4 py-2 rounded-md transition-colors"
>
Run Test
</button>
{result && (
<div
className={`border rounded-lg px-4 py-3 text-sm font-medium ${
result.startsWith("🚫")
? "border-red-200 bg-red-50 text-red-700"
: "border-green-200 bg-green-50 text-green-700"
}`}
>
{result}
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,798 @@
"use client";
import React, { useState } from "react";
import {
SearchIcon,
PlusIcon,
ChevronDownIcon,
ChevronUpIcon,
XIcon,
CheckIcon,
ExternalLinkIcon,
KeyIcon,
ServerIcon,
AlertCircleIcon,
InfoIcon,
} from "lucide-react";
type GuardrailStatus = "active" | "pending" | "rejected";
type TeamGuardrail = {
id: string;
team: string;
name: string;
endpoint: string;
status: GuardrailStatus;
model: string;
forwardKey: boolean;
description: string;
method: "POST" | "GET";
customHeaders: {
key: string;
value: string;
}[];
submittedAt: string;
submittedBy: string;
};
const SAMPLE_GUARDRAILS: TeamGuardrail[] = [
{
id: "1",
team: "ML Platform",
name: "Prompt Injection Detector",
endpoint: "https://guardrails.ml-platform.internal/validate",
status: "active",
model: "gpt-4o-mini",
forwardKey: true,
description:
"Detects prompt injection attacks and jailbreak attempts before they reach the model.",
method: "POST",
customHeaders: [
{ key: "X-Service-Name", value: "ml-platform-guardrail" },
{ key: "X-Environment", value: "production" },
],
submittedAt: "2024-01-15",
submittedBy: "alice@company.com",
},
{
id: "2",
team: "Data Science",
name: "PII Redaction Guard",
endpoint: "https://ds-guardrails.company.com/pii-check",
status: "active",
model: "claude-3-haiku",
forwardKey: true,
description:
"Identifies and redacts personally identifiable information from prompts and responses.",
method: "POST",
customHeaders: [{ key: "X-Team", value: "data-science" }],
submittedAt: "2024-01-18",
submittedBy: "bob@company.com",
},
{
id: "3",
team: "Security",
name: "SQL Injection Preventer",
endpoint: "https://security-gd.internal/sql-guard",
status: "pending",
model: "gpt-4o",
forwardKey: false,
description:
"Prevents SQL injection patterns from being passed through AI-generated queries.",
method: "POST",
customHeaders: [],
submittedAt: "2024-02-01",
submittedBy: "charlie@company.com",
},
{
id: "4",
team: "Customer Success",
name: "Tone & Brand Compliance",
endpoint: "https://cs-guardrails.company.com/brand",
status: "pending",
model: "gpt-4o-mini",
forwardKey: true,
description:
"Ensures AI responses align with brand voice guidelines and customer-facing tone standards.",
method: "POST",
customHeaders: [
{ key: "X-Brand-Version", value: "v2.1" },
{ key: "X-Strictness", value: "high" },
],
submittedAt: "2024-02-05",
submittedBy: "diana@company.com",
},
{
id: "5",
team: "Legal",
name: "Legal Disclaimer Enforcer",
endpoint: "https://legal-gd.internal/compliance",
status: "active",
model: "gpt-4-turbo",
forwardKey: false,
description:
"Ensures all AI-generated content includes required legal disclaimers and compliance notices.",
method: "POST",
customHeaders: [{ key: "X-Jurisdiction", value: "US" }],
submittedAt: "2024-01-10",
submittedBy: "eve@company.com",
},
{
id: "6",
team: "Finance",
name: "Financial Advice Blocker",
endpoint: "https://finance-gd.company.com/validate",
status: "rejected",
model: "gpt-4o-mini",
forwardKey: true,
description:
"Blocks specific financial advice patterns not covered by the built-in LiteLLM filter.",
method: "POST",
customHeaders: [],
submittedAt: "2024-01-28",
submittedBy: "frank@company.com",
},
];
const STATUS_CONFIG: Record<
GuardrailStatus,
{ label: string; bg: string; text: string; dot: string }
> = {
active: {
label: "Active",
bg: "bg-green-50",
text: "text-green-700",
dot: "bg-green-500",
},
pending: {
label: "Pending Review",
bg: "bg-yellow-50",
text: "text-yellow-700",
dot: "bg-yellow-500",
},
rejected: {
label: "Rejected",
bg: "bg-red-50",
text: "text-red-700",
dot: "bg-red-500",
},
};
const TEAM_COLORS: Record<string, string> = {
"ML Platform": "bg-purple-100 text-purple-700",
"Data Science": "bg-blue-100 text-blue-700",
Security: "bg-red-100 text-red-700",
"Customer Success": "bg-orange-100 text-orange-700",
Legal: "bg-gray-100 text-gray-700",
Finance: "bg-green-100 text-green-700",
};
function StatCard({
label,
value,
color,
}: {
label: string;
value: number;
color: string;
}) {
return (
<div className="bg-white border border-gray-200 rounded-lg px-4 py-3">
<div className={`text-2xl font-bold ${color}`}>{value}</div>
<div className="text-xs text-gray-500 mt-0.5">{label}</div>
</div>
);
}
function Toggle({
enabled,
onToggle,
}: {
enabled: boolean;
onToggle: () => void;
}) {
return (
<button
type="button"
onClick={onToggle}
role="switch"
aria-checked={enabled}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 ${
enabled ? "bg-blue-500" : "bg-gray-200"
}`}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow transition-transform ${
enabled ? "translate-x-4" : "translate-x-0.5"
}`}
/>
</button>
);
}
type GuardrailCardProps = {
guardrail: TeamGuardrail;
isSelected: boolean;
isHeadersExpanded: boolean;
onSelect: () => void;
onToggleForwardKey: () => void;
onToggleHeaders: () => void;
onApprove: () => void;
onReject: () => void;
};
function GuardrailCard({
guardrail: g,
isSelected,
isHeadersExpanded,
onSelect,
onToggleForwardKey,
onToggleHeaders,
onApprove,
onReject,
}: GuardrailCardProps) {
const status = STATUS_CONFIG[g.status];
const teamColor = TEAM_COLORS[g.team] ?? "bg-gray-100 text-gray-700";
return (
<div
className={`bg-white border rounded-lg p-4 transition-all ${
isSelected ? "border-blue-400 ring-1 ring-blue-200" : "border-gray-200"
}`}
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1.5 flex-wrap">
<span
className={`text-xs font-medium px-2 py-0.5 rounded-full ${teamColor}`}
>
Team: {g.team}
</span>
<span
className={`inline-flex items-center gap-1.5 text-xs font-medium px-2 py-0.5 rounded-full ${status.bg} ${status.text}`}
>
<span className={`w-1.5 h-1.5 rounded-full ${status.dot}`} />
{status.label}
</span>
</div>
<h3 className="text-sm font-semibold text-gray-900 mb-1">{g.name}</h3>
<p className="text-xs text-gray-500 mb-2 line-clamp-1">
{g.description}
</p>
<div className="flex items-center gap-1.5 mb-2">
<ServerIcon className="h-3.5 w-3.5 text-gray-400 flex-shrink-0" />
<code className="text-xs text-gray-500 font-mono truncate">
{g.endpoint}
</code>
</div>
<div className="flex items-center gap-4 text-xs text-gray-500">
<span>
Model: <span className="font-medium text-gray-700">{g.model}</span>
</span>
<span>
Submitted:{" "}
<span className="font-medium text-gray-700">{g.submittedAt}</span>
</span>
</div>
</div>
<div className="flex flex-col items-end gap-2 flex-shrink-0">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 whitespace-nowrap">
Forward API Key
</span>
<Toggle enabled={g.forwardKey} onToggle={onToggleForwardKey} />
</div>
<div className="flex items-center gap-2 mt-1">
<button
type="button"
onClick={onSelect}
className="text-xs border border-gray-300 text-gray-600 hover:bg-gray-50 px-3 py-1.5 rounded-md transition-colors font-medium"
>
{isSelected ? "Close" : "Review"}
</button>
{g.status === "pending" && (
<>
<button
type="button"
onClick={onApprove}
className="text-xs bg-green-500 hover:bg-green-600 text-white px-3 py-1.5 rounded-md transition-colors font-medium"
>
Approve
</button>
<button
type="button"
onClick={onReject}
className="text-xs border border-red-300 text-red-600 hover:bg-red-50 px-3 py-1.5 rounded-md transition-colors font-medium"
>
Reject
</button>
</>
)}
</div>
</div>
</div>
<div className="mt-3 pt-3 border-t border-gray-100">
<button
type="button"
onClick={onToggleHeaders}
className="flex items-center gap-1.5 text-xs text-gray-500 hover:text-gray-700 transition-colors"
>
{isHeadersExpanded ? (
<ChevronUpIcon className="h-3.5 w-3.5" />
) : (
<ChevronDownIcon className="h-3.5 w-3.5" />
)}
Custom Headers
{g.customHeaders.length > 0 && (
<span className="ml-1 bg-gray-100 text-gray-600 rounded-full px-1.5 py-0.5 text-xs">
{g.customHeaders.length}
</span>
)}
</button>
{isHeadersExpanded && (
<div className="mt-2">
{g.customHeaders.length === 0 ? (
<p className="text-xs text-gray-400 italic">
No custom headers configured.
</p>
) : (
<div className="space-y-1">
{g.customHeaders.map((h, i) => (
<div
key={`${h.key}-${i}`}
className="flex items-center gap-2 text-xs font-mono"
>
<span className="text-gray-500 bg-gray-50 border border-gray-200 rounded px-2 py-0.5">
{h.key}
</span>
<span className="text-gray-400">:</span>
<span className="text-gray-700 bg-gray-50 border border-gray-200 rounded px-2 py-0.5">
{h.value}
</span>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
);
}
function ConfigRow({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
<div>
<div className="text-xs font-semibold text-gray-500 mb-1">{label}</div>
<div>{children}</div>
</div>
);
}
type DetailPanelProps = {
guardrail: TeamGuardrail;
onClose: () => void;
onApprove: () => void;
onReject: () => void;
onToggleForwardKey: () => void;
};
function DetailPanel({
guardrail: g,
onClose,
onApprove,
onReject,
onToggleForwardKey,
}: DetailPanelProps) {
const status = STATUS_CONFIG[g.status];
const teamColor = TEAM_COLORS[g.team] ?? "bg-gray-100 text-gray-700";
return (
<div className="w-96 flex-shrink-0 bg-white overflow-auto">
<div className="p-5">
<div className="flex items-start justify-between mb-4">
<div>
<div className="flex items-center gap-2 mb-1">
<span
className={`text-xs font-medium px-2 py-0.5 rounded-full ${teamColor}`}
>
Team: {g.team}
</span>
<span
className={`inline-flex items-center gap-1.5 text-xs font-medium px-2 py-0.5 rounded-full ${status.bg} ${status.text}`}
>
<span className={`w-1.5 h-1.5 rounded-full ${status.dot}`} />
{status.label}
</span>
</div>
<h2 className="text-base font-semibold text-gray-900">{g.name}</h2>
<p className="text-xs text-gray-500 mt-0.5">
Submitted by {g.submittedBy} on {g.submittedAt}
</p>
</div>
<button
type="button"
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
aria-label="Close detail panel"
>
<XIcon className="h-4 w-4" />
</button>
</div>
<p className="text-sm text-gray-600 mb-5">{g.description}</p>
<div className="space-y-4">
<ConfigRow label="Endpoint">
<div className="flex items-center gap-1.5">
<code className="text-xs font-mono text-gray-700 break-all">
{g.endpoint}
</code>
<a
href={g.endpoint}
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-blue-500 flex-shrink-0"
>
<ExternalLinkIcon className="h-3.5 w-3.5" />
</a>
</div>
</ConfigRow>
<ConfigRow label="Method">
<span className="text-xs font-mono font-medium text-gray-700 bg-gray-100 px-2 py-0.5 rounded">
{g.method}
</span>
</ConfigRow>
<ConfigRow label="Validation Model">
<span className="text-xs font-medium text-gray-700">{g.model}</span>
</ConfigRow>
<div className="border border-blue-100 bg-blue-50 rounded-lg p-3">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1.5">
<KeyIcon className="h-3.5 w-3.5 text-blue-500" />
<span className="text-xs font-semibold text-blue-800">
Forward LiteLLM API Key
</span>
</div>
<Toggle enabled={g.forwardKey} onToggle={onToggleForwardKey} />
</div>
<p className="text-xs text-blue-700 leading-relaxed">
When enabled, the caller&apos;s LiteLLM API key is forwarded as an{" "}
<code className="font-mono bg-blue-100 px-1 rounded">
Authorization
</code>{" "}
header to your guardrail endpoint. This allows your guardrail to
authenticate model calls using the original caller&apos;s
credentials.
</p>
</div>
<div>
<div className="flex items-center gap-1.5 mb-2">
<span className="text-xs font-semibold text-gray-700">
Custom Headers
</span>
{g.customHeaders.length > 0 && (
<span className="bg-gray-100 text-gray-600 rounded-full px-1.5 py-0.5 text-xs">
{g.customHeaders.length}
</span>
)}
</div>
{g.customHeaders.length === 0 ? (
<p className="text-xs text-gray-400 italic">
No custom headers configured.
</p>
) : (
<div className="border border-gray-200 rounded-md overflow-hidden">
<table className="w-full text-xs">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
<th className="text-left px-3 py-2 text-gray-500 font-medium">
Key
</th>
<th className="text-left px-3 py-2 text-gray-500 font-medium">
Value
</th>
</tr>
</thead>
<tbody>
{g.customHeaders.map((h, i) => (
<tr
key={`${h.key}-${i}`}
className={
i < g.customHeaders.length - 1
? "border-b border-gray-100"
: ""
}
>
<td className="px-3 py-2 font-mono text-gray-700">
{h.key}
</td>
<td className="px-3 py-2 font-mono text-gray-600">
{h.value}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<div className="flex items-start gap-2 bg-gray-50 border border-gray-200 rounded-lg p-3">
<InfoIcon className="h-3.5 w-3.5 text-gray-400 flex-shrink-0 mt-0.5" />
<p className="text-xs text-gray-500 leading-relaxed">
This guardrail runs on a separate instance. It receives the user
request, validates it using{" "}
<span className="font-medium">{g.model}</span>, and forwards the
result to the next step in the pipeline. See{" "}
<a
href="https://docs.litellm.ai/docs/adding_provider/generic_guardrail_api"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
LiteLLM Generic Guardrail API docs
</a>{" "}
for configuration details.
</p>
</div>
</div>
<div className="mt-5 pt-4 border-t border-gray-100 space-y-2">
<button
type="button"
className="w-full flex items-center justify-center gap-2 border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium py-2 rounded-md transition-colors"
>
<ExternalLinkIcon className="h-4 w-4" />
Test Endpoint
</button>
{g.status === "pending" && (
<div className="flex gap-2">
<button
type="button"
onClick={onApprove}
className="flex-1 flex items-center justify-center gap-1.5 bg-green-500 hover:bg-green-600 text-white text-sm font-medium py-2 rounded-md transition-colors"
>
<CheckIcon className="h-4 w-4" />
Approve
</button>
<button
type="button"
onClick={onReject}
className="flex-1 flex items-center justify-center gap-1.5 border border-red-300 text-red-600 hover:bg-red-50 text-sm font-medium py-2 rounded-md transition-colors"
>
<XIcon className="h-4 w-4" />
Reject
</button>
</div>
)}
</div>
</div>
</div>
);
}
type ConfirmDialogProps = {
action: "approve" | "reject";
guardrailName: string;
onConfirm: () => void;
onCancel: () => void;
};
function ConfirmDialog({
action,
guardrailName,
onConfirm,
onCancel,
}: ConfirmDialogProps) {
const isApprove = action === "approve";
return (
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl p-6 max-w-sm w-full mx-4">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center mb-4 ${
isApprove ? "bg-green-100" : "bg-red-100"
}`}
>
{isApprove ? (
<CheckIcon className="h-5 w-5 text-green-600" />
) : (
<AlertCircleIcon className="h-5 w-5 text-red-600" />
)}
</div>
<h3 className="text-base font-semibold text-gray-900 mb-1">
{isApprove ? "Approve Guardrail" : "Reject Guardrail"}
</h3>
<p className="text-sm text-gray-500 mb-5">
Are you sure you want to {action}{" "}
<span className="font-medium text-gray-700">&quot;{guardrailName}&quot;</span>?{" "}
{isApprove
? "This will make it active and available for use."
: "This will mark it as rejected and notify the team."}
</p>
<div className="flex gap-3">
<button
type="button"
onClick={onCancel}
className="flex-1 border border-gray-300 text-gray-700 hover:bg-gray-50 text-sm font-medium py-2 rounded-md transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={onConfirm}
className={`flex-1 text-white text-sm font-medium py-2 rounded-md transition-colors ${
isApprove
? "bg-green-500 hover:bg-green-600"
: "bg-red-500 hover:bg-red-600"
}`}
>
{isApprove ? "Approve" : "Reject"}
</button>
</div>
</div>
</div>
);
}
export function TeamGuardrailsTab() {
const [guardrails, setGuardrails] = useState<TeamGuardrail[]>(SAMPLE_GUARDRAILS);
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState<
"all" | GuardrailStatus
>("all");
const [selectedId, setSelectedId] = useState<string | null>(null);
const [expandedHeaders, setExpandedHeaders] = useState<Set<string>>(new Set());
const [confirmAction, setConfirmAction] = useState<{
id: string;
action: "approve" | "reject";
} | null>(null);
const filtered = guardrails.filter((g) => {
const matchesSearch =
g.name.toLowerCase().includes(search.toLowerCase()) ||
g.team.toLowerCase().includes(search.toLowerCase()) ||
g.endpoint.toLowerCase().includes(search.toLowerCase());
const matchesStatus = statusFilter === "all" || g.status === statusFilter;
return matchesSearch && matchesStatus;
});
const selected = guardrails.find((g) => g.id === selectedId) ?? null;
const totalCount = guardrails.length;
const pendingCount = guardrails.filter((g) => g.status === "pending").length;
const activeCount = guardrails.filter((g) => g.status === "active").length;
const rejectedCount = guardrails.filter((g) => g.status === "rejected").length;
function toggleForwardKey(id: string) {
setGuardrails((prev) =>
prev.map((g) =>
g.id === id ? { ...g, forwardKey: !g.forwardKey } : g
)
);
}
function handleApprove(id: string) {
setGuardrails((prev) =>
prev.map((g) => (g.id === id ? { ...g, status: "active" as const } : g))
);
setConfirmAction(null);
if (selectedId === id) setSelectedId(null);
}
function handleReject(id: string) {
setGuardrails((prev) =>
prev.map((g) =>
g.id === id ? { ...g, status: "rejected" as const } : g
)
);
setConfirmAction(null);
if (selectedId === id) setSelectedId(null);
}
function toggleHeaders(id: string) {
setExpandedHeaders((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}
return (
<div className="flex h-full">
<div
className={`flex-1 min-w-0 p-6 overflow-auto ${
selected ? "border-r border-gray-200" : ""
}`}
>
<div className="grid grid-cols-4 gap-4 mb-6">
<StatCard label="Total Submitted" value={totalCount} color="text-gray-900" />
<StatCard
label="Pending Review"
value={pendingCount}
color="text-yellow-600"
/>
<StatCard label="Active" value={activeCount} color="text-green-600" />
<StatCard label="Rejected" value={rejectedCount} color="text-red-600" />
</div>
<div className="flex items-center gap-3 mb-5">
<div className="relative flex-1 max-w-xs">
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Search guardrails..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-9 pr-4 py-2 border border-gray-200 rounded-md text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<select
value={statusFilter}
onChange={(e) =>
setStatusFilter(e.target.value as typeof statusFilter)
}
className="border border-gray-200 rounded-md px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white"
>
<option value="all">All Status</option>
<option value="pending">Pending Review</option>
<option value="active">Active</option>
<option value="rejected">Rejected</option>
</select>
<button
type="button"
className="ml-auto flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium px-4 py-2 rounded-md transition-colors"
>
<PlusIcon className="h-4 w-4" />
Add Guardrail
</button>
</div>
<div className="space-y-3">
{filtered.length === 0 && (
<div className="text-center py-12 text-gray-400 text-sm">
No guardrails match your filters.
</div>
)}
{filtered.map((g) => (
<GuardrailCard
key={g.id}
guardrail={g}
isSelected={selectedId === g.id}
isHeadersExpanded={expandedHeaders.has(g.id)}
onSelect={() => setSelectedId(selectedId === g.id ? null : g.id)}
onToggleForwardKey={() => toggleForwardKey(g.id)}
onToggleHeaders={() => toggleHeaders(g.id)}
onApprove={() => setConfirmAction({ id: g.id, action: "approve" })}
onReject={() => setConfirmAction({ id: g.id, action: "reject" })}
/>
))}
</div>
</div>
{selected && (
<DetailPanel
guardrail={selected}
onClose={() => setSelectedId(null)}
onApprove={() =>
setConfirmAction({ id: selected.id, action: "approve" })
}
onReject={() =>
setConfirmAction({ id: selected.id, action: "reject" })
}
onToggleForwardKey={() => toggleForwardKey(selected.id)}
/>
)}
{confirmAction && (
<ConfirmDialog
action={confirmAction.action}
guardrailName={
guardrails.find((g) => g.id === confirmAction.id)?.name ?? ""
}
onConfirm={() =>
confirmAction.action === "approve"
? handleApprove(confirmAction.id)
: handleReject(confirmAction.id)
}
onCancel={() => setConfirmAction(null)}
/>
)}
</div>
);
}

View file

@ -14,7 +14,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsx": "preserve",
"incremental": true,
"plugins": [
{