mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-03 07:23:33 +00:00
refactor(ui): move guardrails text onto i18n (#49524)
Moves the text the /guardrails page writes itself onto react-i18next under a new top-level `guardrails` block in en.json, placed right before `guardrailsMonitor`. Catalog data (garden cards, custom code templates, option lists) stores ParseKeys and translates at render time. Provider and vendor names, API values, code samples and test payloads stay raw guardrail_info.tsx and custom_code/CustomCodeModal.tsx cross the 800-line max-lines cap only because Prettier wraps the new t() calls, so each gets a max-lines suppression entry instead of a split. The react/no-unescaped-entities entry for add_guardrail_form.tsx is pruned because its apostrophes moved into en.json
This commit is contained in:
parent
1bbfb3a02f
commit
d3de601946
46 changed files with 3242 additions and 1132 deletions
|
|
@ -215,9 +215,6 @@
|
|||
},
|
||||
"max-lines": {
|
||||
"count": 1
|
||||
},
|
||||
"react/no-unescaped-entities": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/guardrails/_components/content_filter/CompetitorIntentConfiguration.tsx": {
|
||||
|
|
@ -247,6 +244,9 @@
|
|||
}
|
||||
},
|
||||
"src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.tsx": {
|
||||
"max-lines": {
|
||||
"count": 1
|
||||
},
|
||||
"no-nested-ternary": {
|
||||
"count": 6
|
||||
},
|
||||
|
|
@ -281,6 +281,9 @@
|
|||
"local/no-complex-jsx-arrow": {
|
||||
"count": 1
|
||||
},
|
||||
"max-lines": {
|
||||
"count": 1
|
||||
},
|
||||
"max-params": {
|
||||
"count": 1
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
|
||||
import { deleteGuardrailCall } from "@/components/networking";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { formatGuardrailMode, getGuardrailLogoAndName } from "./guardrail_info_helpers";
|
||||
|
||||
const DANGLING_REFERENCE_WARNING =
|
||||
"Keys, teams, and policies that list this guardrail will silently stop running it. Policy pipeline steps that use it will block every request with a 400 error, unless the step's on_error is next or allow (or its on_fail, when on_error is not set).";
|
||||
|
||||
export interface DeletableGuardrail {
|
||||
guardrail_id: string;
|
||||
guardrail_name?: string | null;
|
||||
|
|
@ -25,6 +23,7 @@ interface GuardrailDeleteModalProps {
|
|||
}
|
||||
|
||||
export function GuardrailDeleteModal({ guardrail, accessToken, onClose, onDeleted }: GuardrailDeleteModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const guardrailName = guardrail ? guardrail.guardrail_name || guardrail.guardrail_id : "";
|
||||
const providerDisplayName = getGuardrailLogoAndName(guardrail?.litellm_params?.guardrail || "").displayName;
|
||||
|
|
@ -34,11 +33,11 @@ export function GuardrailDeleteModal({ guardrail, accessToken, onClose, onDelete
|
|||
setIsDeleting(true);
|
||||
try {
|
||||
await deleteGuardrailCall(accessToken, guardrail.guardrail_id);
|
||||
toast.success(`Guardrail "${guardrailName}" deleted successfully`);
|
||||
toast.success(t("guardrails.deleteModal.toast.deleted", { name: guardrailName }));
|
||||
onClose();
|
||||
onDeleted();
|
||||
} catch {
|
||||
toast.fromError("Failed to delete guardrail");
|
||||
toast.fromError(t("guardrails.deleteModal.toast.deleteFailed"));
|
||||
onClose();
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
|
|
@ -48,16 +47,21 @@ export function GuardrailDeleteModal({ guardrail, accessToken, onClose, onDelete
|
|||
return (
|
||||
<DeleteResourceModal
|
||||
isOpen={guardrail !== null}
|
||||
title="Delete Guardrail"
|
||||
alertMessage={DANGLING_REFERENCE_WARNING}
|
||||
message={`Are you sure you want to delete guardrail: ${guardrailName}? This action cannot be undone.`}
|
||||
resourceInformationTitle="Guardrail Information"
|
||||
title={t("guardrails.deleteModal.title")}
|
||||
alertMessage={t("guardrails.deleteModal.danglingReferenceWarning")}
|
||||
message={t("guardrails.deleteModal.message", { name: guardrailName })}
|
||||
resourceInformationTitle={t("guardrails.deleteModal.infoTitle")}
|
||||
resourceInformation={[
|
||||
{ label: "Name", value: guardrail?.guardrail_name },
|
||||
{ label: "ID", value: guardrail?.guardrail_id, code: true },
|
||||
{ label: "Provider", value: providerDisplayName },
|
||||
{ label: "Mode", value: formatGuardrailMode(guardrail?.litellm_params?.mode) },
|
||||
{ label: "Default On", value: guardrail?.litellm_params?.default_on ? "Yes" : "No" },
|
||||
{ label: t("guardrails.deleteModal.info.name"), value: guardrail?.guardrail_name },
|
||||
{ label: t("guardrails.deleteModal.info.id"), value: guardrail?.guardrail_id, code: true },
|
||||
{ label: t("guardrails.deleteModal.info.provider"), value: providerDisplayName },
|
||||
{ label: t("guardrails.deleteModal.info.mode"), value: formatGuardrailMode(guardrail?.litellm_params?.mode) },
|
||||
{
|
||||
label: t("guardrails.deleteModal.info.defaultOn"),
|
||||
value: guardrail?.litellm_params?.default_on
|
||||
? t("guardrails.deleteModal.info.yes")
|
||||
: t("guardrails.deleteModal.info.no"),
|
||||
},
|
||||
]}
|
||||
onCancel={onClose}
|
||||
onOk={handleDelete}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import type { ParseKeys } from "i18next";
|
||||
import { CircleHelp } from "lucide-react";
|
||||
import React, { useEffect, useId } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useController,
|
||||
useWatch,
|
||||
|
|
@ -121,22 +123,24 @@ export const GuardrailField: React.FC<GuardrailFieldProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const SKIP_MESSAGE_ITEMS = [
|
||||
{ label: "Use global default", value: "inherit" },
|
||||
{ label: "Yes — exclude from guardrail scan", value: "yes" },
|
||||
{ label: "No — always include in scan", value: "no" },
|
||||
const SKIP_MESSAGE_ITEMS: { labelKey: ParseKeys; value: string }[] = [
|
||||
{ labelKey: "guardrails.formField.skipMessage.options.inherit", value: "inherit" },
|
||||
{ labelKey: "guardrails.formField.skipMessage.options.yes", value: "yes" },
|
||||
{ labelKey: "guardrails.formField.skipMessage.options.no", value: "no" },
|
||||
];
|
||||
|
||||
export const SkipMessageSelect: React.FC<{ control: GuardrailFieldControlProps }> = ({ control }) => {
|
||||
const { t } = useTranslation();
|
||||
const { id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy } = control;
|
||||
const items = SKIP_MESSAGE_ITEMS.map(({ labelKey, value: itemValue }) => ({ label: t(labelKey), value: itemValue }));
|
||||
|
||||
return (
|
||||
<Select items={SKIP_MESSAGE_ITEMS} value={asText(value) || null} onValueChange={onChange}>
|
||||
<Select items={items} value={asText(value) || null} onValueChange={onChange}>
|
||||
<SelectTrigger id={id} aria-invalid={ariaInvalid} aria-describedby={ariaDescribedBy} className="w-full">
|
||||
<SelectValue placeholder="Select an option" />
|
||||
<SelectValue placeholder={t("guardrails.formField.skipMessage.placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SKIP_MESSAGE_ITEMS.map((item) => (
|
||||
{items.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -150,8 +154,9 @@ export const LoggingOnlyScopeSelect: React.FC<{
|
|||
control: GuardrailFieldControlProps;
|
||||
directionalScopeSupported: boolean;
|
||||
}> = ({ control, directionalScopeSupported }) => {
|
||||
const { t } = useTranslation();
|
||||
const { id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy } = control;
|
||||
const items = getLoggingOnlyScopeOptions(directionalScopeSupported);
|
||||
const items = getLoggingOnlyScopeOptions(directionalScopeSupported, t);
|
||||
|
||||
useEffect(() => {
|
||||
const currentChoice = isLoggingOnlyScopeChoice(value) ? value : "default";
|
||||
|
|
@ -162,7 +167,7 @@ export const LoggingOnlyScopeSelect: React.FC<{
|
|||
return (
|
||||
<Select items={items} value={asText(value) || "default"} onValueChange={onChange}>
|
||||
<SelectTrigger id={id} aria-invalid={ariaInvalid} aria-describedby={ariaDescribedBy} className="w-full">
|
||||
<SelectValue placeholder="Select an option" />
|
||||
<SelectValue placeholder={t("guardrails.formField.loggingOnlyScope.placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{items.map((item) => (
|
||||
|
|
@ -180,6 +185,7 @@ export const LoggingOnlyScopeField: React.FC<{
|
|||
mode: unknown;
|
||||
directionalScopeSupported: boolean;
|
||||
}> = ({ control, mode, directionalScopeSupported }) => {
|
||||
const { t } = useTranslation();
|
||||
const scopeChoice = useWatch({ control, name: "logging_only_scope_choice" });
|
||||
const continueDisabled = scopeChoice === "input" || scopeChoice === "output";
|
||||
if (!modeIncludesLoggingOnly(mode)) return null;
|
||||
|
|
@ -190,8 +196,8 @@ export const LoggingOnlyScopeField: React.FC<{
|
|||
control={control}
|
||||
name="logging_only_scope_choice"
|
||||
label={labelWithHint(
|
||||
"Logging only scope",
|
||||
"Which direction a logging_only scan observes. Observe-only scans never block; pre_call and post_call on this guardrail still block.",
|
||||
t("guardrails.formField.loggingOnlyScope.label"),
|
||||
t("guardrails.formField.loggingOnlyScope.hint"),
|
||||
)}
|
||||
>
|
||||
{(fieldControl) => (
|
||||
|
|
@ -202,8 +208,8 @@ export const LoggingOnlyScopeField: React.FC<{
|
|||
control={control}
|
||||
name="logging_only_continue_on_input_failure"
|
||||
label={labelWithHint(
|
||||
"Continue observing the response after a flagged request",
|
||||
"When on, a flagged or failing request scan is logged and the response is still scanned, so both verdicts land.",
|
||||
t("guardrails.formField.continueOnInputFailure.label"),
|
||||
t("guardrails.formField.continueOnInputFailure.hint"),
|
||||
)}
|
||||
>
|
||||
{(fieldControl) => (
|
||||
|
|
@ -214,7 +220,7 @@ export const LoggingOnlyScopeField: React.FC<{
|
|||
onCheckedChange={fieldControl.onChange}
|
||||
aria-invalid={fieldControl["aria-invalid"]}
|
||||
aria-describedby={fieldControl["aria-describedby"]}
|
||||
aria-label="Continue observing the response after a flagged request"
|
||||
aria-label={t("guardrails.formField.continueOnInputFailure.label")}
|
||||
/>
|
||||
)}
|
||||
</GuardrailField>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
|
|
@ -15,39 +16,53 @@ type GuardrailModeParams = {
|
|||
logging_only_continue_on_input_failure?: boolean | null;
|
||||
};
|
||||
|
||||
export const GuardrailModeCard: React.FC<{ litellmParams: GuardrailModeParams }> = ({ litellmParams }) => (
|
||||
<Card className="block p-6">
|
||||
<p>Mode</p>
|
||||
<div className="mt-2">
|
||||
<h3 className="text-lg font-medium">{formatGuardrailMode(litellmParams.mode) || "-"}</h3>
|
||||
<Badge variant={litellmParams.default_on ? "secondary" : "outline"}>
|
||||
{litellmParams.default_on ? "Default On" : "Default Off"}
|
||||
</Badge>
|
||||
</div>
|
||||
{modeIncludesLoggingOnly(litellmParams.mode) && (
|
||||
<div className="mt-4">
|
||||
<p>Logging only scope</p>
|
||||
<h3 className="text-lg font-medium">{formatLoggingOnlyScope(litellmParams.logging_only_scope)}</h3>
|
||||
<p className="mt-2">Continue after a flagged request</p>
|
||||
<h3 className="text-lg font-medium">{loggingOnlyContinueFromParams(litellmParams) ? "On" : "Off"}</h3>
|
||||
export const GuardrailModeCard: React.FC<{ litellmParams: GuardrailModeParams }> = ({ litellmParams }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card className="block p-6">
|
||||
<p>{t("guardrails.modeDisplay.mode")}</p>
|
||||
<div className="mt-2">
|
||||
<h3 className="text-lg font-medium">{formatGuardrailMode(litellmParams.mode) || "-"}</h3>
|
||||
<Badge variant={litellmParams.default_on ? "secondary" : "outline"}>
|
||||
{litellmParams.default_on ? t("guardrails.modeDisplay.defaultOn") : t("guardrails.modeDisplay.defaultOff")}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
{modeIncludesLoggingOnly(litellmParams.mode) && (
|
||||
<div className="mt-4">
|
||||
<p>{t("guardrails.modeDisplay.loggingOnlyScope")}</p>
|
||||
<h3 className="text-lg font-medium">{formatLoggingOnlyScope(litellmParams.logging_only_scope, t)}</h3>
|
||||
<p className="mt-2">{t("guardrails.modeDisplay.continueAfterFlagged")}</p>
|
||||
<h3 className="text-lg font-medium">
|
||||
{loggingOnlyContinueFromParams(litellmParams)
|
||||
? t("guardrails.modeDisplay.on")
|
||||
: t("guardrails.modeDisplay.off")}
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const GuardrailModeRows: React.FC<{ litellmParams: GuardrailModeParams }> = ({ litellmParams }) => (
|
||||
<>
|
||||
<div>
|
||||
<p className="font-medium">Mode</p>
|
||||
<div>{formatGuardrailMode(litellmParams.mode) || "-"}</div>
|
||||
</div>
|
||||
{modeIncludesLoggingOnly(litellmParams.mode) && (
|
||||
export const GuardrailModeRows: React.FC<{ litellmParams: GuardrailModeParams }> = ({ litellmParams }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<p className="font-medium">Logging only scope</p>
|
||||
<div>{formatLoggingOnlyScope(litellmParams.logging_only_scope)}</div>
|
||||
<p className="font-medium">Continue after a flagged request</p>
|
||||
<div>{loggingOnlyContinueFromParams(litellmParams) ? "On" : "Off"}</div>
|
||||
<p className="font-medium">{t("guardrails.modeDisplay.mode")}</p>
|
||||
<div>{formatGuardrailMode(litellmParams.mode) || "-"}</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
{modeIncludesLoggingOnly(litellmParams.mode) && (
|
||||
<div>
|
||||
<p className="font-medium">{t("guardrails.modeDisplay.loggingOnlyScope")}</p>
|
||||
<div>{formatLoggingOnlyScope(litellmParams.logging_only_scope, t)}</div>
|
||||
<p className="font-medium">{t("guardrails.modeDisplay.continueAfterFlagged")}</p>
|
||||
<div>
|
||||
{loggingOnlyContinueFromParams(litellmParams)
|
||||
? t("guardrails.modeDisplay.on")
|
||||
: t("guardrails.modeDisplay.off")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DEFAULT_DECISION_THRESHOLD } from "./decision_model/decisionModelQuestion";
|
||||
import { GuardrailModeRows } from "./GuardrailModeDisplay";
|
||||
|
|
@ -17,29 +18,32 @@ const DecisionModelSection = ({
|
|||
}: {
|
||||
decisionModel: string;
|
||||
checks: DecisionModelCheckView[];
|
||||
}) => (
|
||||
<div>
|
||||
<p className="font-medium">Decision Model</p>
|
||||
<div className="font-mono">{decisionModel}</div>
|
||||
<p className="mt-2 font-medium">Checks</p>
|
||||
<div className="mt-2 overflow-hidden rounded-lg border border-border">
|
||||
<div className="flex border-b border-border bg-muted/40 px-4 py-2 text-xs font-semibold">
|
||||
<span className="flex-1">Check</span>
|
||||
<span className="w-24 text-right">Action</span>
|
||||
<span className="w-24 text-right">Threshold</span>
|
||||
</div>
|
||||
{checks.map((check) => (
|
||||
<div key={check.name} className="flex items-center border-b border-border px-4 py-2 text-sm last:border-b-0">
|
||||
<span className="flex-1">{check.name.replace(/_/g, " ")}</span>
|
||||
<span className="w-24 text-right">
|
||||
<Badge variant="secondary">{check.action ?? "block"}</Badge>
|
||||
</span>
|
||||
<span className="w-24 text-right">{check.threshold ?? DEFAULT_DECISION_THRESHOLD}</span>
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<p className="font-medium">{t("guardrails.readOnlyDetails.decisionModel.heading")}</p>
|
||||
<div className="font-mono">{decisionModel}</div>
|
||||
<p className="mt-2 font-medium">{t("guardrails.readOnlyDetails.decisionModel.checks")}</p>
|
||||
<div className="mt-2 overflow-hidden rounded-lg border border-border">
|
||||
<div className="flex border-b border-border bg-muted/40 px-4 py-2 text-xs font-semibold">
|
||||
<span className="flex-1">{t("guardrails.readOnlyDetails.decisionModel.columns.check")}</span>
|
||||
<span className="w-24 text-right">{t("guardrails.readOnlyDetails.decisionModel.columns.action")}</span>
|
||||
<span className="w-24 text-right">{t("guardrails.readOnlyDetails.decisionModel.columns.threshold")}</span>
|
||||
</div>
|
||||
))}
|
||||
{checks.map((check) => (
|
||||
<div key={check.name} className="flex items-center border-b border-border px-4 py-2 text-sm last:border-b-0">
|
||||
<span className="flex-1">{check.name.replace(/_/g, " ")}</span>
|
||||
<span className="w-24 text-right">
|
||||
<Badge variant="secondary">{check.action ?? "block"}</Badge>
|
||||
</span>
|
||||
<span className="w-24 text-right">{check.threshold ?? DEFAULT_DECISION_THRESHOLD}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export const GuardrailReadOnlyDetails = ({
|
||||
guardrailId,
|
||||
|
|
@ -72,45 +76,52 @@ export const GuardrailReadOnlyDetails = ({
|
|||
updatedAt: string;
|
||||
showToolPermission: boolean;
|
||||
toolPermissionConfig: ToolPermissionConfig;
|
||||
}) => (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="font-medium">Guardrail ID</p>
|
||||
<div className="font-mono">{guardrailId}</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Guardrail Name</p>
|
||||
<div>{guardrailName || "Unnamed Guardrail"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Provider</p>
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
<GuardrailModeRows litellmParams={litellmParams} />
|
||||
<GuardrailStreamScopeDetail raw={streamScope} />
|
||||
<div>
|
||||
<p className="font-medium">Default On</p>
|
||||
<Badge variant={defaultOn ? "secondary" : "outline"}>{defaultOn ? "Yes" : "No"}</Badge>
|
||||
</div>
|
||||
{litellmParams.guardrail === "decision_model" && litellmParams.decision_model && (
|
||||
<DecisionModelSection decisionModel={litellmParams.decision_model} checks={litellmParams.checks ?? []} />
|
||||
)}
|
||||
{piiEntityCount > 0 && (
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="font-medium">PII Protection</p>
|
||||
<div className="mt-2">
|
||||
<Badge variant="secondary">{piiEntityCount} PII entities configured</Badge>
|
||||
</div>
|
||||
<p className="font-medium">{t("guardrails.readOnlyDetails.guardrailId")}</p>
|
||||
<div className="font-mono">{guardrailId}</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium">Created At</p>
|
||||
<div>{createdAt}</div>
|
||||
<div>
|
||||
<p className="font-medium">{t("guardrails.readOnlyDetails.guardrailName")}</p>
|
||||
<div>{guardrailName || t("guardrails.readOnlyDetails.unnamed")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{t("guardrails.readOnlyDetails.provider")}</p>
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
<GuardrailModeRows litellmParams={litellmParams} />
|
||||
<GuardrailStreamScopeDetail raw={streamScope} />
|
||||
<div>
|
||||
<p className="font-medium">{t("guardrails.readOnlyDetails.defaultOn")}</p>
|
||||
<Badge variant={defaultOn ? "secondary" : "outline"}>
|
||||
{defaultOn ? t("guardrails.readOnlyDetails.yes") : t("guardrails.readOnlyDetails.no")}
|
||||
</Badge>
|
||||
</div>
|
||||
{litellmParams.guardrail === "decision_model" && litellmParams.decision_model && (
|
||||
<DecisionModelSection decisionModel={litellmParams.decision_model} checks={litellmParams.checks ?? []} />
|
||||
)}
|
||||
{piiEntityCount > 0 && (
|
||||
<div>
|
||||
<p className="font-medium">{t("guardrails.readOnlyDetails.piiProtection")}</p>
|
||||
<div className="mt-2">
|
||||
<Badge variant="secondary">
|
||||
{piiEntityCount} {t("guardrails.readOnlyDetails.piiEntitiesConfigured", { count: piiEntityCount })}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium">{t("guardrails.readOnlyDetails.createdAt")}</p>
|
||||
<div>{createdAt}</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{t("guardrails.readOnlyDetails.lastUpdated")}</p>
|
||||
<div>{updatedAt}</div>
|
||||
</div>
|
||||
{showToolPermission && <ToolPermissionRulesEditor value={toolPermissionConfig} disabled />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Last Updated</p>
|
||||
<div>{updatedAt}</div>
|
||||
</div>
|
||||
{showToolPermission && <ToolPermissionRulesEditor value={toolPermissionConfig} disabled />}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Code, Trash2 } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DeletableGuardrail, GuardrailDeleteModal } from "./GuardrailDeleteModal";
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ export function GuardrailSettingsActions({
|
|||
accessToken,
|
||||
onDeleted,
|
||||
}: GuardrailSettingsActionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
if (isEditing || guardrail.guardrail_definition_location === "config") return null;
|
||||
|
|
@ -29,16 +31,16 @@ export function GuardrailSettingsActions({
|
|||
{guardrail.litellm_params?.guardrail === "custom_code" ? (
|
||||
<Button variant="outline" onClick={onEditCode}>
|
||||
<Code />
|
||||
Edit Code
|
||||
{t("guardrails.settingsActions.editCode")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" onClick={onEdit}>
|
||||
Edit Settings
|
||||
{t("guardrails.settingsActions.editSettings")}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="destructive" onClick={() => setIsModalOpen(true)}>
|
||||
<Trash2 />
|
||||
Delete
|
||||
{t("guardrails.settingsActions.delete")}
|
||||
</Button>
|
||||
<GuardrailDeleteModal
|
||||
guardrail={isModalOpen ? guardrail : null}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Copy, Info } from "lucide-react";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -25,6 +26,7 @@ export function GuardrailTestPanel({
|
|||
errors,
|
||||
onClose,
|
||||
}: GuardrailTestPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [inputText, setInputText] = useState("");
|
||||
const [metadataText, setMetadataText] = useState("");
|
||||
const [metadataError, setMetadataError] = useState<string | null>(null);
|
||||
|
|
@ -36,24 +38,24 @@ export function GuardrailTestPanel({
|
|||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return { metadata: null, error: "Metadata must be a JSON object" };
|
||||
return { metadata: null, error: t("guardrails.testPanel.errors.metadataNotObject") };
|
||||
}
|
||||
return { metadata: parsed, error: null };
|
||||
} catch {
|
||||
return { metadata: null, error: "Invalid JSON" };
|
||||
return { metadata: null, error: t("guardrails.testPanel.errors.invalidJson") };
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!inputText.trim()) {
|
||||
toast.fromError("Please enter text to test");
|
||||
toast.fromError(t("guardrails.testPanel.toast.emptyText"));
|
||||
return;
|
||||
}
|
||||
|
||||
const { metadata, error } = parseMetadata(metadataText);
|
||||
if (error) {
|
||||
setMetadataError(error);
|
||||
toast.fromError(`Metadata: ${error}`);
|
||||
toast.fromError(t("guardrails.testPanel.toast.metadataError", { error }));
|
||||
return;
|
||||
}
|
||||
setMetadataError(null);
|
||||
|
|
@ -99,9 +101,9 @@ export function GuardrailTestPanel({
|
|||
const handleCopyInput = async () => {
|
||||
const success = await copyToClipboard(inputText);
|
||||
if (success) {
|
||||
toast.success("Input copied to clipboard");
|
||||
toast.success(t("guardrails.testPanel.toast.copied"));
|
||||
} else {
|
||||
toast.fromError("Failed to copy input");
|
||||
toast.fromError(t("guardrails.testPanel.toast.copyFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -112,7 +114,7 @@ export function GuardrailTestPanel({
|
|||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="mb-1 flex items-center space-x-2">
|
||||
<h2 className="text-lg font-semibold">Test Guardrails:</h2>
|
||||
<h2 className="text-lg font-semibold">{t("guardrails.testPanel.title")}</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{guardrailNames.map((name) => (
|
||||
<div
|
||||
|
|
@ -125,7 +127,11 @@ export function GuardrailTestPanel({
|
|||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Test {guardrailNames.length > 1 ? "guardrails" : "guardrail"} and compare results
|
||||
<Trans
|
||||
i18nKey="guardrails.testPanel.subtitle"
|
||||
count={guardrailNames.length}
|
||||
components={{ noun: <></> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -137,7 +143,7 @@ export function GuardrailTestPanel({
|
|||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium">Input Text</label>
|
||||
<label className="text-sm font-medium">{t("guardrails.testPanel.inputLabel")}</label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
|
|
@ -146,13 +152,13 @@ export function GuardrailTestPanel({
|
|||
</span>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Press Enter to submit. Use Shift+Enter for new line.</TooltipContent>
|
||||
<TooltipContent>{t("guardrails.testPanel.inputTooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{inputText && (
|
||||
<Button size="sm" variant="secondary" onClick={handleCopyInput}>
|
||||
<Copy />
|
||||
Copy Input
|
||||
{t("guardrails.testPanel.copyInput")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -160,21 +166,26 @@ export function GuardrailTestPanel({
|
|||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Enter text to test with guardrails..."
|
||||
placeholder={t("guardrails.testPanel.inputPlaceholder")}
|
||||
rows={8}
|
||||
className="font-mono text-sm field-sizing-fixed"
|
||||
/>
|
||||
<div className="mt-1 flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Press <Kbd>Enter</Kbd> to submit • <Kbd>Shift+Enter</Kbd> for new line
|
||||
<Trans i18nKey="guardrails.testPanel.keyboardHint" components={{ kbd: <Kbd /> }} />
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<Trans
|
||||
i18nKey="guardrails.testPanel.characters"
|
||||
components={{ length: <>{String(inputText.length)}</> }}
|
||||
/>
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">Characters: {inputText.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<label className="text-sm font-medium">Metadata (optional)</label>
|
||||
<label className="text-sm font-medium">{t("guardrails.testPanel.metadataLabel")}</label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
|
|
@ -183,10 +194,7 @@ export function GuardrailTestPanel({
|
|||
</span>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
JSON object forwarded to the guardrail as request_data['metadata']. Custom guardrails can
|
||||
read per-request configuration from it.
|
||||
</TooltipContent>
|
||||
<TooltipContent>{t("guardrails.testPanel.metadataTooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Textarea
|
||||
|
|
@ -214,8 +222,8 @@ export function GuardrailTestPanel({
|
|||
>
|
||||
{isLoading && <UiLoadingSpinner className="size-4" />}
|
||||
{isLoading
|
||||
? `Testing ${guardrailNames.length} guardrail${guardrailNames.length > 1 ? "s" : ""}...`
|
||||
: `Test ${guardrailNames.length} guardrail${guardrailNames.length > 1 ? "s" : ""}`}
|
||||
? t("guardrails.testPanel.submit.running", { count: guardrailNames.length })
|
||||
: t("guardrails.testPanel.submit.idle", { count: guardrailNames.length })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { FlaskConical, Search } from "lucide-react";
|
||||
import GuardrailTestPanel from "./GuardrailTestPanel";
|
||||
import { applyGuardrail } from "@/components/networking";
|
||||
|
|
@ -47,6 +48,7 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
|
|||
accessToken,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedGuardrails, setSelectedGuardrails] = useState<Set<string>>(new Set());
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [testResults, setTestResults] = useState<TestResult[]>([]);
|
||||
|
|
@ -107,10 +109,10 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
|
|||
setIsTesting(false);
|
||||
|
||||
if (results.length > 0) {
|
||||
toast.success(`${results.length} guardrail${results.length > 1 ? "s" : ""} applied successfully`);
|
||||
toast.success(t("guardrails.testPlayground.toast.applied", { count: results.length }));
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
toast.fromError(`${errors.length} guardrail${errors.length > 1 ? "s" : ""} failed`);
|
||||
toast.fromError(t("guardrails.testPlayground.toast.failed", { count: errors.length }));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -123,13 +125,13 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
|
|||
<div className="flex w-1/4 flex-col overflow-hidden border-r border-border">
|
||||
<div className="border-b border-border p-4">
|
||||
<div className="mb-3">
|
||||
<h3 className="mb-3 text-lg font-semibold">Guardrails</h3>
|
||||
<h3 className="mb-3 text-lg font-semibold">{t("guardrails.testPlayground.sidebarTitle")}</h3>
|
||||
<InputGroup>
|
||||
<InputGroupAddon>
|
||||
<Search className="size-4 text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
placeholder="Search guardrails..."
|
||||
placeholder={t("guardrails.testPlayground.searchPlaceholder")}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
|
|
@ -144,7 +146,9 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
|
|||
</div>
|
||||
) : filteredGuardrails.length === 0 ? (
|
||||
<div className="p-4 text-center text-muted-foreground">
|
||||
{searchQuery ? "No guardrails match your search" : "No guardrails available"}
|
||||
{searchQuery
|
||||
? t("guardrails.testPlayground.noMatches")
|
||||
: t("guardrails.testPlayground.noGuardrails")}
|
||||
</div>
|
||||
) : (
|
||||
<ul className="m-0 list-none p-0">
|
||||
|
|
@ -168,11 +172,11 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
|
|||
</div>
|
||||
<div className="mt-1 space-y-1 text-xs">
|
||||
<div>
|
||||
<span className="font-medium">Type: </span>
|
||||
<span className="font-medium">{t("guardrails.testPlayground.typeLabel")}</span>
|
||||
<span className="text-muted-foreground">{guardrail.litellm_params.guardrail}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Mode: </span>
|
||||
<span className="font-medium">{t("guardrails.testPlayground.modeLabel")}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{formatGuardrailMode(guardrail.litellm_params.mode)}
|
||||
</span>
|
||||
|
|
@ -186,7 +190,13 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
|
|||
|
||||
<div className="border-t border-border bg-muted/40 p-3">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{selectedGuardrails.size} of {filteredGuardrails.length} selected
|
||||
<Trans
|
||||
i18nKey="guardrails.testPlayground.selectedCount"
|
||||
components={{
|
||||
selected: <>{String(selectedGuardrails.size)}</>,
|
||||
total: <>{String(filteredGuardrails.length)}</>,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -194,17 +204,15 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
|
|||
{/* Right Panel - Test Area */}
|
||||
<div className="flex w-3/4 flex-col">
|
||||
<div className="flex items-center justify-between border-b border-border p-4">
|
||||
<h2 className="mb-0 text-xl font-semibold">Guardrail Testing Playground</h2>
|
||||
<h2 className="mb-0 text-xl font-semibold">{t("guardrails.testPlayground.title")}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
{selectedGuardrails.size === 0 ? (
|
||||
<div className="flex h-full flex-col items-center justify-center text-muted-foreground">
|
||||
<FlaskConical className="mb-4 size-12" />
|
||||
<p className="mb-2 text-lg font-medium">Select Guardrails to Test</p>
|
||||
<p className="max-w-md text-center">
|
||||
Choose one or more guardrails from the left sidebar to start testing and comparing results.
|
||||
</p>
|
||||
<p className="mb-2 text-lg font-medium">{t("guardrails.testPlayground.emptyState.title")}</p>
|
||||
<p className="max-w-md text-center">{t("guardrails.testPlayground.emptyState.description")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Check, ChevronDown, ChevronRight, Clock, Copy } from "lucide-react";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -22,6 +23,7 @@ interface GuardrailTestResultsProps {
|
|||
}
|
||||
|
||||
export function GuardrailTestResults({ results, errors }: GuardrailTestResultsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [collapsedResults, setCollapsedResults] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleResultCollapse = (guardrailName: string) => {
|
||||
|
|
@ -68,7 +70,7 @@ export function GuardrailTestResults({ results, errors }: GuardrailTestResultsPr
|
|||
|
||||
return (
|
||||
<div className="space-y-3 border-t border-border pt-4">
|
||||
<h3 className="text-sm font-semibold">Results</h3>
|
||||
<h3 className="text-sm font-semibold">{t("guardrails.testResults.title")}</h3>
|
||||
|
||||
{/* Success Results */}
|
||||
{results &&
|
||||
|
|
@ -93,7 +95,12 @@ export function GuardrailTestResults({ results, errors }: GuardrailTestResultsPr
|
|||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
|
||||
<Clock className="size-3" />
|
||||
<span className="font-medium">{result.latency}ms</span>
|
||||
<span className="font-medium">
|
||||
<Trans
|
||||
i18nKey="guardrails.testResults.latency"
|
||||
components={{ value: <>{String(result.latency)}</> }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<Button
|
||||
|
|
@ -102,14 +109,14 @@ export function GuardrailTestResults({ results, errors }: GuardrailTestResultsPr
|
|||
onClick={async () => {
|
||||
const success = await copyToClipboard(result.response_text);
|
||||
if (success) {
|
||||
toast.success("Result copied to clipboard");
|
||||
toast.success(t("guardrails.testResults.toast.copied"));
|
||||
} else {
|
||||
toast.fromError("Failed to copy result");
|
||||
toast.fromError(t("guardrails.testResults.toast.copyFailed"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Copy />
|
||||
Copy
|
||||
{t("guardrails.testResults.copy")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -117,13 +124,16 @@ export function GuardrailTestResults({ results, errors }: GuardrailTestResultsPr
|
|||
{!isCollapsed && (
|
||||
<>
|
||||
<div className="rounded-sm border border-success/20 bg-background p-3">
|
||||
<label className="mb-2 block text-xs font-medium text-muted-foreground">Output Text</label>
|
||||
<label className="mb-2 block text-xs font-medium text-muted-foreground">
|
||||
{t("guardrails.testResults.outputLabel")}
|
||||
</label>
|
||||
<div className="font-mono text-sm whitespace-pre-wrap wrap-break-word">
|
||||
{result.response_text}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<span className="font-medium">Characters:</span> {result.response_text.length}
|
||||
<span className="font-medium">{t("guardrails.testResults.charactersLabel")}</span>{" "}
|
||||
{result.response_text.length}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -162,11 +172,19 @@ export function GuardrailTestResults({ results, errors }: GuardrailTestResultsPr
|
|||
className="cursor-pointer text-sm font-medium text-destructive"
|
||||
onClick={() => toggleResultCollapse(errorItem.guardrailName)}
|
||||
>
|
||||
{errorItem.guardrailName} - Error
|
||||
<Trans
|
||||
i18nKey="guardrails.testResults.errorTitle"
|
||||
components={{ name: <>{errorItem.guardrailName}</> }}
|
||||
/>
|
||||
</p>
|
||||
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
|
||||
<Clock className="size-3" />
|
||||
<span className="font-medium">{errorItem.latency}ms</span>
|
||||
<span className="font-medium">
|
||||
<Trans
|
||||
i18nKey="guardrails.testResults.latency"
|
||||
components={{ value: <>{String(errorItem.latency)}</> }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{!isCollapsed && <p className="mt-1 text-sm text-destructive">{errorItem.error.message}</p>}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { parseAsString, useQueryState } from "nuqs";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ChevronDown, Code, Plus } from "lucide-react";
|
||||
import { getGuardrailsList } from "@/components/networking";
|
||||
|
|
@ -32,6 +33,7 @@ interface GuardrailsResponse {
|
|||
}
|
||||
|
||||
const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole }) => {
|
||||
const { t } = useTranslation();
|
||||
const [guardrailsList, setGuardrailsList] = useState<Guardrail[]>([]);
|
||||
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
||||
const [isCustomCodeModalVisible, setIsCustomCodeModalVisible] = useState(false);
|
||||
|
|
@ -104,18 +106,18 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
|||
{isAdmin && (
|
||||
<>
|
||||
<TabsTrigger value="garden" className="flex-none">
|
||||
Guardrail Garden
|
||||
{t("guardrails.panel.tabs.garden")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="guardrails" className="flex-none">
|
||||
Guardrails
|
||||
{t("guardrails.panel.tabs.guardrails")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="playground" className="flex-none" disabled={!accessToken}>
|
||||
Test Playground
|
||||
{t("guardrails.panel.tabs.playground")}
|
||||
</TabsTrigger>
|
||||
</>
|
||||
)}
|
||||
<TabsTrigger value="submitted" className="flex-none">
|
||||
Submitted Guardrails
|
||||
{t("guardrails.panel.tabs.submitted")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
|
|
@ -130,17 +132,17 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
|||
<DropdownMenu>
|
||||
<DropdownMenuTrigger disabled={!accessToken} className={cn(buttonVariants({ variant: "default" }))}>
|
||||
<Plus />
|
||||
Add New Guardrail
|
||||
{t("guardrails.panel.addMenu.trigger")}
|
||||
<ChevronDown />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuItem onClick={handleAddGuardrail}>
|
||||
<Plus />
|
||||
Add Provider Guardrail
|
||||
{t("guardrails.panel.addMenu.provider")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleAddCustomCodeGuardrail}>
|
||||
<Code />
|
||||
Create Custom Code Guardrail
|
||||
{t("guardrails.panel.addMenu.customCode")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { GuardrailField, labelWithHint, type GuardrailFormControl } from "./GuardrailFormField";
|
||||
import {
|
||||
|
|
@ -7,14 +8,6 @@ import {
|
|||
isGuardrailStreamScope,
|
||||
} from "./guardrail_info_helpers";
|
||||
|
||||
const STREAM_SCOPE_ITEMS = STREAM_SCOPE_OPTIONS.map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
}));
|
||||
|
||||
const REQUEST_SHAPE_HINT =
|
||||
"Run this guardrail on streaming requests, non-streaming requests, or both, for each selected mode.";
|
||||
|
||||
export const StreamScopeFields = ({
|
||||
modes,
|
||||
value,
|
||||
|
|
@ -24,8 +17,14 @@ export const StreamScopeFields = ({
|
|||
value: Record<string, GuardrailStreamScope>;
|
||||
onChange: (next: Record<string, GuardrailStreamScope>) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
if (modes.length === 0) return null;
|
||||
|
||||
const streamScopeItems = STREAM_SCOPE_OPTIONS.map((option) => ({
|
||||
label: t(option.labelKey),
|
||||
value: option.value,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{modes.map((mode) => {
|
||||
|
|
@ -33,10 +32,10 @@ export const StreamScopeFields = ({
|
|||
return (
|
||||
<div key={mode} className="space-y-1">
|
||||
<label htmlFor={`stream-scope-${mode}`} className="block text-sm font-medium text-foreground">
|
||||
{mode} applies to
|
||||
{t("guardrails.streamScope.appliesTo", { mode })}
|
||||
</label>
|
||||
<Select
|
||||
items={STREAM_SCOPE_ITEMS}
|
||||
items={streamScopeItems}
|
||||
value={selected}
|
||||
onValueChange={(next: GuardrailStreamScope | null) => {
|
||||
if (!isGuardrailStreamScope(next)) return;
|
||||
|
|
@ -44,10 +43,10 @@ export const StreamScopeFields = ({
|
|||
}}
|
||||
>
|
||||
<SelectTrigger id={`stream-scope-${mode}`} className="w-full">
|
||||
<SelectValue placeholder="Streaming and non-streaming" />
|
||||
<SelectValue placeholder={t("guardrails.streamScope.placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{STREAM_SCOPE_OPTIONS.map((option) => (
|
||||
{streamScopeItems.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
|
|
@ -61,34 +60,39 @@ export const StreamScopeFields = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const StreamScopeFormField = ({ control, modes }: { control: GuardrailFormControl; modes: string[] }) => (
|
||||
<GuardrailField
|
||||
control={control}
|
||||
name="stream_scope_by_mode"
|
||||
label={labelWithHint("Request shape", REQUEST_SHAPE_HINT)}
|
||||
>
|
||||
{({ value, onChange }) => (
|
||||
<StreamScopeFields
|
||||
modes={modes}
|
||||
value={(value as Record<string, GuardrailStreamScope> | undefined) ?? {}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
</GuardrailField>
|
||||
);
|
||||
export const StreamScopeFormField = ({ control, modes }: { control: GuardrailFormControl; modes: string[] }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<GuardrailField
|
||||
control={control}
|
||||
name="stream_scope_by_mode"
|
||||
label={labelWithHint(t("guardrails.streamScope.requestShape"), t("guardrails.streamScope.requestShapeHint"))}
|
||||
>
|
||||
{({ value, onChange }) => (
|
||||
<StreamScopeFields
|
||||
modes={modes}
|
||||
value={(value as Record<string, GuardrailStreamScope> | undefined) ?? {}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
</GuardrailField>
|
||||
);
|
||||
};
|
||||
|
||||
export const GuardrailStreamScopeCaption = ({ raw }: { raw: unknown }) => {
|
||||
const label = formatGuardrailStreamScope(raw);
|
||||
const { t } = useTranslation();
|
||||
const label = formatGuardrailStreamScope(raw, t);
|
||||
if (!label) return null;
|
||||
return <p className="mt-1 text-sm text-muted-foreground">{label}</p>;
|
||||
};
|
||||
|
||||
export const GuardrailStreamScopeDetail = ({ raw }: { raw: unknown }) => {
|
||||
const label = formatGuardrailStreamScope(raw);
|
||||
const { t } = useTranslation();
|
||||
const label = formatGuardrailStreamScope(raw, t);
|
||||
if (!label) return null;
|
||||
return (
|
||||
<div>
|
||||
<p className="font-medium">Request shape</p>
|
||||
<p className="font-medium">{t("guardrails.streamScope.requestShape")}</p>
|
||||
<div>{label}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import type { ParseKeys, TFunction } from "i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useDebouncedValue } from "@tanstack/react-pacer/debouncer";
|
||||
import { DEBOUNCE_WAIT_MS } from "@/utils/debounceConstants";
|
||||
import {
|
||||
|
|
@ -41,42 +43,50 @@ import { useZodForm } from "@/lib/forms/useZodForm";
|
|||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const GUARDRAIL_MODES = [
|
||||
{ value: "pre_call", label: "Pre Call" },
|
||||
{ value: "post_call", label: "Post Call" },
|
||||
{ value: "during_call", label: "During Call" },
|
||||
] as const;
|
||||
const GUARDRAIL_MODES: { value: string; labelKey: ParseKeys }[] = [
|
||||
{ value: "pre_call", labelKey: "guardrails.teamTab.modes.preCall" },
|
||||
{ value: "post_call", labelKey: "guardrails.teamTab.modes.postCall" },
|
||||
{ value: "during_call", labelKey: "guardrails.teamTab.modes.duringCall" },
|
||||
];
|
||||
|
||||
const submitGuardrailSchema = z.object({
|
||||
team_id: z
|
||||
.string()
|
||||
.nullable()
|
||||
.pipe(z.string({ error: "Select a team" }).min(1, "Select a team")),
|
||||
guardrail_name: z.string().min(1, "Enter a guardrail name"),
|
||||
mode: z.string().min(1, "Select a mode"),
|
||||
api_base: z.string().min(1, "Enter the API base URL").refine(isValidUrl, "Must be a valid URL"),
|
||||
extra_litellm_params: z.string().superRefine((value, ctx) => {
|
||||
if (!value) return;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(value);
|
||||
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
ctx.addIssue({ code: "custom", message: "Must be a JSON object" });
|
||||
const buildSubmitGuardrailSchema = (t: TFunction) =>
|
||||
z.object({
|
||||
team_id: z
|
||||
.string()
|
||||
.nullable()
|
||||
.pipe(
|
||||
z
|
||||
.string({ error: t("guardrails.teamTab.validation.teamRequired") })
|
||||
.min(1, t("guardrails.teamTab.validation.teamRequired")),
|
||||
),
|
||||
guardrail_name: z.string().min(1, t("guardrails.teamTab.validation.nameRequired")),
|
||||
mode: z.string().min(1, t("guardrails.teamTab.validation.modeRequired")),
|
||||
api_base: z
|
||||
.string()
|
||||
.min(1, t("guardrails.teamTab.validation.apiBaseRequired"))
|
||||
.refine(isValidUrl, t("guardrails.teamTab.validation.apiBaseInvalid")),
|
||||
extra_litellm_params: z.string().superRefine((value, ctx) => {
|
||||
if (!value) return;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(value);
|
||||
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
ctx.addIssue({ code: "custom", message: t("guardrails.teamTab.validation.notJsonObject") });
|
||||
}
|
||||
} catch {
|
||||
ctx.addIssue({ code: "custom", message: t("guardrails.teamTab.validation.invalidJson") });
|
||||
}
|
||||
} catch {
|
||||
ctx.addIssue({ code: "custom", message: "Invalid JSON" });
|
||||
}
|
||||
}),
|
||||
guardrail_info: z.string().superRefine((value, ctx) => {
|
||||
if (!value) return;
|
||||
try {
|
||||
JSON.parse(value);
|
||||
} catch {
|
||||
ctx.addIssue({ code: "custom", message: "Invalid JSON" });
|
||||
}
|
||||
}),
|
||||
});
|
||||
}),
|
||||
guardrail_info: z.string().superRefine((value, ctx) => {
|
||||
if (!value) return;
|
||||
try {
|
||||
JSON.parse(value);
|
||||
} catch {
|
||||
ctx.addIssue({ code: "custom", message: t("guardrails.teamTab.validation.invalidJson") });
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
type SubmitGuardrailValues = z.output<typeof submitGuardrailSchema>;
|
||||
type SubmitGuardrailValues = z.output<ReturnType<typeof buildSubmitGuardrailSchema>>;
|
||||
|
||||
const EMPTY_SUBMIT_VALUES: SubmitGuardrailValues = {
|
||||
team_id: "",
|
||||
|
|
@ -180,21 +190,21 @@ function submissionToTeamGuardrail(item: GuardrailSubmissionItem): TeamGuardrail
|
|||
};
|
||||
}
|
||||
|
||||
const STATUS_CONFIG: Record<GuardrailStatus, { label: string; bg: string; text: string; dot: string }> = {
|
||||
const STATUS_CONFIG: Record<GuardrailStatus, { labelKey: ParseKeys; bg: string; text: string; dot: string }> = {
|
||||
active: {
|
||||
label: "Active",
|
||||
labelKey: "guardrails.teamTab.status.active",
|
||||
bg: "bg-success/10",
|
||||
text: "text-success",
|
||||
dot: "bg-success",
|
||||
},
|
||||
pending: {
|
||||
label: "Pending Review",
|
||||
labelKey: "guardrails.teamTab.status.pending",
|
||||
bg: "bg-warning/10",
|
||||
text: "text-warning",
|
||||
dot: "bg-warning",
|
||||
},
|
||||
rejected: {
|
||||
label: "Rejected",
|
||||
labelKey: "guardrails.teamTab.status.rejected",
|
||||
bg: "bg-destructive/10",
|
||||
text: "text-destructive",
|
||||
dot: "bg-destructive",
|
||||
|
|
@ -309,6 +319,7 @@ function GuardrailCard({
|
|||
onApprove,
|
||||
onReject,
|
||||
}: GuardrailCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const status = STATUS_CONFIG[g.status];
|
||||
const teamColor = TEAM_COLORS[g.team] ?? "bg-muted text-foreground";
|
||||
return (
|
||||
|
|
@ -320,12 +331,14 @@ function GuardrailCard({
|
|||
<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={`text-xs font-medium px-2 py-0.5 rounded-full ${teamColor}`}>
|
||||
<Trans i18nKey="guardrails.teamTab.card.team" components={{ 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}
|
||||
{t(status.labelKey)}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-foreground mb-1">{g.name}</h3>
|
||||
|
|
@ -336,16 +349,24 @@ function GuardrailCard({
|
|||
</div>
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span>
|
||||
Model: <span className="font-medium text-foreground">{g.model}</span>
|
||||
<Trans
|
||||
i18nKey="guardrails.teamTab.card.model"
|
||||
components={{ value: <span className="font-medium text-foreground">{g.model}</span> }}
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
Submitted: <span className="font-medium text-foreground">{g.submittedAt}</span>
|
||||
<Trans
|
||||
i18nKey="guardrails.teamTab.card.submitted"
|
||||
components={{ value: <span className="font-medium text-foreground">{g.submittedAt}</span> }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">Forward API Key</span>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{t("guardrails.teamTab.card.forwardApiKey")}
|
||||
</span>
|
||||
<Toggle enabled={g.forwardKey} onToggle={onToggleForwardKey} disabled={!isAdmin} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
|
|
@ -354,7 +375,7 @@ function GuardrailCard({
|
|||
onClick={onSelect}
|
||||
className="text-xs border border-border text-muted-foreground hover:bg-muted px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
{isSelected ? "Close" : "Review"}
|
||||
{isSelected ? t("guardrails.teamTab.card.close") : t("guardrails.teamTab.card.review")}
|
||||
</button>
|
||||
{isAdmin && g.status === "pending" && (
|
||||
<>
|
||||
|
|
@ -363,14 +384,14 @@ function GuardrailCard({
|
|||
onClick={onApprove}
|
||||
className="text-xs bg-success hover:bg-success/80 text-success-foreground px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Approve
|
||||
{t("guardrails.teamTab.card.approve")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onReject}
|
||||
className="text-xs border border-destructive/30 text-destructive hover:bg-destructive/10 px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Reject
|
||||
{t("guardrails.teamTab.card.reject")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -384,7 +405,7 @@ function GuardrailCard({
|
|||
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{isHeadersExpanded ? <ChevronUpIcon className="h-3.5 w-3.5" /> : <ChevronDownIcon className="h-3.5 w-3.5" />}
|
||||
Static headers
|
||||
{t("guardrails.teamTab.card.staticHeaders")}
|
||||
{g.customHeaders.length > 0 && (
|
||||
<span className="ml-1 bg-muted text-muted-foreground rounded-full px-1.5 py-0.5 text-xs">
|
||||
{g.customHeaders.length}
|
||||
|
|
@ -394,7 +415,7 @@ function GuardrailCard({
|
|||
{isHeadersExpanded && (
|
||||
<div className="mt-2">
|
||||
{g.customHeaders.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground italic">No static headers configured.</p>
|
||||
<p className="text-xs text-muted-foreground italic">{t("guardrails.teamTab.card.noStaticHeaders")}</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{g.customHeaders.map((h, i) => (
|
||||
|
|
@ -447,6 +468,7 @@ function DetailPanel({
|
|||
onUpdateCustomHeaders,
|
||||
onUpdateExtraHeaders,
|
||||
}: DetailPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [configExpanded, setConfigExpanded] = useState(false);
|
||||
const [newExtraHeader, setNewExtraHeader] = useState("");
|
||||
const [newStaticHeaderKey, setNewStaticHeaderKey] = useState("");
|
||||
|
|
@ -459,31 +481,36 @@ function DetailPanel({
|
|||
<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={`text-xs font-medium px-2 py-0.5 rounded-full ${teamColor}`}>
|
||||
<Trans i18nKey="guardrails.teamTab.detail.team" components={{ 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}
|
||||
{t(status.labelKey)}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-base font-semibold text-foreground">{g.name}</h2>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Submitted by {g.submittedBy} on {g.submittedAt}
|
||||
<Trans
|
||||
i18nKey="guardrails.teamTab.detail.submittedBy"
|
||||
components={{ by: <>{g.submittedBy}</>, at: <>{g.submittedAt}</> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
aria-label="Close detail panel"
|
||||
aria-label={t("guardrails.teamTab.detail.closeLabel")}
|
||||
>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-5">{g.description}</p>
|
||||
<div className="space-y-4">
|
||||
<ConfigRow label="Endpoint">
|
||||
<ConfigRow label={t("guardrails.teamTab.detail.endpoint")}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<code className="text-xs font-mono text-foreground break-all">{g.endpoint}</code>
|
||||
<a
|
||||
|
|
@ -496,7 +523,7 @@ function DetailPanel({
|
|||
</a>
|
||||
</div>
|
||||
</ConfigRow>
|
||||
<ConfigRow label="Method">
|
||||
<ConfigRow label={t("guardrails.teamTab.detail.method")}>
|
||||
<span className="text-xs font-mono font-medium text-foreground bg-muted px-2 py-0.5 rounded-sm">
|
||||
{g.method}
|
||||
</span>
|
||||
|
|
@ -505,29 +532,35 @@ function DetailPanel({
|
|||
<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-info" />
|
||||
<span className="text-xs font-semibold text-info">Forward LiteLLM API Key</span>
|
||||
<span className="text-xs font-semibold text-info">
|
||||
{t("guardrails.teamTab.detail.forwardKeyTitle")}
|
||||
</span>
|
||||
</div>
|
||||
<Toggle enabled={g.forwardKey} onToggle={onToggleForwardKey} disabled={!isAdmin} />
|
||||
</div>
|
||||
<p className="text-xs text-info leading-relaxed">
|
||||
When enabled, the caller's LiteLLM API key is forwarded as an{" "}
|
||||
<code className="font-mono bg-info/15 px-1 rounded-sm">Authorization</code> header to your guardrail
|
||||
endpoint. This allows your guardrail to authenticate model calls using the original caller's
|
||||
credentials.
|
||||
<Trans
|
||||
i18nKey="guardrails.teamTab.detail.forwardKeyDescription"
|
||||
components={{ code: <code className="font-mono bg-info/15 px-1 rounded-sm" /> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<span className="text-xs font-semibold text-foreground">Static headers</span>
|
||||
<span className="text-xs font-semibold text-foreground">
|
||||
{t("guardrails.teamTab.detail.staticHeaders")}
|
||||
</span>
|
||||
{g.customHeaders.length > 0 && (
|
||||
<span className="bg-muted text-muted-foreground rounded-full px-1.5 py-0.5 text-xs">
|
||||
{g.customHeaders.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mb-2">Sent with every request to the guardrail.</p>
|
||||
<p className="text-xs text-muted-foreground mb-2">{t("guardrails.teamTab.detail.staticHeadersHint")}</p>
|
||||
{g.customHeaders.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground italic mb-2">No static headers configured.</p>
|
||||
<p className="text-xs text-muted-foreground italic mb-2">
|
||||
{t("guardrails.teamTab.detail.noStaticHeaders")}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="list-none space-y-1 mb-2">
|
||||
{g.customHeaders.map((h, i) => (
|
||||
|
|
@ -543,7 +576,7 @@ function DetailPanel({
|
|||
type="button"
|
||||
onClick={() => onUpdateCustomHeaders(g.customHeaders.filter((_, idx) => idx !== i))}
|
||||
className="text-muted-foreground hover:text-destructive shrink-0"
|
||||
aria-label={`Remove ${h.key}`}
|
||||
aria-label={t("guardrails.teamTab.detail.removeHeader", { name: h.key })}
|
||||
>
|
||||
<XIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
|
@ -558,7 +591,7 @@ function DetailPanel({
|
|||
type="text"
|
||||
value={newStaticHeaderKey}
|
||||
onChange={(e) => setNewStaticHeaderKey(e.target.value)}
|
||||
placeholder="Header name (e.g. X-API-Key)"
|
||||
placeholder={t("guardrails.teamTab.detail.headerNamePlaceholder")}
|
||||
className="flex-1 min-w-0 text-xs font-mono border border-border rounded-sm px-2 py-1.5 text-foreground placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
|
|
@ -577,7 +610,7 @@ function DetailPanel({
|
|||
type="text"
|
||||
value={newStaticHeaderValue}
|
||||
onChange={(e) => setNewStaticHeaderValue(e.target.value)}
|
||||
placeholder="Value"
|
||||
placeholder={t("guardrails.teamTab.detail.headerValuePlaceholder")}
|
||||
className="flex-1 min-w-0 text-xs font-mono border border-border rounded-sm px-2 py-1.5 text-foreground placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
|
|
@ -605,14 +638,16 @@ function DetailPanel({
|
|||
}}
|
||||
className="text-xs font-medium text-info border border-info/20 bg-info/10 hover:bg-info/15 px-2 py-1.5 rounded-sm transition-colors shrink-0"
|
||||
>
|
||||
Add
|
||||
{t("guardrails.teamTab.detail.addStaticHeader")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<span className="text-xs font-semibold text-foreground">Forward client headers</span>
|
||||
<span className="text-xs font-semibold text-foreground">
|
||||
{t("guardrails.teamTab.detail.forwardClientHeaders")}
|
||||
</span>
|
||||
{g.extraHeaders.length > 0 && (
|
||||
<span className="bg-muted text-muted-foreground rounded-full px-1.5 py-0.5 text-xs">
|
||||
{g.extraHeaders.length}
|
||||
|
|
@ -620,10 +655,12 @@ function DetailPanel({
|
|||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
Allowed header names to forward from the client request to the guardrail (e.g. x-request-id).
|
||||
{t("guardrails.teamTab.detail.forwardClientHeadersHint")}
|
||||
</p>
|
||||
{g.extraHeaders.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground italic mb-2">No forward client headers configured.</p>
|
||||
<p className="text-xs text-muted-foreground italic mb-2">
|
||||
{t("guardrails.teamTab.detail.noForwardClientHeaders")}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="list-none space-y-1 mb-2">
|
||||
{g.extraHeaders.map((name, i) => (
|
||||
|
|
@ -637,7 +674,7 @@ function DetailPanel({
|
|||
type="button"
|
||||
onClick={() => onUpdateExtraHeaders(g.extraHeaders.filter((_, idx) => idx !== i))}
|
||||
className="text-muted-foreground hover:text-destructive shrink-0"
|
||||
aria-label={`Remove ${name}`}
|
||||
aria-label={t("guardrails.teamTab.detail.removeHeader", { name })}
|
||||
>
|
||||
<XIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
|
@ -652,7 +689,7 @@ function DetailPanel({
|
|||
type="text"
|
||||
value={newExtraHeader}
|
||||
onChange={(e) => setNewExtraHeader(e.target.value)}
|
||||
placeholder="e.g. x-request-id"
|
||||
placeholder={t("guardrails.teamTab.detail.forwardHeaderPlaceholder")}
|
||||
className="flex-1 min-w-0 text-xs font-mono border border-border rounded-sm px-2 py-1.5 text-foreground placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
|
|
@ -676,7 +713,7 @@ function DetailPanel({
|
|||
}}
|
||||
className="text-xs font-medium text-info border border-info/20 bg-info/10 hover:bg-info/15 px-2 py-1.5 rounded-sm transition-colors"
|
||||
>
|
||||
Add
|
||||
{t("guardrails.teamTab.detail.addForwardHeader")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -687,7 +724,7 @@ function DetailPanel({
|
|||
onClick={() => setConfigExpanded(!configExpanded)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 text-left text-xs font-semibold text-foreground bg-muted hover:bg-border transition-colors"
|
||||
>
|
||||
<span>Equivalent config</span>
|
||||
<span>{t("guardrails.teamTab.detail.equivalentConfig")}</span>
|
||||
{configExpanded ? (
|
||||
<ChevronUpIcon className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
|
|
@ -703,17 +740,19 @@ function DetailPanel({
|
|||
<div className="flex items-start gap-2 bg-muted border border-border rounded-lg p-3">
|
||||
<InfoIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
This guardrail runs on a separate instance. It receives the user request 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-info hover:underline"
|
||||
>
|
||||
LiteLLM Generic Guardrail API docs
|
||||
</a>{" "}
|
||||
for configuration details.
|
||||
<Trans
|
||||
i18nKey="guardrails.teamTab.detail.separateInstanceNote"
|
||||
components={{
|
||||
docs: (
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/adding_provider/generic_guardrail_api"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info hover:underline"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -723,7 +762,7 @@ function DetailPanel({
|
|||
className="w-full flex items-center justify-center gap-2 border border-border text-foreground hover:bg-muted text-sm font-medium py-2 rounded-md transition-colors"
|
||||
>
|
||||
<ExternalLinkIcon className="h-4 w-4" />
|
||||
Test Endpoint
|
||||
{t("guardrails.teamTab.detail.testEndpoint")}
|
||||
</button>
|
||||
{isAdmin && g.status === "pending" && (
|
||||
<div className="flex gap-2">
|
||||
|
|
@ -733,7 +772,7 @@ function DetailPanel({
|
|||
className="flex-1 flex items-center justify-center gap-1.5 bg-success hover:bg-success/80 text-success-foreground text-sm font-medium py-2 rounded-md transition-colors"
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
Approve
|
||||
{t("guardrails.teamTab.detail.approve")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -741,7 +780,7 @@ function DetailPanel({
|
|||
className="flex-1 flex items-center justify-center gap-1.5 border border-destructive/30 text-destructive hover:bg-destructive/10 text-sm font-medium py-2 rounded-md transition-colors"
|
||||
>
|
||||
<XIcon className="h-4 w-4" />
|
||||
Reject
|
||||
{t("guardrails.teamTab.detail.reject")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -759,6 +798,7 @@ type ConfirmDialogProps = {
|
|||
};
|
||||
|
||||
function ConfirmDialog({ action, guardrailName, onConfirm, onCancel }: ConfirmDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const isApprove = action === "approve";
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-overlay">
|
||||
|
|
@ -775,14 +815,15 @@ function ConfirmDialog({ action, guardrailName, onConfirm, onCancel }: ConfirmDi
|
|||
)}
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-foreground mb-1">
|
||||
{isApprove ? "Approve Guardrail" : "Reject Guardrail"}
|
||||
{isApprove ? t("guardrails.teamTab.confirm.approveTitle") : t("guardrails.teamTab.confirm.rejectTitle")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-5">
|
||||
Are you sure you want to {action}{" "}
|
||||
<span className="font-medium text-foreground">"{guardrailName}"</span>?{" "}
|
||||
{isApprove
|
||||
? "This will make it active and available for use."
|
||||
: "This will mark it as rejected and notify the team."}
|
||||
<Trans
|
||||
i18nKey={
|
||||
isApprove ? "guardrails.teamTab.confirm.approveMessage" : "guardrails.teamTab.confirm.rejectMessage"
|
||||
}
|
||||
components={{ name: <span className="font-medium text-foreground">"{guardrailName}"</span> }}
|
||||
/>
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
|
|
@ -790,7 +831,7 @@ function ConfirmDialog({ action, guardrailName, onConfirm, onCancel }: ConfirmDi
|
|||
onClick={onCancel}
|
||||
className="flex-1 border border-border text-foreground hover:bg-muted text-sm font-medium py-2 rounded-md transition-colors"
|
||||
>
|
||||
Cancel
|
||||
{t("guardrails.teamTab.confirm.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -801,7 +842,7 @@ function ConfirmDialog({ action, guardrailName, onConfirm, onCancel }: ConfirmDi
|
|||
: "bg-destructive text-destructive-foreground hover:bg-destructive/80"
|
||||
}`}
|
||||
>
|
||||
{isApprove ? "Approve" : "Reject"}
|
||||
{isApprove ? t("guardrails.teamTab.confirm.approve") : t("guardrails.teamTab.confirm.reject")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -814,6 +855,7 @@ interface TeamGuardrailsTabProps {
|
|||
}
|
||||
|
||||
export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const { userRole } = useAuthorized();
|
||||
const isAdmin = userRole ? isProxyAdminRole(userRole) : false;
|
||||
const [guardrails, setGuardrails] = useState<TeamGuardrail[]>([]);
|
||||
|
|
@ -835,7 +877,12 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSubmitModalOpen, setIsSubmitModalOpen] = useState(false);
|
||||
const submitGuardrailSchema = useMemo(() => buildSubmitGuardrailSchema(t), [t]);
|
||||
const submitForm = useZodForm(submitGuardrailSchema, { defaultValues: EMPTY_SUBMIT_VALUES });
|
||||
const guardrailModeItems = useMemo(
|
||||
() => GUARDRAIL_MODES.map(({ value, labelKey }) => ({ value, label: t(labelKey) })),
|
||||
[t],
|
||||
);
|
||||
const registerGuardrail = useRegisterGuardrail();
|
||||
|
||||
const fetchSubmissions = useCallback(async () => {
|
||||
|
|
@ -855,12 +902,12 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
setGuardrails(res.submissions.map(submissionToTeamGuardrail));
|
||||
setSummary(res.summary);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load submissions");
|
||||
setError(err instanceof Error ? err.message : t("guardrails.teamTab.list.loadFailed"));
|
||||
setGuardrails([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [accessToken, statusFilter, searchDebounced]);
|
||||
}, [accessToken, statusFilter, searchDebounced, t]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSubmissions();
|
||||
|
|
@ -880,7 +927,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
litellm_params,
|
||||
guardrail_info: values.guardrail_info ? JSON.parse(values.guardrail_info) : undefined,
|
||||
});
|
||||
toast.success("Guardrail submitted for review");
|
||||
toast.success(t("guardrails.teamTab.toast.submitted"));
|
||||
setIsSubmitModalOpen(false);
|
||||
submitForm.reset();
|
||||
fetchSubmissions();
|
||||
|
|
@ -906,9 +953,11 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
litellm_params: { forward_api_key: newValue },
|
||||
});
|
||||
setGuardrails((prev) => prev.map((x) => (x.id === id ? { ...x, forwardKey: newValue } : x)));
|
||||
toast.success(newValue ? "Forward API key enabled" : "Forward API key disabled");
|
||||
toast.success(
|
||||
newValue ? t("guardrails.teamTab.toast.forwardKeyEnabled") : t("guardrails.teamTab.toast.forwardKeyDisabled"),
|
||||
);
|
||||
} catch {
|
||||
toast.fromError("Failed to update forward API key");
|
||||
toast.fromError(t("guardrails.teamTab.toast.forwardKeyFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -932,9 +981,9 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
: x,
|
||||
),
|
||||
);
|
||||
toast.success("Static headers updated");
|
||||
toast.success(t("guardrails.teamTab.toast.staticHeadersUpdated"));
|
||||
} catch {
|
||||
toast.fromError("Failed to update static headers");
|
||||
toast.fromError(t("guardrails.teamTab.toast.staticHeadersFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -945,9 +994,9 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
litellm_params: { extra_headers: extraHeaders },
|
||||
});
|
||||
setGuardrails((prev) => prev.map((x) => (x.id === id ? { ...x, extraHeaders } : x)));
|
||||
toast.success("Forward client headers updated");
|
||||
toast.success(t("guardrails.teamTab.toast.forwardHeadersUpdated"));
|
||||
} catch {
|
||||
toast.fromError("Failed to update forward client headers");
|
||||
toast.fromError(t("guardrails.teamTab.toast.forwardHeadersFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -958,9 +1007,9 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
setConfirmAction(null);
|
||||
if (selectedId === id) setSelectedId(null);
|
||||
await fetchSubmissions();
|
||||
toast.success("Guardrail approved");
|
||||
toast.success(t("guardrails.teamTab.toast.approved"));
|
||||
} catch {
|
||||
toast.fromError("Failed to approve guardrail");
|
||||
toast.fromError(t("guardrails.teamTab.toast.approveFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -971,9 +1020,9 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
setConfirmAction(null);
|
||||
if (selectedId === id) setSelectedId(null);
|
||||
await fetchSubmissions();
|
||||
toast.success("Guardrail rejected");
|
||||
toast.success(t("guardrails.teamTab.toast.rejected"));
|
||||
} catch {
|
||||
toast.fromError("Failed to reject guardrail");
|
||||
toast.fromError(t("guardrails.teamTab.toast.rejectFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -990,32 +1039,32 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
<div className="flex h-full">
|
||||
<div className={`flex-1 min-w-0 p-6 overflow-auto ${selected ? "border-r border-border" : ""}`}>
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
<StatCard label="Total Submitted" value={totalCount} color="text-foreground" />
|
||||
<StatCard label="Pending Review" value={pendingCount} color="text-warning" />
|
||||
<StatCard label="Active" value={activeCount} color="text-success" />
|
||||
<StatCard label="Rejected" value={rejectedCount} color="text-destructive" />
|
||||
<StatCard label={t("guardrails.teamTab.stats.total")} value={totalCount} color="text-foreground" />
|
||||
<StatCard label={t("guardrails.teamTab.stats.pending")} value={pendingCount} color="text-warning" />
|
||||
<StatCard label={t("guardrails.teamTab.stats.active")} value={activeCount} color="text-success" />
|
||||
<StatCard label={t("guardrails.teamTab.stats.rejected")} value={rejectedCount} color="text-destructive" />
|
||||
</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-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search guardrails..."
|
||||
placeholder={t("guardrails.teamTab.toolbar.searchPlaceholder")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full pl-9 pr-4 py-2 border border-border rounded-md text-sm text-foreground placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-info"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
aria-label="Filter by status"
|
||||
aria-label={t("guardrails.teamTab.toolbar.statusFilterLabel")}
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)}
|
||||
className="border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-hidden focus:ring-1 focus:ring-ring focus:border-info bg-background"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="pending">Pending Review</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
<option value="all">{t("guardrails.teamTab.toolbar.statusFilter.all")}</option>
|
||||
<option value="pending">{t("guardrails.teamTab.toolbar.statusFilter.pending")}</option>
|
||||
<option value="active">{t("guardrails.teamTab.toolbar.statusFilter.active")}</option>
|
||||
<option value="rejected">{t("guardrails.teamTab.toolbar.statusFilter.rejected")}</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -1023,14 +1072,18 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
className="ml-auto flex items-center gap-2 bg-info hover:bg-info/80 text-info-foreground text-sm font-medium px-4 py-2 rounded-md transition-colors"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Guardrail
|
||||
{t("guardrails.teamTab.toolbar.addGuardrail")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{isLoading && <div className="text-center py-12 text-muted-foreground text-sm">Loading submissions…</div>}
|
||||
{isLoading && (
|
||||
<div className="text-center py-12 text-muted-foreground text-sm">
|
||||
{t("guardrails.teamTab.list.loading")}
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="text-center py-12 text-destructive text-sm">{error}</div>}
|
||||
{!isLoading && !error && filtered.length === 0 && (
|
||||
<div className="text-center py-12 text-muted-foreground text-sm">No guardrails match your filters.</div>
|
||||
<div className="text-center py-12 text-muted-foreground text-sm">{t("guardrails.teamTab.list.empty")}</div>
|
||||
)}
|
||||
{!isLoading &&
|
||||
!error &&
|
||||
|
|
@ -1084,23 +1137,33 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Submit Guardrail for Review</DialogTitle>
|
||||
<DialogTitle>{t("guardrails.teamTab.submitModal.title")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="rounded-md bg-info/10 border border-info/20 px-4 py-3 text-sm text-info mb-4">
|
||||
Your guardrail will be sent for admin review before it becomes active.
|
||||
{t("guardrails.teamTab.submitModal.notice")}
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<form onSubmit={handleSubmitGuardrail}>
|
||||
<FieldGroup>
|
||||
<FormField control={submitForm.control} name="team_id" label="Team">
|
||||
<FormField control={submitForm.control} name="team_id" label={t("guardrails.teamTab.submitModal.team")}>
|
||||
{({ id, value, onChange }) => <TeamDropdown id={id} value={value} onChange={onChange} />}
|
||||
</FormField>
|
||||
<FormField control={submitForm.control} name="guardrail_name" label="Guardrail Name">
|
||||
{({ ref, ...field }) => <Input {...field} ref={ref} placeholder="e.g. pii-detection" />}
|
||||
<FormField
|
||||
control={submitForm.control}
|
||||
name="guardrail_name"
|
||||
label={t("guardrails.teamTab.submitModal.guardrailName")}
|
||||
>
|
||||
{({ ref, ...field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
ref={ref}
|
||||
placeholder={t("guardrails.teamTab.submitModal.guardrailNamePlaceholder")}
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField control={submitForm.control} name="mode" label="Mode">
|
||||
<FormField control={submitForm.control} name="mode" label={t("guardrails.teamTab.submitModal.mode")}>
|
||||
{({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => (
|
||||
<Select items={GUARDRAIL_MODES} value={value} onValueChange={onChange}>
|
||||
<Select items={guardrailModeItems} value={value} onValueChange={onChange}>
|
||||
<SelectTrigger
|
||||
id={id}
|
||||
aria-invalid={ariaInvalid}
|
||||
|
|
@ -1110,7 +1173,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{GUARDRAIL_MODES.map((mode) => (
|
||||
{guardrailModeItems.map((mode) => (
|
||||
<SelectItem key={mode.value} value={mode.value} title={mode.label}>
|
||||
{mode.label}
|
||||
</SelectItem>
|
||||
|
|
@ -1119,7 +1182,11 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
</Select>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField control={submitForm.control} name="api_base" label="API Base URL">
|
||||
<FormField
|
||||
control={submitForm.control}
|
||||
name="api_base"
|
||||
label={t("guardrails.teamTab.submitModal.apiBase")}
|
||||
>
|
||||
{({ ref, ...field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
|
|
@ -1133,8 +1200,8 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
control={submitForm.control}
|
||||
name="extra_litellm_params"
|
||||
label={labelWithHint(
|
||||
"Additional litellm_params (optional)",
|
||||
"JSON object merged into litellm_params. e.g. forward_api_key, headers, model, unreachable_fallback",
|
||||
t("guardrails.teamTab.submitModal.extraParams"),
|
||||
t("guardrails.teamTab.submitModal.extraParamsHint"),
|
||||
)}
|
||||
>
|
||||
{({ ref, ...field }) => (
|
||||
|
|
@ -1147,7 +1214,11 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField control={submitForm.control} name="guardrail_info" label="Guardrail Info (optional)">
|
||||
<FormField
|
||||
control={submitForm.control}
|
||||
name="guardrail_info"
|
||||
label={t("guardrails.teamTab.submitModal.guardrailInfo")}
|
||||
>
|
||||
{({ ref, ...field }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
|
|
@ -1169,9 +1240,9 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
submitForm.reset();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t("guardrails.teamTab.submitModal.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleSubmitGuardrail}>Submit for Review</Button>
|
||||
<Button onClick={handleSubmitGuardrail}>{t("guardrails.teamTab.submitModal.submit")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import type { ParseKeys } from "i18next";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useForm, useWatch, type UseFormReturn } from "react-hook-form";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { toast } from "@/lib/toast";
|
||||
import {
|
||||
createGuardrailCall,
|
||||
|
|
@ -84,14 +86,14 @@ import ToolPermissionRulesEditor, { ToolPermissionConfig } from "./tool_permissi
|
|||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
|
||||
// Define human-friendly descriptions for each mode
|
||||
const modeDescriptions = {
|
||||
pre_call: "Before LLM Call - Runs before the LLM call and checks the input (Recommended)",
|
||||
during_call: "During LLM Call - Runs in parallel with the LLM call, with response held until check completes",
|
||||
post_call: "After LLM Call - Runs after the LLM call and checks only the output",
|
||||
logging_only: "Logging Only - Only runs on logging callbacks without affecting the LLM call",
|
||||
pre_mcp_call: "Before MCP Tool Call - Runs before MCP tool execution and validates tool calls",
|
||||
during_mcp_call: "During MCP Tool Call - Runs in parallel with MCP tool execution for monitoring",
|
||||
post_mcp_call: "After MCP Tool Call - Runs after MCP tool execution and checks the tool result",
|
||||
const MODE_DESCRIPTION_KEYS: Record<string, ParseKeys> = {
|
||||
pre_call: "guardrails.addForm.modeDescriptions.preCall",
|
||||
during_call: "guardrails.addForm.modeDescriptions.duringCall",
|
||||
post_call: "guardrails.addForm.modeDescriptions.postCall",
|
||||
logging_only: "guardrails.addForm.modeDescriptions.loggingOnly",
|
||||
pre_mcp_call: "guardrails.addForm.modeDescriptions.preMcpCall",
|
||||
during_mcp_call: "guardrails.addForm.modeDescriptions.duringMcpCall",
|
||||
post_mcp_call: "guardrails.addForm.modeDescriptions.postMcpCall",
|
||||
};
|
||||
|
||||
interface GuardrailPreset {
|
||||
|
|
@ -197,9 +199,9 @@ const INITIAL_VALUES: GuardrailFormValues = {
|
|||
stream_scope_by_mode: {},
|
||||
};
|
||||
|
||||
const ALWAYS_ON_ITEMS = [
|
||||
{ label: "Yes", value: true },
|
||||
{ label: "No", value: false },
|
||||
const ALWAYS_ON_ITEMS: { labelKey: ParseKeys; value: boolean }[] = [
|
||||
{ labelKey: "guardrails.addForm.fields.alwaysOn.yes", value: true },
|
||||
{ labelKey: "guardrails.addForm.fields.alwaysOn.no", value: false },
|
||||
];
|
||||
|
||||
const DEFAULT_MODES = ["pre_call", "during_call", "post_call", "logging_only"];
|
||||
|
|
@ -231,6 +233,7 @@ interface ProviderParamsResponse {
|
|||
}
|
||||
|
||||
const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, accessToken, onSuccess, preset }) => {
|
||||
const { t } = useTranslation();
|
||||
const form = useForm<GuardrailFormValues>({ defaultValues: INITIAL_VALUES });
|
||||
const watchedMode = useWatch({ control: form.control, name: "mode" });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -313,12 +316,12 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
setDecisionModelGroups(parseDecisionModelGroups(modelGroupsResp?.data));
|
||||
} catch (error) {
|
||||
console.error("Error fetching guardrail data:", error);
|
||||
toast.fromError("Failed to load guardrail configuration");
|
||||
toast.fromError(t("guardrails.addForm.toasts.loadFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [accessToken]);
|
||||
}, [accessToken, t]);
|
||||
|
||||
// Apply preset when settings are loaded and form becomes visible
|
||||
useEffect(() => {
|
||||
|
|
@ -449,14 +452,14 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
// Validate configuration steps
|
||||
if (currentStep === 1) {
|
||||
if (shouldRenderPIIConfigSettings(selectedProvider) && selectedEntities.length === 0) {
|
||||
toast.fromError("Please select at least one PII entity to continue");
|
||||
toast.fromError(t("guardrails.addForm.toasts.piiEntityRequired"));
|
||||
return;
|
||||
}
|
||||
if (isDecisionModelProvider) {
|
||||
if (!(await form.trigger(["decision_model"]))) {
|
||||
return;
|
||||
}
|
||||
const questionsProblem = decisionChecksProblem(decisionChecks);
|
||||
const questionsProblem = decisionChecksProblem(decisionChecks, t);
|
||||
if (questionsProblem) {
|
||||
toast.fromError(questionsProblem);
|
||||
return;
|
||||
|
|
@ -500,7 +503,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
setLoading(true);
|
||||
// First validate currently visible fields
|
||||
if (!(await form.trigger())) {
|
||||
toast.fromError("Failed to create guardrail: please fix the highlighted fields");
|
||||
toast.fromError(t("guardrails.addForm.toasts.fixHighlightedFields"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -589,9 +592,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
const hasContentFilterSelections =
|
||||
selectedPatterns.length > 0 || blockedWords.length > 0 || selectedContentCategories.length > 0;
|
||||
if (!hasContentFilterSelections && !hasCompetitorIntent) {
|
||||
toast.fromError(
|
||||
"Please configure at least one content filter setting (category, pattern, keyword, or competitor intent)",
|
||||
);
|
||||
toast.fromError(t("guardrails.addForm.toasts.contentFilterRequired"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -645,7 +646,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
// For some guardrails, the config values need to be in litellm_params
|
||||
guardrailData.guardrail_info = configObj;
|
||||
} catch (error) {
|
||||
toast.fromError("Invalid JSON in configuration");
|
||||
toast.fromError(t("guardrails.addForm.toasts.invalidJson"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -654,13 +655,13 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
if (guardrailProvider === "llm_as_a_judge") {
|
||||
const criteria: GuardrailCriterion[] = values.criteria ?? [];
|
||||
if (criteria.length === 0) {
|
||||
toast.fromError("Add at least one evaluation criterion");
|
||||
toast.fromError(t("guardrails.addForm.toasts.criterionRequired"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const weightTotal = criteria.reduce((sum, c) => sum + (Number(c?.weight) || 0), 0);
|
||||
if (weightTotal !== 100) {
|
||||
toast.fromError(`Criterion weights must sum to 100% (currently ${weightTotal}%)`);
|
||||
toast.fromError(t("guardrails.addForm.toasts.criterionWeights", { total: weightTotal }));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -675,7 +676,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
}
|
||||
|
||||
if (guardrailProvider === "decision_model") {
|
||||
const questionsProblem = decisionChecksProblem(decisionChecks);
|
||||
const questionsProblem = decisionChecksProblem(decisionChecks, t);
|
||||
if (questionsProblem) {
|
||||
toast.fromError(questionsProblem);
|
||||
setLoading(false);
|
||||
|
|
@ -688,7 +689,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
|
||||
if (guardrailProvider === "tool_permission") {
|
||||
if (toolPermissionConfig.rules.length === 0) {
|
||||
toast.fromError("Add at least one tool permission rule");
|
||||
toast.fromError(t("guardrails.addForm.toasts.toolRuleRequired"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -765,7 +766,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
|
||||
await createGuardrailCall(accessToken, guardrailData);
|
||||
|
||||
toast.success("Guardrail created successfully");
|
||||
toast.success(t("guardrails.addForm.toasts.created"));
|
||||
|
||||
// Reset form and close modal
|
||||
resetForm();
|
||||
|
|
@ -773,7 +774,11 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to create guardrail:", error);
|
||||
toast.fromError("Failed to create guardrail: " + (error instanceof Error ? error.message : String(error)));
|
||||
toast.fromError(
|
||||
t("guardrails.addForm.toasts.createFailed", {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -794,24 +799,30 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
(term) => providerFilter.contains(term, query),
|
||||
);
|
||||
const supportedModes = getSupportedModesForProvider(guardrailSettings, selectedProvider) ?? DEFAULT_MODES;
|
||||
const alwaysOnItems = ALWAYS_ON_ITEMS.map(({ labelKey, value }) => ({ label: t(labelKey), value }));
|
||||
return (
|
||||
<FieldGroup>
|
||||
<GuardrailField
|
||||
control={form.control}
|
||||
name="guardrail_name"
|
||||
label="Guardrail Name"
|
||||
rules={requiredRule("Please enter a guardrail name")}
|
||||
label={t("guardrails.addForm.fields.name.label")}
|
||||
rules={requiredRule(t("guardrails.addForm.fields.name.required"))}
|
||||
>
|
||||
{({ ref, value, ...field }) => (
|
||||
<Input {...field} ref={ref} value={asText(value)} placeholder="Enter a name for this guardrail" />
|
||||
<Input
|
||||
{...field}
|
||||
ref={ref}
|
||||
value={asText(value)}
|
||||
placeholder={t("guardrails.addForm.fields.name.placeholder")}
|
||||
/>
|
||||
)}
|
||||
</GuardrailField>
|
||||
|
||||
<GuardrailField
|
||||
control={form.control}
|
||||
name="provider"
|
||||
label="Guardrail Provider"
|
||||
rules={requiredRule("Please select a provider")}
|
||||
label={t("guardrails.addForm.fields.provider.label")}
|
||||
rules={requiredRule(t("guardrails.addForm.fields.provider.required"))}
|
||||
>
|
||||
{({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => (
|
||||
<Combobox
|
||||
|
|
@ -830,11 +841,11 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
id={id}
|
||||
aria-invalid={ariaInvalid}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
placeholder="Select a guardrail provider"
|
||||
placeholder={t("guardrails.addForm.fields.provider.placeholder")}
|
||||
className="w-full"
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No matching providers</ComboboxEmpty>
|
||||
<ComboboxEmpty>{t("guardrails.addForm.fields.provider.empty")}</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(key: string) => (
|
||||
<ComboboxItem key={key} value={key}>
|
||||
|
|
@ -857,8 +868,8 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
<GuardrailField
|
||||
control={form.control}
|
||||
name="mode"
|
||||
label={labelWithHint("Mode", "How the guardrail should be applied")}
|
||||
rules={requiredRule("Please select a mode")}
|
||||
label={labelWithHint(t("guardrails.addForm.fields.mode.label"), t("guardrails.addForm.fields.mode.hint"))}
|
||||
rules={requiredRule(t("guardrails.addForm.fields.mode.required"))}
|
||||
>
|
||||
{({ id, value, onChange }) => (
|
||||
<MultiSelect
|
||||
|
|
@ -866,7 +877,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
options={supportedModes.map((mode) => ({
|
||||
label: mode,
|
||||
value: mode,
|
||||
description: modeDescriptions[mode as keyof typeof modeDescriptions],
|
||||
description: MODE_DESCRIPTION_KEYS[mode] ? t(MODE_DESCRIPTION_KEYS[mode]) : undefined,
|
||||
}))}
|
||||
value={asStringArray(value)}
|
||||
onValueChange={onChange}
|
||||
|
|
@ -880,20 +891,23 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
<GuardrailField
|
||||
control={form.control}
|
||||
name="default_on"
|
||||
label={labelWithHint("Always On", "If enabled, this guardrail will be applied to all requests by default.")}
|
||||
label={labelWithHint(
|
||||
t("guardrails.addForm.fields.alwaysOn.label"),
|
||||
t("guardrails.addForm.fields.alwaysOn.hint"),
|
||||
)}
|
||||
>
|
||||
{({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => (
|
||||
<Select
|
||||
items={ALWAYS_ON_ITEMS}
|
||||
items={alwaysOnItems}
|
||||
value={typeof value === "boolean" ? value : null}
|
||||
onValueChange={(next: boolean | null) => onChange(next)}
|
||||
>
|
||||
<SelectTrigger id={id} aria-invalid={ariaInvalid} aria-describedby={ariaDescribedBy} className="w-full">
|
||||
<SelectValue placeholder="Select an option" />
|
||||
<SelectValue placeholder={t("guardrails.addForm.fields.alwaysOn.placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={true}>Yes</SelectItem>
|
||||
<SelectItem value={false}>No</SelectItem>
|
||||
<SelectItem value={true}>{t("guardrails.addForm.fields.alwaysOn.yes")}</SelectItem>
|
||||
<SelectItem value={false}>{t("guardrails.addForm.fields.alwaysOn.no")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
|
@ -903,8 +917,8 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
control={form.control}
|
||||
name="skip_system_message_choice"
|
||||
label={labelWithHint(
|
||||
"Skip system messages in guardrail",
|
||||
"Unified guardrails only: omit role: system from guardrail evaluation input (OpenAI chat + Anthropic messages). The model still receives full messages. Use global default follows litellm_settings.skip_system_message_in_guardrail.",
|
||||
t("guardrails.addForm.fields.skipSystemMessages.label"),
|
||||
t("guardrails.addForm.fields.skipSystemMessages.hint"),
|
||||
)}
|
||||
>
|
||||
{(fieldControl) => <SkipMessageSelect control={fieldControl} />}
|
||||
|
|
@ -914,8 +928,8 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
control={form.control}
|
||||
name="skip_tool_message_choice"
|
||||
label={labelWithHint(
|
||||
"Skip tool messages in guardrail",
|
||||
"Unified guardrails only: omit role: tool from guardrail evaluation input (OpenAI chat + Anthropic messages). The model still receives full messages. Use global default follows litellm_settings.skip_tool_message_in_guardrail.",
|
||||
t("guardrails.addForm.fields.skipToolMessages.label"),
|
||||
t("guardrails.addForm.fields.skipToolMessages.hint"),
|
||||
)}
|
||||
>
|
||||
{(fieldControl) => <SkipMessageSelect control={fieldControl} />}
|
||||
|
|
@ -1078,14 +1092,13 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
<div className="space-y-6">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure settings for a specific call type. Most guardrails don't need this — skip it unless you're using a
|
||||
specific endpoint like <code>/v1/realtime</code>.
|
||||
<Trans i18nKey="guardrails.addForm.endpoint.intro" components={{ code: <code /> }} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="guardrail-call-type" className="mb-1 block text-sm font-medium text-foreground">
|
||||
Call type
|
||||
{t("guardrails.addForm.endpoint.callType.label")}
|
||||
</label>
|
||||
<Select
|
||||
items={CALL_TYPE_ITEMS}
|
||||
|
|
@ -1096,7 +1109,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
}}
|
||||
>
|
||||
<SelectTrigger id="guardrail-call-type" className="w-65">
|
||||
<SelectValue placeholder="Select a call type" />
|
||||
<SelectValue placeholder={t("guardrails.addForm.endpoint.callType.placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CALL_TYPE_ITEMS.map((item) => (
|
||||
|
|
@ -1106,7 +1119,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-xs text-muted-foreground">More call types coming soon.</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{t("guardrails.addForm.endpoint.callType.comingSoon")}</p>
|
||||
</div>
|
||||
|
||||
{selectedEndpointType === "realtime" && (
|
||||
|
|
@ -1116,7 +1129,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
onClick={() => setEndpointSettingsOpen((o) => !o)}
|
||||
className="flex w-full items-center justify-between bg-muted px-4 py-3 text-sm font-medium text-foreground hover:bg-muted/70"
|
||||
>
|
||||
<span>/v1/realtime settings</span>
|
||||
<span>{t("guardrails.addForm.endpoint.realtime.title")}</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-muted-foreground transition-transform ${endpointSettingsOpen ? "rotate-180" : ""}`}
|
||||
fill="none"
|
||||
|
|
@ -1135,17 +1148,16 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
htmlFor="guardrail-end-session-after"
|
||||
className="mb-1 block text-sm font-medium text-foreground"
|
||||
>
|
||||
End session after X violations
|
||||
{t("guardrails.addForm.endpoint.realtime.endSessionAfter.label")}
|
||||
</label>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
Automatically close the session after this many guardrail violations. Leave empty to never
|
||||
auto-close.
|
||||
{t("guardrails.addForm.endpoint.realtime.endSessionAfter.description")}
|
||||
</p>
|
||||
<Input
|
||||
id="guardrail-end-session-after"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="e.g. 3"
|
||||
placeholder={t("guardrails.addForm.endpoint.realtime.endSessionAfter.placeholder")}
|
||||
value={endSessionAfterNFails ?? ""}
|
||||
onChange={(e) =>
|
||||
setEndSessionAfterNFails(e.target.value ? parseInt(e.target.value, 10) : undefined)
|
||||
|
|
@ -1155,7 +1167,9 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">On violation</label>
|
||||
<label className="mb-2 block text-sm font-medium text-foreground">
|
||||
{t("guardrails.addForm.endpoint.realtime.onViolation.label")}
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{(["warn", "end_session"] as const).map((opt) => (
|
||||
<label key={opt} className="flex items-start gap-2 cursor-pointer">
|
||||
|
|
@ -1169,12 +1183,14 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
/>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{opt === "warn" ? "Warn" : "End session"}
|
||||
{opt === "warn"
|
||||
? t("guardrails.addForm.endpoint.realtime.onViolation.warn.label")
|
||||
: t("guardrails.addForm.endpoint.realtime.onViolation.endSession.label")}
|
||||
</span>
|
||||
<p className="m-0 text-xs text-muted-foreground">
|
||||
{opt === "warn"
|
||||
? "Bot speaks the message, session continues"
|
||||
: "Bot speaks the message, connection closes immediately"}
|
||||
? t("guardrails.addForm.endpoint.realtime.onViolation.warn.description")
|
||||
: t("guardrails.addForm.endpoint.realtime.onViolation.endSession.description")}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
|
@ -1187,16 +1203,15 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
htmlFor="guardrail-realtime-message"
|
||||
className="mb-1 block text-sm font-medium text-foreground"
|
||||
>
|
||||
Message the user hears
|
||||
{t("guardrails.addForm.endpoint.realtime.message.label")}
|
||||
</label>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
What the bot says aloud when this guardrail fires. Falls back to the default violation message if
|
||||
empty.
|
||||
{t("guardrails.addForm.endpoint.realtime.message.description")}
|
||||
</p>
|
||||
<Textarea
|
||||
id="guardrail-realtime-message"
|
||||
rows={3}
|
||||
placeholder="e.g. I'm not able to continue this conversation. Please contact us at 1-800-774-2678."
|
||||
placeholder={t("guardrails.addForm.endpoint.realtime.message.placeholder")}
|
||||
value={realtimeViolationMessage}
|
||||
onChange={(e) => setRealtimeViolationMessage(e.target.value)}
|
||||
className="w-full resize-none"
|
||||
|
|
@ -1213,28 +1228,28 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
const getStepConfigs = () => {
|
||||
if (shouldRenderContentFilterConfigSettings(selectedProvider)) {
|
||||
return [
|
||||
{ title: "Basic Info", optional: false },
|
||||
{ title: "Topics", optional: false },
|
||||
{ title: "Patterns", optional: false },
|
||||
{ title: "Keywords", optional: false },
|
||||
{ title: "Endpoint Settings (Optional)", optional: true },
|
||||
{ title: t("guardrails.addForm.steps.basicInfo"), optional: false },
|
||||
{ title: t("guardrails.addForm.steps.topics"), optional: false },
|
||||
{ title: t("guardrails.addForm.steps.patterns"), optional: false },
|
||||
{ title: t("guardrails.addForm.steps.keywords"), optional: false },
|
||||
{ title: t("guardrails.addForm.steps.endpointSettings"), optional: true },
|
||||
];
|
||||
}
|
||||
if (shouldRenderPIIConfigSettings(selectedProvider)) {
|
||||
return [
|
||||
{ title: "Basic Info", optional: false },
|
||||
{ title: "PII Configuration", optional: false },
|
||||
{ title: t("guardrails.addForm.steps.basicInfo"), optional: false },
|
||||
{ title: t("guardrails.addForm.steps.piiConfiguration"), optional: false },
|
||||
];
|
||||
}
|
||||
if (isDecisionModelProvider) {
|
||||
return [
|
||||
{ title: "Basic Info", optional: false },
|
||||
{ title: "Decision Model Configuration", optional: false },
|
||||
{ title: t("guardrails.addForm.steps.basicInfo"), optional: false },
|
||||
{ title: t("guardrails.addForm.steps.decisionModelConfiguration"), optional: false },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ title: "Basic Info", optional: false },
|
||||
{ title: "Provider Configuration", optional: false },
|
||||
{ title: t("guardrails.addForm.steps.basicInfo"), optional: false },
|
||||
{ title: t("guardrails.addForm.steps.providerConfiguration"), optional: false },
|
||||
];
|
||||
};
|
||||
|
||||
|
|
@ -1250,7 +1265,9 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
<div className="flex min-w-0 flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border px-6 py-4">
|
||||
<DialogTitle className="m-0 text-base font-semibold text-foreground">Create guardrail</DialogTitle>
|
||||
<DialogTitle className="m-0 text-base font-semibold text-foreground">
|
||||
{t("guardrails.addForm.title")}
|
||||
</DialogTitle>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
|
|
@ -1290,9 +1307,15 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
>
|
||||
<span className={`text-sm ${getStepTitleClass(isDone, isCurrent)}`}>{step.title}</span>
|
||||
{step.optional && !isCurrent && (
|
||||
<span className="text-[11px] text-muted-foreground">optional</span>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{t("guardrails.addForm.steps.optional")}
|
||||
</span>
|
||||
)}
|
||||
{isDone && (
|
||||
<span className="text-[11px] text-info hover:underline">
|
||||
{t("guardrails.addForm.steps.edit")}
|
||||
</span>
|
||||
)}
|
||||
{isDone && <span className="text-[11px] text-info hover:underline">Edit</span>}
|
||||
</div>
|
||||
|
||||
{/* Expanded form content for current step */}
|
||||
|
|
@ -1307,21 +1330,21 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
{/* Bottom bar */}
|
||||
<div className="flex items-center justify-end space-x-3 border-t border-border px-6 py-3">
|
||||
<Button type="button" variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
{t("guardrails.addForm.actions.cancel")}
|
||||
</Button>
|
||||
{currentStep > 0 && (
|
||||
<Button type="button" variant="outline" onClick={prevStep}>
|
||||
Previous
|
||||
{t("guardrails.addForm.actions.previous")}
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < stepConfigs.length - 1 ? (
|
||||
<Button type="button" onClick={nextStep}>
|
||||
Next
|
||||
{t("guardrails.addForm.actions.next")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="button" onClick={handleSubmit} disabled={loading}>
|
||||
{loading && <UiLoadingSpinner className="size-4" />}
|
||||
Create Guardrail
|
||||
{t("guardrails.addForm.actions.create")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "@/components/shared/DataTable";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ACTION_ITEMS, SEVERITY_ITEMS } from "./action_options";
|
||||
import { ACTION_ITEMS, SEVERITY_ITEMS, translateOptions } from "./action_options";
|
||||
|
||||
interface ContentCategory {
|
||||
id: string;
|
||||
|
|
@ -30,9 +31,12 @@ const CategoryTable: React.FC<CategoryTableProps> = ({
|
|||
onRemove,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const actionItems = translateOptions(ACTION_ITEMS, t);
|
||||
const severityItems = translateOptions(SEVERITY_ITEMS, t);
|
||||
const columns: ColumnDef<ContentCategory>[] = [
|
||||
{
|
||||
header: "Category",
|
||||
header: t("guardrails.contentFilter.categoryTable.columns.category"),
|
||||
accessorKey: "display_name",
|
||||
cell: ({ row }) => {
|
||||
const { category, display_name: displayName } = row.original;
|
||||
|
|
@ -45,7 +49,7 @@ const CategoryTable: React.FC<CategoryTableProps> = ({
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Severity Threshold",
|
||||
header: t("guardrails.contentFilter.categoryTable.columns.severityThreshold"),
|
||||
accessorKey: "severity_threshold",
|
||||
size: 180,
|
||||
cell: ({ row }) => {
|
||||
|
|
@ -55,17 +59,21 @@ const CategoryTable: React.FC<CategoryTableProps> = ({
|
|||
}
|
||||
return (
|
||||
<Select
|
||||
items={SEVERITY_ITEMS}
|
||||
items={severityItems}
|
||||
value={severity}
|
||||
onValueChange={(value: string | null) =>
|
||||
value && onSeverityChange?.(id, value as "high" | "medium" | "low")
|
||||
}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-[150px]" aria-label="Severity Threshold">
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-[150px]"
|
||||
aria-label={t("guardrails.contentFilter.categoryTable.columns.severityThreshold")}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SEVERITY_ITEMS.map((item) => (
|
||||
{severityItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -76,7 +84,7 @@ const CategoryTable: React.FC<CategoryTableProps> = ({
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Action",
|
||||
header: t("guardrails.contentFilter.categoryTable.columns.action"),
|
||||
accessorKey: "action",
|
||||
size: 150,
|
||||
cell: ({ row }) => {
|
||||
|
|
@ -86,15 +94,19 @@ const CategoryTable: React.FC<CategoryTableProps> = ({
|
|||
}
|
||||
return (
|
||||
<Select
|
||||
items={ACTION_ITEMS}
|
||||
items={actionItems}
|
||||
value={action}
|
||||
onValueChange={(value: string | null) => value && onActionChange?.(id, value as "BLOCK" | "MASK")}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-[120px]" aria-label="Action">
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-[120px]"
|
||||
aria-label={t("guardrails.contentFilter.categoryTable.columns.action")}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACTION_ITEMS.map((item) => (
|
||||
{actionItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -114,14 +126,16 @@ const CategoryTable: React.FC<CategoryTableProps> = ({
|
|||
cell: ({ row }) => (
|
||||
<Button variant="ghost" size="sm" onClick={() => onRemove?.(row.original.id)}>
|
||||
<Trash2 />
|
||||
Delete
|
||||
{t("guardrails.contentFilter.categoryTable.delete")}
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (categories.length === 0) {
|
||||
return <div className="py-10 text-center text-muted-foreground">No categories configured.</div>;
|
||||
return (
|
||||
<div className="py-10 text-center text-muted-foreground">{t("guardrails.contentFilter.categoryTable.empty")}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <DataTable data={categories} columns={columns} getRowId={(row) => row.id} size="compact" />;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import type { ParseKeys } from "i18next";
|
||||
import React, { useEffect, useId, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { getMajorAirlines } from "@/components/networking";
|
||||
import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field";
|
||||
|
|
@ -49,26 +51,43 @@ const DEFAULT_CONFIG: CompetitorIntentConfig = {
|
|||
threshold_low: 0.3,
|
||||
};
|
||||
|
||||
const INTENT_TYPES = [
|
||||
{ value: "airline", label: "Airline (auto-load competitors from IATA)" },
|
||||
{ value: "generic", label: "Generic (specify competitors manually)" },
|
||||
] as const;
|
||||
const INTENT_TYPES: readonly { value: CompetitorIntentConfig["competitor_intent_type"]; labelKey: ParseKeys }[] = [
|
||||
{ value: "airline", labelKey: "guardrails.contentFilter.competitorIntent.intentTypes.airline" },
|
||||
{ value: "generic", labelKey: "guardrails.contentFilter.competitorIntent.intentTypes.generic" },
|
||||
];
|
||||
|
||||
const COMPETITOR_COMPARISON_POLICIES = [
|
||||
{ value: "refuse", label: "Refuse (block request)" },
|
||||
{ value: "reframe", label: "Reframe (suggest alternative)" },
|
||||
] as const;
|
||||
type PolicyChoice = "refuse" | "reframe";
|
||||
|
||||
const POSSIBLE_COMPETITOR_COMPARISON_POLICIES = [
|
||||
{ value: "refuse", label: "Refuse (block request)" },
|
||||
{ value: "reframe", label: "Reframe (suggest alternative to backend LLM)" },
|
||||
] as const;
|
||||
const COMPETITOR_COMPARISON_POLICIES: readonly { value: PolicyChoice; labelKey: ParseKeys }[] = [
|
||||
{ value: "refuse", labelKey: "guardrails.contentFilter.competitorIntent.policies.refuse" },
|
||||
{ value: "reframe", labelKey: "guardrails.contentFilter.competitorIntent.policies.reframe" },
|
||||
];
|
||||
|
||||
const POSSIBLE_COMPETITOR_COMPARISON_POLICIES: readonly { value: PolicyChoice; labelKey: ParseKeys }[] = [
|
||||
{ value: "refuse", labelKey: "guardrails.contentFilter.competitorIntent.policies.refuse" },
|
||||
{ value: "reframe", labelKey: "guardrails.contentFilter.competitorIntent.policies.reframeToBackend" },
|
||||
];
|
||||
|
||||
const THRESHOLDS = [
|
||||
{ field: "threshold_high", label: "High", hint: "e.g. 0.7", fallback: 0.7 },
|
||||
{ field: "threshold_medium", label: "Medium", hint: "e.g. 0.45", fallback: 0.45 },
|
||||
{ field: "threshold_low", label: "Low", hint: "e.g. 0.3", fallback: 0.3 },
|
||||
] as const;
|
||||
{
|
||||
field: "threshold_high",
|
||||
labelKey: "guardrails.contentFilter.competitorIntent.thresholds.high",
|
||||
hint: 0.7,
|
||||
fallback: 0.7,
|
||||
},
|
||||
{
|
||||
field: "threshold_medium",
|
||||
labelKey: "guardrails.contentFilter.competitorIntent.thresholds.medium",
|
||||
hint: 0.45,
|
||||
fallback: 0.45,
|
||||
},
|
||||
{
|
||||
field: "threshold_low",
|
||||
labelKey: "guardrails.contentFilter.competitorIntent.thresholds.low",
|
||||
hint: 0.3,
|
||||
fallback: 0.3,
|
||||
},
|
||||
] as const satisfies readonly { field: string; labelKey: ParseKeys; hint: number; fallback: number }[];
|
||||
|
||||
const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps> = ({
|
||||
enabled,
|
||||
|
|
@ -76,6 +95,7 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
onChange,
|
||||
accessToken,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const effectiveConfig = config ?? DEFAULT_CONFIG;
|
||||
const [airlineOptions, setAirlineOptions] = useState<MajorAirline[]>([]);
|
||||
const [loadingAirlines, setLoadingAirlines] = useState(false);
|
||||
|
|
@ -139,7 +159,7 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
|
||||
const header = (
|
||||
<CardHeader className="gap-0">
|
||||
<CardTitle className="text-base">Competitor Intent Filter</CardTitle>
|
||||
<CardTitle className="text-base">{t("guardrails.contentFilter.competitorIntent.title")}</CardTitle>
|
||||
<CardAction>
|
||||
<Switch checked={enabled} onCheckedChange={handleEnabledChange} />
|
||||
</CardAction>
|
||||
|
|
@ -152,8 +172,7 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
{header}
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Block or reframe competitor comparison questions. When enabled, airline type auto-loads competitors from
|
||||
IATA; generic type requires manual competitor list.
|
||||
{t("guardrails.contentFilter.competitorIntent.disabledDescription")}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -175,19 +194,30 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
})
|
||||
: [];
|
||||
|
||||
const intentTypes = INTENT_TYPES.map(({ value, labelKey }) => ({ value, label: t(labelKey) }));
|
||||
const competitorComparisonPolicies = COMPETITOR_COMPARISON_POLICIES.map(({ value, labelKey }) => ({
|
||||
value,
|
||||
label: t(labelKey),
|
||||
}));
|
||||
const possibleCompetitorComparisonPolicies = POSSIBLE_COMPETITOR_COMPARISON_POLICIES.map(({ value, labelKey }) => ({
|
||||
value,
|
||||
label: t(labelKey),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{header}
|
||||
<CardContent>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Block or reframe competitor comparison questions. Airline type uses major airlines (excluding your brand);
|
||||
generic requires manual competitor list.
|
||||
{t("guardrails.contentFilter.competitorIntent.enabledDescription")}
|
||||
</p>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`${fieldId}-type`}>Type</FieldLabel>
|
||||
<FieldLabel htmlFor={`${fieldId}-type`}>
|
||||
{t("guardrails.contentFilter.competitorIntent.typeLabel")}
|
||||
</FieldLabel>
|
||||
<Select
|
||||
items={INTENT_TYPES}
|
||||
items={intentTypes}
|
||||
value={effectiveConfig.competitor_intent_type}
|
||||
onValueChange={(v: string | null) => v !== null && handleConfigChange("competitor_intent_type", v)}
|
||||
>
|
||||
|
|
@ -195,7 +225,7 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{INTENT_TYPES.map((type) => (
|
||||
{intentTypes.map((type) => (
|
||||
<SelectItem key={type.value} value={type.value} title={type.label}>
|
||||
{type.label}
|
||||
</SelectItem>
|
||||
|
|
@ -205,7 +235,9 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`${fieldId}-brand-self`}>Your Brand (brand_self)</FieldLabel>
|
||||
<FieldLabel htmlFor={`${fieldId}-brand-self`}>
|
||||
{t("guardrails.contentFilter.competitorIntent.brandSelfLabel", { field: "brand_self" })}
|
||||
</FieldLabel>
|
||||
<TagsInput
|
||||
id={`${fieldId}-brand-self`}
|
||||
value={effectiveConfig.brand_self}
|
||||
|
|
@ -219,49 +251,57 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
loading={loadingAirlines}
|
||||
placeholder={
|
||||
effectiveConfig.competitor_intent_type === "airline"
|
||||
? "Search or select airline, or type to add custom"
|
||||
: "Type and press Enter to add"
|
||||
? t("guardrails.contentFilter.competitorIntent.airlinePlaceholder")
|
||||
: t("guardrails.contentFilter.competitorIntent.typeToAddPlaceholder")
|
||||
}
|
||||
/>
|
||||
<FieldDescription>
|
||||
{effectiveConfig.competitor_intent_type === "airline"
|
||||
? "Select your airline from the list (excluded from competitors) or type to add a custom term"
|
||||
: "Names/codes users use for your brand"}
|
||||
? t("guardrails.contentFilter.competitorIntent.airlineBrandDescription")
|
||||
: t("guardrails.contentFilter.competitorIntent.genericBrandDescription")}
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
|
||||
{effectiveConfig.competitor_intent_type === "airline" && (
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`${fieldId}-locations`}>Locations (optional)</FieldLabel>
|
||||
<FieldLabel htmlFor={`${fieldId}-locations`}>
|
||||
{t("guardrails.contentFilter.competitorIntent.locationsLabel")}
|
||||
</FieldLabel>
|
||||
<TagsInput
|
||||
id={`${fieldId}-locations`}
|
||||
value={effectiveConfig.locations ?? []}
|
||||
onValueChange={(v) => handleNestedArrayChange("locations", v)}
|
||||
tokenSeparators={[","]}
|
||||
placeholder="Type and press Enter to add"
|
||||
placeholder={t("guardrails.contentFilter.competitorIntent.typeToAddPlaceholder")}
|
||||
/>
|
||||
<FieldDescription>Countries, cities, airports for disambiguation (e.g. qatar, doha)</FieldDescription>
|
||||
<FieldDescription>{t("guardrails.contentFilter.competitorIntent.locationsDescription")}</FieldDescription>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{effectiveConfig.competitor_intent_type === "generic" && (
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`${fieldId}-competitors`}>Competitors</FieldLabel>
|
||||
<FieldLabel htmlFor={`${fieldId}-competitors`}>
|
||||
{t("guardrails.contentFilter.competitorIntent.competitorsLabel")}
|
||||
</FieldLabel>
|
||||
<TagsInput
|
||||
id={`${fieldId}-competitors`}
|
||||
value={effectiveConfig.competitors ?? []}
|
||||
onValueChange={(v) => handleNestedArrayChange("competitors", v)}
|
||||
tokenSeparators={[","]}
|
||||
placeholder="Type and press Enter to add"
|
||||
placeholder={t("guardrails.contentFilter.competitorIntent.typeToAddPlaceholder")}
|
||||
/>
|
||||
<FieldDescription>Competitor names to detect (required for generic type)</FieldDescription>
|
||||
<FieldDescription>
|
||||
{t("guardrails.contentFilter.competitorIntent.competitorsDescription")}
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`${fieldId}-competitor-comparison`}>Policy: Competitor comparison</FieldLabel>
|
||||
<FieldLabel htmlFor={`${fieldId}-competitor-comparison`}>
|
||||
{t("guardrails.contentFilter.competitorIntent.competitorComparisonLabel")}
|
||||
</FieldLabel>
|
||||
<Select
|
||||
items={COMPETITOR_COMPARISON_POLICIES}
|
||||
items={competitorComparisonPolicies}
|
||||
value={effectiveConfig.policy?.competitor_comparison ?? "refuse"}
|
||||
onValueChange={(v: string | null) => v !== null && handlePolicyChange("competitor_comparison", v)}
|
||||
>
|
||||
|
|
@ -269,7 +309,7 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COMPETITOR_COMPARISON_POLICIES.map((policy) => (
|
||||
{competitorComparisonPolicies.map((policy) => (
|
||||
<SelectItem key={policy.value} value={policy.value} title={policy.label}>
|
||||
{policy.label}
|
||||
</SelectItem>
|
||||
|
|
@ -280,10 +320,10 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
|
||||
<Field>
|
||||
<FieldLabel htmlFor={`${fieldId}-possible-competitor-comparison`}>
|
||||
Policy: Possible competitor comparison
|
||||
{t("guardrails.contentFilter.competitorIntent.possibleCompetitorComparisonLabel")}
|
||||
</FieldLabel>
|
||||
<Select
|
||||
items={POSSIBLE_COMPETITOR_COMPARISON_POLICIES}
|
||||
items={possibleCompetitorComparisonPolicies}
|
||||
value={effectiveConfig.policy?.possible_competitor_comparison ?? "reframe"}
|
||||
onValueChange={(v: string | null) =>
|
||||
v !== null && handlePolicyChange("possible_competitor_comparison", v)
|
||||
|
|
@ -293,7 +333,7 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{POSSIBLE_COMPETITOR_COMPARISON_POLICIES.map((policy) => (
|
||||
{possibleCompetitorComparisonPolicies.map((policy) => (
|
||||
<SelectItem key={policy.value} value={policy.value} title={policy.label}>
|
||||
{policy.label}
|
||||
</SelectItem>
|
||||
|
|
@ -303,11 +343,11 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel>Confidence thresholds</FieldLabel>
|
||||
<FieldLabel>{t("guardrails.contentFilter.competitorIntent.thresholdsLabel")}</FieldLabel>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{THRESHOLDS.map((threshold) => (
|
||||
<Field key={threshold.field} className="w-20">
|
||||
<FieldLabel htmlFor={`${fieldId}-${threshold.field}`}>{threshold.label}</FieldLabel>
|
||||
<FieldLabel htmlFor={`${fieldId}-${threshold.field}`}>{t(threshold.labelKey)}</FieldLabel>
|
||||
<ThresholdInput
|
||||
id={`${fieldId}-${threshold.field}`}
|
||||
value={effectiveConfig[threshold.field] ?? threshold.fallback}
|
||||
|
|
@ -316,26 +356,35 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
|
|||
max={1}
|
||||
step={0.05}
|
||||
/>
|
||||
<FieldDescription>{threshold.hint}</FieldDescription>
|
||||
<FieldDescription>
|
||||
{t("guardrails.contentFilter.competitorIntent.thresholds.example", { value: threshold.hint })}
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
))}
|
||||
</div>
|
||||
<FieldDescription>
|
||||
Classify competitor intent by confidence (0–1). Higher confidence -> stronger intent.
|
||||
{t("guardrails.contentFilter.competitorIntent.thresholdsHelp.intro")}
|
||||
<ul className="mt-1 mb-0 list-disc pl-5">
|
||||
<li>
|
||||
<strong>High (≥)</strong>: Treat as full competitor comparison -> uses "Competitor
|
||||
comparison" policy
|
||||
<Trans
|
||||
i18nKey="guardrails.contentFilter.competitorIntent.thresholdsHelp.high"
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Medium (≥)</strong>: Treat as possible comparison -> uses "Possible competitor
|
||||
comparison" policy
|
||||
<Trans
|
||||
i18nKey="guardrails.contentFilter.competitorIntent.thresholdsHelp.medium"
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Low (≥)</strong>: Log only; allow request. Below Low -> allow with no action
|
||||
<Trans
|
||||
i18nKey="guardrails.contentFilter.competitorIntent.thresholdsHelp.low"
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
Raise thresholds to be more permissive; lower them to be stricter.
|
||||
{t("guardrails.contentFilter.competitorIntent.thresholdsHelp.outro")}
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronRight, FileText, Plus, Trash2 } from "lucide-react";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { getCategoryYaml } from "@/components/networking";
|
||||
|
|
@ -16,7 +17,7 @@ import {
|
|||
ComboboxList,
|
||||
} from "@/components/ui/combobox";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ACTION_ITEMS, SEVERITY_ITEMS } from "./action_options";
|
||||
import { ACTION_ITEMS, SEVERITY_ITEMS, translateOptions } from "./action_options";
|
||||
|
||||
interface ContentCategory {
|
||||
name: string;
|
||||
|
|
@ -54,6 +55,9 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
|
|||
pendingSelection,
|
||||
onPendingSelectionChange,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const actionItems = translateOptions(ACTION_ITEMS, t);
|
||||
const severityItems = translateOptions(SEVERITY_ITEMS, t);
|
||||
// Use controlled state if parent provides it, otherwise use local state
|
||||
const [localSelectedCategoryName, setLocalSelectedCategoryName] = React.useState<string>("");
|
||||
const selectedCategoryName = pendingSelection !== undefined ? pendingSelection : localSelectedCategoryName;
|
||||
|
|
@ -174,7 +178,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
|
|||
|
||||
const columns: ColumnDef<SelectedCategory>[] = [
|
||||
{
|
||||
header: "Category",
|
||||
header: t("guardrails.contentFilter.categoryConfiguration.columns.category"),
|
||||
accessorKey: "display_name",
|
||||
cell: ({ row }) => {
|
||||
const category = availableCategories.find((c) => c.name === row.original.category);
|
||||
|
|
@ -187,20 +191,24 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Action",
|
||||
header: t("guardrails.contentFilter.categoryConfiguration.columns.action"),
|
||||
accessorKey: "action",
|
||||
size: 150,
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
items={ACTION_ITEMS}
|
||||
items={actionItems}
|
||||
value={row.original.action}
|
||||
onValueChange={(value: string | null) => value && onCategoryUpdate(row.original.id, "action", value)}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-full" aria-label="Action">
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-full"
|
||||
aria-label={t("guardrails.contentFilter.categoryConfiguration.columns.action")}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACTION_ITEMS.map((item) => (
|
||||
{actionItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<Badge variant={item.value === "BLOCK" ? "destructive" : "secondary"}>{item.value}</Badge>
|
||||
</SelectItem>
|
||||
|
|
@ -210,22 +218,26 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
|
|||
),
|
||||
},
|
||||
{
|
||||
header: "Severity Threshold",
|
||||
header: t("guardrails.contentFilter.categoryConfiguration.columns.severityThreshold"),
|
||||
accessorKey: "severity_threshold",
|
||||
size: 180,
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
items={SEVERITY_ITEMS}
|
||||
items={severityItems}
|
||||
value={row.original.severity_threshold}
|
||||
onValueChange={(value: string | null) =>
|
||||
value && onCategoryUpdate(row.original.id, "severity_threshold", value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-full" aria-label="Severity Threshold">
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-full"
|
||||
aria-label={t("guardrails.contentFilter.categoryConfiguration.columns.severityThreshold")}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SEVERITY_ITEMS.map((item) => (
|
||||
{severityItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -241,7 +253,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
|
|||
cell: ({ row }) => (
|
||||
<Button variant="outline" size="sm" onClick={() => onCategoryRemove(row.original.id)}>
|
||||
<Trash2 />
|
||||
Remove
|
||||
{t("guardrails.contentFilter.categoryConfiguration.remove")}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
|
|
@ -256,9 +268,9 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
|
|||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<CardTitle>Blocked topics</CardTitle>
|
||||
<CardTitle>{t("guardrails.contentFilter.categoryConfiguration.title")}</CardTitle>
|
||||
<p className="text-xs font-normal text-muted-foreground">
|
||||
Select topics to block using keyword and semantic analysis
|
||||
{t("guardrails.contentFilter.categoryConfiguration.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
|
@ -270,9 +282,12 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
|
|||
onValueChange={(category: ContentCategory | null) => setSelectedCategoryName(category?.name ?? "")}
|
||||
itemToStringLabel={(category: ContentCategory) => category.display_name}
|
||||
>
|
||||
<ComboboxInput className="w-full" placeholder="Select a content category" />
|
||||
<ComboboxInput
|
||||
className="w-full"
|
||||
placeholder={t("guardrails.contentFilter.categoryConfiguration.placeholder")}
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No matching categories</ComboboxEmpty>
|
||||
<ComboboxEmpty>{t("guardrails.contentFilter.categoryConfiguration.noMatches")}</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(cat: ContentCategory) => (
|
||||
<ComboboxItem key={cat.name} value={cat}>
|
||||
|
|
@ -287,7 +302,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
|
|||
</Combobox>
|
||||
<Button onClick={handleAddCategory} disabled={!selectedCategoryName}>
|
||||
<Plus />
|
||||
Add
|
||||
{t("guardrails.contentFilter.categoryConfiguration.add")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -295,7 +310,8 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
|
|||
{selectedCategoryName && (
|
||||
<div className="mb-4 rounded-md border border-border bg-muted/40 p-3">
|
||||
<div className="mb-2 text-sm font-medium">
|
||||
Preview: {availableCategories.find((c) => c.name === selectedCategoryName)?.display_name}
|
||||
{t("guardrails.contentFilter.categoryConfiguration.preview")}{" "}
|
||||
{availableCategories.find((c) => c.name === selectedCategoryName)?.display_name}
|
||||
{categoryFileTypes[selectedCategoryName] && (
|
||||
<span className="ml-2 text-xs font-normal text-muted-foreground">
|
||||
({categoryFileTypes[selectedCategoryName]?.toUpperCase()})
|
||||
|
|
@ -303,13 +319,17 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
|
|||
)}
|
||||
</div>
|
||||
{loadingPreviewYaml ? (
|
||||
<div className="p-4 text-center text-muted-foreground">Loading content...</div>
|
||||
<div className="p-4 text-center text-muted-foreground">
|
||||
{t("guardrails.contentFilter.categoryConfiguration.loadingContent")}
|
||||
</div>
|
||||
) : previewYaml ? (
|
||||
<pre className="m-0 max-h-[300px] max-w-full overflow-auto rounded-md border border-border bg-background p-3 text-xs leading-relaxed break-words whitespace-pre-wrap">
|
||||
<code>{previewYaml}</code>
|
||||
</pre>
|
||||
) : (
|
||||
<div className="p-2 text-center text-xs text-muted-foreground">Unable to load category content</div>
|
||||
<div className="p-2 text-center text-xs text-muted-foreground">
|
||||
{t("guardrails.contentFilter.categoryConfiguration.previewUnavailable")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -339,18 +359,25 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
|
|||
<ChevronRight className={`size-4 transition-transform ${isExpanded ? "rotate-90" : ""}`} />
|
||||
<FileText className="size-4" />
|
||||
<span>
|
||||
View {fileType.toUpperCase()} for {category.display_name}
|
||||
{t("guardrails.contentFilter.categoryConfiguration.viewContent", {
|
||||
fileType: fileType.toUpperCase(),
|
||||
name: category.display_name,
|
||||
})}
|
||||
</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
{loadingYaml[category.category] ? (
|
||||
<div className="p-4 text-center text-muted-foreground">Loading content...</div>
|
||||
<div className="p-4 text-center text-muted-foreground">
|
||||
{t("guardrails.contentFilter.categoryConfiguration.loadingContent")}
|
||||
</div>
|
||||
) : categoryYaml[category.category] ? (
|
||||
<pre className="m-0 max-h-[400px] overflow-auto rounded-md bg-muted p-4 text-xs leading-relaxed">
|
||||
<code>{categoryYaml[category.category]}</code>
|
||||
</pre>
|
||||
) : (
|
||||
<div className="p-4 text-center text-muted-foreground">Content will load when expanded</div>
|
||||
<div className="p-4 text-center text-muted-foreground">
|
||||
{t("guardrails.contentFilter.categoryConfiguration.contentLoadsOnExpand")}
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
|
@ -360,7 +387,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
|
|||
</>
|
||||
) : (
|
||||
<div className="rounded-md border border-dashed border-border p-6 text-center text-muted-foreground">
|
||||
No blocked topics selected. Add topics to detect and block harmful content.
|
||||
{t("guardrails.contentFilter.categoryConfiguration.empty")}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Plus, Upload } from "lucide-react";
|
||||
import { validateBlockedWordsFile } from "@/components/networking";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
|
@ -102,6 +103,7 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
competitorIntentConfig = null,
|
||||
onCompetitorIntentChange,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [patternModalVisible, setPatternModalVisible] = useState(false);
|
||||
const [keywordModalVisible, setKeywordModalVisible] = useState(false);
|
||||
const [customPatternModalVisible, setCustomPatternModalVisible] = useState(false);
|
||||
|
|
@ -119,7 +121,7 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
|
||||
const handleAddPrebuiltPattern = () => {
|
||||
if (!selectedPatternName) {
|
||||
toast.error("Please select a pattern");
|
||||
toast.error(t("guardrails.contentFilter.configuration.toasts.selectPattern"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +142,7 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
|
||||
const handleAddCustomPattern = () => {
|
||||
if (!customPatternName || !customPatternRegex) {
|
||||
toast.error("Please provide pattern name and regex");
|
||||
toast.error(t("guardrails.contentFilter.configuration.toasts.patternNameAndRegexRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +162,7 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
|
||||
const handleAddKeyword = () => {
|
||||
if (!newKeyword) {
|
||||
toast.error("Please enter a keyword");
|
||||
toast.error(t("guardrails.contentFilter.configuration.toasts.keywordRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -188,14 +190,17 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
if (onFileUpload) {
|
||||
onFileUpload(content);
|
||||
}
|
||||
toast.success(result.message || "File uploaded successfully");
|
||||
toast.success(result.message || t("guardrails.contentFilter.configuration.toasts.uploadSucceeded"));
|
||||
} else {
|
||||
const errorMessage = result.error || (result.errors && result.errors.join(", ")) || "Invalid file";
|
||||
toast.error(`Validation failed: ${errorMessage}`);
|
||||
const errorMessage =
|
||||
result.error ||
|
||||
(result.errors && result.errors.join(", ")) ||
|
||||
t("guardrails.contentFilter.configuration.toasts.invalidFile");
|
||||
toast.error(t("guardrails.contentFilter.configuration.toasts.validationFailed", { error: errorMessage }));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(`Failed to upload file: ${error}`);
|
||||
toast.error(t("guardrails.contentFilter.configuration.toasts.uploadFailed", { error: String(error) }));
|
||||
} finally {
|
||||
setUploadValidating(false);
|
||||
}
|
||||
|
|
@ -219,10 +224,7 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
<div className="space-y-6">
|
||||
{!showStep && (
|
||||
<div>
|
||||
<p className="text-muted-foreground">
|
||||
Configure patterns, keywords, and content categories to detect and filter sensitive information in requests
|
||||
and responses.
|
||||
</p>
|
||||
<p className="text-muted-foreground">{t("guardrails.contentFilter.configuration.intro")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -230,9 +232,9 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<CardTitle>Pattern Detection</CardTitle>
|
||||
<CardTitle>{t("guardrails.contentFilter.configuration.patterns.title")}</CardTitle>
|
||||
<p className="text-sm font-normal text-muted-foreground">
|
||||
Detect sensitive information using regex patterns (SSN, credit cards, API keys, etc.)
|
||||
{t("guardrails.contentFilter.configuration.patterns.description")}
|
||||
</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
|
@ -240,11 +242,11 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
<Button onClick={() => setPatternModalVisible(true)}>
|
||||
<Plus />
|
||||
Add prebuilt pattern
|
||||
{t("guardrails.contentFilter.configuration.patterns.addPrebuilt")}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setCustomPatternModalVisible(true)}>
|
||||
<Plus />
|
||||
Add custom regex
|
||||
{t("guardrails.contentFilter.configuration.patterns.addCustom")}
|
||||
</Button>
|
||||
</div>
|
||||
<PatternTable
|
||||
|
|
@ -260,9 +262,9 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<CardTitle>Blocked Keywords</CardTitle>
|
||||
<CardTitle>{t("guardrails.contentFilter.configuration.keywords.title")}</CardTitle>
|
||||
<p className="text-sm font-normal text-muted-foreground">
|
||||
Block or mask specific sensitive terms and phrases
|
||||
{t("guardrails.contentFilter.configuration.keywords.description")}
|
||||
</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
|
@ -270,7 +272,7 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
<Button onClick={() => setKeywordModalVisible(true)}>
|
||||
<Plus />
|
||||
Add keyword
|
||||
{t("guardrails.contentFilter.configuration.keywords.addKeyword")}
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
|
|
@ -286,7 +288,7 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
|
|||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
{uploadValidating ? <UiLoadingSpinner className="size-4" /> : <Upload />}
|
||||
Upload YAML file
|
||||
{t("guardrails.contentFilter.configuration.keywords.uploadYaml")}
|
||||
</Button>
|
||||
</div>
|
||||
<KeywordTable keywords={blockedWords} onActionChange={onBlockedWordUpdate} onRemove={onBlockedWordRemove} />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import PatternTable from "./PatternTable";
|
||||
|
|
@ -56,6 +57,8 @@ const ContentFilterDisplay: React.FC<ContentFilterDisplayProps> = ({
|
|||
onCategorySeverityChange,
|
||||
onCategoryRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (patterns.length === 0 && blockedWords.length === 0 && categories.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -69,8 +72,11 @@ const ContentFilterDisplay: React.FC<ContentFilterDisplayProps> = ({
|
|||
<Card className="mt-6">
|
||||
<CardContent>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-lg font-semibold">Content Categories</p>
|
||||
<Badge variant="secondary">{categories.length} categories configured</Badge>
|
||||
<p className="text-lg font-semibold">{t("guardrails.contentFilter.display.categoriesTitle")}</p>
|
||||
<Badge variant="secondary">
|
||||
{categories.length}{" "}
|
||||
{t("guardrails.contentFilter.display.categoriesConfigured", { count: categories.length })}
|
||||
</Badge>
|
||||
</div>
|
||||
<CategoryTable
|
||||
categories={categories}
|
||||
|
|
@ -87,8 +93,10 @@ const ContentFilterDisplay: React.FC<ContentFilterDisplayProps> = ({
|
|||
<Card className="mt-6">
|
||||
<CardContent>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-lg font-semibold">Pattern Detection</p>
|
||||
<Badge variant="secondary">{patterns.length} patterns configured</Badge>
|
||||
<p className="text-lg font-semibold">{t("guardrails.contentFilter.display.patternsTitle")}</p>
|
||||
<Badge variant="secondary">
|
||||
{patterns.length} {t("guardrails.contentFilter.display.patternsConfigured", { count: patterns.length })}
|
||||
</Badge>
|
||||
</div>
|
||||
<PatternTable
|
||||
patterns={patterns}
|
||||
|
|
@ -103,8 +111,11 @@ const ContentFilterDisplay: React.FC<ContentFilterDisplayProps> = ({
|
|||
<Card className="mt-6">
|
||||
<CardContent>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-lg font-semibold">Blocked Keywords</p>
|
||||
<Badge variant="secondary">{blockedWords.length} keywords configured</Badge>
|
||||
<p className="text-lg font-semibold">{t("guardrails.contentFilter.display.keywordsTitle")}</p>
|
||||
<Badge variant="secondary">
|
||||
{blockedWords.length}{" "}
|
||||
{t("guardrails.contentFilter.display.keywordsConfigured", { count: blockedWords.length })}
|
||||
</Badge>
|
||||
</div>
|
||||
<KeywordTable
|
||||
keywords={blockedWords}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { TriangleAlert } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Alert, AlertDescription } from "@/components/shared/Alert";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import ContentFilterConfiguration from "./ContentFilterConfiguration";
|
||||
|
|
@ -74,6 +75,7 @@ const ContentFilterManager: React.FC<ContentFilterManagerProps> = ({
|
|||
onDataChange,
|
||||
onUnsavedChanges,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedPatterns, setSelectedPatterns] = useState<Pattern[]>([]);
|
||||
const [blockedWords, setBlockedWords] = useState<BlockedWord[]>([]);
|
||||
const [selectedContentCategories, setSelectedContentCategories] = useState<SelectedContentCategory[]>([]);
|
||||
|
|
@ -234,15 +236,13 @@ const ContentFilterManager: React.FC<ContentFilterManagerProps> = ({
|
|||
return (
|
||||
<>
|
||||
<div className="my-6 flex items-center gap-4">
|
||||
<span className="shrink-0 font-medium">Content Filter Configuration</span>
|
||||
<span className="shrink-0 font-medium">{t("guardrails.contentFilter.manager.heading")}</span>
|
||||
<Separator className="flex-1" />
|
||||
</div>
|
||||
{hasUnsavedChanges && (
|
||||
<Alert variant="warning" className="mb-4">
|
||||
<TriangleAlert />
|
||||
<AlertDescription>
|
||||
You have unsaved changes to patterns or keywords. Remember to click "Save Changes" at the bottom.
|
||||
</AlertDescription>
|
||||
<AlertDescription>{t("guardrails.contentFilter.manager.unsavedChanges")}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="mb-6">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ACTION_ITEMS } from "./action_options";
|
||||
import { ACTION_ITEMS, translateOptions } from "./action_options";
|
||||
|
||||
interface CustomPatternModalProps {
|
||||
visible: boolean;
|
||||
|
|
@ -28,50 +29,61 @@ const CustomPatternModal: React.FC<CustomPatternModalProps> = ({
|
|||
onAdd,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const actionItems = translateOptions(ACTION_ITEMS, t);
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add custom regex pattern</DialogTitle>
|
||||
<DialogTitle>{t("guardrails.contentFilter.customPatternModal.title")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<p className="font-semibold">Pattern name</p>
|
||||
<p className="font-semibold">{t("guardrails.contentFilter.customPatternModal.nameLabel")}</p>
|
||||
<Input
|
||||
className="mt-2"
|
||||
placeholder="e.g., internal_id, employee_code"
|
||||
placeholder={t("guardrails.contentFilter.customPatternModal.namePlaceholder", {
|
||||
example: "internal_id, employee_code",
|
||||
})}
|
||||
value={patternName}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-semibold">Regex pattern</p>
|
||||
<p className="font-semibold">{t("guardrails.contentFilter.customPatternModal.regexLabel")}</p>
|
||||
<Input
|
||||
className="mt-2"
|
||||
placeholder="e.g., ID-[0-9]{6}"
|
||||
placeholder={t("guardrails.contentFilter.customPatternModal.regexPlaceholder", {
|
||||
example: "ID-[0-9]{6}",
|
||||
})}
|
||||
value={patternRegex}
|
||||
onChange={(e) => onRegexChange(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Enter a valid regular expression to match sensitive data</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("guardrails.contentFilter.customPatternModal.regexHelp")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-semibold">Action</p>
|
||||
<p className="font-semibold">{t("guardrails.contentFilter.customPatternModal.actionLabel")}</p>
|
||||
<p className="mt-1 mb-2 text-muted-foreground">
|
||||
Choose what action the guardrail should take when this pattern is detected
|
||||
{t("guardrails.contentFilter.customPatternModal.actionDescription")}
|
||||
</p>
|
||||
<Select
|
||||
items={ACTION_ITEMS}
|
||||
items={actionItems}
|
||||
value={patternAction}
|
||||
onValueChange={(value: string | null) => value && onActionChange(value as "BLOCK" | "MASK")}
|
||||
>
|
||||
<SelectTrigger className="w-full" aria-label="Action">
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
aria-label={t("guardrails.contentFilter.customPatternModal.actionLabel")}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACTION_ITEMS.map((item) => (
|
||||
{actionItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -83,9 +95,9 @@ const CustomPatternModal: React.FC<CustomPatternModalProps> = ({
|
|||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
{t("guardrails.contentFilter.customPatternModal.cancel")}
|
||||
</Button>
|
||||
<Button onClick={onAdd}>Add</Button>
|
||||
<Button onClick={onAdd}>{t("guardrails.contentFilter.customPatternModal.add")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ACTION_ITEMS } from "./action_options";
|
||||
import { ACTION_ITEMS, translateOptions } from "./action_options";
|
||||
|
||||
interface KeywordModalProps {
|
||||
visible: boolean;
|
||||
|
|
@ -29,39 +30,41 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||
onAdd,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const actionItems = translateOptions(ACTION_ITEMS, t);
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add blocked keyword</DialogTitle>
|
||||
<DialogTitle>{t("guardrails.contentFilter.keywordModal.title")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<p className="font-semibold">Keyword</p>
|
||||
<p className="font-semibold">{t("guardrails.contentFilter.keywordModal.keywordLabel")}</p>
|
||||
<Input
|
||||
className="mt-2"
|
||||
placeholder="Enter sensitive keyword or phrase"
|
||||
placeholder={t("guardrails.contentFilter.keywordModal.keywordPlaceholder")}
|
||||
value={keyword}
|
||||
onChange={(e) => onKeywordChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-semibold">Action</p>
|
||||
<p className="font-semibold">{t("guardrails.contentFilter.keywordModal.actionLabel")}</p>
|
||||
<p className="mt-1 mb-2 text-muted-foreground">
|
||||
Choose what action the guardrail should take when this keyword is detected
|
||||
{t("guardrails.contentFilter.keywordModal.actionDescription")}
|
||||
</p>
|
||||
<Select
|
||||
items={ACTION_ITEMS}
|
||||
items={actionItems}
|
||||
value={action}
|
||||
onValueChange={(value: string | null) => value && onActionChange(value as "BLOCK" | "MASK")}
|
||||
>
|
||||
<SelectTrigger className="w-full" aria-label="Action">
|
||||
<SelectTrigger className="w-full" aria-label={t("guardrails.contentFilter.keywordModal.actionLabel")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACTION_ITEMS.map((item) => (
|
||||
{actionItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -71,10 +74,10 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-semibold">Description (optional)</p>
|
||||
<p className="font-semibold">{t("guardrails.contentFilter.keywordModal.descriptionLabel")}</p>
|
||||
<Textarea
|
||||
className="mt-2 field-sizing-fixed"
|
||||
placeholder="Explain why this keyword is sensitive"
|
||||
placeholder={t("guardrails.contentFilter.keywordModal.descriptionPlaceholder")}
|
||||
value={description}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
rows={3}
|
||||
|
|
@ -84,9 +87,9 @@ const KeywordModal: React.FC<KeywordModalProps> = ({
|
|||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
{t("guardrails.contentFilter.keywordModal.cancel")}
|
||||
</Button>
|
||||
<Button onClick={onAdd}>Add</Button>
|
||||
<Button onClick={onAdd}>{t("guardrails.contentFilter.keywordModal.add")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Trash2 } from "lucide-react";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DataTable } from "@/components/shared/DataTable";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ACTION_ITEMS } from "./action_options";
|
||||
import { ACTION_ITEMS, translateOptions } from "./action_options";
|
||||
|
||||
interface BlockedWord {
|
||||
id: string;
|
||||
|
|
@ -20,26 +21,32 @@ interface KeywordTableProps {
|
|||
}
|
||||
|
||||
const KeywordTable: React.FC<KeywordTableProps> = ({ keywords, onActionChange, onRemove }) => {
|
||||
const { t } = useTranslation();
|
||||
const actionItems = translateOptions(ACTION_ITEMS, t);
|
||||
const columns: ColumnDef<BlockedWord>[] = [
|
||||
{
|
||||
header: "Keyword",
|
||||
header: t("guardrails.contentFilter.keywordTable.columns.keyword"),
|
||||
accessorKey: "keyword",
|
||||
},
|
||||
{
|
||||
header: "Action",
|
||||
header: t("guardrails.contentFilter.keywordTable.columns.action"),
|
||||
accessorKey: "action",
|
||||
size: 150,
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
items={ACTION_ITEMS}
|
||||
items={actionItems}
|
||||
value={row.original.action}
|
||||
onValueChange={(value: string | null) => value && onActionChange(row.original.id, "action", value)}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-[120px]" aria-label="Action">
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-[120px]"
|
||||
aria-label={t("guardrails.contentFilter.keywordTable.columns.action")}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACTION_ITEMS.map((item) => (
|
||||
{actionItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -49,7 +56,7 @@ const KeywordTable: React.FC<KeywordTableProps> = ({ keywords, onActionChange, o
|
|||
),
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
header: t("guardrails.contentFilter.keywordTable.columns.description"),
|
||||
accessorKey: "description",
|
||||
cell: ({ row }) => row.original.description || "-",
|
||||
},
|
||||
|
|
@ -60,14 +67,16 @@ const KeywordTable: React.FC<KeywordTableProps> = ({ keywords, onActionChange, o
|
|||
cell: ({ row }) => (
|
||||
<Button variant="ghost" size="sm" onClick={() => onRemove(row.original.id)}>
|
||||
<Trash2 />
|
||||
Delete
|
||||
{t("guardrails.contentFilter.keywordTable.delete")}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (keywords.length === 0) {
|
||||
return <div className="py-10 text-center text-muted-foreground">No keywords added.</div>;
|
||||
return (
|
||||
<div className="py-10 text-center text-muted-foreground">{t("guardrails.contentFilter.keywordTable.empty")}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <DataTable data={keywords} columns={columns} getRowId={(row) => row.id} size="compact" />;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Combobox,
|
||||
|
|
@ -13,7 +14,7 @@ import {
|
|||
} from "@/components/ui/combobox";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ACTION_ITEMS } from "./action_options";
|
||||
import { ACTION_ITEMS, translateOptions } from "./action_options";
|
||||
|
||||
interface PrebuiltPattern {
|
||||
name: string;
|
||||
|
|
@ -55,6 +56,8 @@ const PatternModal: React.FC<PatternModalProps> = ({
|
|||
onAdd,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const actionItems = translateOptions(ACTION_ITEMS, t);
|
||||
const selectedPattern = prebuiltPatterns.find((pattern) => pattern.name === selectedPatternName) ?? null;
|
||||
const patternGroups = categories
|
||||
.map((category) => ({
|
||||
|
|
@ -67,12 +70,12 @@ const PatternModal: React.FC<PatternModalProps> = ({
|
|||
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[800px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add prebuilt pattern</DialogTitle>
|
||||
<DialogTitle>{t("guardrails.contentFilter.patternModal.title")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<p className="font-semibold">Pattern type</p>
|
||||
<p className="font-semibold">{t("guardrails.contentFilter.patternModal.patternTypeLabel")}</p>
|
||||
<Combobox
|
||||
items={patternGroups}
|
||||
value={selectedPattern}
|
||||
|
|
@ -80,9 +83,12 @@ const PatternModal: React.FC<PatternModalProps> = ({
|
|||
itemToStringLabel={(pattern: PrebuiltPattern) => pattern.display_name}
|
||||
filter={matchesPatternQuery}
|
||||
>
|
||||
<ComboboxInput className="mt-2 w-full" placeholder="Choose pattern type" />
|
||||
<ComboboxInput
|
||||
className="mt-2 w-full"
|
||||
placeholder={t("guardrails.contentFilter.patternModal.patternTypePlaceholder")}
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No matching patterns</ComboboxEmpty>
|
||||
<ComboboxEmpty>{t("guardrails.contentFilter.patternModal.noMatches")}</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(group: PatternGroup) => (
|
||||
<ComboboxGroup key={group.category} items={group.items}>
|
||||
|
|
@ -102,20 +108,20 @@ const PatternModal: React.FC<PatternModalProps> = ({
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-semibold">Action</p>
|
||||
<p className="font-semibold">{t("guardrails.contentFilter.patternModal.actionLabel")}</p>
|
||||
<p className="mt-1 mb-2 text-muted-foreground">
|
||||
Choose what action the guardrail should take when this pattern is detected
|
||||
{t("guardrails.contentFilter.patternModal.actionDescription")}
|
||||
</p>
|
||||
<Select
|
||||
items={ACTION_ITEMS}
|
||||
items={actionItems}
|
||||
value={patternAction}
|
||||
onValueChange={(value: string | null) => value && onActionChange(value as "BLOCK" | "MASK")}
|
||||
>
|
||||
<SelectTrigger className="w-full" aria-label="Action">
|
||||
<SelectTrigger className="w-full" aria-label={t("guardrails.contentFilter.patternModal.actionLabel")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACTION_ITEMS.map((item) => (
|
||||
{actionItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -127,9 +133,9 @@ const PatternModal: React.FC<PatternModalProps> = ({
|
|||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
{t("guardrails.contentFilter.patternModal.cancel")}
|
||||
</Button>
|
||||
<Button onClick={onAdd}>Add</Button>
|
||||
<Button onClick={onAdd}>{t("guardrails.contentFilter.patternModal.add")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "@/components/shared/DataTable";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ACTION_ITEMS } from "./action_options";
|
||||
import { ACTION_ITEMS, translateOptions } from "./action_options";
|
||||
|
||||
interface Pattern {
|
||||
id: string;
|
||||
|
|
@ -23,20 +24,28 @@ interface PatternTableProps {
|
|||
}
|
||||
|
||||
const PatternTable: React.FC<PatternTableProps> = ({ patterns, onActionChange, onRemove }) => {
|
||||
const { t } = useTranslation();
|
||||
const actionItems = translateOptions(ACTION_ITEMS, t);
|
||||
const columns: ColumnDef<Pattern>[] = [
|
||||
{
|
||||
header: "Type",
|
||||
header: t("guardrails.contentFilter.patternTable.columns.type"),
|
||||
accessorKey: "type",
|
||||
size: 100,
|
||||
cell: ({ row }) => <Badge variant="secondary">{row.original.type === "prebuilt" ? "Prebuilt" : "Custom"}</Badge>,
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="secondary">
|
||||
{row.original.type === "prebuilt"
|
||||
? t("guardrails.contentFilter.patternTable.types.prebuilt")
|
||||
: t("guardrails.contentFilter.patternTable.types.custom")}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Pattern name",
|
||||
header: t("guardrails.contentFilter.patternTable.columns.name"),
|
||||
accessorKey: "name",
|
||||
cell: ({ row }) => row.original.display_name || row.original.name,
|
||||
},
|
||||
{
|
||||
header: "Regex pattern",
|
||||
header: t("guardrails.contentFilter.patternTable.columns.regex"),
|
||||
accessorKey: "pattern",
|
||||
cell: ({ row }) =>
|
||||
row.original.pattern ? (
|
||||
|
|
@ -46,20 +55,24 @@ const PatternTable: React.FC<PatternTableProps> = ({ patterns, onActionChange, o
|
|||
),
|
||||
},
|
||||
{
|
||||
header: "Action",
|
||||
header: t("guardrails.contentFilter.patternTable.columns.action"),
|
||||
accessorKey: "action",
|
||||
size: 150,
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
items={ACTION_ITEMS}
|
||||
items={actionItems}
|
||||
value={row.original.action}
|
||||
onValueChange={(value: string | null) => value && onActionChange(row.original.id, value as "BLOCK" | "MASK")}
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-[120px]" aria-label="Action">
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-[120px]"
|
||||
aria-label={t("guardrails.contentFilter.patternTable.columns.action")}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACTION_ITEMS.map((item) => (
|
||||
{actionItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -75,14 +88,16 @@ const PatternTable: React.FC<PatternTableProps> = ({ patterns, onActionChange, o
|
|||
cell: ({ row }) => (
|
||||
<Button variant="ghost" size="sm" onClick={() => onRemove(row.original.id)}>
|
||||
<Trash2 />
|
||||
Delete
|
||||
{t("guardrails.contentFilter.patternTable.delete")}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (patterns.length === 0) {
|
||||
return <div className="py-10 text-center text-muted-foreground">No patterns added.</div>;
|
||||
return (
|
||||
<div className="py-10 text-center text-muted-foreground">{t("guardrails.contentFilter.patternTable.empty")}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <DataTable data={patterns} columns={columns} getRowId={(row) => row.id} size="compact" />;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,20 @@
|
|||
export const ACTION_ITEMS = [
|
||||
{ value: "BLOCK", label: "Block" },
|
||||
{ value: "MASK", label: "Mask" },
|
||||
] as const;
|
||||
import type { ParseKeys, TFunction } from "i18next";
|
||||
|
||||
export const SEVERITY_ITEMS = [
|
||||
{ value: "high", label: "High" },
|
||||
{ value: "medium", label: "Medium" },
|
||||
{ value: "low", label: "Low" },
|
||||
] as const;
|
||||
interface OptionItem<V extends string> {
|
||||
value: V;
|
||||
labelKey: ParseKeys;
|
||||
}
|
||||
|
||||
export const ACTION_ITEMS: readonly OptionItem<"BLOCK" | "MASK">[] = [
|
||||
{ value: "BLOCK", labelKey: "guardrails.contentFilter.options.action.block" },
|
||||
{ value: "MASK", labelKey: "guardrails.contentFilter.options.action.mask" },
|
||||
];
|
||||
|
||||
export const SEVERITY_ITEMS: readonly OptionItem<"high" | "medium" | "low">[] = [
|
||||
{ value: "high", labelKey: "guardrails.contentFilter.options.severity.high" },
|
||||
{ value: "medium", labelKey: "guardrails.contentFilter.options.severity.medium" },
|
||||
{ value: "low", labelKey: "guardrails.contentFilter.options.severity.low" },
|
||||
];
|
||||
|
||||
export const translateOptions = <V extends string>(items: readonly OptionItem<V>[], t: TFunction) =>
|
||||
items.map(({ value, labelKey }) => ({ value, label: t(labelKey) }));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
choiceToLoggingOnlyScope,
|
||||
effectiveLoggingOnlyContinue,
|
||||
|
|
@ -9,8 +10,9 @@ import {
|
|||
} from "../guardrail_info_helpers";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
const LOGGING_ONLY_SCOPE_ITEMS = getLoggingOnlyScopeOptions(true);
|
||||
const LOGGING_ONLY_SCOPE_CHOICES: ReadonlySet<string> = new Set(LOGGING_ONLY_SCOPE_ITEMS.map(({ value }) => value));
|
||||
const LOGGING_ONLY_SCOPE_CHOICES: ReadonlySet<string> = new Set(
|
||||
getLoggingOnlyScopeOptions(true).map(({ value }) => value),
|
||||
);
|
||||
|
||||
const isLoggingOnlyScopeChoice = (value: unknown): value is LoggingOnlyScopeChoice =>
|
||||
typeof value === "string" && LOGGING_ONLY_SCOPE_CHOICES.has(value);
|
||||
|
|
@ -18,29 +20,35 @@ const isLoggingOnlyScopeChoice = (value: unknown): value is LoggingOnlyScopeChoi
|
|||
export const CustomCodeLoggingOnlyScopeSelect: React.FC<{
|
||||
value: LoggingOnlyScopeChoice;
|
||||
onChange: (choice: LoggingOnlyScopeChoice) => void;
|
||||
}> = ({ value, onChange }) => (
|
||||
<div className="w-[240px]">
|
||||
<label className="mb-1 block text-xs font-medium text-muted-foreground">Logging only scope</label>
|
||||
<Select
|
||||
items={LOGGING_ONLY_SCOPE_ITEMS}
|
||||
value={value}
|
||||
onValueChange={(nextValue) => {
|
||||
if (isLoggingOnlyScopeChoice(nextValue)) onChange(nextValue);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full" aria-label="Logging only scope">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LOGGING_ONLY_SCOPE_ITEMS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}> = ({ value, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const items = useMemo(() => getLoggingOnlyScopeOptions(true, t), [t]);
|
||||
return (
|
||||
<div className="w-[240px]">
|
||||
<label className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||
{t("guardrails.customCode.loggingOnlyScope.label")}
|
||||
</label>
|
||||
<Select
|
||||
items={items}
|
||||
value={value}
|
||||
onValueChange={(nextValue) => {
|
||||
if (isLoggingOnlyScopeChoice(nextValue)) onChange(nextValue);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full" aria-label={t("guardrails.customCode.loggingOnlyScope.label")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{items.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getCustomCodeLoggingOnlyScopeCreate = (
|
||||
mode: string[],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
import React, { useState, useRef, useEffect, useMemo } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { CheckCircle2, ChevronRight, Code, ExternalLink, PlayCircle, Save, Users, XCircle } from "lucide-react";
|
||||
import { createGuardrailCall, updateGuardrailCall, testCustomCodeGuardrail } from "@/components/networking";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
|
@ -47,9 +48,9 @@ import {
|
|||
} from "../guardrail_info_helpers";
|
||||
import {
|
||||
CODE_TEMPLATES,
|
||||
MODE_OPTION_BY_VALUE,
|
||||
DEFAULT_OPEN_PRIMITIVE_CATEGORY,
|
||||
MODE_OPTIONS,
|
||||
PRIMITIVES,
|
||||
PRIMITIVE_CATEGORIES,
|
||||
TEMPLATE_ITEMS,
|
||||
type ModeOption,
|
||||
} from "./custom_code_catalog";
|
||||
|
|
@ -78,6 +79,7 @@ interface CustomCodeModalProps {
|
|||
}
|
||||
|
||||
const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onSuccess, accessToken, editData }) => {
|
||||
const { t } = useTranslation();
|
||||
const anchor = useComboboxAnchor();
|
||||
const isEditMode = !!editData;
|
||||
const [guardrailName, setGuardrailName] = useState("");
|
||||
|
|
@ -268,15 +270,15 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
// Save guardrail (create or update)
|
||||
const handleSave = async () => {
|
||||
if (!guardrailName.trim()) {
|
||||
toast.fromError("Please enter a guardrail name");
|
||||
toast.fromError(t("guardrails.customCode.toast.nameRequired"));
|
||||
return;
|
||||
}
|
||||
if (!code.trim()) {
|
||||
toast.fromError("Please enter custom code");
|
||||
toast.fromError(t("guardrails.customCode.toast.codeRequired"));
|
||||
return;
|
||||
}
|
||||
if (!accessToken) {
|
||||
toast.fromError("No access token available");
|
||||
toast.fromError(t("guardrails.customCode.toast.noAccessToken"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -319,7 +321,7 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
}
|
||||
|
||||
await updateGuardrailCall(accessToken, editData.guardrail_id, updateData);
|
||||
toast.success("Custom code guardrail updated successfully");
|
||||
toast.success(t("guardrails.customCode.toast.updated"));
|
||||
} else {
|
||||
// Create new guardrail
|
||||
const streamScope = streamScopePayload(mode, streamScopeByMode);
|
||||
|
|
@ -337,15 +339,16 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
};
|
||||
|
||||
await createGuardrailCall(accessToken, guardrailData);
|
||||
toast.success("Custom code guardrail created successfully");
|
||||
toast.success(t("guardrails.customCode.toast.created"));
|
||||
}
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to save guardrail:", error);
|
||||
toast.fromError(
|
||||
`Failed to ${isEditMode ? "update" : "create"} guardrail: ` +
|
||||
(error instanceof Error ? error.message : String(error)),
|
||||
t(isEditMode ? "guardrails.customCode.toast.updateFailed" : "guardrails.customCode.toast.createFailed", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
|
|
@ -355,7 +358,7 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
// Test guardrail using backend endpoint
|
||||
const handleTest = async () => {
|
||||
if (!accessToken) {
|
||||
setTestResult({ error: "No access token available" });
|
||||
setTestResult({ error: t("guardrails.customCode.test.errors.noAccessToken") });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -368,7 +371,7 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
try {
|
||||
parsedInput = JSON.parse(testInput);
|
||||
} catch (e) {
|
||||
setTestResult({ error: "Invalid test input JSON" });
|
||||
setTestResult({ error: t("guardrails.customCode.test.errors.invalidInput") });
|
||||
setIsTesting(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -405,12 +408,12 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
error_type: response.error_type,
|
||||
});
|
||||
} else {
|
||||
setTestResult({ error: "Unknown error occurred" });
|
||||
setTestResult({ error: t("guardrails.customCode.test.errors.unknown") });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to test custom code:", error);
|
||||
setTestResult({
|
||||
error: error instanceof Error ? error.message : "Failed to test custom code",
|
||||
error: error instanceof Error ? error.message : t("guardrails.customCode.test.errors.failed"),
|
||||
});
|
||||
} finally {
|
||||
setIsTesting(false);
|
||||
|
|
@ -418,7 +421,19 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
};
|
||||
|
||||
const lineCount = code.split("\n").length;
|
||||
const selectedModeOptions = mode.map((value) => MODE_OPTION_BY_VALUE[value]).filter(Boolean);
|
||||
const modeOptions: ModeOption[] = useMemo(
|
||||
() => MODE_OPTIONS.map(({ value, labelKey }) => ({ value, label: t(labelKey) })),
|
||||
[t],
|
||||
);
|
||||
const modeOptionByValue: Record<string, ModeOption> = useMemo(
|
||||
() => Object.fromEntries(modeOptions.map((option) => [option.value, option])),
|
||||
[modeOptions],
|
||||
);
|
||||
const templateItems = useMemo(
|
||||
() => TEMPLATE_ITEMS.map(({ value, labelKey }) => ({ value, label: t(labelKey) })),
|
||||
[t],
|
||||
);
|
||||
const selectedModeOptions = mode.map((value) => modeOptionByValue[value]).filter(Boolean);
|
||||
const rawEditMode = editData?.litellm_params?.mode;
|
||||
const tagScopedModeLabel =
|
||||
rawEditMode !== null && typeof rawEditMode === "object" && !Array.isArray(rawEditMode)
|
||||
|
|
@ -430,30 +445,38 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[1400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
{isEditMode ? "Edit Custom Guardrail" : "Create Custom Guardrail"}
|
||||
{isEditMode ? t("guardrails.customCode.title.edit") : t("guardrails.customCode.title.create")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>Define custom logic using Python-like syntax</DialogDescription>
|
||||
<DialogDescription>{t("guardrails.customCode.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Top Controls */}
|
||||
<div className="flex flex-wrap items-center gap-4 border-b border-border py-4">
|
||||
<div className="min-w-[160px] max-w-[200px] flex-1">
|
||||
<label className="mb-1 block text-xs font-medium text-muted-foreground">Guardrail Name</label>
|
||||
<label className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||
{t("guardrails.customCode.nameLabel")}
|
||||
</label>
|
||||
<Input
|
||||
value={guardrailName}
|
||||
onChange={(e) => setGuardrailName(e.target.value)}
|
||||
placeholder="e.g., block-pii-custom"
|
||||
placeholder={t("guardrails.customCode.namePlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[280px]">
|
||||
<label className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||
{tagScopedModeLabel ? "Mode (tag-scoped, read-only)" : "Mode (can select multiple)"}
|
||||
{tagScopedModeLabel
|
||||
? t("guardrails.customCode.modeLabel.tagScoped")
|
||||
: t("guardrails.customCode.modeLabel.multiple")}
|
||||
</label>
|
||||
{tagScopedModeLabel ? (
|
||||
<Input value={tagScopedModeLabel} disabled aria-label="Mode (tag-scoped)" />
|
||||
<Input
|
||||
value={tagScopedModeLabel}
|
||||
disabled
|
||||
aria-label={t("guardrails.customCode.tagScopedModeAriaLabel")}
|
||||
/>
|
||||
) : (
|
||||
<Combobox
|
||||
items={MODE_OPTIONS}
|
||||
items={modeOptions}
|
||||
value={selectedModeOptions}
|
||||
onValueChange={(options: ModeOption[]) => setMode(options.map((option) => option.value))}
|
||||
multiple
|
||||
|
|
@ -464,10 +487,12 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
{option.label}
|
||||
</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput placeholder={mode.length === 0 ? "Select modes" : undefined} />
|
||||
<ComboboxChipsInput
|
||||
placeholder={mode.length === 0 ? t("guardrails.customCode.modePlaceholder") : undefined}
|
||||
/>
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
<ComboboxEmpty>No matching modes</ComboboxEmpty>
|
||||
<ComboboxEmpty>{t("guardrails.customCode.noMatchingModes")}</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(option: ModeOption) => (
|
||||
<ComboboxItem key={option.value} value={option}>
|
||||
|
|
@ -484,31 +509,31 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
)}
|
||||
{mode.includes("logging_only") && (
|
||||
<div className="flex shrink-0 items-center gap-2 pt-5">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Continue observing the response after a flagged request
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">{t("guardrails.customCode.continueObserving")}</span>
|
||||
<Switch
|
||||
checked={loggingOnlyScopeChoice === "default" && loggingOnlyContinue}
|
||||
disabled={loggingOnlyScopeChoice !== "default"}
|
||||
onCheckedChange={setLoggingOnlyContinue}
|
||||
aria-label="Continue observing the response after a flagged request"
|
||||
aria-label={t("guardrails.customCode.continueObserving")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-[180px]">
|
||||
<label className="mb-1 block text-xs font-medium text-muted-foreground">Template</label>
|
||||
<label className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||
{t("guardrails.customCode.templateLabel")}
|
||||
</label>
|
||||
<Select
|
||||
items={TEMPLATE_ITEMS}
|
||||
items={templateItems}
|
||||
value={selectedTemplate}
|
||||
onValueChange={(value: string | null) => value && handleTemplateChange(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full" aria-label="Template">
|
||||
<SelectTrigger className="w-full" aria-label={t("guardrails.customCode.templateLabel")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>STANDARD</SelectLabel>
|
||||
{TEMPLATE_ITEMS.map((template) => (
|
||||
<SelectLabel>{t("guardrails.customCode.standardTemplates")}</SelectLabel>
|
||||
{templateItems.map((template) => (
|
||||
<SelectItem key={template.value} value={template.value}>
|
||||
{template.label}
|
||||
</SelectItem>
|
||||
|
|
@ -521,15 +546,19 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
className="flex w-full items-center gap-1 rounded-sm px-2 py-1.5 text-xs text-primary hover:bg-accent"
|
||||
>
|
||||
<Users className="size-3.5" />
|
||||
<span>Browse Community templates</span>
|
||||
<span>{t("guardrails.customCode.browseCommunity")}</span>
|
||||
<ExternalLink className="size-2.5" />
|
||||
</button>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pt-5">
|
||||
<span className="text-sm text-muted-foreground">Default On</span>
|
||||
<Switch checked={defaultOn} onCheckedChange={setDefaultOn} aria-label="Default On" />
|
||||
<span className="text-sm text-muted-foreground">{t("guardrails.customCode.defaultOn")}</span>
|
||||
<Switch
|
||||
checked={defaultOn}
|
||||
onCheckedChange={setDefaultOn}
|
||||
aria-label={t("guardrails.customCode.defaultOn")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{mode.length > 0 && (
|
||||
|
|
@ -543,8 +572,10 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
{/* Code Editor */}
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="mb-2 flex shrink-0 items-center justify-between">
|
||||
<span className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">Python Logic</span>
|
||||
<span className="text-xs text-muted-foreground">Restricted environment (no imports)</span>
|
||||
<span className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
|
||||
{t("guardrails.customCode.pythonLogic")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{t("guardrails.customCode.restrictedEnvironment")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="relative rounded-lg overflow-hidden border border-gray-700 bg-[#1e1e1e] shrink-0"
|
||||
|
|
@ -591,59 +622,87 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
<CollapsibleTrigger className="flex w-full items-center gap-2 p-3 text-sm font-medium">
|
||||
<ChevronRight className={`size-4 transition-transform ${testExpanded ? "rotate-90" : ""}`} />
|
||||
<PlayCircle className="size-4 text-muted-foreground" />
|
||||
Test Your Guardrail
|
||||
{t("guardrails.customCode.test.title")}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="p-3 pt-0">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="block text-xs font-medium text-muted-foreground">Test Input (JSON)</label>
|
||||
<label className="block text-xs font-medium text-muted-foreground">
|
||||
{t("guardrails.customCode.test.inputLabel")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">Load example:</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("guardrails.customCode.test.loadExample")}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTestInput(JSON.stringify(TEST_INPUT_EXAMPLES.pre_call.data, null, 2))}
|
||||
className="px-2 py-1 text-xs rounded-sm border border-warning/20 bg-warning/10 text-warning hover:bg-warning/15 transition-colors"
|
||||
>
|
||||
Pre-call
|
||||
{t("guardrails.customCode.test.examples.preCall")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTestInput(JSON.stringify(TEST_INPUT_EXAMPLES.pre_mcp_call.data, null, 2))}
|
||||
className="px-2 py-1 text-xs rounded-sm border border-purple-200 bg-purple-50 text-purple-700 hover:bg-purple-100 transition-colors dark:border-purple-800 dark:bg-purple-950 dark:text-purple-300 dark:hover:bg-purple-900"
|
||||
>
|
||||
Pre MCP
|
||||
{t("guardrails.customCode.test.examples.preMcp")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTestInput(JSON.stringify(TEST_INPUT_EXAMPLES.post_call.data, null, 2))}
|
||||
className="px-2 py-1 text-xs rounded-sm border border-success/20 bg-success/10 text-success hover:bg-success/15 transition-colors"
|
||||
>
|
||||
Post-call
|
||||
{t("guardrails.customCode.test.examples.postCall")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2 rounded-sm border border-border bg-muted/40 p-2 text-xs text-muted-foreground">
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
|
||||
<div>
|
||||
<strong>texts</strong>: Message content (always)
|
||||
<Trans
|
||||
i18nKey="guardrails.customCode.test.fields.texts"
|
||||
components={{ field: <strong>texts</strong> }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<strong>images</strong>: Base64 images (vision)
|
||||
<Trans
|
||||
i18nKey="guardrails.customCode.test.fields.images"
|
||||
components={{ field: <strong>images</strong> }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<strong>tools</strong>: Tool definitions <span className="text-warning">(pre_call)</span>, MCP
|
||||
as OpenAI tool <span className="text-purple-600">(pre_mcp_call)</span>
|
||||
<Trans
|
||||
i18nKey="guardrails.customCode.test.fields.tools"
|
||||
components={{
|
||||
field: <strong>tools</strong>,
|
||||
preCall: <span className="text-warning">(pre_call)</span>,
|
||||
preMcpCall: <span className="text-purple-600">(pre_mcp_call)</span>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<strong>tool_calls</strong>: LLM tool calls <span className="text-success">(post_call)</span>
|
||||
<Trans
|
||||
i18nKey="guardrails.customCode.test.fields.toolCalls"
|
||||
components={{
|
||||
field: <strong>tool_calls</strong>,
|
||||
postCall: <span className="text-success">(post_call)</span>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<strong>structured_messages</strong>: Full messages{" "}
|
||||
<Trans
|
||||
i18nKey="guardrails.customCode.test.fields.structuredMessages"
|
||||
components={{ field: <strong>structured_messages</strong> }}
|
||||
/>{" "}
|
||||
<span className="text-warning">(pre_call)</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>model</strong>: Model name (always)
|
||||
<Trans
|
||||
i18nKey="guardrails.customCode.test.fields.model"
|
||||
components={{ field: <strong>model</strong> }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -658,7 +717,7 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
<div className="flex items-center gap-3">
|
||||
<Button size="sm" onClick={handleTest} disabled={isTesting} aria-busy={isTesting}>
|
||||
{isTesting ? <UiLoadingSpinner className="size-4" /> : <PlayCircle />}
|
||||
{isTesting ? "Running..." : "Run Test"}
|
||||
{isTesting ? t("guardrails.customCode.test.running") : t("guardrails.customCode.test.run")}
|
||||
</Button>
|
||||
{testResult && (
|
||||
<div
|
||||
|
|
@ -682,15 +741,19 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
</>
|
||||
) : testResult.action === "allow" ? (
|
||||
<>
|
||||
<CheckCircle2 className="size-4" /> Allowed
|
||||
<CheckCircle2 className="size-4" />
|
||||
{` ${t("guardrails.customCode.test.result.allowed")}`}
|
||||
</>
|
||||
) : testResult.action === "block" ? (
|
||||
<>
|
||||
<XCircle className="size-4" /> Blocked: {testResult.reason}
|
||||
<XCircle className="size-4" />
|
||||
{` ${t("guardrails.customCode.test.result.blocked")} `}
|
||||
{testResult.reason}
|
||||
</>
|
||||
) : testResult.action === "modify" ? (
|
||||
<>
|
||||
<CheckCircle2 className="size-4" /> Modified
|
||||
<CheckCircle2 className="size-4" />
|
||||
{` ${t("guardrails.customCode.test.result.modified")}`}
|
||||
{testResult.texts && testResult.texts.length > 0 && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">
|
||||
-> {testResult.texts[0].substring(0, 50)}
|
||||
|
|
@ -700,7 +763,8 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 className="size-4" /> {testResult.action || "Unknown"}
|
||||
<CheckCircle2 className="size-4" />{" "}
|
||||
{testResult.action || t("guardrails.customCode.test.result.unknownAction")}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -716,15 +780,15 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
<Users className="size-5 text-info" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium">Built a useful guardrail?</div>
|
||||
<div className="text-sm font-medium">{t("guardrails.customCode.contribute.title")}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Share it with the community and help others build faster
|
||||
{t("guardrails.customCode.contribute.description")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" onClick={() => window.open("https://github.com/BerriAI/litellm-guardrails", "_blank")}>
|
||||
<ExternalLink />
|
||||
Contribute Template
|
||||
{t("guardrails.customCode.contribute.button")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -733,19 +797,19 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
<div className="w-[300px] shrink-0 overflow-auto border-l border-border pl-6">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Code className="size-4 text-muted-foreground" />
|
||||
<span className="font-semibold">Available Primitives</span>
|
||||
<span className="font-semibold">{t("guardrails.customCode.primitives.title")}</span>
|
||||
</div>
|
||||
<p className="mb-3 text-xs text-muted-foreground">Click to copy functions to clipboard</p>
|
||||
<p className="mb-3 text-xs text-muted-foreground">{t("guardrails.customCode.primitives.hint")}</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
{Object.entries(PRIMITIVES).map(([category, primitives]) => (
|
||||
{PRIMITIVE_CATEGORIES.map(({ id, labelKey, primitives }) => (
|
||||
<Collapsible
|
||||
key={category}
|
||||
defaultOpen={category === "Return Values"}
|
||||
key={id}
|
||||
defaultOpen={id === DEFAULT_OPEN_PRIMITIVE_CATEGORY}
|
||||
className="rounded-lg border border-border"
|
||||
>
|
||||
<CollapsibleTrigger className="group flex w-full items-center justify-between px-3 py-2 text-sm font-medium">
|
||||
{category}
|
||||
{t(labelKey)}
|
||||
<ChevronRight className="size-4 transition-transform group-data-panel-open:rotate-90" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="px-3 pb-3">
|
||||
|
|
@ -760,12 +824,13 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
>
|
||||
{copiedPrimitive === p.name ? (
|
||||
<span className="flex items-center gap-1 font-mono text-xs">
|
||||
<CheckCircle2 className="size-3.5" /> Copied!
|
||||
<CheckCircle2 className="size-3.5" />
|
||||
{` ${t("guardrails.customCode.primitives.copied")}`}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<div className="font-mono text-xs">{p.name}</div>
|
||||
<div className="mt-0.5 text-[10px] text-muted-foreground">{p.desc}</div>
|
||||
<div className="mt-0.5 text-[10px] text-muted-foreground">{t(p.descKey)}</div>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
|
@ -780,14 +845,14 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({ visible, onClose, onS
|
|||
|
||||
{/* Footer */}
|
||||
<div className="mt-4 flex items-center justify-between border-t border-border pt-4">
|
||||
<span className="text-xs text-muted-foreground">Changes are auto-saved to local draft</span>
|
||||
<span className="text-xs text-muted-foreground">{t("guardrails.customCode.footer.autosaveNote")}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
{t("guardrails.customCode.footer.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isSaving || !guardrailName.trim()} aria-busy={isSaving}>
|
||||
{isSaving ? <UiLoadingSpinner className="size-4" /> : <Save />}
|
||||
{isEditMode ? "Update Guardrail" : "Save Guardrail"}
|
||||
{isEditMode ? t("guardrails.customCode.footer.update") : t("guardrails.customCode.footer.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { ParseKeys } from "i18next";
|
||||
|
||||
export const CODE_TEMPLATES = {
|
||||
empty: {
|
||||
name: "Empty Template",
|
||||
nameKey: "guardrails.customCode.templates.empty",
|
||||
code: `async def apply_guardrail(inputs, request_data, input_type):
|
||||
# inputs: {texts, images, tools, tool_calls, structured_messages, model}
|
||||
# request_data: {model, user_id, team_id, end_user_id, metadata}
|
||||
|
|
@ -8,7 +10,7 @@ export const CODE_TEMPLATES = {
|
|||
return allow()`,
|
||||
},
|
||||
blockSSN: {
|
||||
name: "Block SSN",
|
||||
nameKey: "guardrails.customCode.templates.blockSsn",
|
||||
code: `def apply_guardrail(inputs, request_data, input_type):
|
||||
for text in inputs["texts"]:
|
||||
if regex_match(text, r"\\d{3}-\\d{2}-\\d{4}"):
|
||||
|
|
@ -16,7 +18,7 @@ export const CODE_TEMPLATES = {
|
|||
return allow()`,
|
||||
},
|
||||
redactEmail: {
|
||||
name: "Redact Emails",
|
||||
nameKey: "guardrails.customCode.templates.redactEmail",
|
||||
code: `def apply_guardrail(inputs, request_data, input_type):
|
||||
pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
|
||||
modified = []
|
||||
|
|
@ -25,7 +27,7 @@ export const CODE_TEMPLATES = {
|
|||
return modify(texts=modified)`,
|
||||
},
|
||||
blockSQL: {
|
||||
name: "Block SQL Injection",
|
||||
nameKey: "guardrails.customCode.templates.blockSql",
|
||||
code: `def apply_guardrail(inputs, request_data, input_type):
|
||||
if input_type != "request":
|
||||
return allow()
|
||||
|
|
@ -35,7 +37,7 @@ export const CODE_TEMPLATES = {
|
|||
return allow()`,
|
||||
},
|
||||
validateJSON: {
|
||||
name: "Validate JSON",
|
||||
nameKey: "guardrails.customCode.templates.validateJson",
|
||||
code: `def apply_guardrail(inputs, request_data, input_type):
|
||||
if input_type != "response":
|
||||
return allow()
|
||||
|
|
@ -51,7 +53,7 @@ export const CODE_TEMPLATES = {
|
|||
return allow()`,
|
||||
},
|
||||
externalAPI: {
|
||||
name: "External API Check (async)",
|
||||
nameKey: "guardrails.customCode.templates.externalApi",
|
||||
code: `async def apply_guardrail(inputs, request_data, input_type):
|
||||
# Call an external moderation API (async for non-blocking)
|
||||
for text in inputs["texts"]:
|
||||
|
|
@ -71,66 +73,129 @@ export const CODE_TEMPLATES = {
|
|||
|
||||
return allow()`,
|
||||
},
|
||||
};
|
||||
} satisfies Record<string, { nameKey: ParseKeys; code: string }>;
|
||||
|
||||
export const PRIMITIVES = {
|
||||
"Return Values": [
|
||||
{ name: "allow()", desc: "Let request/response through" },
|
||||
{ name: "block(reason)", desc: "Reject with message" },
|
||||
{ name: "flag(reason, metadata={})", desc: "Let through, record a non-blocking violation" },
|
||||
{ name: "modify(texts=[], images=[], tool_calls=[])", desc: "Transform content" },
|
||||
],
|
||||
"HTTP Requests (async)": [
|
||||
{ name: "await http_request(url, method, headers, body)", desc: "Make async HTTP request" },
|
||||
{ name: "await http_get(url, headers)", desc: "Async GET request" },
|
||||
{ name: "await http_post(url, body, headers)", desc: "Async POST request" },
|
||||
],
|
||||
"Regex Functions": [
|
||||
{ name: "regex_match(text, pattern)", desc: "Returns True if pattern found" },
|
||||
{ name: "regex_replace(text, pattern, replacement)", desc: "Replace all matches" },
|
||||
{ name: "regex_find_all(text, pattern)", desc: "Return list of matches" },
|
||||
],
|
||||
"JSON Functions": [
|
||||
{ name: "json_parse(text)", desc: "Parse JSON string, returns None on error" },
|
||||
{ name: "json_stringify(obj)", desc: "Convert to JSON string" },
|
||||
{ name: "json_schema_valid(obj, schema)", desc: "Validate against JSON schema" },
|
||||
],
|
||||
"URL Functions": [
|
||||
{ name: "extract_urls(text)", desc: "Extract all URLs from text" },
|
||||
{ name: "is_valid_url(url)", desc: "Check if URL is valid" },
|
||||
{ name: "all_urls_valid(text)", desc: "Check all URLs in text are valid" },
|
||||
],
|
||||
"Code Detection": [
|
||||
{ name: "detect_code(text)", desc: "Returns True if code detected" },
|
||||
{ name: "detect_code_languages(text)", desc: "Returns list of detected languages" },
|
||||
{ name: 'contains_code_language(text, ["sql"])', desc: "Check for specific languages" },
|
||||
],
|
||||
"Text Utilities": [
|
||||
{ name: "contains(text, substring)", desc: "Check if substring exists" },
|
||||
{ name: "contains_any(text, [substr1, substr2])", desc: "Check if any substring exists" },
|
||||
{ name: "word_count(text)", desc: "Count words" },
|
||||
{ name: "char_count(text)", desc: "Count characters" },
|
||||
{ name: "lower(text) / upper(text) / trim(text)", desc: "String transforms" },
|
||||
],
|
||||
};
|
||||
interface Primitive {
|
||||
name: string;
|
||||
descKey: ParseKeys;
|
||||
}
|
||||
|
||||
export const MODE_OPTIONS = [
|
||||
{ value: "pre_call", label: "pre_call (Request)" },
|
||||
{ value: "post_call", label: "post_call (Response)" },
|
||||
{ value: "during_call", label: "during_call (Parallel)" },
|
||||
{ value: "logging_only", label: "logging_only" },
|
||||
{ value: "pre_mcp_call", label: "pre_mcp_call (Before MCP Tool Call)" },
|
||||
{ value: "post_mcp_call", label: "post_mcp_call (After MCP Tool Call)" },
|
||||
{ value: "during_mcp_call", label: "during_mcp_call (During MCP Tool Call)" },
|
||||
interface PrimitiveCategory {
|
||||
id: string;
|
||||
labelKey: ParseKeys;
|
||||
primitives: Primitive[];
|
||||
}
|
||||
|
||||
export const DEFAULT_OPEN_PRIMITIVE_CATEGORY = "returnValues";
|
||||
|
||||
export const PRIMITIVE_CATEGORIES: PrimitiveCategory[] = [
|
||||
{
|
||||
id: "returnValues",
|
||||
labelKey: "guardrails.customCode.primitives.categories.returnValues",
|
||||
primitives: [
|
||||
{ name: "allow()", descKey: "guardrails.customCode.primitives.descriptions.allow" },
|
||||
{ name: "block(reason)", descKey: "guardrails.customCode.primitives.descriptions.block" },
|
||||
{ name: "flag(reason, metadata={})", descKey: "guardrails.customCode.primitives.descriptions.flag" },
|
||||
{
|
||||
name: "modify(texts=[], images=[], tool_calls=[])",
|
||||
descKey: "guardrails.customCode.primitives.descriptions.modify",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "httpRequests",
|
||||
labelKey: "guardrails.customCode.primitives.categories.httpRequests",
|
||||
primitives: [
|
||||
{
|
||||
name: "await http_request(url, method, headers, body)",
|
||||
descKey: "guardrails.customCode.primitives.descriptions.httpRequest",
|
||||
},
|
||||
{ name: "await http_get(url, headers)", descKey: "guardrails.customCode.primitives.descriptions.httpGet" },
|
||||
{
|
||||
name: "await http_post(url, body, headers)",
|
||||
descKey: "guardrails.customCode.primitives.descriptions.httpPost",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "regex",
|
||||
labelKey: "guardrails.customCode.primitives.categories.regex",
|
||||
primitives: [
|
||||
{ name: "regex_match(text, pattern)", descKey: "guardrails.customCode.primitives.descriptions.regexMatch" },
|
||||
{
|
||||
name: "regex_replace(text, pattern, replacement)",
|
||||
descKey: "guardrails.customCode.primitives.descriptions.regexReplace",
|
||||
},
|
||||
{ name: "regex_find_all(text, pattern)", descKey: "guardrails.customCode.primitives.descriptions.regexFindAll" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "json",
|
||||
labelKey: "guardrails.customCode.primitives.categories.json",
|
||||
primitives: [
|
||||
{ name: "json_parse(text)", descKey: "guardrails.customCode.primitives.descriptions.jsonParse" },
|
||||
{ name: "json_stringify(obj)", descKey: "guardrails.customCode.primitives.descriptions.jsonStringify" },
|
||||
{
|
||||
name: "json_schema_valid(obj, schema)",
|
||||
descKey: "guardrails.customCode.primitives.descriptions.jsonSchemaValid",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "url",
|
||||
labelKey: "guardrails.customCode.primitives.categories.url",
|
||||
primitives: [
|
||||
{ name: "extract_urls(text)", descKey: "guardrails.customCode.primitives.descriptions.extractUrls" },
|
||||
{ name: "is_valid_url(url)", descKey: "guardrails.customCode.primitives.descriptions.isValidUrl" },
|
||||
{ name: "all_urls_valid(text)", descKey: "guardrails.customCode.primitives.descriptions.allUrlsValid" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "codeDetection",
|
||||
labelKey: "guardrails.customCode.primitives.categories.codeDetection",
|
||||
primitives: [
|
||||
{ name: "detect_code(text)", descKey: "guardrails.customCode.primitives.descriptions.detectCode" },
|
||||
{
|
||||
name: "detect_code_languages(text)",
|
||||
descKey: "guardrails.customCode.primitives.descriptions.detectCodeLanguages",
|
||||
},
|
||||
{
|
||||
name: 'contains_code_language(text, ["sql"])',
|
||||
descKey: "guardrails.customCode.primitives.descriptions.containsCodeLanguage",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "textUtilities",
|
||||
labelKey: "guardrails.customCode.primitives.categories.textUtilities",
|
||||
primitives: [
|
||||
{ name: "contains(text, substring)", descKey: "guardrails.customCode.primitives.descriptions.contains" },
|
||||
{
|
||||
name: "contains_any(text, [substr1, substr2])",
|
||||
descKey: "guardrails.customCode.primitives.descriptions.containsAny",
|
||||
},
|
||||
{ name: "word_count(text)", descKey: "guardrails.customCode.primitives.descriptions.wordCount" },
|
||||
{ name: "char_count(text)", descKey: "guardrails.customCode.primitives.descriptions.charCount" },
|
||||
{
|
||||
name: "lower(text) / upper(text) / trim(text)",
|
||||
descKey: "guardrails.customCode.primitives.descriptions.stringTransforms",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const TEMPLATE_ITEMS = Object.entries(CODE_TEMPLATES).map(([key, template]) => ({
|
||||
value: key,
|
||||
label: template.name,
|
||||
}));
|
||||
export const MODE_OPTIONS: { value: string; labelKey: ParseKeys }[] = [
|
||||
{ value: "pre_call", labelKey: "guardrails.customCode.modes.preCall" },
|
||||
{ value: "post_call", labelKey: "guardrails.customCode.modes.postCall" },
|
||||
{ value: "during_call", labelKey: "guardrails.customCode.modes.duringCall" },
|
||||
{ value: "logging_only", labelKey: "guardrails.customCode.modes.loggingOnly" },
|
||||
{ value: "pre_mcp_call", labelKey: "guardrails.customCode.modes.preMcpCall" },
|
||||
{ value: "post_mcp_call", labelKey: "guardrails.customCode.modes.postMcpCall" },
|
||||
{ value: "during_mcp_call", labelKey: "guardrails.customCode.modes.duringMcpCall" },
|
||||
];
|
||||
|
||||
export type ModeOption = (typeof MODE_OPTIONS)[number];
|
||||
|
||||
export const MODE_OPTION_BY_VALUE: Record<string, ModeOption> = Object.fromEntries(
|
||||
MODE_OPTIONS.map((option) => [option.value, option]),
|
||||
export const TEMPLATE_ITEMS: { value: string; labelKey: ParseKeys }[] = Object.entries(CODE_TEMPLATES).map(
|
||||
([key, template]) => ({ value: key, labelKey: template.nameKey }),
|
||||
);
|
||||
|
||||
export type ModeOption = { value: string; label: string };
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import type { ParseKeys } from "i18next";
|
||||
import { Info, Plus, X } from "lucide-react";
|
||||
import React from "react";
|
||||
import { useWatch } from "react-hook-form";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -36,9 +38,9 @@ export interface DecisionModelFieldsProps {
|
|||
control: GuardrailFormControl;
|
||||
}
|
||||
|
||||
const ACTION_ITEMS = [
|
||||
{ label: "Block", value: "block" },
|
||||
{ label: "Log only", value: "log" },
|
||||
const ACTION_ITEMS: { labelKey: ParseKeys; value: "block" | "log" }[] = [
|
||||
{ labelKey: "guardrails.decisionModel.actionOptions.block", value: "block" },
|
||||
{ labelKey: "guardrails.decisionModel.actionOptions.log", value: "log" },
|
||||
];
|
||||
|
||||
const ProviderLogo: React.FC<{ provider: string }> = ({ provider }) => (
|
||||
|
|
@ -46,16 +48,21 @@ const ProviderLogo: React.FC<{ provider: string }> = ({ provider }) => (
|
|||
);
|
||||
|
||||
const NoDecisionModels: React.FC<{ providerFilter: string | null }> = ({ providerFilter }) => {
|
||||
const source = providerFilter ? ` from the ${getProviderLogoAndName(providerFilter).displayName} provider` : "";
|
||||
const { t } = useTranslation();
|
||||
const description = providerFilter
|
||||
? t("guardrails.decisionModel.noModels.descriptionForProvider", {
|
||||
provider: getProviderLogoAndName(providerFilter).displayName,
|
||||
})
|
||||
: t("guardrails.decisionModel.noModels.description");
|
||||
return (
|
||||
<Alert variant="info" role="note" aria-label="No decision models">
|
||||
<Alert variant="info" role="note" aria-label={t("guardrails.decisionModel.noModels.ariaLabel")}>
|
||||
<Info />
|
||||
<AlertTitle>No decision models yet</AlertTitle>
|
||||
<AlertTitle>{t("guardrails.decisionModel.noModels.title")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p>
|
||||
Add a decision model{source} to use this guardrail. A model shows up here once its mode is decisions.{" "}
|
||||
{description}{" "}
|
||||
<a href={uiHref("models-and-endpoints")} target="_blank" rel="noopener noreferrer">
|
||||
Add a model
|
||||
{t("guardrails.decisionModel.noModels.addModel")}
|
||||
</a>
|
||||
</p>
|
||||
</AlertDescription>
|
||||
|
|
@ -93,9 +100,11 @@ const DecisionModelFields: React.FC<DecisionModelFieldsProps> = ({
|
|||
onChecksChange,
|
||||
control,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const selectedModel = asText(useWatch({ control, name: "decision_model" }));
|
||||
const duplicateNames = duplicateDecisionCheckNames(checks);
|
||||
const providerByModel = new Map(decisionModels.map((option) => [option.model, option.provider]));
|
||||
const actionItems = ACTION_ITEMS.map(({ labelKey, value }) => ({ label: t(labelKey), value }));
|
||||
|
||||
const updateCheck = (id: string, patch: Partial<DecisionModelCheckDraft>) =>
|
||||
onChecksChange(checks.map((check) => (check.id === id ? { ...check, ...patch } : check)));
|
||||
|
|
@ -107,19 +116,14 @@ const DecisionModelFields: React.FC<DecisionModelFieldsProps> = ({
|
|||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="rounded-md border border-success/20 bg-success/10 px-3.5 py-2.5 text-[13px] text-success">
|
||||
The <strong>Decision Model</strong> answers yes/no questions about each request (pre_call, during_call) or
|
||||
response (post_call). A question whose probability reaches its threshold blocks the call, or is recorded when
|
||||
set to Log only.
|
||||
<Trans i18nKey="guardrails.decisionModel.intro" components={{ strong: <strong /> }} />
|
||||
</div>
|
||||
|
||||
<GuardrailField
|
||||
control={control}
|
||||
name="decision_model"
|
||||
label={labelWithHint(
|
||||
"Decision Model",
|
||||
"A decisions-API model on this proxy, such as typesafe/jev-latest. It scores every question below.",
|
||||
)}
|
||||
rules={requiredRule("Select a decision model")}
|
||||
label={labelWithHint(t("guardrails.decisionModel.model.label"), t("guardrails.decisionModel.model.hint"))}
|
||||
rules={requiredRule(t("guardrails.decisionModel.model.required"))}
|
||||
>
|
||||
{({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => (
|
||||
<Combobox
|
||||
|
|
@ -134,11 +138,15 @@ const DecisionModelFields: React.FC<DecisionModelFieldsProps> = ({
|
|||
id={id}
|
||||
aria-invalid={ariaInvalid}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
placeholder={decisionModels.length > 0 ? "Select a decision model" : "No decision models yet"}
|
||||
placeholder={
|
||||
decisionModels.length > 0
|
||||
? t("guardrails.decisionModel.model.placeholder")
|
||||
: t("guardrails.decisionModel.model.placeholderEmpty")
|
||||
}
|
||||
className="w-full"
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No matching models</ComboboxEmpty>
|
||||
<ComboboxEmpty>{t("guardrails.decisionModel.model.empty")}</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(model: string) => {
|
||||
const provider = providerByModel.get(model);
|
||||
|
|
@ -160,11 +168,8 @@ const DecisionModelFields: React.FC<DecisionModelFieldsProps> = ({
|
|||
{decisionModels.length === 0 && <NoDecisionModels providerFilter={providerFilter} />}
|
||||
|
||||
<Field>
|
||||
<FieldLabel>Questions</FieldLabel>
|
||||
<p className="m-0 text-xs text-muted-foreground">
|
||||
Yes/no questions the decision model answers about the text. Block stops the call once a score reaches the
|
||||
threshold, and Log only records it.
|
||||
</p>
|
||||
<FieldLabel>{t("guardrails.decisionModel.questions.label")}</FieldLabel>
|
||||
<p className="m-0 text-xs text-muted-foreground">{t("guardrails.decisionModel.questions.description")}</p>
|
||||
{checks.map((check, index) => {
|
||||
const position = index + 1;
|
||||
return (
|
||||
|
|
@ -172,30 +177,36 @@ const DecisionModelFields: React.FC<DecisionModelFieldsProps> = ({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => removeCheck(check.id)}
|
||||
aria-label={`Remove question ${position}`}
|
||||
aria-label={t("guardrails.decisionModel.question.remove", { position })}
|
||||
className="absolute top-2 right-2 p-1 text-muted-foreground transition-colors hover:text-destructive"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
<div>
|
||||
<span className="mb-1 block text-xs font-medium text-muted-foreground">Name</span>
|
||||
<span className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||
{t("guardrails.decisionModel.question.nameLabel")}
|
||||
</span>
|
||||
<Input
|
||||
aria-label={`Question ${position} name`}
|
||||
placeholder="Name (e.g. invoice_policy)"
|
||||
aria-label={t("guardrails.decisionModel.question.nameAriaLabel", { position })}
|
||||
placeholder={t("guardrails.decisionModel.question.namePlaceholder")}
|
||||
className="bg-background"
|
||||
value={check.name}
|
||||
onChange={(event) => updateCheck(check.id, { name: event.target.value })}
|
||||
/>
|
||||
{duplicateNames.has(check.name.trim()) && (
|
||||
<p className="m-0 mt-1 text-xs text-destructive">Another question already uses this name</p>
|
||||
<p className="m-0 mt-1 text-xs text-destructive">
|
||||
{t("guardrails.decisionModel.question.duplicateName")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="mb-1 block text-xs font-medium text-muted-foreground">Question</span>
|
||||
<span className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||
{t("guardrails.decisionModel.question.questionLabel")}
|
||||
</span>
|
||||
<Textarea
|
||||
aria-label={`Question ${position}`}
|
||||
aria-label={t("guardrails.decisionModel.question.questionAriaLabel", { position })}
|
||||
rows={2}
|
||||
placeholder="The question the decision model answers, e.g. Does the text ask about invoices?"
|
||||
placeholder={t("guardrails.decisionModel.question.questionPlaceholder")}
|
||||
className="w-full resize-none bg-background"
|
||||
value={check.instructions}
|
||||
onChange={(event) => updateCheck(check.id, { instructions: event.target.value })}
|
||||
|
|
@ -203,19 +214,24 @@ const DecisionModelFields: React.FC<DecisionModelFieldsProps> = ({
|
|||
</div>
|
||||
<div className="flex items-end gap-6">
|
||||
<div className="w-32">
|
||||
<span className="mb-1 block text-xs font-medium text-muted-foreground">Action</span>
|
||||
<span className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||
{t("guardrails.decisionModel.question.actionLabel")}
|
||||
</span>
|
||||
<Select
|
||||
items={ACTION_ITEMS}
|
||||
items={actionItems}
|
||||
value={check.action}
|
||||
onValueChange={(next: string | null) =>
|
||||
next && updateCheck(check.id, { action: next as "block" | "log" })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full bg-background" aria-label={`Question ${position} action`}>
|
||||
<SelectTrigger
|
||||
className="w-full bg-background"
|
||||
aria-label={t("guardrails.decisionModel.question.actionAriaLabel", { position })}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACTION_ITEMS.map((item) => (
|
||||
{actionItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -224,10 +240,12 @@ const DecisionModelFields: React.FC<DecisionModelFieldsProps> = ({
|
|||
</Select>
|
||||
</div>
|
||||
<div className="w-56">
|
||||
<span className="mb-1 block text-xs font-medium text-muted-foreground">Threshold</span>
|
||||
<span className="mb-1 block text-xs font-medium text-muted-foreground">
|
||||
{t("guardrails.decisionModel.question.thresholdLabel")}
|
||||
</span>
|
||||
<div className="flex h-9 items-center">
|
||||
<ThresholdSlider
|
||||
aria-label={`Question ${position} threshold`}
|
||||
aria-label={t("guardrails.decisionModel.question.thresholdAriaLabel", { position })}
|
||||
value={check.threshold}
|
||||
onChange={(next) => updateCheck(check.id, { threshold: next })}
|
||||
/>
|
||||
|
|
@ -240,7 +258,7 @@ const DecisionModelFields: React.FC<DecisionModelFieldsProps> = ({
|
|||
<div>
|
||||
<Button variant="outline" size="sm" onClick={addCheck}>
|
||||
<Plus className="size-3" />
|
||||
Add question
|
||||
{t("guardrails.decisionModel.questions.add")}
|
||||
</Button>
|
||||
</div>
|
||||
</Field>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StatusBadge } from "@/components/shared/table_cells";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FieldLabel } from "@/components/ui/field";
|
||||
|
|
@ -27,6 +28,7 @@ export interface DecisionTestSectionProps {
|
|||
}
|
||||
|
||||
const DecisionTestSection: React.FC<DecisionTestSectionProps> = ({ accessToken, model, checks }) => {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState("");
|
||||
const [runs, setRuns] = useState<DecisionTestRun[]>([]);
|
||||
const [running, setRunning] = useState(false);
|
||||
|
|
@ -35,9 +37,9 @@ const DecisionTestSection: React.FC<DecisionTestSectionProps> = ({ accessToken,
|
|||
|
||||
const runnableQuestions = runnableDecisionQuestions(checks);
|
||||
let disabledReason: string | null = null;
|
||||
if (!model) disabledReason = "Select a decision model to run a test";
|
||||
else if (!input.trim()) disabledReason = "Type an input to run a test";
|
||||
else if (runnableQuestions.length === 0) disabledReason = "Add a question to run a test";
|
||||
if (!model) disabledReason = t("guardrails.decisionModel.test.disabled.noModel");
|
||||
else if (!input.trim()) disabledReason = t("guardrails.decisionModel.test.disabled.noInput");
|
||||
else if (runnableQuestions.length === 0) disabledReason = t("guardrails.decisionModel.test.disabled.noQuestions");
|
||||
const canRun = !disabledReason && !running;
|
||||
|
||||
const runTest = async () => {
|
||||
|
|
@ -79,16 +81,13 @@ const DecisionTestSection: React.FC<DecisionTestSectionProps> = ({ accessToken,
|
|||
return (
|
||||
<div className="space-y-3 border-t border-border pt-4">
|
||||
<div className="space-y-1">
|
||||
<FieldLabel>Test</FieldLabel>
|
||||
<p className="m-0 text-xs text-muted-foreground">
|
||||
Try your questions on a sample input before you save. Uses the thresholds above. Each run is a real decision
|
||||
model call, so it is billed and shows up in Logs.
|
||||
</p>
|
||||
<FieldLabel>{t("guardrails.decisionModel.test.label")}</FieldLabel>
|
||||
<p className="m-0 text-xs text-muted-foreground">{t("guardrails.decisionModel.test.description")}</p>
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label="Test input"
|
||||
aria-label={t("guardrails.decisionModel.test.inputAriaLabel")}
|
||||
rows={3}
|
||||
placeholder="Paste a sample input to score against your questions"
|
||||
placeholder={t("guardrails.decisionModel.test.inputPlaceholder")}
|
||||
className="w-full resize-none"
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
|
|
@ -101,16 +100,14 @@ const DecisionTestSection: React.FC<DecisionTestSectionProps> = ({ accessToken,
|
|||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="button" onClick={runTest} disabled={!canRun}>
|
||||
{running ? "Running…" : "Run test"}
|
||||
{running ? t("guardrails.decisionModel.test.running") : t("guardrails.decisionModel.test.run")}
|
||||
</Button>
|
||||
{!running && disabledReason && <p className="m-0 text-xs text-muted-foreground">{disabledReason}</p>}
|
||||
{!running && !disabledReason && <p className="m-0 text-xs text-muted-foreground">{runTestShortcutLabel()}</p>}
|
||||
</div>
|
||||
{runs.length > 0 && (
|
||||
<>
|
||||
<p className="m-0 text-xs text-muted-foreground">
|
||||
Latest first. Each run keeps its input so you can compare.
|
||||
</p>
|
||||
<p className="m-0 text-xs text-muted-foreground">{t("guardrails.decisionModel.test.historyHint")}</p>
|
||||
<div className="space-y-2" data-slot="decision-test-history">
|
||||
{runs.map((run) => {
|
||||
const visible = "error" in run ? [] : visibleTestResults(run, checks, model);
|
||||
|
|
@ -123,25 +120,32 @@ const DecisionTestSection: React.FC<DecisionTestSectionProps> = ({ accessToken,
|
|||
{visible.length > 0 && (
|
||||
<StatusBadge
|
||||
tone={DECISION_TEST_CHIP_TONE[decisionTestOverall(visible)]}
|
||||
label={decisionTestOverall(visible) === "block" ? "Block" : "Pass"}
|
||||
label={
|
||||
decisionTestOverall(visible) === "block"
|
||||
? t("guardrails.decisionModel.test.overall.block")
|
||||
: t("guardrails.decisionModel.test.overall.pass")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{"error" in run && <p className="m-0 mt-1.5 text-xs text-destructive">{run.error}</p>}
|
||||
{!("error" in run) && visible.length === 0 && (
|
||||
<p className="m-0 mt-1.5 text-xs text-muted-foreground">
|
||||
The model or questions changed since this run. Run it again to score them.
|
||||
{t("guardrails.decisionModel.test.stale")}
|
||||
</p>
|
||||
)}
|
||||
{visible.length > 0 && (
|
||||
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
||||
{visible.map(({ check, result }) => {
|
||||
const chip = decisionTestChip(result, check);
|
||||
const score = result.kind === "probability" ? result.probability.toFixed(2) : "No answer";
|
||||
const score =
|
||||
result.kind === "probability"
|
||||
? result.probability.toFixed(2)
|
||||
: t("guardrails.decisionModel.test.chip.noAnswer");
|
||||
let label = "";
|
||||
if (chip === "block") label = "Block";
|
||||
else if (chip === "logged") label = "Pass · logged";
|
||||
else if (chip === "pass") label = "Pass";
|
||||
if (chip === "block") label = t("guardrails.decisionModel.test.chip.block");
|
||||
else if (chip === "logged") label = t("guardrails.decisionModel.test.chip.logged");
|
||||
else if (chip === "pass") label = t("guardrails.decisionModel.test.chip.pass");
|
||||
return (
|
||||
<StatusBadge
|
||||
key={check.name}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import type { TFunction } from "i18next";
|
||||
import i18n from "@/i18n/i18n";
|
||||
|
||||
export interface DecisionModelCheckDraft {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -27,13 +30,16 @@ export function duplicateDecisionCheckNames(checks: readonly DecisionModelCheckD
|
|||
return new Set(names.filter((name, index) => names.indexOf(name) !== index));
|
||||
}
|
||||
|
||||
export function decisionChecksProblem(checks: readonly DecisionModelCheckDraft[]): string | null {
|
||||
export function decisionChecksProblem(
|
||||
checks: readonly DecisionModelCheckDraft[],
|
||||
t: TFunction = i18n.t.bind(i18n),
|
||||
): string | null {
|
||||
const trimmed = trimDecisionChecks(checks);
|
||||
if (trimmed.length === 0) return "Add at least one question";
|
||||
if (trimmed.length === 0) return t("guardrails.decisionModel.problems.noQuestions");
|
||||
if (trimmed.some((check) => !check.name || !check.instructions)) {
|
||||
return "Give every question a name and a question, or remove it";
|
||||
return t("guardrails.decisionModel.problems.incompleteQuestion");
|
||||
}
|
||||
if (duplicateDecisionCheckNames(checks).size > 0) return "Each question needs a different name";
|
||||
if (duplicateDecisionCheckNames(checks).size > 0) return t("guardrails.decisionModel.problems.duplicateNames");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import type { ParseKeys, TFunction } from "i18next";
|
||||
import { MoreHorizontal, Trash2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { DataTableSortHeader } from "@/components/shared/DataTable";
|
||||
import { DateCell, IdentityCell, StatusBadge } from "@/components/shared/table_cells";
|
||||
|
|
@ -18,7 +20,7 @@ import { cn } from "@/lib/cva.config";
|
|||
import { formatGuardrailMode, getGuardrailLogoAndName } from "./guardrail_info_helpers";
|
||||
import { Logo } from "@/components/molecules/logo/Logo";
|
||||
|
||||
const CONFIG_DELETE_HINT = "Config guardrails are defined in the config file and cannot be deleted from the dashboard.";
|
||||
const CONFIG_DELETE_HINT_KEY: ParseKeys = "guardrails.table.configDeleteHint";
|
||||
|
||||
function GuardrailProviderCell({ provider }: { provider: string }) {
|
||||
const { logo, displayName } = getGuardrailLogoAndName(provider);
|
||||
|
|
@ -36,12 +38,13 @@ interface GuardrailRowActionsProps {
|
|||
}
|
||||
|
||||
function GuardrailRowActions({ guardrail, onDeleteClick }: GuardrailRowActionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const isConfigGuardrail = guardrail.guardrail_definition_location === GuardrailDefinitionLocation.CONFIG;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
aria-label="Open guardrail actions"
|
||||
aria-label={t("guardrails.table.actions.open")}
|
||||
data-testid={`guardrail-actions-${guardrail.guardrail_id}`}
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), "text-muted-foreground")}
|
||||
>
|
||||
|
|
@ -52,11 +55,13 @@ function GuardrailRowActions({ guardrail, onDeleteClick }: GuardrailRowActionsPr
|
|||
variant="destructive"
|
||||
disabled={isConfigGuardrail}
|
||||
data-testid="guardrail-action-delete"
|
||||
title={isConfigGuardrail ? CONFIG_DELETE_HINT : undefined}
|
||||
onClick={() => onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || "Unnamed Guardrail")}
|
||||
title={isConfigGuardrail ? t(CONFIG_DELETE_HINT_KEY) : undefined}
|
||||
onClick={() =>
|
||||
onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || t("guardrails.table.unnamed"))
|
||||
}
|
||||
>
|
||||
<Trash2 />
|
||||
Delete
|
||||
{t("guardrails.table.actions.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
@ -66,17 +71,19 @@ function GuardrailRowActions({ guardrail, onDeleteClick }: GuardrailRowActionsPr
|
|||
interface GuardrailTableColumnsDeps {
|
||||
onGuardrailClick: (guardrailId: string) => void;
|
||||
onDeleteClick: (guardrailId: string, guardrailName: string) => void;
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
export const getGuardrailTableColumns = ({
|
||||
onGuardrailClick,
|
||||
onDeleteClick,
|
||||
t,
|
||||
}: GuardrailTableColumnsDeps): ColumnDef<Guardrail>[] => [
|
||||
{
|
||||
id: "guardrail_id",
|
||||
accessorKey: "guardrail_id",
|
||||
meta: { title: "Guardrail ID" },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title="Guardrail ID" />,
|
||||
meta: { title: t("guardrails.table.columns.guardrailId") },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title={t("guardrails.table.columns.guardrailId")} />,
|
||||
size: 200,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
|
|
@ -90,8 +97,8 @@ export const getGuardrailTableColumns = ({
|
|||
{
|
||||
id: "guardrail_name",
|
||||
accessorKey: "guardrail_name",
|
||||
meta: { title: "Name" },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title="Name" />,
|
||||
meta: { title: t("guardrails.table.columns.name") },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title={t("guardrails.table.columns.name")} />,
|
||||
size: 200,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
|
|
@ -105,16 +112,16 @@ export const getGuardrailTableColumns = ({
|
|||
},
|
||||
{
|
||||
id: "provider",
|
||||
meta: { title: "Provider" },
|
||||
header: "Provider",
|
||||
meta: { title: t("guardrails.table.columns.provider") },
|
||||
header: t("guardrails.table.columns.provider"),
|
||||
size: 180,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => <GuardrailProviderCell provider={row.original.litellm_params.guardrail} />,
|
||||
},
|
||||
{
|
||||
id: "mode",
|
||||
meta: { title: "Mode" },
|
||||
header: "Mode",
|
||||
meta: { title: t("guardrails.table.columns.mode") },
|
||||
header: t("guardrails.table.columns.mode"),
|
||||
size: 130,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
|
|
@ -128,22 +135,25 @@ export const getGuardrailTableColumns = ({
|
|||
},
|
||||
{
|
||||
id: "default_on",
|
||||
meta: { title: "Default On" },
|
||||
header: "Default On",
|
||||
meta: { title: t("guardrails.table.columns.defaultOn") },
|
||||
header: t("guardrails.table.columns.defaultOn"),
|
||||
size: 120,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const isDefaultOn = !!row.original.litellm_params?.default_on;
|
||||
return (
|
||||
<StatusBadge tone={isDefaultOn ? "success" : "neutral"} label={isDefaultOn ? "Default On" : "Default Off"} />
|
||||
<StatusBadge
|
||||
tone={isDefaultOn ? "success" : "neutral"}
|
||||
label={isDefaultOn ? t("guardrails.table.defaultOnStatus.on") : t("guardrails.table.defaultOnStatus.off")}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "created_at",
|
||||
accessorKey: "created_at",
|
||||
meta: { title: "Created At" },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title="Created At" />,
|
||||
meta: { title: t("guardrails.table.columns.createdAt") },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title={t("guardrails.table.columns.createdAt")} />,
|
||||
size: 150,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => <DateCell value={row.original.created_at} />,
|
||||
|
|
@ -151,8 +161,8 @@ export const getGuardrailTableColumns = ({
|
|||
{
|
||||
id: "updated_at",
|
||||
accessorKey: "updated_at",
|
||||
meta: { title: "Updated At" },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title="Updated At" />,
|
||||
meta: { title: t("guardrails.table.columns.updatedAt") },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title={t("guardrails.table.columns.updatedAt")} />,
|
||||
size: 150,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => <DateCell value={row.original.updated_at} />,
|
||||
|
|
@ -160,7 +170,7 @@ export const getGuardrailTableColumns = ({
|
|||
{
|
||||
id: "actions",
|
||||
meta: { className: "text-right", headerClassName: "text-right" },
|
||||
header: () => <span className="sr-only">Actions</span>,
|
||||
header: () => <span className="sr-only">{t("guardrails.table.columns.actions")}</span>,
|
||||
size: 64,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
import { useTerminology } from "@/contexts/TerminologyContext";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowRight, Search } from "lucide-react";
|
||||
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
|
||||
import { GuardrailCardInfo, ALL_CARDS } from "./guardrail_garden_data";
|
||||
import {
|
||||
GuardrailCardInfo,
|
||||
ALL_CARDS,
|
||||
gardenCardDescription,
|
||||
gardenCardName,
|
||||
gardenTagLabel,
|
||||
} from "./guardrail_garden_data";
|
||||
import GuardrailCard from "./guardrail_garden_card";
|
||||
import GuardrailDetailView from "./guardrail_garden_detail";
|
||||
|
||||
|
|
@ -12,6 +19,7 @@ interface GuardrailGardenProps {
|
|||
}
|
||||
|
||||
const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardrailCreated }) => {
|
||||
const { t } = useTranslation();
|
||||
const term = useTerminology();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedCard, setSelectedCard] = useState<GuardrailCardInfo | null>(null);
|
||||
|
|
@ -23,9 +31,9 @@ const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardr
|
|||
if (!searchQuery) return true;
|
||||
const q = searchQuery.toLowerCase();
|
||||
return (
|
||||
card.name.toLowerCase().includes(q) ||
|
||||
term(card.description).toLowerCase().includes(q) ||
|
||||
card.tags.some((t) => t.toLowerCase().includes(q))
|
||||
gardenCardName(card, t).toLowerCase().includes(q) ||
|
||||
term(gardenCardDescription(card, t)).toLowerCase().includes(q) ||
|
||||
card.tags.some((tag) => gardenTagLabel(tag, t).toLowerCase().includes(q))
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -52,7 +60,7 @@ const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardr
|
|||
<Search className="size-4 text-muted-foreground" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
placeholder="Search guardrails"
|
||||
placeholder={t("guardrails.garden.searchPlaceholder")}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
|
|
@ -62,25 +70,23 @@ const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardr
|
|||
<section className="mb-10" aria-labelledby="guardrail-garden-litellm">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<h2 id="guardrail-garden-litellm" className="m-0 text-xl font-semibold text-foreground">
|
||||
LiteLLM Content Filter
|
||||
{t("guardrails.garden.litellmHeading")}
|
||||
</h2>
|
||||
<span
|
||||
className="inline-flex cursor-pointer items-center gap-1.5 text-sm text-primary"
|
||||
onClick={() => setShowAllLitellm(!showAllLitellm)}
|
||||
>
|
||||
{showAllLitellm ? (
|
||||
<>Show less</>
|
||||
<>{t("guardrails.garden.showLess")}</>
|
||||
) : (
|
||||
<>
|
||||
<ArrowRight className="size-3" />
|
||||
{`Show all (${litellmCards.length})`}
|
||||
{t("guardrails.garden.showAll", { total: litellmCards.length })}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 mb-5 text-[13px] text-muted-foreground">
|
||||
Built-in guardrails powered by LiteLLM. Zero latency, no external dependencies, no additional cost.
|
||||
</p>
|
||||
<p className="mt-1 mb-5 text-[13px] text-muted-foreground">{t("guardrails.garden.litellmDescription")}</p>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-4">
|
||||
{(showAllLitellm ? litellmCards : litellmCards.slice(0, CARDS_PER_ROW * VISIBLE_ROWS)).map((card) => (
|
||||
<GuardrailCard key={card.id} card={card} onClick={() => setSelectedCard(card)} />
|
||||
|
|
@ -90,12 +96,9 @@ const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardr
|
|||
|
||||
<section className="mb-10" aria-labelledby="guardrail-garden-decision-model">
|
||||
<h2 id="guardrail-garden-decision-model" className="mt-0 mb-1 text-xl font-semibold text-foreground">
|
||||
Decision Model
|
||||
{t("guardrails.garden.decisionModelHeading")}
|
||||
</h2>
|
||||
<p className="mt-1 mb-5 text-[13px] text-muted-foreground">
|
||||
Ask a decision model yes/no questions about each request or response, and block or log when a score reaches
|
||||
its threshold.
|
||||
</p>
|
||||
<p className="mt-1 mb-5 text-[13px] text-muted-foreground">{t("guardrails.garden.decisionModelDescription")}</p>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-4">
|
||||
{decisionModelCards.map((card) => (
|
||||
<GuardrailCard key={card.id} card={card} onClick={() => setSelectedCard(card)} />
|
||||
|
|
@ -105,11 +108,9 @@ const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardr
|
|||
|
||||
<section className="mb-10" aria-labelledby="guardrail-garden-partner">
|
||||
<h2 id="guardrail-garden-partner" className="mt-0 mb-1 text-xl font-semibold text-foreground">
|
||||
Partner Guardrails
|
||||
{t("guardrails.garden.partnerHeading")}
|
||||
</h2>
|
||||
<p className="mt-1 mb-5 text-[13px] text-muted-foreground">
|
||||
{term("Third-party guardrail integrations from leading AI security providers.")}
|
||||
</p>
|
||||
<p className="mt-1 mb-5 text-[13px] text-muted-foreground">{term(t("guardrails.garden.partnerDescription"))}</p>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-4">
|
||||
{partnerCards.map((card) => (
|
||||
<GuardrailCard key={card.id} card={card} onClick={() => setSelectedCard(card)} />
|
||||
|
|
|
|||
|
|
@ -1,28 +1,33 @@
|
|||
import { useTerminology } from "@/contexts/TerminologyContext";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CircleCheck } from "lucide-react";
|
||||
import { GuardrailCardInfo } from "./guardrail_garden_data";
|
||||
import { GuardrailCardInfo, gardenCardDescription, gardenCardName } from "./guardrail_garden_data";
|
||||
import { Logo } from "@/components/molecules/logo/Logo";
|
||||
|
||||
const GuardrailCard: React.FC<{ card: GuardrailCardInfo; onClick: () => void }> = ({ card, onClick }) => {
|
||||
const { t } = useTranslation();
|
||||
const term = useTerminology();
|
||||
const name = gardenCardName(card, t);
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="flex min-h-[170px] cursor-pointer flex-col rounded-xl border border-border bg-card px-5 pt-5 pb-4 transition-[border-color,box-shadow] hover:border-primary/40 hover:shadow-sm"
|
||||
>
|
||||
<div className="mb-2.5 flex items-center gap-2.5">
|
||||
<Logo src={card.logo} label={card.name} className="w-7 h-7 rounded-md object-contain shrink-0" />
|
||||
<span className="text-sm leading-tight font-semibold text-foreground">{card.name}</span>
|
||||
<Logo src={card.logo} label={name} className="w-7 h-7 rounded-md object-contain shrink-0" />
|
||||
<span className="text-sm leading-tight font-semibold text-foreground">{name}</span>
|
||||
</div>
|
||||
|
||||
<p className="line-clamp-3 m-0 flex-1 text-xs leading-relaxed text-muted-foreground">{term(card.description)}</p>
|
||||
<p className="line-clamp-3 m-0 flex-1 text-xs leading-relaxed text-muted-foreground">
|
||||
{term(gardenCardDescription(card, t))}
|
||||
</p>
|
||||
|
||||
{card.eval && (
|
||||
<div className="mt-2.5 flex items-center gap-1 text-success">
|
||||
<CircleCheck className="size-3" />
|
||||
<span className="text-[11px] font-medium">
|
||||
F1: {card.eval.f1}% · {card.eval.testCases} test cases
|
||||
F1: {card.eval.f1}% · {card.eval.testCases} {t("guardrails.gardenCard.testCases")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import type { ParseKeys, TFunction } from "i18next";
|
||||
import { Providers, providerLogoMap } from "@/components/provider_info_helpers";
|
||||
import { guardrailLogoMap } from "./guardrail_info_helpers";
|
||||
|
||||
export interface GuardrailCardInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
nameKey?: ParseKeys;
|
||||
description: string;
|
||||
descriptionKey?: ParseKeys;
|
||||
category: "litellm" | "decision_model" | "partner";
|
||||
subcategory?: string;
|
||||
subcategoryKey?: ParseKeys;
|
||||
logo: string;
|
||||
tags: string[];
|
||||
eval?: {
|
||||
|
|
@ -19,16 +22,71 @@ export interface GuardrailCardInfo {
|
|||
providerKey?: string;
|
||||
}
|
||||
|
||||
export const GARDEN_TAG_LABEL_KEYS: Readonly<Partial<Record<string, ParseKeys>>> = {
|
||||
"Content Category": "guardrails.gardenCatalog.tags.contentCategory",
|
||||
"Topic Blocker": "guardrails.gardenCatalog.tags.topicBlocker",
|
||||
Safety: "guardrails.gardenCatalog.tags.safety",
|
||||
Bias: "guardrails.gardenCatalog.tags.bias",
|
||||
"Prompt Injection": "guardrails.gardenCatalog.tags.promptInjection",
|
||||
Toxicity: "guardrails.gardenCatalog.tags.toxicity",
|
||||
PII: "guardrails.gardenCatalog.tags.pii",
|
||||
Regex: "guardrails.gardenCatalog.tags.regex",
|
||||
"Data Protection": "guardrails.gardenCatalog.tags.dataProtection",
|
||||
Keywords: "guardrails.gardenCatalog.tags.keywords",
|
||||
Blocklist: "guardrails.gardenCatalog.tags.blocklist",
|
||||
Code: "guardrails.gardenCatalog.tags.code",
|
||||
Competitor: "guardrails.gardenCatalog.tags.competitor",
|
||||
"Decision Model": "guardrails.gardenCatalog.tags.decisionModel",
|
||||
"Content Safety": "guardrails.gardenCatalog.tags.contentSafety",
|
||||
Security: "guardrails.gardenCatalog.tags.security",
|
||||
"Content Moderation": "guardrails.gardenCatalog.tags.contentModeration",
|
||||
"Open Source": "guardrails.gardenCatalog.tags.openSource",
|
||||
Validation: "guardrails.gardenCatalog.tags.validation",
|
||||
Enterprise: "guardrails.gardenCatalog.tags.enterprise",
|
||||
"Threat Detection": "guardrails.gardenCatalog.tags.threatDetection",
|
||||
Hallucination: "guardrails.gardenCatalog.tags.hallucination",
|
||||
Policy: "guardrails.gardenCatalog.tags.policy",
|
||||
Compliance: "guardrails.gardenCatalog.tags.compliance",
|
||||
Governance: "guardrails.gardenCatalog.tags.governance",
|
||||
Gateway: "guardrails.gardenCatalog.tags.gateway",
|
||||
Monitoring: "guardrails.gardenCatalog.tags.monitoring",
|
||||
Grounding: "guardrails.gardenCatalog.tags.grounding",
|
||||
RAG: "guardrails.gardenCatalog.tags.rag",
|
||||
Firewall: "guardrails.gardenCatalog.tags.firewall",
|
||||
Agentic: "guardrails.gardenCatalog.tags.agentic",
|
||||
"Tool Misuse": "guardrails.gardenCatalog.tags.toolMisuse",
|
||||
Skills: "guardrails.gardenCatalog.tags.skills",
|
||||
Observability: "guardrails.gardenCatalog.tags.observability",
|
||||
"Data Privacy": "guardrails.gardenCatalog.tags.dataPrivacy",
|
||||
Streaming: "guardrails.gardenCatalog.tags.streaming",
|
||||
Runtime: "guardrails.gardenCatalog.tags.runtime",
|
||||
Secrets: "guardrails.gardenCatalog.tags.secrets",
|
||||
Redaction: "guardrails.gardenCatalog.tags.redaction",
|
||||
};
|
||||
|
||||
export const gardenCardName = (card: GuardrailCardInfo, t: TFunction): string =>
|
||||
card.nameKey ? t(card.nameKey) : card.name;
|
||||
|
||||
export const gardenCardDescription = (card: GuardrailCardInfo, t: TFunction): string =>
|
||||
card.descriptionKey ? t(card.descriptionKey) : card.description;
|
||||
|
||||
export const gardenTagLabel = (tag: string, t: TFunction): string => {
|
||||
const key = GARDEN_TAG_LABEL_KEYS[tag];
|
||||
return key ? t(key) : tag;
|
||||
};
|
||||
|
||||
const litellmContentFilterLogo = guardrailLogoMap["LiteLLM Content Filter"];
|
||||
|
||||
export const LITELLM_CONTENT_FILTER_CARDS: GuardrailCardInfo[] = [
|
||||
{
|
||||
id: "cf_denied_financial",
|
||||
name: "Denied Financial Advice",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.deniedFinancial.name",
|
||||
description:
|
||||
"Detects requests for personalized financial advice, investment recommendations, or financial planning.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.deniedFinancial.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Topic Blocker"],
|
||||
eval: {
|
||||
|
|
@ -42,9 +100,11 @@ export const LITELLM_CONTENT_FILTER_CARDS: GuardrailCardInfo[] = [
|
|||
{
|
||||
id: "cf_denied_insults",
|
||||
name: "Insults & Personal Attacks",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.deniedInsults.name",
|
||||
description: "Detects insults, name-calling, and personal attacks directed at the chatbot, staff, or other people.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.deniedInsults.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Topic Blocker"],
|
||||
eval: {
|
||||
|
|
@ -58,184 +118,224 @@ export const LITELLM_CONTENT_FILTER_CARDS: GuardrailCardInfo[] = [
|
|||
{
|
||||
id: "cf_denied_legal",
|
||||
name: "Denied Legal Advice",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.deniedLegal.name",
|
||||
description: "Detects requests for unauthorized legal advice, case analysis, or legal recommendations.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.deniedLegal.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Topic Blocker"],
|
||||
},
|
||||
{
|
||||
id: "cf_denied_medical",
|
||||
name: "Denied Medical Advice",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.deniedMedical.name",
|
||||
description: "Detects requests for medical diagnosis, treatment recommendations, or health advice.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.deniedMedical.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Topic Blocker"],
|
||||
},
|
||||
{
|
||||
id: "cf_harmful_violence",
|
||||
name: "Harmful Violence",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.harmfulViolence.name",
|
||||
description: "Detects content related to violence, criminal planning, attacks, and violent threats.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.harmfulViolence.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Safety"],
|
||||
},
|
||||
{
|
||||
id: "cf_harmful_self_harm",
|
||||
name: "Harmful Self-Harm",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.harmfulSelfHarm.name",
|
||||
description: "Detects content related to self-harm, suicide, and dangerous self-destructive behavior.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.harmfulSelfHarm.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Safety"],
|
||||
},
|
||||
{
|
||||
id: "cf_harmful_child_safety",
|
||||
name: "Harmful Child Safety",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.harmfulChildSafety.name",
|
||||
description: "Detects content that could endanger child safety or exploit minors.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.harmfulChildSafety.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Safety"],
|
||||
},
|
||||
{
|
||||
id: "cf_harmful_illegal_weapons",
|
||||
name: "Harmful Illegal Weapons",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.harmfulIllegalWeapons.name",
|
||||
description: "Detects content related to illegal weapons manufacturing, distribution, or acquisition.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.harmfulIllegalWeapons.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Safety"],
|
||||
},
|
||||
{
|
||||
id: "cf_bias_gender",
|
||||
name: "Bias: Gender",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.biasGender.name",
|
||||
description: "Detects gender-based discrimination, stereotypes, and biased language.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.biasGender.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Bias"],
|
||||
},
|
||||
{
|
||||
id: "cf_bias_racial",
|
||||
name: "Bias: Racial",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.biasRacial.name",
|
||||
description: "Detects racial discrimination, stereotypes, and racially biased content.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.biasRacial.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Bias"],
|
||||
},
|
||||
{
|
||||
id: "cf_bias_religious",
|
||||
name: "Bias: Religious",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.biasReligious.name",
|
||||
description: "Detects religious discrimination, intolerance, and religiously biased content.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.biasReligious.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Bias"],
|
||||
},
|
||||
{
|
||||
id: "cf_bias_sexual_orientation",
|
||||
name: "Bias: Sexual Orientation",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.biasSexualOrientation.name",
|
||||
description: "Detects discrimination based on sexual orientation and related biased content.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.biasSexualOrientation.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Bias"],
|
||||
},
|
||||
{
|
||||
id: "cf_prompt_injection_jailbreak",
|
||||
name: "Prompt Injection: Jailbreak",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.promptInjectionJailbreak.name",
|
||||
description: "Detects jailbreak attempts designed to bypass AI safety guidelines and restrictions.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.promptInjectionJailbreak.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Prompt Injection"],
|
||||
},
|
||||
{
|
||||
id: "cf_prompt_injection_data_exfil",
|
||||
name: "Prompt Injection: Data Exfiltration",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.promptInjectionDataExfil.name",
|
||||
description: "Detects attempts to extract sensitive data through prompt manipulation.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.promptInjectionDataExfil.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Prompt Injection"],
|
||||
},
|
||||
{
|
||||
id: "cf_prompt_injection_sql",
|
||||
name: "Prompt Injection: SQL",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.promptInjectionSql.name",
|
||||
description: "Detects SQL injection attempts embedded in prompts.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.promptInjectionSql.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Prompt Injection"],
|
||||
},
|
||||
{
|
||||
id: "cf_prompt_injection_malicious_code",
|
||||
name: "Prompt Injection: Malicious Code",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.promptInjectionMaliciousCode.name",
|
||||
description: "Detects attempts to inject malicious code through prompts.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.promptInjectionMaliciousCode.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Prompt Injection"],
|
||||
},
|
||||
{
|
||||
id: "cf_prompt_injection_system_prompt",
|
||||
name: "Prompt Injection: System Prompt",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.promptInjectionSystemPrompt.name",
|
||||
description: "Detects attempts to extract or override system prompts.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.promptInjectionSystemPrompt.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Prompt Injection"],
|
||||
},
|
||||
{
|
||||
id: "cf_toxic_abuse",
|
||||
name: "Toxic & Abusive Language",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.toxicAbuse.name",
|
||||
description: "Detects toxic, abusive, and hateful language across multiple languages (EN, AU, DE, ES, FR).",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.toxicAbuse.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Toxicity"],
|
||||
},
|
||||
{
|
||||
id: "cf_patterns",
|
||||
name: "Pattern Matching",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.patterns.name",
|
||||
description:
|
||||
"Detect and block sensitive data patterns like SSNs, credit card numbers, API keys, and custom regex patterns.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.patterns.description",
|
||||
category: "litellm",
|
||||
subcategory: "Patterns",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.patterns",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["PII", "Regex", "Data Protection"],
|
||||
},
|
||||
{
|
||||
id: "cf_keywords",
|
||||
name: "Keyword Blocking",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.keywords.name",
|
||||
description:
|
||||
"Block or mask content containing specific keywords or phrases. Upload custom word lists or add individual terms.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.keywords.description",
|
||||
category: "litellm",
|
||||
subcategory: "Keywords",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.keywords",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Keywords", "Blocklist"],
|
||||
},
|
||||
{
|
||||
id: "block_code_execution",
|
||||
name: "Block Code Execution",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.blockCodeExecution.name",
|
||||
description:
|
||||
"Detects markdown fenced code blocks in requests and responses. Block or mask executable code (e.g. Python, JavaScript, Bash) by language with configurable confidence.",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.blockCodeExecution.description",
|
||||
category: "litellm",
|
||||
subcategory: "Code Safety",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.codeSafety",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Code", "Safety", "Prompt Injection"],
|
||||
},
|
||||
{
|
||||
id: "cf_competitor_intent",
|
||||
name: "Competitor Name Blocking",
|
||||
nameKey: "guardrails.gardenCatalog.contentFilter.competitorIntent.name",
|
||||
description:
|
||||
"Block or reframe competitor comparison and ranking intent. Detect when users ask to compare or recommend competitors (airline or generic competitor lists).",
|
||||
descriptionKey: "guardrails.gardenCatalog.contentFilter.competitorIntent.description",
|
||||
category: "litellm",
|
||||
subcategory: "Content Category",
|
||||
subcategoryKey: "guardrails.gardenCatalog.subcategories.contentCategory",
|
||||
logo: litellmContentFilterLogo,
|
||||
tags: ["Content Category", "Competitor", "Topic Blocker"],
|
||||
},
|
||||
|
|
@ -247,6 +347,7 @@ export const DECISION_MODEL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "TypeSafe Jev",
|
||||
description:
|
||||
"TypeSafe's Jev decision model answers your yes/no questions about each request, such as prompt injection or policy checks.",
|
||||
descriptionKey: "guardrails.gardenCatalog.decisionModel.typesafeJev.description",
|
||||
category: "decision_model",
|
||||
logo: providerLogoMap[Providers.TypeSafe] ?? "",
|
||||
tags: ["Decision Model", "TypeSafe", "Prompt Injection"],
|
||||
|
|
@ -255,7 +356,9 @@ export const DECISION_MODEL_CARDS: GuardrailCardInfo[] = [
|
|||
{
|
||||
id: "dm_perplexity",
|
||||
name: "Perplexity Decision",
|
||||
nameKey: "guardrails.gardenCatalog.decisionModel.perplexity.name",
|
||||
description: "Perplexity's pplx-decider model answers your yes/no questions about each request or response.",
|
||||
descriptionKey: "guardrails.gardenCatalog.decisionModel.perplexity.description",
|
||||
category: "decision_model",
|
||||
logo: providerLogoMap[Providers.Perplexity] ?? "",
|
||||
tags: ["Decision Model", "Perplexity"],
|
||||
|
|
@ -264,8 +367,10 @@ export const DECISION_MODEL_CARDS: GuardrailCardInfo[] = [
|
|||
{
|
||||
id: "dm_microsoft",
|
||||
name: "Microsoft Decision Model",
|
||||
nameKey: "guardrails.gardenCatalog.decisionModel.microsoft.name",
|
||||
description:
|
||||
"Microsoft Foundry decision models, such as Microsoft-Decision-1, answer your yes/no questions about each request or response.",
|
||||
descriptionKey: "guardrails.gardenCatalog.decisionModel.microsoft.description",
|
||||
category: "decision_model",
|
||||
logo: providerLogoMap[Providers.Azure_AI_Studio] ?? "",
|
||||
tags: ["Decision Model", "Microsoft"],
|
||||
|
|
@ -274,8 +379,10 @@ export const DECISION_MODEL_CARDS: GuardrailCardInfo[] = [
|
|||
{
|
||||
id: "dm_openai",
|
||||
name: "OpenAI Decision Model",
|
||||
nameKey: "guardrails.gardenCatalog.decisionModel.openai.name",
|
||||
description:
|
||||
"OpenAI decision models, such as gpt-6-luna, answer your yes/no questions about each request or response.",
|
||||
descriptionKey: "guardrails.gardenCatalog.decisionModel.openai.description",
|
||||
category: "decision_model",
|
||||
logo: providerLogoMap[Providers.OpenAI] ?? "",
|
||||
tags: ["Decision Model", "OpenAI"],
|
||||
|
|
@ -284,8 +391,10 @@ export const DECISION_MODEL_CARDS: GuardrailCardInfo[] = [
|
|||
{
|
||||
id: "dm_databricks",
|
||||
name: "Databricks Decision Model",
|
||||
nameKey: "guardrails.gardenCatalog.decisionModel.databricks.name",
|
||||
description:
|
||||
"Decision models served on Databricks, such as OpenJev, answer your yes/no questions about each request or response.",
|
||||
descriptionKey: "guardrails.gardenCatalog.decisionModel.databricks.description",
|
||||
category: "decision_model",
|
||||
logo: providerLogoMap[Providers.Databricks] ?? "",
|
||||
tags: ["Decision Model", "Databricks"],
|
||||
|
|
@ -299,6 +408,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "Presidio PII",
|
||||
description:
|
||||
"Microsoft Presidio for PII detection and anonymization. Supports 30+ entity types with configurable actions.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.presidio.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Presidio PII"],
|
||||
tags: ["PII", "Microsoft"],
|
||||
|
|
@ -308,6 +418,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "bedrock",
|
||||
name: "Bedrock Guardrail",
|
||||
description: "AWS Bedrock Guardrails for content filtering, topic avoidance, and sensitive information detection.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.bedrock.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Bedrock Guardrail"],
|
||||
tags: ["AWS", "Content Safety"],
|
||||
|
|
@ -317,6 +428,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "lakera",
|
||||
name: "Lakera",
|
||||
description: "AI security platform protecting against prompt injections, data leakage, and harmful content.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.lakera.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Lakera"],
|
||||
tags: ["Security", "Prompt Injection"],
|
||||
|
|
@ -326,6 +438,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "openai_moderation",
|
||||
name: "OpenAI Moderation",
|
||||
description: "OpenAI's content moderation API for detecting harmful content across multiple categories.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.openaiModeration.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["OpenAI Moderation"],
|
||||
tags: ["Content Moderation", "OpenAI"],
|
||||
|
|
@ -334,6 +447,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "google_model_armor",
|
||||
name: "Google Cloud Model Armor",
|
||||
description: "Google Cloud's model protection service for safe and responsible AI deployments.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.googleModelArmor.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Google Cloud Model Armor"],
|
||||
tags: ["Google Cloud", "Safety"],
|
||||
|
|
@ -342,6 +456,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "guardrails_ai",
|
||||
name: "Guardrails AI",
|
||||
description: "Open-source framework for adding structural, type, and quality guarantees to LLM outputs.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.guardrailsAi.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Guardrails AI"],
|
||||
tags: ["Open Source", "Validation"],
|
||||
|
|
@ -350,6 +465,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "zscaler",
|
||||
name: "Zscaler AI Guard",
|
||||
description: "Enterprise AI security from Zscaler for monitoring and protecting AI/ML workloads.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.zscaler.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Zscaler AI Guard"],
|
||||
tags: ["Enterprise", "Security"],
|
||||
|
|
@ -358,6 +474,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "panw",
|
||||
name: "PANW Prisma AIRS",
|
||||
description: "Palo Alto Networks Prisma AI Runtime Security for securing AI applications in production.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.panw.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["PANW Prisma AIRS"],
|
||||
tags: ["Enterprise", "Security"],
|
||||
|
|
@ -367,6 +484,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "Cisco AI Defense",
|
||||
description:
|
||||
"Cisco AI Defense Inspection API for runtime protection: prompt injection, PII/PCI/PHI, harassment, hate speech, profanity, violence, and code detection.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.ciscoAiDefense.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Cisco AI Defense"],
|
||||
tags: ["Enterprise", "Security", "Prompt Injection", "PII"],
|
||||
|
|
@ -376,6 +494,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "noma",
|
||||
name: "Noma Security",
|
||||
description: "AI security platform for detecting and preventing AI-specific threats and vulnerabilities.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.noma.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Noma Security"],
|
||||
tags: ["Security", "Threat Detection"],
|
||||
|
|
@ -384,6 +503,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "aporia",
|
||||
name: "Aporia AI",
|
||||
description: "Real-time AI guardrails for hallucination detection, topic control, and policy enforcement.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.aporia.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Aporia AI"],
|
||||
tags: ["Hallucination", "Policy"],
|
||||
|
|
@ -392,6 +512,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "aim",
|
||||
name: "AIM Guardrail",
|
||||
description: "AIM Security guardrails for comprehensive AI threat detection and mitigation.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.aim.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["AIM Guardrail"],
|
||||
tags: ["Security", "Threat Detection"],
|
||||
|
|
@ -400,6 +521,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "cato_networks",
|
||||
name: "Cato Networks Guardrail",
|
||||
description: "Cato Networks guardrails for comprehensive AI threat detection and mitigation.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.catoNetworks.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Cato Networks Guardrail"],
|
||||
tags: ["Security", "Threat Detection"],
|
||||
|
|
@ -408,6 +530,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "prompt_security",
|
||||
name: "Prompt Security",
|
||||
description: "Protect against prompt injection attacks, data leakage, and other LLM security threats.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.promptSecurity.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Prompt Security"],
|
||||
tags: ["Prompt Injection", "Security"],
|
||||
|
|
@ -416,6 +539,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "lasso",
|
||||
name: "Lasso Guardrail",
|
||||
description: "Content moderation and safety guardrails for responsible AI deployments.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.lasso.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Lasso Guardrail"],
|
||||
tags: ["Content Moderation"],
|
||||
|
|
@ -424,6 +548,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "pangea",
|
||||
name: "Pangea Guardrail",
|
||||
description: "Pangea's AI guardrails for secure, compliant, and trustworthy AI applications.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.pangea.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Pangea Guardrail"],
|
||||
tags: ["Compliance", "Security"],
|
||||
|
|
@ -432,6 +557,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "enkryptai",
|
||||
name: "EnkryptAI",
|
||||
description: "AI security and governance platform for enterprise AI safety and compliance.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.enkryptai.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["EnkryptAI"],
|
||||
tags: ["Enterprise", "Governance"],
|
||||
|
|
@ -440,6 +566,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "javelin",
|
||||
name: "Javelin Guardrails",
|
||||
description: "AI gateway with built-in guardrails for secure and compliant AI operations.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.javelin.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Javelin Guardrails"],
|
||||
tags: ["Gateway", "Security"],
|
||||
|
|
@ -448,6 +575,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "pillar",
|
||||
name: "Pillar Guardrail",
|
||||
description: "AI safety platform for monitoring, testing, and securing AI systems.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.pillar.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Pillar Guardrail"],
|
||||
tags: ["Monitoring", "Safety"],
|
||||
|
|
@ -456,6 +584,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
id: "akto",
|
||||
name: "Akto Guardrail",
|
||||
description: "AI security platform from Akto.io with automatic monitoring and guardrails for AI/ML applications.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.akto.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Akto"],
|
||||
tags: ["Security", "Safety", "Monitoring"],
|
||||
|
|
@ -465,6 +594,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "PromptGuard",
|
||||
description:
|
||||
"AI security gateway with prompt injection detection, PII redaction, topic filtering, entity blocklists, and hallucination detection. Self-hostable with drop-in proxy integration.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.promptguard.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["PromptGuard"],
|
||||
tags: ["Security", "Prompt Injection", "PII"],
|
||||
|
|
@ -482,6 +612,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "XecGuard",
|
||||
description:
|
||||
"CyCraft XecGuard AI security gateway. Multi-policy scanning (prompt injection, harmful content, PII, system-prompt enforcement) plus RAG context grounding.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.xecguard.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["XecGuard"],
|
||||
tags: ["Security", "Policy", "Grounding", "RAG"],
|
||||
|
|
@ -492,6 +623,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "DeepKeep AI Firewall",
|
||||
description:
|
||||
"DeepKeep AI Firewall for comprehensive LLM security — prompt injection detection, PII protection, content moderation, and policy enforcement with configurable guardrail pipelines.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.deepkeep.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["DeepKeep AI Firewall"],
|
||||
tags: ["Security", "Prompt Injection", "PII", "Firewall"],
|
||||
|
|
@ -502,6 +634,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "RepelloAI Argus",
|
||||
description:
|
||||
"RepelloAI Argus scans prompts and responses against policies configured per asset in the Repello dashboard.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.repelloai.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["RepelloAI Argus"],
|
||||
tags: ["Security", "Policy", "Prompt Injection"],
|
||||
|
|
@ -512,6 +645,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "Straiker",
|
||||
description:
|
||||
"Defend AI Agentic Guardrails: Indirect/Direct Prompt Injection, Tool Misuse, Malicious MCP and Skills",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.straiker.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Straiker"],
|
||||
tags: ["Agentic", "Prompt Injection", "Tool Misuse", "MCP", "Skills"],
|
||||
|
|
@ -522,6 +656,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "Alice",
|
||||
description:
|
||||
"Policy-based guardrails for prompts and model responses, evaluated per application so one proxy can enforce a different policy set per team or product.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.alice.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Alice"],
|
||||
tags: ["Content Moderation", "Prompt Injection", "PII", "Policy"],
|
||||
|
|
@ -532,6 +667,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "Microsoft Agent 365",
|
||||
description:
|
||||
"Microsoft Agent 365 tool-call governance: Defender threat evaluation and observability for MCP tool calls, acting on behalf of the signed-in user",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.agent365.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Microsoft Agent 365"],
|
||||
tags: ["Agentic", "MCP", "Tool Misuse", "Observability"],
|
||||
|
|
@ -542,6 +678,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "LLM Shield Proxy",
|
||||
description:
|
||||
"Self-hosted PII redaction that puts the original values back into the model's response, so the provider never receives personal data while the end user still sees it.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.llmShieldProxy.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["LLM Shield Proxy"],
|
||||
tags: ["PII", "Data Privacy", "Compliance", "Streaming"],
|
||||
|
|
@ -552,6 +689,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "Conduct Guard",
|
||||
description:
|
||||
"Conduct Guard evaluates prompts against workspace rules before the model call: prompt injection, PII, and custom policies, with block, warning, and approval verdicts.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.conduct.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Conduct Guard"],
|
||||
tags: ["Security", "Prompt Injection", "PII", "Policy"],
|
||||
|
|
@ -562,6 +700,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "Wingback",
|
||||
description:
|
||||
"Wingback runtime security for LiteLLM — PII detection, prompt injection, toxic content, and policy enforcement via the Wingback connectors service.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.wingback.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Wingback"],
|
||||
tags: ["Security", "PII", "Prompt Injection", "Runtime"],
|
||||
|
|
@ -572,6 +711,7 @@ export const PARTNER_GUARDRAIL_CARDS: GuardrailCardInfo[] = [
|
|||
name: "Airia Guardrail",
|
||||
description:
|
||||
"Airia AI security and governance platform. Runtime guardrails for prompt injection, data leakage prevention, and responsible AI, enforced on every prompt and response.",
|
||||
descriptionKey: "guardrails.gardenCatalog.partner.airia.description",
|
||||
category: "partner",
|
||||
logo: guardrailLogoMap["Airia Guardrail"],
|
||||
tags: ["Security", "Prompt Injection", "PII", "Secrets", "Redaction"],
|
||||
|
|
|
|||
|
|
@ -1,17 +1,28 @@
|
|||
import { useTerminology } from "@/contexts/TerminologyContext";
|
||||
import React, { useState } from "react";
|
||||
import type { ParseKeys } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/cva.config";
|
||||
import AddGuardrailForm from "./add_guardrail_form";
|
||||
import { Logo } from "@/components/molecules/logo/Logo";
|
||||
import { GUARDRAIL_PRESETS } from "./guardrail_garden_configs";
|
||||
import { GuardrailCardInfo } from "./guardrail_garden_data";
|
||||
import { GuardrailCardInfo, gardenCardDescription, gardenCardName, gardenTagLabel } from "./guardrail_garden_data";
|
||||
|
||||
const CATEGORY_LABELS: Record<GuardrailCardInfo["category"], { provider: string; type: string }> = {
|
||||
litellm: { provider: "LiteLLM Content Filter", type: "Content Filter" },
|
||||
decision_model: { provider: "Decision Model", type: "Decision Model" },
|
||||
partner: { provider: "Partner Guardrail", type: "Partner" },
|
||||
const CATEGORY_LABEL_KEYS: Record<GuardrailCardInfo["category"], { provider: ParseKeys; type: ParseKeys }> = {
|
||||
litellm: {
|
||||
provider: "guardrails.gardenDetail.categories.litellm.provider",
|
||||
type: "guardrails.gardenDetail.categories.litellm.type",
|
||||
},
|
||||
decision_model: {
|
||||
provider: "guardrails.gardenDetail.categories.decisionModel.provider",
|
||||
type: "guardrails.gardenDetail.categories.decisionModel.type",
|
||||
},
|
||||
partner: {
|
||||
provider: "guardrails.gardenDetail.categories.partner.provider",
|
||||
type: "guardrails.gardenDetail.categories.partner.type",
|
||||
},
|
||||
};
|
||||
|
||||
interface GuardrailDetailViewProps {
|
||||
|
|
@ -22,34 +33,66 @@ interface GuardrailDetailViewProps {
|
|||
}
|
||||
|
||||
const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack, accessToken, onGuardrailCreated }) => {
|
||||
const { t } = useTranslation();
|
||||
const term = useTerminology();
|
||||
const name = gardenCardName(card, t);
|
||||
const description = term(gardenCardDescription(card, t));
|
||||
const [isAddFormVisible, setIsAddFormVisible] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
|
||||
const detailRows = [
|
||||
{ property: "Provider", value: CATEGORY_LABELS[card.category].provider },
|
||||
...(card.subcategory ? [{ property: "Subcategory", value: card.subcategory }] : []),
|
||||
...(card.category === "decision_model"
|
||||
? [{ property: "Cost", value: "Billed per call by the decision model provider" }]
|
||||
{
|
||||
property: t("guardrails.gardenDetail.detailRows.provider"),
|
||||
value: t(CATEGORY_LABEL_KEYS[card.category].provider),
|
||||
},
|
||||
...(card.subcategoryKey
|
||||
? [{ property: t("guardrails.gardenDetail.detailRows.subcategory"), value: t(card.subcategoryKey) }]
|
||||
: []),
|
||||
...(card.category === "decision_model"
|
||||
? [
|
||||
{
|
||||
property: t("guardrails.gardenDetail.detailRows.cost"),
|
||||
value: t("guardrails.gardenDetail.detailValues.decisionModelCost"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(card.category === "litellm"
|
||||
? [
|
||||
{
|
||||
property: t("guardrails.gardenDetail.detailRows.cost"),
|
||||
value: t("guardrails.gardenDetail.detailValues.litellmCost"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(card.category === "litellm"
|
||||
? [
|
||||
{
|
||||
property: t("guardrails.gardenDetail.detailRows.externalDependencies"),
|
||||
value: t("guardrails.gardenDetail.detailValues.noExternalDependencies"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(card.category === "litellm"
|
||||
? [{ property: t("guardrails.gardenDetail.detailRows.latency"), value: card.eval?.latency || "<1ms" }]
|
||||
: []),
|
||||
...(card.category === "litellm" ? [{ property: "Cost", value: "$0 / request" }] : []),
|
||||
...(card.category === "litellm" ? [{ property: "External Dependencies", value: "None" }] : []),
|
||||
...(card.category === "litellm" ? [{ property: "Latency", value: card.eval?.latency || "<1ms" }] : []),
|
||||
];
|
||||
|
||||
const evalRows = card.eval
|
||||
? [
|
||||
{ metric: "Precision", value: `${card.eval.precision}%` },
|
||||
{ metric: "Recall", value: `${card.eval.recall}%` },
|
||||
{ metric: "F1 Score", value: `${card.eval.f1}%` },
|
||||
{ metric: "Test Cases", value: String(card.eval.testCases) },
|
||||
{ metric: "False Positives", value: "0" },
|
||||
{ metric: "False Negatives", value: "0" },
|
||||
{ metric: "Latency (p50)", value: card.eval.latency },
|
||||
{ metric: t("guardrails.gardenDetail.evalMetrics.precision"), value: `${card.eval.precision}%` },
|
||||
{ metric: t("guardrails.gardenDetail.evalMetrics.recall"), value: `${card.eval.recall}%` },
|
||||
{ metric: t("guardrails.gardenDetail.evalMetrics.f1Score"), value: `${card.eval.f1}%` },
|
||||
{ metric: t("guardrails.gardenDetail.evalMetrics.testCases"), value: String(card.eval.testCases) },
|
||||
{ metric: t("guardrails.gardenDetail.evalMetrics.falsePositives"), value: "0" },
|
||||
{ metric: t("guardrails.gardenDetail.evalMetrics.falseNegatives"), value: "0" },
|
||||
{ metric: t("guardrails.gardenDetail.evalMetrics.latencyP50"), value: card.eval.latency },
|
||||
]
|
||||
: [];
|
||||
|
||||
const tabs = [{ key: "overview", label: "Overview" }, ...(card.eval ? [{ key: "eval", label: "Eval Results" }] : [])];
|
||||
const tabs = [
|
||||
{ key: "overview", label: t("guardrails.gardenDetail.tabs.overview") },
|
||||
...(card.eval ? [{ key: "eval", label: t("guardrails.gardenDetail.tabs.evalResults") }] : []),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[960px]">
|
||||
|
|
@ -59,21 +102,21 @@ const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack,
|
|||
className="mb-6 inline-flex cursor-pointer items-center gap-1.5 text-sm text-muted-foreground"
|
||||
>
|
||||
<ArrowLeft className="size-3" />
|
||||
<span>{card.name}</span>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
|
||||
{/* ── Header block (Vertex-style) ── */}
|
||||
<div className="mb-2 flex items-center gap-4">
|
||||
<Logo src={card.logo} label={card.name} className="w-10 h-10 rounded-lg object-contain shrink-0" />
|
||||
<h1 className="m-0 text-[28px] font-normal leading-tight text-foreground">{card.name}</h1>
|
||||
<Logo src={card.logo} label={name} className="w-10 h-10 rounded-lg object-contain shrink-0" />
|
||||
<h1 className="m-0 text-[28px] font-normal leading-tight text-foreground">{name}</h1>
|
||||
</div>
|
||||
|
||||
<p className="m-0 mb-5 text-sm leading-relaxed text-muted-foreground">{term(card.description)}</p>
|
||||
<p className="m-0 mb-5 text-sm leading-relaxed text-muted-foreground">{description}</p>
|
||||
|
||||
{/* Action buttons — outlined style like Vertex */}
|
||||
<div className="mb-8 flex gap-2.5">
|
||||
<Button variant="outline" className="rounded-full" onClick={() => setIsAddFormVisible(true)}>
|
||||
Create Guardrail
|
||||
{t("guardrails.gardenDetail.createGuardrail")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -102,17 +145,23 @@ const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack,
|
|||
<div className="flex gap-16">
|
||||
{/* Left column — overview + details table */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="m-0 mb-3 text-lg font-normal text-foreground">Overview</h2>
|
||||
<p className="m-0 mb-8 text-sm leading-[1.7] text-foreground">{term(card.description)}</p>
|
||||
<h2 className="m-0 mb-3 text-lg font-normal text-foreground">
|
||||
{t("guardrails.gardenDetail.overviewHeading")}
|
||||
</h2>
|
||||
<p className="m-0 mb-8 text-sm leading-[1.7] text-foreground">{description}</p>
|
||||
|
||||
<h2 className="m-0 mb-1 text-lg font-normal text-foreground">Guardrail Details</h2>
|
||||
<p className="m-0 mb-4 text-[13px] text-muted-foreground">Details are as follows</p>
|
||||
<h2 className="m-0 mb-1 text-lg font-normal text-foreground">
|
||||
{t("guardrails.gardenDetail.detailsHeading")}
|
||||
</h2>
|
||||
<p className="m-0 mb-4 text-[13px] text-muted-foreground">{t("guardrails.gardenDetail.detailsSubtitle")}</p>
|
||||
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="w-50 py-3 text-left font-medium text-muted-foreground">Property</th>
|
||||
<th className="py-3 text-left font-medium text-muted-foreground">{card.name}</th>
|
||||
<th className="w-50 py-3 text-left font-medium text-muted-foreground">
|
||||
{t("guardrails.gardenDetail.propertyColumn")}
|
||||
</th>
|
||||
<th className="py-3 text-left font-medium text-muted-foreground">{name}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -130,27 +179,27 @@ const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack,
|
|||
<div className="w-60 shrink-0">
|
||||
{/* Guardrail ID */}
|
||||
<div className="mb-7">
|
||||
<div className="mb-1 text-xs text-muted-foreground">Guardrail ID</div>
|
||||
<div className="mb-1 text-xs text-muted-foreground">{t("guardrails.gardenDetail.guardrailIdLabel")}</div>
|
||||
<div className="break-all text-[13px] text-foreground">litellm/{card.id}</div>
|
||||
</div>
|
||||
|
||||
{/* Type */}
|
||||
<div className="mb-7">
|
||||
<div className="mb-1 text-xs text-muted-foreground">Type</div>
|
||||
<div className="text-[13px] text-foreground">{CATEGORY_LABELS[card.category].type}</div>
|
||||
<div className="mb-1 text-xs text-muted-foreground">{t("guardrails.gardenDetail.typeLabel")}</div>
|
||||
<div className="text-[13px] text-foreground">{t(CATEGORY_LABEL_KEYS[card.category].type)}</div>
|
||||
</div>
|
||||
|
||||
{/* Tags — pill style like Vertex */}
|
||||
{card.tags.length > 0 && (
|
||||
<div className="mb-7">
|
||||
<div className="mb-2 text-xs text-muted-foreground">Tags</div>
|
||||
<div className="mb-2 text-xs text-muted-foreground">{t("guardrails.gardenDetail.tagsLabel")}</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{card.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-2xl border border-border bg-card px-3 py-1 text-xs text-foreground"
|
||||
>
|
||||
{tag}
|
||||
{gardenTagLabel(tag, t)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -162,12 +211,18 @@ const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack,
|
|||
|
||||
{activeTab === "eval" && (
|
||||
<div>
|
||||
<h2 className="m-0 mb-4 text-lg font-normal text-foreground">Eval Results</h2>
|
||||
<h2 className="m-0 mb-4 text-lg font-normal text-foreground">
|
||||
{t("guardrails.gardenDetail.evalResultsHeading")}
|
||||
</h2>
|
||||
<table className="w-full max-w-[560px] border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted">
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground">Metric</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground">Value</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground">
|
||||
{t("guardrails.gardenDetail.metricColumn")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground">
|
||||
{t("guardrails.gardenDetail.valueColumn")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ import { ArrowLeft, Ban, CheckIcon, Code, CopyIcon, EyeOff, Info } from "lucide-
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import type { ParseKeys } from "i18next";
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { Logo } from "@/components/molecules/logo/Logo";
|
||||
import { FieldGroup } from "@/components/ui/field";
|
||||
|
|
@ -58,9 +60,9 @@ import GuardrailProviderFields from "./guardrail_provider_fields";
|
|||
import PiiConfiguration from "./pii_configuration";
|
||||
import ToolPermissionRulesEditor, { ToolPermissionConfig } from "./tool_permission/ToolPermissionRulesEditor";
|
||||
|
||||
const DEFAULT_ON_ITEMS = [
|
||||
{ label: "Yes", value: true },
|
||||
{ label: "No", value: false },
|
||||
const DEFAULT_ON_ITEMS: readonly { labelKey: ParseKeys; value: boolean }[] = [
|
||||
{ labelKey: "guardrails.info.settings.defaultOnOptions.yes", value: true },
|
||||
{ labelKey: "guardrails.info.settings.defaultOnOptions.no", value: false },
|
||||
];
|
||||
|
||||
const SectionHeading: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
|
|
@ -79,6 +81,7 @@ export interface GuardrailInfoProps {
|
|||
}
|
||||
|
||||
const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose, onDeleted, accessToken, isAdmin }) => {
|
||||
const { t } = useTranslation();
|
||||
const [guardrailData, setGuardrailData] = useState<any>(null);
|
||||
const [guardrailProviderSpecificParams, setGuardrailProviderSpecificParams] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -193,7 +196,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
setSelectedPiiActions({});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.fromError("Failed to load guardrail information");
|
||||
toast.fromError(t("guardrails.info.toasts.loadFailed"));
|
||||
console.error("Error fetching guardrail info:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -486,19 +489,19 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
|
||||
// Only proceed with update if there are actual changes
|
||||
if (Object.keys(updateData).length === 0) {
|
||||
toast.info("No changes detected");
|
||||
toast.info(t("guardrails.info.toasts.noChanges"));
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await updateGuardrailCall(accessToken, guardrailId, updateData);
|
||||
toast.success("Guardrail updated successfully");
|
||||
toast.success(t("guardrails.info.toasts.updated"));
|
||||
setHasUnsavedContentFilterChanges(false);
|
||||
fetchGuardrailInfo();
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
console.error("Error updating guardrail:", error);
|
||||
toast.fromError("Failed to update guardrail");
|
||||
toast.fromError(t("guardrails.info.toasts.updateFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -511,18 +514,23 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
const submitLatest = useCallback((values: GuardrailFormValues) => submitRef.current(values), []);
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-4">Loading...</div>;
|
||||
return <div className="p-4">{t("guardrails.info.loading")}</div>;
|
||||
}
|
||||
|
||||
const backButton = (
|
||||
<Button variant="ghost" onClick={onClose} className="mb-4">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to Guardrails
|
||||
{t("guardrails.info.back")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (!guardrailData) {
|
||||
return <div className="p-4">{backButton}Guardrail not found</div>;
|
||||
return (
|
||||
<div className="p-4">
|
||||
{backButton}
|
||||
{t("guardrails.info.notFound")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const formatDate = (dateString?: string) => (dateString ? new Date(dateString).toLocaleString() : "-");
|
||||
|
|
@ -546,7 +554,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<div className="p-4">
|
||||
<div>
|
||||
{backButton}
|
||||
<h1 className="text-2xl font-semibold">{guardrailData.guardrail_name || "Unnamed Guardrail"}</h1>
|
||||
<h1 className="text-2xl font-semibold">{guardrailData.guardrail_name || t("guardrails.info.unnamed")}</h1>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<p className="text-muted-foreground font-mono">{guardrailData.guardrail_id}</p>
|
||||
|
||||
|
|
@ -568,11 +576,11 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<Tabs defaultValue="overview">
|
||||
<TabsList variant="line" className="mb-4 h-auto w-full justify-start rounded-none border-b p-0">
|
||||
<TabsTrigger value="overview" className="flex-none rounded-none px-4 py-2">
|
||||
Overview
|
||||
{t("guardrails.info.tabs.overview")}
|
||||
</TabsTrigger>
|
||||
{isAdmin && (
|
||||
<TabsTrigger value="settings" className="flex-none rounded-none px-4 py-2">
|
||||
Settings
|
||||
{t("guardrails.info.tabs.settings")}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
|
@ -582,7 +590,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<TabsContent value="overview" keepMounted>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<Card className="block p-6">
|
||||
<p>Provider</p>
|
||||
<p>{t("guardrails.info.overview.provider")}</p>
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<Logo src={logo} label={displayName} className="w-6 h-6" />
|
||||
<h3 className="text-lg font-medium">{displayName}</h3>
|
||||
|
|
@ -593,10 +601,12 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<GuardrailStreamScopeCaption raw={guardrailData.litellm_params?.stream_scope} />
|
||||
|
||||
<Card className="block p-6">
|
||||
<p>Created At</p>
|
||||
<p>{t("guardrails.info.overview.createdAt")}</p>
|
||||
<div className="mt-2">
|
||||
<h3 className="text-lg font-medium">{formatDate(guardrailData.created_at)}</h3>
|
||||
<p>Last Updated: {formatDate(guardrailData.updated_at)}</p>
|
||||
<p>
|
||||
{t("guardrails.info.overview.lastUpdated")} {formatDate(guardrailData.updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
@ -605,9 +615,12 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && (
|
||||
<Card className="block mt-6 p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="font-medium">PII Protection</p>
|
||||
<p className="font-medium">{t("guardrails.info.overview.piiProtection")}</p>
|
||||
<Badge variant="secondary">
|
||||
{Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities configured
|
||||
{Object.keys(guardrailData.litellm_params.pii_entities_config).length}{" "}
|
||||
{t("guardrails.info.overview.piiEntitiesConfigured", {
|
||||
count: Object.keys(guardrailData.litellm_params.pii_entities_config).length,
|
||||
})}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -616,11 +629,13 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
{guardrailData.litellm_params?.pii_entities_config &&
|
||||
Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && (
|
||||
<Card className="block mt-6 p-6">
|
||||
<p className="mb-4 text-lg font-semibold">PII Entity Configuration</p>
|
||||
<p className="mb-4 text-lg font-semibold">{t("guardrails.info.overview.piiEntityConfiguration")}</p>
|
||||
<div className="border rounded-lg overflow-hidden shadow-xs">
|
||||
<div className="bg-muted px-5 py-3 border-b flex">
|
||||
<p className="flex-1 font-semibold text-foreground">Entity Type</p>
|
||||
<p className="flex-1 font-semibold text-foreground">Configuration</p>
|
||||
<p className="flex-1 font-semibold text-foreground">{t("guardrails.info.overview.entityType")}</p>
|
||||
<p className="flex-1 font-semibold text-foreground">
|
||||
{t("guardrails.info.overview.configuration")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
{Object.entries(guardrailData.litellm_params?.pii_entities_config).map(([key, value]) => (
|
||||
|
|
@ -655,12 +670,12 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="text-info" />
|
||||
<p className="font-medium text-lg">Custom Code</p>
|
||||
<p className="font-medium text-lg">{t("guardrails.info.overview.customCode")}</p>
|
||||
</div>
|
||||
{isAdmin && !isConfigGuardrail && (
|
||||
<Button variant="outline" size="sm" onClick={() => setCustomCodeModalVisible(true)}>
|
||||
<Code />
|
||||
Edit Code
|
||||
{t("guardrails.info.overview.editCode")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -689,10 +704,14 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<TabsContent value="settings" keepMounted>
|
||||
<Card className="block p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<h3 className="mr-auto text-lg font-medium">Guardrail Settings</h3>
|
||||
<h3 className="mr-auto text-lg font-medium">{t("guardrails.info.settings.heading")}</h3>
|
||||
{isConfigGuardrail && (
|
||||
<SimpleTooltip content="Guardrail is defined in the config file and cannot be edited.">
|
||||
<Info role="img" aria-label="Config guardrail details" className="size-4 text-muted-foreground" />
|
||||
<SimpleTooltip content={t("guardrails.info.settings.configTooltip")}>
|
||||
<Info
|
||||
role="img"
|
||||
aria-label={t("guardrails.info.settings.configDetailsLabel")}
|
||||
className="size-4 text-muted-foreground"
|
||||
/>
|
||||
</SimpleTooltip>
|
||||
)}
|
||||
<GuardrailSettingsActions
|
||||
|
|
@ -713,18 +732,27 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<GuardrailField
|
||||
control={form.control}
|
||||
name="guardrail_name"
|
||||
label="Guardrail Name"
|
||||
rules={requiredRule("Please input a guardrail name")}
|
||||
label={t("guardrails.info.settings.nameLabel")}
|
||||
rules={requiredRule(t("guardrails.info.settings.nameRequired"))}
|
||||
>
|
||||
{({ ref, value, ...field }) => (
|
||||
<Input {...field} ref={ref} value={asText(value)} placeholder="Enter guardrail name" />
|
||||
<Input
|
||||
{...field}
|
||||
ref={ref}
|
||||
value={asText(value)}
|
||||
placeholder={t("guardrails.info.settings.namePlaceholder")}
|
||||
/>
|
||||
)}
|
||||
</GuardrailField>
|
||||
|
||||
<GuardrailField control={form.control} name="default_on" label="Default On">
|
||||
<GuardrailField
|
||||
control={form.control}
|
||||
name="default_on"
|
||||
label={t("guardrails.info.settings.defaultOnLabel")}
|
||||
>
|
||||
{({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": describedBy }) => (
|
||||
<Select
|
||||
items={DEFAULT_ON_ITEMS}
|
||||
items={DEFAULT_ON_ITEMS.map(({ labelKey, value }) => ({ label: t(labelKey), value }))}
|
||||
value={typeof value === "boolean" ? value : null}
|
||||
onValueChange={(next: boolean | null) => onChange(next)}
|
||||
>
|
||||
|
|
@ -734,11 +762,15 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
aria-describedby={describedBy}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
<SelectValue placeholder={t("guardrails.info.settings.defaultOnPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={true}>Yes</SelectItem>
|
||||
<SelectItem value={false}>No</SelectItem>
|
||||
<SelectItem value={true}>
|
||||
{t("guardrails.info.settings.defaultOnOptions.yes")}
|
||||
</SelectItem>
|
||||
<SelectItem value={false}>
|
||||
{t("guardrails.info.settings.defaultOnOptions.no")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
|
@ -753,8 +785,8 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
control={form.control}
|
||||
name="skip_system_message_choice"
|
||||
label={labelWithHint(
|
||||
"Skip system messages in guardrail",
|
||||
"Unified guardrails: omit role: system from guardrail input (LLM still gets full messages). Use global default follows litellm_settings.skip_system_message_in_guardrail.",
|
||||
t("guardrails.info.settings.skipSystemMessagesLabel"),
|
||||
t("guardrails.info.settings.skipSystemMessagesHint"),
|
||||
)}
|
||||
>
|
||||
{(fieldControl) => <SkipMessageSelect control={fieldControl} />}
|
||||
|
|
@ -764,8 +796,8 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
control={form.control}
|
||||
name="skip_tool_message_choice"
|
||||
label={labelWithHint(
|
||||
"Skip tool messages in guardrail",
|
||||
"Unified guardrails: omit role: tool from guardrail input (LLM still gets full messages). Use global default follows litellm_settings.skip_tool_message_in_guardrail.",
|
||||
t("guardrails.info.settings.skipToolMessagesLabel"),
|
||||
t("guardrails.info.settings.skipToolMessagesHint"),
|
||||
)}
|
||||
>
|
||||
{(fieldControl) => <SkipMessageSelect control={fieldControl} />}
|
||||
|
|
@ -777,7 +809,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
/>
|
||||
{guardrailData.litellm_params?.guardrail === "presidio" && (
|
||||
<>
|
||||
<SectionHeading>PII Protection</SectionHeading>
|
||||
<SectionHeading>{t("guardrails.info.settings.sections.piiProtection")}</SectionHeading>
|
||||
<div className="mb-6">
|
||||
{guardrailSettings && (
|
||||
<PiiConfiguration
|
||||
|
|
@ -804,7 +836,9 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
/>
|
||||
|
||||
{(guardrailData.litellm_params?.guardrail === "tool_permission" ||
|
||||
guardrailProviderSpecificParams) && <SectionHeading>Provider Settings</SectionHeading>}
|
||||
guardrailProviderSpecificParams) && (
|
||||
<SectionHeading>{t("guardrails.info.settings.sections.providerSettings")}</SectionHeading>
|
||||
)}
|
||||
|
||||
{guardrailData.litellm_params?.guardrail === "tool_permission" ? (
|
||||
<ToolPermissionRulesEditor value={toolPermissionConfig} onChange={setToolPermissionConfig} />
|
||||
|
|
@ -848,8 +882,12 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
</>
|
||||
)}
|
||||
|
||||
<SectionHeading>Advanced Settings</SectionHeading>
|
||||
<GuardrailField control={form.control} name="guardrail_info" label="Guardrail Information">
|
||||
<SectionHeading>{t("guardrails.info.settings.sections.advancedSettings")}</SectionHeading>
|
||||
<GuardrailField
|
||||
control={form.control}
|
||||
name="guardrail_info"
|
||||
label={t("guardrails.info.settings.guardrailInfoLabel")}
|
||||
>
|
||||
{({ ref, value, ...field }) => (
|
||||
<Textarea {...field} ref={ref} value={asText(value)} rows={5} />
|
||||
)}
|
||||
|
|
@ -865,9 +903,9 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
resetToolPermissionEditor();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t("guardrails.info.settings.cancel")}
|
||||
</Button>
|
||||
<Button type="submit">Save Changes</Button>
|
||||
<Button type="submit">{t("guardrails.info.settings.save")}</Button>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { ParseKeys, TFunction } from "i18next";
|
||||
import i18n from "@/i18n/i18n";
|
||||
import airiaLogo from "../../../../../public/assets/logos/airia.svg";
|
||||
import aimSecurityLogo from "../../../../../public/assets/logos/aim_security.jpeg";
|
||||
import aktoLogo from "../../../../../public/assets/logos/akto.svg";
|
||||
|
|
@ -133,10 +135,10 @@ export const normalizeLoggingOnlyScopeChoice = (
|
|||
directionalScopeSupported: boolean,
|
||||
): LoggingOnlyScopeChoice => (directionalScopeSupported || choice === "default" ? choice : "default");
|
||||
|
||||
const LOGGING_ONLY_SCOPE_OPTIONS: LoggingOnlyScopeOption[] = [
|
||||
{ label: "Default (request and response)", value: "default" },
|
||||
{ label: "Input only (request)", value: "input" },
|
||||
{ label: "Output only (response)", value: "output" },
|
||||
const LOGGING_ONLY_SCOPE_OPTIONS: { labelKey: ParseKeys; value: LoggingOnlyScopeChoice }[] = [
|
||||
{ labelKey: "guardrails.modeOptions.loggingOnlyScope.default", value: "default" },
|
||||
{ labelKey: "guardrails.modeOptions.loggingOnlyScope.input", value: "input" },
|
||||
{ labelKey: "guardrails.modeOptions.loggingOnlyScope.output", value: "output" },
|
||||
];
|
||||
|
||||
export const loggingOnlyScopeToChoice = (v: string | null | undefined): LoggingOnlyScopeChoice =>
|
||||
|
|
@ -185,10 +187,10 @@ export const getLoggingOnlyScopeUpdate = (
|
|||
};
|
||||
};
|
||||
|
||||
export const formatLoggingOnlyScope = (v: string | null | undefined): string => {
|
||||
if (v === "input") return "Input only (request)";
|
||||
if (v === "output") return "Output only (response)";
|
||||
return "Default (request and response)";
|
||||
export const formatLoggingOnlyScope = (v: string | null | undefined, t: TFunction = i18n.t.bind(i18n)): string => {
|
||||
if (v === "input") return t("guardrails.modeOptions.loggingOnlyScope.input");
|
||||
if (v === "output") return t("guardrails.modeOptions.loggingOnlyScope.output");
|
||||
return t("guardrails.modeOptions.loggingOnlyScope.default");
|
||||
};
|
||||
|
||||
export const modeIncludesLoggingOnly = (raw: unknown): boolean => {
|
||||
|
|
@ -203,10 +205,13 @@ export const modeIncludesLoggingOnly = (raw: unknown): boolean => {
|
|||
return toModeArray(fallback).includes("logging_only") || taggedModes;
|
||||
};
|
||||
|
||||
export const getLoggingOnlyScopeOptions = (directionalScopeSupported: boolean): LoggingOnlyScopeOption[] =>
|
||||
directionalScopeSupported
|
||||
? LOGGING_ONLY_SCOPE_OPTIONS
|
||||
: LOGGING_ONLY_SCOPE_OPTIONS.filter((option) => option.value === "default");
|
||||
export const getLoggingOnlyScopeOptions = (
|
||||
directionalScopeSupported: boolean,
|
||||
t: TFunction = i18n.t.bind(i18n),
|
||||
): LoggingOnlyScopeOption[] =>
|
||||
LOGGING_ONLY_SCOPE_OPTIONS.filter((option) => directionalScopeSupported || option.value === "default").map(
|
||||
({ labelKey, value }) => ({ label: t(labelKey), value }),
|
||||
);
|
||||
|
||||
export const supportsDirectionalLoggingOnlyScope = (
|
||||
settings: { providers_without_directional_logging_only_scope?: string[] } | null,
|
||||
|
|
@ -392,10 +397,10 @@ export function choiceToSkipToolForCreate(choice: SkipToolMessageChoice | undefi
|
|||
export const GUARDRAIL_STREAM_SCOPES = ["both", "streaming", "non_streaming"] as const;
|
||||
export type GuardrailStreamScope = (typeof GUARDRAIL_STREAM_SCOPES)[number];
|
||||
|
||||
export const STREAM_SCOPE_OPTIONS: { value: GuardrailStreamScope; label: string }[] = [
|
||||
{ value: "both", label: "Streaming and non-streaming" },
|
||||
{ value: "streaming", label: "Streaming only" },
|
||||
{ value: "non_streaming", label: "Non-streaming only" },
|
||||
export const STREAM_SCOPE_OPTIONS: { value: GuardrailStreamScope; labelKey: ParseKeys }[] = [
|
||||
{ value: "both", labelKey: "guardrails.modeOptions.streamScope.both" },
|
||||
{ value: "streaming", labelKey: "guardrails.modeOptions.streamScope.streaming" },
|
||||
{ value: "non_streaming", labelKey: "guardrails.modeOptions.streamScope.nonStreaming" },
|
||||
];
|
||||
|
||||
export const isGuardrailStreamScope = (value: unknown): value is GuardrailStreamScope =>
|
||||
|
|
@ -427,16 +432,17 @@ export const streamScopePayload = (
|
|||
return Object.fromEntries(Object.entries(perMode).filter((entry) => entry[1] !== "both"));
|
||||
};
|
||||
|
||||
export const formatGuardrailStreamScope = (raw: unknown): string => {
|
||||
export const formatGuardrailStreamScope = (raw: unknown, t: TFunction = i18n.t.bind(i18n)): string => {
|
||||
if (isGuardrailStreamScope(raw)) {
|
||||
return STREAM_SCOPE_OPTIONS.find((option) => option.value === raw)?.label ?? raw;
|
||||
const option = STREAM_SCOPE_OPTIONS.find((candidate) => candidate.value === raw);
|
||||
return option ? t(option.labelKey) : raw;
|
||||
}
|
||||
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
||||
const entries = Object.entries(raw as Record<string, unknown>).filter(
|
||||
(entry): entry is [string, GuardrailStreamScope] => isGuardrailStreamScope(entry[1]),
|
||||
);
|
||||
if (entries.length === 0) return "";
|
||||
return entries.map(([mode, scope]) => `${mode}: ${formatGuardrailStreamScope(scope)}`).join(", ");
|
||||
return entries.map(([mode, scope]) => `${mode}: ${formatGuardrailStreamScope(scope, t)}`).join(", ");
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import type { ParseKeys } from "i18next";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MultiSelect } from "@/components/shared/MultiSelect";
|
||||
import { PasswordInput } from "@/components/shared/PasswordInput";
|
||||
import NumericalInput from "@/components/shared/numerical_input";
|
||||
|
|
@ -41,9 +43,9 @@ interface DictFieldProps {
|
|||
value: unknown;
|
||||
}
|
||||
|
||||
const BOOLEAN_ITEMS = [
|
||||
{ label: "True", value: true },
|
||||
{ label: "False", value: false },
|
||||
const BOOLEAN_ITEMS: { labelKey: ParseKeys; value: boolean }[] = [
|
||||
{ labelKey: "guardrails.optionalParams.boolean.true", value: true },
|
||||
{ labelKey: "guardrails.optionalParams.boolean.false", value: false },
|
||||
];
|
||||
|
||||
const isSecretKey = (fieldKey: string): boolean =>
|
||||
|
|
@ -59,11 +61,13 @@ const BooleanSelect: React.FC<{ control: GuardrailFieldControlProps; placeholder
|
|||
control,
|
||||
placeholder,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy } = control;
|
||||
const items = BOOLEAN_ITEMS.map(({ labelKey, value: itemValue }) => ({ label: t(labelKey), value: itemValue }));
|
||||
|
||||
return (
|
||||
<Select
|
||||
items={BOOLEAN_ITEMS}
|
||||
items={items}
|
||||
value={typeof value === "boolean" ? value : null}
|
||||
onValueChange={(next: boolean | null) => onChange(next)}
|
||||
>
|
||||
|
|
@ -71,14 +75,15 @@ const BooleanSelect: React.FC<{ control: GuardrailFieldControlProps; placeholder
|
|||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={true}>True</SelectItem>
|
||||
<SelectItem value={false}>False</SelectItem>
|
||||
<SelectItem value={true}>{t("guardrails.optionalParams.boolean.true")}</SelectItem>
|
||||
<SelectItem value={false}>{t("guardrails.optionalParams.boolean.false")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
const DictField: React.FC<DictFieldProps> = ({ field, fullFieldKey, control, value }) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedEntries, setSelectedEntries] = React.useState<Array<{ key: string; id: string }>>([]);
|
||||
const [availableKeys, setAvailableKeys] = React.useState<string[]>(field.dict_key_options || []);
|
||||
|
||||
|
|
@ -133,7 +138,7 @@ const DictField: React.FC<DictFieldProps> = ({ field, fullFieldKey, control, val
|
|||
id={fieldControl.id}
|
||||
name={fieldControl.name}
|
||||
step={1}
|
||||
placeholder={`Enter ${entry.key} value`}
|
||||
placeholder={t("guardrails.optionalParams.dict.enterValue", { name: entry.key })}
|
||||
value={asText(fieldControl.value)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
fieldControl.onChange(toNumberValue(event.target.value))
|
||||
|
|
@ -146,7 +151,12 @@ const DictField: React.FC<DictFieldProps> = ({ field, fullFieldKey, control, val
|
|||
}
|
||||
|
||||
if (field.dict_value_type === "boolean") {
|
||||
return <BooleanSelect control={fieldControl} placeholder={`Select ${entry.key} value`} />;
|
||||
return (
|
||||
<BooleanSelect
|
||||
control={fieldControl}
|
||||
placeholder={t("guardrails.optionalParams.dict.selectValue", { name: entry.key })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -154,7 +164,7 @@ const DictField: React.FC<DictFieldProps> = ({ field, fullFieldKey, control, val
|
|||
id={fieldControl.id}
|
||||
name={fieldControl.name}
|
||||
ref={fieldControl.ref}
|
||||
placeholder={`Enter ${entry.key} value`}
|
||||
placeholder={t("guardrails.optionalParams.dict.enterValue", { name: entry.key })}
|
||||
value={asText(fieldControl.value)}
|
||||
onChange={fieldControl.onChange}
|
||||
onBlur={fieldControl.onBlur}
|
||||
|
|
@ -170,7 +180,7 @@ const DictField: React.FC<DictFieldProps> = ({ field, fullFieldKey, control, val
|
|||
className="text-destructive hover:text-destructive/80"
|
||||
onClick={() => removeEntry(entry.id, entry.key)}
|
||||
>
|
||||
Remove
|
||||
{t("guardrails.optionalParams.dict.remove")}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -184,7 +194,7 @@ const DictField: React.FC<DictFieldProps> = ({ field, fullFieldKey, control, val
|
|||
onValueChange={(next: string | null) => next && addEntry(next)}
|
||||
>
|
||||
<SelectTrigger className="w-50">
|
||||
<SelectValue placeholder="Select category to configure" />
|
||||
<SelectValue placeholder={t("guardrails.optionalParams.dict.keyPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableKeys.map((key) => (
|
||||
|
|
@ -194,7 +204,7 @@ const DictField: React.FC<DictFieldProps> = ({ field, fullFieldKey, control, val
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-sm text-muted-foreground">Select a category to add threshold configuration</span>
|
||||
<span className="text-sm text-muted-foreground">{t("guardrails.optionalParams.dict.addHint")}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -297,6 +307,7 @@ const GuardrailOptionalParams: React.FC<GuardrailOptionalParamsProps> = ({
|
|||
control,
|
||||
values,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const renderField = (fieldKey: string, field: ProviderParam) => {
|
||||
const fullFieldKey = `${parentFieldKey}.${fieldKey}`;
|
||||
const value = values?.[fieldKey];
|
||||
|
|
@ -318,7 +329,11 @@ const GuardrailOptionalParams: React.FC<GuardrailOptionalParamsProps> = ({
|
|||
name={fullFieldKey}
|
||||
label={<span className="text-base">{fieldKey}</span>}
|
||||
description={field.description}
|
||||
rules={field.required ? requiredRule(`${fieldKey} is required`) : undefined}
|
||||
rules={
|
||||
field.required
|
||||
? requiredRule(t("guardrails.optionalParams.errors.required", { field: fieldKey }))
|
||||
: undefined
|
||||
}
|
||||
defaultValue={value !== undefined ? value : field.default_value}
|
||||
>
|
||||
{(fieldControl) => <OptionalParamInput descriptor={field} fieldKey={fieldKey} control={fieldControl} />}
|
||||
|
|
@ -334,9 +349,9 @@ const GuardrailOptionalParams: React.FC<GuardrailOptionalParamsProps> = ({
|
|||
return (
|
||||
<div className="guardrail-optional-params">
|
||||
<div className="mb-8 border-b border-border pb-4">
|
||||
<h3 className="mb-2 text-lg font-semibold text-foreground">Optional Parameters</h3>
|
||||
<h3 className="mb-2 text-lg font-semibold text-foreground">{t("guardrails.optionalParams.title")}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{optionalParams.description || "Configure additional settings for this guardrail provider"}
|
||||
{optionalParams.description || t("guardrails.optionalParams.defaultDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import type { ParseKeys, TFunction } from "i18next";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
guardrail_provider_map,
|
||||
populateGuardrailProviders,
|
||||
|
|
@ -55,9 +57,9 @@ interface ProviderParamsResponse {
|
|||
[provider: string]: { [key: string]: ProviderParam };
|
||||
}
|
||||
|
||||
const BOOLEAN_ITEMS = [
|
||||
{ label: "True", value: true },
|
||||
{ label: "False", value: false },
|
||||
const BOOLEAN_ITEMS: { labelKey: ParseKeys; value: boolean }[] = [
|
||||
{ labelKey: "guardrails.providerFields.boolean.true", value: true },
|
||||
{ labelKey: "guardrails.providerFields.boolean.false", value: false },
|
||||
];
|
||||
|
||||
const isSecretKey = (fieldKey: string): boolean =>
|
||||
|
|
@ -68,14 +70,16 @@ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|||
|
||||
// Object fields hold the raw text while the user types, so submission must be
|
||||
// blocked until the value parses to a plain JSON object (or is cleared).
|
||||
const jsonObjectRule = (fieldKey: string): GuardrailFieldRules => ({
|
||||
const jsonObjectRule = (fieldKey: string, t: TFunction): GuardrailFieldRules => ({
|
||||
validate: (value: unknown) =>
|
||||
value === undefined || isPlainObject(value) ? true : `${fieldKey} must be a valid JSON object`,
|
||||
value === undefined || isPlainObject(value)
|
||||
? true
|
||||
: t("guardrails.providerFields.errors.invalidJsonObject", { field: fieldKey }),
|
||||
});
|
||||
|
||||
// Commits a parsed object (or undefined for a cleared field) to the form on
|
||||
// blur; anything else stays as raw text so jsonObjectRule blocks submission.
|
||||
const commitObjectField = (raw: string, onChange: (value: unknown) => void): void => {
|
||||
const commitObjectField = (raw: string, onChange: (value: unknown) => void, t: TFunction): void => {
|
||||
const next = raw.trim();
|
||||
if (next === "") {
|
||||
onChange(undefined);
|
||||
|
|
@ -90,15 +94,15 @@ const commitObjectField = (raw: string, onChange: (value: unknown) => void): voi
|
|||
if (isPlainObject(parsed)) {
|
||||
onChange(parsed);
|
||||
} else {
|
||||
toast.error("Enter a valid JSON object for this configuration");
|
||||
toast.error(t("guardrails.providerFields.toasts.invalidJsonObject"));
|
||||
}
|
||||
};
|
||||
|
||||
const fieldRules = (field: ProviderParam, fieldKey: string): GuardrailFieldRules | undefined => {
|
||||
const fieldRules = (field: ProviderParam, fieldKey: string, t: TFunction): GuardrailFieldRules | undefined => {
|
||||
if (field.type === "object") {
|
||||
return jsonObjectRule(fieldKey);
|
||||
return jsonObjectRule(fieldKey, t);
|
||||
}
|
||||
return field.required ? requiredRule(`${fieldKey} is required`) : undefined;
|
||||
return field.required ? requiredRule(t("guardrails.providerFields.errors.required", { field: fieldKey })) : undefined;
|
||||
};
|
||||
|
||||
interface ProviderFieldInputProps {
|
||||
|
|
@ -108,6 +112,7 @@ interface ProviderFieldInputProps {
|
|||
}
|
||||
|
||||
const ProviderFieldInput: React.FC<ProviderFieldInputProps> = ({ descriptor, fieldKey, control }) => {
|
||||
const { t } = useTranslation();
|
||||
const { id, value, onChange, onBlur, ref, name, ...aria } = control;
|
||||
|
||||
if (descriptor.type === "select" && descriptor.options) {
|
||||
|
|
@ -146,7 +151,7 @@ const ProviderFieldInput: React.FC<ProviderFieldInputProps> = ({ descriptor, fie
|
|||
if (descriptor.type === "bool" || descriptor.type === "boolean") {
|
||||
return (
|
||||
<Select
|
||||
items={BOOLEAN_ITEMS}
|
||||
items={BOOLEAN_ITEMS.map(({ labelKey, value: itemValue }) => ({ label: t(labelKey), value: itemValue }))}
|
||||
value={typeof value === "boolean" ? value : null}
|
||||
onValueChange={(next: boolean | null) => onChange(next)}
|
||||
>
|
||||
|
|
@ -154,8 +159,8 @@ const ProviderFieldInput: React.FC<ProviderFieldInputProps> = ({ descriptor, fie
|
|||
<SelectValue placeholder={descriptor.description} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={true}>True</SelectItem>
|
||||
<SelectItem value={false}>False</SelectItem>
|
||||
<SelectItem value={true}>{t("guardrails.providerFields.boolean.true")}</SelectItem>
|
||||
<SelectItem value={false}>{t("guardrails.providerFields.boolean.false")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
|
@ -193,7 +198,7 @@ const ProviderFieldInput: React.FC<ProviderFieldInputProps> = ({ descriptor, fie
|
|||
value={objectValue}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
onBlur={(event) => {
|
||||
commitObjectField(event.target.value, onChange);
|
||||
commitObjectField(event.target.value, onChange, t);
|
||||
onBlur();
|
||||
}}
|
||||
{...aria}
|
||||
|
|
@ -252,9 +257,10 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
|||
providerParams: providerParamsProp = null,
|
||||
value = null,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [providerParams, setProviderParams] = useState<ProviderParamsResponse | null>(providerParamsProp);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loadFailed, setLoadFailed] = useState(false);
|
||||
|
||||
// Fetch provider-specific parameters when component mounts
|
||||
useEffect(() => {
|
||||
|
|
@ -268,7 +274,7 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
|||
if (!accessToken) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setLoadFailed(false);
|
||||
|
||||
try {
|
||||
const data = await getGuardrailProviderSpecificParams(accessToken);
|
||||
|
|
@ -279,7 +285,7 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
|||
populateGuardrailProviderMap(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching provider params:", error);
|
||||
setError("Failed to load provider parameters");
|
||||
setLoadFailed(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -301,14 +307,14 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
|||
return (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<UiLoadingSpinner className="size-4" />
|
||||
Loading provider parameters...
|
||||
{t("guardrails.providerFields.loading")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (error) {
|
||||
return <div className="text-destructive">{error}</div>;
|
||||
if (loadFailed) {
|
||||
return <div className="text-destructive">{t("guardrails.providerFields.loadFailed")}</div>;
|
||||
}
|
||||
|
||||
// Get the provider key matching the selected provider in the guardrail_provider_map
|
||||
|
|
@ -318,7 +324,7 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
|||
const providerFields = providerParams && providerParams[providerKey];
|
||||
|
||||
if (!providerFields || Object.keys(providerFields).length === 0) {
|
||||
return <div>No configuration fields available for this provider.</div>;
|
||||
return <div>{t("guardrails.providerFields.noFields")}</div>;
|
||||
}
|
||||
|
||||
// Fields to skip for content filter provider (handled in dedicated steps)
|
||||
|
|
@ -376,7 +382,7 @@ const GuardrailProviderFields: React.FC<GuardrailProviderFieldsProps> = ({
|
|||
control={control}
|
||||
name={fullFieldKey}
|
||||
label={labelWithHint(fieldKey, field.description)}
|
||||
rules={fieldRules(field, fieldKey)}
|
||||
rules={fieldRules(field, fieldKey, t)}
|
||||
defaultValue={resolvedInitialValue}
|
||||
>
|
||||
{(fieldControl) => <ProviderFieldInput descriptor={field} fieldKey={fieldKey} control={fieldControl} />}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { SortingState } from "@tanstack/react-table";
|
||||
import { Inbox } from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { DataTable } from "@/components/shared/DataTable";
|
||||
import { Guardrail } from "@/components/guardrails/types";
|
||||
|
|
@ -19,13 +20,14 @@ interface GuardrailTableProps {
|
|||
const DEFAULT_SORTING: SortingState = [{ id: "created_at", desc: true }];
|
||||
|
||||
function EmptyState() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-1 py-6">
|
||||
<div className="mb-1 flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<Inbox className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="text-sm font-medium text-foreground">No guardrails yet</div>
|
||||
<div className="text-sm text-muted-foreground">Add a guardrail to start filtering requests and responses.</div>
|
||||
<div className="text-sm font-medium text-foreground">{t("guardrails.table.empty.title")}</div>
|
||||
<div className="text-sm text-muted-foreground">{t("guardrails.table.empty.description")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -36,11 +38,12 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
|
|||
onDeleteClick,
|
||||
onGuardrailClick,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [sorting, setSorting] = useState<SortingState>(DEFAULT_SORTING);
|
||||
|
||||
const columns = useMemo(
|
||||
() => getGuardrailTableColumns({ onGuardrailClick, onDeleteClick }),
|
||||
[onGuardrailClick, onDeleteClick],
|
||||
() => getGuardrailTableColumns({ onGuardrailClick, onDeleteClick, t }),
|
||||
[onGuardrailClick, onDeleteClick, t],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -53,7 +56,7 @@ const GuardrailTable: React.FC<GuardrailTableProps> = ({
|
|||
sorting={sorting}
|
||||
onSortingChange={setSorting}
|
||||
isLoading={isLoading}
|
||||
loadingMessage="Loading guardrails…"
|
||||
loadingMessage={t("guardrails.table.loading")}
|
||||
noDataMessage={<EmptyState />}
|
||||
size="compact"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import type { ParseKeys } from "i18next";
|
||||
import { Plus, X } from "lucide-react";
|
||||
import React from "react";
|
||||
import { useController } from "react-hook-form";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -33,9 +35,9 @@ interface LLMJudgeFieldsProps {
|
|||
|
||||
const DEFAULT_CRITERIA: GuardrailCriterion[] = [{ name: "", weight: 100, description: "" }];
|
||||
|
||||
const ON_FAILURE_ITEMS = [
|
||||
{ label: "Block (return 422)", value: "block" },
|
||||
{ label: "Log only", value: "log" },
|
||||
const ON_FAILURE_ITEMS: { labelKey: ParseKeys; value: string }[] = [
|
||||
{ labelKey: "guardrails.llmJudge.onFailure.options.block", value: "block" },
|
||||
{ labelKey: "guardrails.llmJudge.onFailure.options.log", value: "log" },
|
||||
];
|
||||
|
||||
const clampToRange = (value: unknown, min: number, max: number): number | null => {
|
||||
|
|
@ -77,28 +79,26 @@ const BoundedNumberInput: React.FC<BoundedNumberInputProps> = ({ control, min, m
|
|||
};
|
||||
|
||||
const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, control }) => {
|
||||
const { t } = useTranslation();
|
||||
const { field } = useController({ control, name: "criteria", defaultValue: DEFAULT_CRITERIA });
|
||||
const criteria: GuardrailCriterion[] = Array.isArray(field.value) ? field.value : [];
|
||||
const setCriteria = field.onChange;
|
||||
|
||||
const weightTotal = criteria.reduce((sum, entry) => sum + (Number(entry?.weight) || 0), 0);
|
||||
const weightOk = weightTotal === 100;
|
||||
const onFailureItems = ON_FAILURE_ITEMS.map(({ labelKey, value }) => ({ label: t(labelKey), value }));
|
||||
|
||||
return (
|
||||
<FieldGroup>
|
||||
<div className="rounded-md border border-success/20 bg-success/10 px-3.5 py-2.5 text-[13px] text-success">
|
||||
The <strong>Judge Model</strong> scores the user request (pre_call, during_call) or the LLM response (post_call)
|
||||
0–100 against your criteria. If the weighted average falls below the threshold, it is blocked (or logged).
|
||||
<Trans i18nKey="guardrails.llmJudge.intro" components={{ strong: <strong /> }} />
|
||||
</div>
|
||||
|
||||
<GuardrailField
|
||||
control={control}
|
||||
name="judge_model"
|
||||
label={labelWithHint(
|
||||
"Judge Model",
|
||||
"The LLM that reads each response and grades it. Pick a capable model — it never sees end-user data beyond what the LLM returned.",
|
||||
)}
|
||||
rules={requiredRule("Select a judge model")}
|
||||
label={labelWithHint(t("guardrails.llmJudge.judgeModel.label"), t("guardrails.llmJudge.judgeModel.hint"))}
|
||||
rules={requiredRule(t("guardrails.llmJudge.judgeModel.required"))}
|
||||
>
|
||||
{({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => (
|
||||
<Combobox items={availableModels} value={asText(value) || null} onValueChange={onChange}>
|
||||
|
|
@ -106,11 +106,11 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, contro
|
|||
id={id}
|
||||
aria-invalid={ariaInvalid}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
placeholder="Select a model"
|
||||
placeholder={t("guardrails.llmJudge.judgeModel.placeholder")}
|
||||
className="w-full"
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No matching models</ComboboxEmpty>
|
||||
<ComboboxEmpty>{t("guardrails.llmJudge.judgeModel.empty")}</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(model: string) => (
|
||||
<ComboboxItem key={model} value={model} title={model}>
|
||||
|
|
@ -126,10 +126,7 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, contro
|
|||
<GuardrailField
|
||||
control={control}
|
||||
name="overall_threshold"
|
||||
label={labelWithHint(
|
||||
"Minimum Score to Pass",
|
||||
"0–100. If the weighted average of criterion scores falls below this, the guardrail triggers. 80 is a good default.",
|
||||
)}
|
||||
label={labelWithHint(t("guardrails.llmJudge.threshold.label"), t("guardrails.llmJudge.threshold.hint"))}
|
||||
defaultValue={80}
|
||||
>
|
||||
{(fieldControl) => <BoundedNumberInput control={fieldControl} min={0} max={100} suffix="/ 100" />}
|
||||
|
|
@ -138,19 +135,16 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, contro
|
|||
<GuardrailField
|
||||
control={control}
|
||||
name="on_failure"
|
||||
label={labelWithHint(
|
||||
"On Failure",
|
||||
"Block: return HTTP 422 when the score is too low. Log: record the result but let the response through.",
|
||||
)}
|
||||
label={labelWithHint(t("guardrails.llmJudge.onFailure.label"), t("guardrails.llmJudge.onFailure.hint"))}
|
||||
defaultValue="block"
|
||||
>
|
||||
{({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy }) => (
|
||||
<Select items={ON_FAILURE_ITEMS} value={asText(value) || null} onValueChange={onChange}>
|
||||
<Select items={onFailureItems} value={asText(value) || null} onValueChange={onChange}>
|
||||
<SelectTrigger id={id} aria-invalid={ariaInvalid} aria-describedby={ariaDescribedBy} className="w-full">
|
||||
<SelectValue placeholder="Select an action" />
|
||||
<SelectValue placeholder={t("guardrails.llmJudge.onFailure.placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ON_FAILURE_ITEMS.map((item) => (
|
||||
{onFailureItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -162,10 +156,7 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, contro
|
|||
|
||||
<Field>
|
||||
<FieldLabel>
|
||||
{labelWithHint(
|
||||
"Evaluation Criteria",
|
||||
"Each criterion is something the judge checks. Weights must add up to 100%.",
|
||||
)}
|
||||
{labelWithHint(t("guardrails.llmJudge.criteria.label"), t("guardrails.llmJudge.criteria.hint"))}
|
||||
</FieldLabel>
|
||||
|
||||
{criteria.map((_, index) => (
|
||||
|
|
@ -174,7 +165,7 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, contro
|
|||
<GuardrailField
|
||||
control={control}
|
||||
name={`criteria.${index}.name`}
|
||||
rules={requiredRule("Enter criterion name")}
|
||||
rules={requiredRule(t("guardrails.llmJudge.criteria.nameRequired"))}
|
||||
className="flex-2"
|
||||
>
|
||||
{({ ref, value, ...field }) => (
|
||||
|
|
@ -182,7 +173,7 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, contro
|
|||
{...field}
|
||||
ref={ref}
|
||||
value={asText(value)}
|
||||
placeholder="Criterion name (e.g. Policy accuracy)"
|
||||
placeholder={t("guardrails.llmJudge.criteria.namePlaceholder")}
|
||||
/>
|
||||
)}
|
||||
</GuardrailField>
|
||||
|
|
@ -190,20 +181,28 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, contro
|
|||
control={control}
|
||||
name={`criteria.${index}.weight`}
|
||||
label={labelWithHint(
|
||||
<span className="text-xs text-muted-foreground">Weight</span>,
|
||||
"How much this criterion counts toward the final score. All weights must add up to 100%.",
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("guardrails.llmJudge.criteria.weightLabel")}
|
||||
</span>,
|
||||
t("guardrails.llmJudge.criteria.weightHint"),
|
||||
)}
|
||||
rules={requiredRule("Enter weight")}
|
||||
rules={requiredRule(t("guardrails.llmJudge.criteria.weightRequired"))}
|
||||
className="flex-1"
|
||||
>
|
||||
{(fieldControl) => (
|
||||
<BoundedNumberInput control={fieldControl} min={0} max={100} suffix="%" placeholder="e.g. 50" />
|
||||
<BoundedNumberInput
|
||||
control={fieldControl}
|
||||
min={0}
|
||||
max={100}
|
||||
suffix="%"
|
||||
placeholder={t("guardrails.llmJudge.criteria.weightPlaceholder")}
|
||||
/>
|
||||
)}
|
||||
</GuardrailField>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label="Remove criterion"
|
||||
aria-label={t("guardrails.llmJudge.criteria.remove")}
|
||||
className="mb-1 text-destructive hover:text-destructive/80"
|
||||
onClick={() => setCriteria(criteria.filter((_, position) => position !== index))}
|
||||
>
|
||||
|
|
@ -213,7 +212,7 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, contro
|
|||
<GuardrailField
|
||||
control={control}
|
||||
name={`criteria.${index}.description`}
|
||||
rules={requiredRule("Describe what to check")}
|
||||
rules={requiredRule(t("guardrails.llmJudge.criteria.descriptionRequired"))}
|
||||
className="mt-2"
|
||||
>
|
||||
{({ ref, value, ...field }) => (
|
||||
|
|
@ -221,7 +220,7 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, contro
|
|||
{...field}
|
||||
ref={ref}
|
||||
value={asText(value)}
|
||||
placeholder="What should the judge check for this criterion?"
|
||||
placeholder={t("guardrails.llmJudge.criteria.descriptionPlaceholder")}
|
||||
/>
|
||||
)}
|
||||
</GuardrailField>
|
||||
|
|
@ -234,12 +233,14 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, contro
|
|||
onClick={() => setCriteria([...criteria, { name: "", weight: 0, description: "" }])}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
Add Criterion
|
||||
{t("guardrails.llmJudge.criteria.add")}
|
||||
</Button>
|
||||
|
||||
{criteria.length > 0 && (
|
||||
<div className={`mt-1.5 text-xs ${weightOk ? "text-success" : "text-warning"}`}>
|
||||
Weights total: {weightTotal}%{weightOk ? " ✓" : " — must add up to 100%"}
|
||||
{weightOk
|
||||
? t("guardrails.llmJudge.criteria.weightsTotalOk", { total: weightTotal })
|
||||
: t("guardrails.llmJudge.criteria.weightsTotalOff", { total: weightTotal })}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { EyeOff, Filter, Info, Ban, X } from "lucide-react";
|
||||
import { PiiEntityCategory } from "@/components/guardrails/types";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
|
@ -42,6 +43,7 @@ export interface CategoryFilterProps {
|
|||
}
|
||||
|
||||
export const CategoryFilter: React.FC<CategoryFilterProps> = ({ categories, selectedCategories, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const anchor = useComboboxAnchor();
|
||||
const categoryNames = categories.map((cat) => cat.category);
|
||||
|
||||
|
|
@ -49,7 +51,7 @@ export const CategoryFilter: React.FC<CategoryFilterProps> = ({ categories, sele
|
|||
<div>
|
||||
<div className="mb-2 flex items-center">
|
||||
<Filter className="mr-1 size-4 text-muted-foreground" />
|
||||
<span className="font-medium text-muted-foreground">Filter by category</span>
|
||||
<span className="font-medium text-muted-foreground">{t("guardrails.pii.categoryFilter.title")}</span>
|
||||
</div>
|
||||
<Combobox items={categoryNames} value={selectedCategories} onValueChange={onChange} multiple>
|
||||
<ComboboxChips render={<div ref={anchor} />} className="mb-4 w-full">
|
||||
|
|
@ -59,11 +61,11 @@ export const CategoryFilter: React.FC<CategoryFilterProps> = ({ categories, sele
|
|||
</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput
|
||||
placeholder={selectedCategories.length === 0 ? "Select categories to filter by" : undefined}
|
||||
placeholder={selectedCategories.length === 0 ? t("guardrails.pii.categoryFilter.placeholder") : undefined}
|
||||
/>
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
<ComboboxEmpty>No matching categories</ComboboxEmpty>
|
||||
<ComboboxEmpty>{t("guardrails.pii.categoryFilter.empty")}</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(category: string) => (
|
||||
<ComboboxItem key={category} value={category}>
|
||||
|
|
@ -85,11 +87,12 @@ export interface QuickActionsProps {
|
|||
}
|
||||
|
||||
export const QuickActions: React.FC<QuickActionsProps> = ({ onSelectAll, onUnselectAll, hasSelectedEntities }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-border bg-muted/40 p-5 shadow-xs">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="text-base font-semibold">Quick Actions</span>
|
||||
<span className="text-base font-semibold">{t("guardrails.pii.quickActions.title")}</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
|
|
@ -98,22 +101,22 @@ export const QuickActions: React.FC<QuickActionsProps> = ({ onSelectAll, onUnsel
|
|||
</span>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Apply action to all PII types at once</TooltipContent>
|
||||
<TooltipContent>{t("guardrails.pii.quickActions.hint")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button variant="outline" onClick={onUnselectAll} disabled={!hasSelectedEntities}>
|
||||
<X />
|
||||
Unselect All
|
||||
{t("guardrails.pii.quickActions.unselectAll")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Button variant="outline" className="h-10 w-full" onClick={() => onSelectAll("MASK")}>
|
||||
<EyeOff />
|
||||
Select All & Mask
|
||||
{t("guardrails.pii.quickActions.selectAllMask")}
|
||||
</Button>
|
||||
<Button variant="outline" className="h-10 w-full" onClick={() => onSelectAll("BLOCK")}>
|
||||
<Ban />
|
||||
Select All & Block
|
||||
{t("guardrails.pii.quickActions.selectAllBlock")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -140,15 +143,16 @@ export const PiiEntityList: React.FC<PiiEntityListProps> = ({
|
|||
onActionSelect,
|
||||
entityToCategoryMap,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-border shadow-xs">
|
||||
<div className="flex border-b border-border bg-muted/40 px-5 py-3">
|
||||
<span className="flex-1 font-semibold">PII Type</span>
|
||||
<span className="w-32 text-right font-semibold">Action</span>
|
||||
<span className="flex-1 font-semibold">{t("guardrails.pii.entityList.typeColumn")}</span>
|
||||
<span className="w-32 text-right font-semibold">{t("guardrails.pii.entityList.actionColumn")}</span>
|
||||
</div>
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
{entities.length === 0 ? (
|
||||
<div className="py-10 text-center text-muted-foreground">No PII types match your filter criteria</div>
|
||||
<div className="py-10 text-center text-muted-foreground">{t("guardrails.pii.entityList.empty")}</div>
|
||||
) : (
|
||||
entities.map((entity) => {
|
||||
const isSelected = selectedEntities.includes(entity);
|
||||
|
|
@ -176,7 +180,10 @@ export const PiiEntityList: React.FC<PiiEntityListProps> = ({
|
|||
onValueChange={(value: string | null) => value && onActionSelect(entity, value)}
|
||||
disabled={!isSelected}
|
||||
>
|
||||
<SelectTrigger className={`w-[120px] ${isSelected ? "" : "opacity-50"}`} aria-label="Action">
|
||||
<SelectTrigger
|
||||
className={`w-[120px] ${isSelected ? "" : "opacity-50"}`}
|
||||
aria-label={t("guardrails.pii.entityList.actionSelect")}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CategoryFilter, PiiEntityList, QuickActions } from "./pii_components";
|
||||
import { PiiConfigurationProps } from "@/components/guardrails/types";
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ const PiiConfiguration: React.FC<PiiConfigurationProps> = ({
|
|||
onActionSelect,
|
||||
entityCategories = [],
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
|
||||
// Create a lookup map to quickly find an entity's category
|
||||
|
|
@ -54,9 +56,11 @@ const PiiConfiguration: React.FC<PiiConfigurationProps> = ({
|
|||
<div className="pii-configuration">
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<div className="flex items-center">
|
||||
<h4 className="m-0 text-lg font-semibold text-foreground">Configure PII Protection</h4>
|
||||
<h4 className="m-0 text-lg font-semibold text-foreground">{t("guardrails.pii.configuration.title")}</h4>
|
||||
</div>
|
||||
<span className="text-muted-foreground">{selectedEntities.length} items selected</span>
|
||||
<span className="text-muted-foreground">
|
||||
{t("guardrails.pii.configuration.selectedCount", { count: selectedEntities.length })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import type { ParseKeys } from "i18next";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Info, Plus, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
|
@ -33,15 +35,17 @@ interface ToolPermissionRulesEditorProps {
|
|||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const DECISION_ITEMS = [
|
||||
{ value: "allow", label: "Allow" },
|
||||
{ value: "deny", label: "Deny" },
|
||||
] as const;
|
||||
const DECISION_ITEMS: readonly { value: ToolPermissionDecision; labelKey: ParseKeys }[] = [
|
||||
{ value: "allow", labelKey: "guardrails.toolPermission.decisionOptions.allow" },
|
||||
{ value: "deny", labelKey: "guardrails.toolPermission.decisionOptions.deny" },
|
||||
];
|
||||
|
||||
const ON_DISALLOWED_ITEMS = [
|
||||
{ value: "block", label: "Block" },
|
||||
{ value: "rewrite", label: "Rewrite" },
|
||||
] as const;
|
||||
const ON_DISALLOWED_ITEMS: readonly { value: ToolPermissionOnDisallowedAction; labelKey: ParseKeys }[] = [
|
||||
{ value: "block", labelKey: "guardrails.toolPermission.onDisallowedOptions.block" },
|
||||
{ value: "rewrite", labelKey: "guardrails.toolPermission.onDisallowedOptions.rewrite" },
|
||||
];
|
||||
|
||||
const TOOL_NAME_EXAMPLE = "^mcp__github_.*$";
|
||||
|
||||
const DEFAULT_CONFIG: ToolPermissionConfig = {
|
||||
rules: [],
|
||||
|
|
@ -57,7 +61,10 @@ const ensureConfig = (config?: ToolPermissionConfig): ToolPermissionConfig => ({
|
|||
});
|
||||
|
||||
const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ value, onChange, disabled = false }) => {
|
||||
const { t } = useTranslation();
|
||||
const config = ensureConfig(value);
|
||||
const decisionItems = DECISION_ITEMS.map((item) => ({ value: item.value, label: t(item.labelKey) }));
|
||||
const onDisallowedItems = ON_DISALLOWED_ITEMS.map((item) => ({ value: item.value, label: t(item.labelKey) }));
|
||||
|
||||
const updateConfig = (partial: Partial<ToolPermissionConfig>) => {
|
||||
const nextConfig: ToolPermissionConfig = {
|
||||
|
|
@ -135,14 +142,14 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
|
|||
size="sm"
|
||||
onClick={() => updateRule(index, { allowed_param_patterns: { "": "" } })}
|
||||
>
|
||||
+ Restrict tool arguments (optional)
|
||||
{t("guardrails.toolPermission.constraints.restrict")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">Argument constraints (dot or array paths)</p>
|
||||
<p className="text-sm text-muted-foreground">{t("guardrails.toolPermission.constraints.title")}</p>
|
||||
{entries.map(([path, pattern], patternIndex) => (
|
||||
<div key={`${rule.id || index}-${patternIndex}`} className="flex items-start gap-2">
|
||||
<Input
|
||||
|
|
@ -160,7 +167,7 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
|
|||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Remove constraint"
|
||||
aria-label={t("guardrails.toolPermission.constraints.remove")}
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
updateAllowedParamEntries(index, (entries) => {
|
||||
|
|
@ -185,7 +192,7 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
|
|||
})
|
||||
}
|
||||
>
|
||||
+ Add another constraint
|
||||
{t("guardrails.toolPermission.constraints.addAnother")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -196,16 +203,15 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
|
|||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-lg font-semibold">LiteLLM Tool Permission Guardrail</p>
|
||||
<p className="text-lg font-semibold">{t("guardrails.toolPermission.title")}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Provide regex patterns (e.g., ^mcp__github_.*$) for tool names or types and optionally constrain payload
|
||||
fields.
|
||||
{t("guardrails.toolPermission.description", { example: TOOL_NAME_EXAMPLE })}
|
||||
</p>
|
||||
</div>
|
||||
{!disabled && (
|
||||
<Button onClick={addRule}>
|
||||
<Plus />
|
||||
Add Rule
|
||||
{t("guardrails.toolPermission.addRule")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -213,22 +219,22 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
|
|||
<Separator className="my-4" />
|
||||
|
||||
{config.rules.length === 0 ? (
|
||||
<div className="py-10 text-center text-muted-foreground">No tool rules added yet</div>
|
||||
<div className="py-10 text-center text-muted-foreground">{t("guardrails.toolPermission.empty")}</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{config.rules.map((rule, index) => (
|
||||
<Card key={rule.id || index} className="bg-muted/40">
|
||||
<CardContent>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<p className="font-semibold">Rule {index + 1}</p>
|
||||
<p className="font-semibold">{t("guardrails.toolPermission.rule.title", { number: index + 1 })}</p>
|
||||
<Button variant="ghost" disabled={disabled} onClick={() => removeRule(index)}>
|
||||
<Trash2 />
|
||||
Remove
|
||||
{t("guardrails.toolPermission.rule.remove")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Rule ID</p>
|
||||
<p className="text-sm font-medium">{t("guardrails.toolPermission.rule.id")}</p>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
placeholder="unique_rule_id"
|
||||
|
|
@ -237,10 +243,10 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Tool Name (optional)</p>
|
||||
<p className="text-sm font-medium">{t("guardrails.toolPermission.rule.toolName")}</p>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
placeholder="^mcp__github_.*$"
|
||||
placeholder={TOOL_NAME_EXAMPLE}
|
||||
value={rule.tool_name ?? ""}
|
||||
onChange={(e) =>
|
||||
updateRule(index, {
|
||||
|
|
@ -253,7 +259,7 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
|
|||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Tool Type (optional)</p>
|
||||
<p className="text-sm font-medium">{t("guardrails.toolPermission.rule.toolType")}</p>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
placeholder="^function$"
|
||||
|
|
@ -268,20 +274,20 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
|
|||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<p className="text-sm font-medium">Decision</p>
|
||||
<p className="text-sm font-medium">{t("guardrails.toolPermission.rule.decision")}</p>
|
||||
<Select
|
||||
items={DECISION_ITEMS}
|
||||
items={decisionItems}
|
||||
disabled={disabled}
|
||||
value={rule.decision}
|
||||
onValueChange={(value: string | null) =>
|
||||
value && updateRule(index, { decision: value as ToolPermissionDecision })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]" aria-label="Decision">
|
||||
<SelectTrigger className="w-[200px]" aria-label={t("guardrails.toolPermission.rule.decision")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DECISION_ITEMS.map((item) => (
|
||||
{decisionItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -301,20 +307,20 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
|
|||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Default action</p>
|
||||
<p className="text-sm font-medium">{t("guardrails.toolPermission.defaultAction")}</p>
|
||||
<Select
|
||||
items={DECISION_ITEMS}
|
||||
items={decisionItems}
|
||||
disabled={disabled}
|
||||
value={config.default_action}
|
||||
onValueChange={(value: string | null) =>
|
||||
value && updateConfig({ default_action: value as ToolPermissionDefaultAction })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full" aria-label="Default action">
|
||||
<SelectTrigger className="w-full" aria-label={t("guardrails.toolPermission.defaultAction")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DECISION_ITEMS.map((item) => (
|
||||
{decisionItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -324,7 +330,7 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
|
|||
</div>
|
||||
<div>
|
||||
<p className="flex items-center gap-1 text-sm font-medium">
|
||||
On disallowed action
|
||||
{t("guardrails.toolPermission.onDisallowed.label")}
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
|
|
@ -333,25 +339,22 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
|
|||
</span>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
Block returns an error when a forbidden tool is invoked. Rewrite strips the tool call but lets the
|
||||
rest of the response continue.
|
||||
</TooltipContent>
|
||||
<TooltipContent>{t("guardrails.toolPermission.onDisallowed.hint")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</p>
|
||||
<Select
|
||||
items={ON_DISALLOWED_ITEMS}
|
||||
items={onDisallowedItems}
|
||||
disabled={disabled}
|
||||
value={config.on_disallowed_action}
|
||||
onValueChange={(value: string | null) =>
|
||||
value && updateConfig({ on_disallowed_action: value as ToolPermissionOnDisallowedAction })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full" aria-label="On disallowed action">
|
||||
<SelectTrigger className="w-full" aria-label={t("guardrails.toolPermission.onDisallowed.label")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ON_DISALLOWED_ITEMS.map((item) => (
|
||||
{onDisallowedItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
@ -362,12 +365,12 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({ v
|
|||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<p className="text-sm font-medium">Violation message (optional)</p>
|
||||
<p className="text-sm font-medium">{t("guardrails.toolPermission.violationMessage.label")}</p>
|
||||
<Textarea
|
||||
className="field-sizing-fixed"
|
||||
disabled={disabled}
|
||||
rows={3}
|
||||
placeholder="This violates our org policy..."
|
||||
placeholder={t("guardrails.toolPermission.violationMessage.placeholder")}
|
||||
value={config.violation_message_template}
|
||||
onChange={(e) => updateConfig({ violation_message_template: e.target.value })}
|
||||
/>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue