mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
feat(ui/): guardrails - team submissions via UI
This commit is contained in:
parent
b232e2f564
commit
a4d9c44191
48 changed files with 1294 additions and 298 deletions
|
|
@ -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");
|
||||
|
||||
|
|
@ -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");
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
15
ui/litellm-dashboard/package-lock.json
generated
15
ui/litellm-dashboard/package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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'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'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">"{guardrailName}"</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue