mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for guardrail garden and info views
This commit is contained in:
parent
327f3ad020
commit
eccf54a990
6 changed files with 334 additions and 93 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState } from "react";
|
||||
import { Input } from "antd";
|
||||
import { SearchOutlined, ArrowRightOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GuardrailCardInfo, ALL_CARDS } from "./guardrail_garden_data";
|
||||
import GuardrailCard from "./guardrail_garden_card";
|
||||
import GuardrailDetailView from "./guardrail_garden_detail";
|
||||
|
|
@ -11,6 +12,7 @@ interface GuardrailGardenProps {
|
|||
}
|
||||
|
||||
const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardrailCreated }) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedCard, setSelectedCard] = useState<GuardrailCardInfo | null>(null);
|
||||
const [showAllLitellm, setShowAllLitellm] = useState(false);
|
||||
|
|
@ -47,7 +49,7 @@ const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardr
|
|||
<div style={{ marginBottom: 24 }}>
|
||||
<Input
|
||||
size="large"
|
||||
placeholder="Search guardrails"
|
||||
placeholder={t("guardrails.guardrailGarden.searchPlaceholder")}
|
||||
prefix={<SearchOutlined style={{ color: "#9ca3af" }} />}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
|
|
@ -58,7 +60,9 @@ const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardr
|
|||
{/* LiteLLM Content Filter Section */}
|
||||
<div style={{ marginBottom: 40 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 4 }}>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, color: "#111827", margin: 0 }}>LiteLLM Content Filter</h2>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, color: "#111827", margin: 0 }}>
|
||||
{t("guardrails.guardrailGarden.litellmSectionTitle")}
|
||||
</h2>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
|
|
@ -71,17 +75,17 @@ const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardr
|
|||
onClick={() => setShowAllLitellm(!showAllLitellm)}
|
||||
>
|
||||
{showAllLitellm ? (
|
||||
<>Show less</>
|
||||
<>{t("guardrails.guardrailGarden.showLess")}</>
|
||||
) : (
|
||||
<>
|
||||
<ArrowRightOutlined style={{ fontSize: 12 }} />
|
||||
{`Show all (${litellmCards.length})`}
|
||||
{t("guardrails.guardrailGarden.showAll", { count: litellmCards.length })}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ fontSize: 13, color: "#6b7280", margin: "4px 0 20px 0" }}>
|
||||
Built-in guardrails powered by LiteLLM. Zero latency, no external dependencies, no additional cost.
|
||||
{t("guardrails.guardrailGarden.litellmSectionDesc")}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -98,9 +102,11 @@ const GuardrailGarden: React.FC<GuardrailGardenProps> = ({ accessToken, onGuardr
|
|||
|
||||
{/* Partner Guardrails Section */}
|
||||
<div style={{ marginBottom: 40 }}>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, color: "#111827", margin: "0 0 4px 0" }}>Partner Guardrails</h2>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 600, color: "#111827", margin: "0 0 4px 0" }}>
|
||||
{t("guardrails.guardrailGarden.partnerSectionTitle")}
|
||||
</h2>
|
||||
<p style={{ fontSize: 13, color: "#6b7280", margin: "4px 0 20px 0" }}>
|
||||
Third-party guardrail integrations from leading AI security providers.
|
||||
{t("guardrails.guardrailGarden.partnerSectionDesc")}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState } from "react";
|
||||
import { CheckCircleFilled } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GuardrailCardInfo } from "./guardrail_garden_data";
|
||||
|
||||
const LogoWithFallback: React.FC<{ src: string; name: string }> = ({ src, name }) => {
|
||||
|
|
@ -38,6 +39,7 @@ const LogoWithFallback: React.FC<{ src: string; name: string }> = ({ src, name }
|
|||
};
|
||||
|
||||
const GuardrailCard: React.FC<{ card: GuardrailCardInfo; onClick: () => void }> = ({ card, onClick }) => {
|
||||
const { t } = useTranslation();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
return (
|
||||
|
|
@ -74,7 +76,7 @@ const GuardrailCard: React.FC<{ card: GuardrailCardInfo; onClick: () => void }>
|
|||
<div style={{ marginTop: 10, display: "flex", alignItems: "center", gap: 4 }}>
|
||||
<CheckCircleFilled style={{ color: "#16a34a", fontSize: 12 }} />
|
||||
<span style={{ fontSize: 11, color: "#16a34a", fontWeight: 500 }}>
|
||||
F1: {card.eval.f1}% · {card.eval.testCases} test cases
|
||||
{t("guardrails.guardrailGardenCard.evalBadge", { f1: card.eval.f1, testCases: card.eval.testCases })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState } from "react";
|
||||
import { Button } from "antd";
|
||||
import { ArrowLeftOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AddGuardrailForm from "./add_guardrail_form";
|
||||
import { GUARDRAIL_PRESETS } from "./guardrail_garden_configs";
|
||||
import { GuardrailCardInfo } from "./guardrail_garden_data";
|
||||
|
|
@ -13,30 +14,53 @@ interface GuardrailDetailViewProps {
|
|||
}
|
||||
|
||||
const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack, accessToken, onGuardrailCreated }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isAddFormVisible, setIsAddFormVisible] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
|
||||
const detailRows = [
|
||||
{ property: "Provider", value: card.category === "litellm" ? "LiteLLM Content Filter" : "Partner Guardrail" },
|
||||
...(card.subcategory ? [{ property: "Subcategory", value: card.subcategory }] : []),
|
||||
...(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" }] : []),
|
||||
{
|
||||
property: t("guardrails.guardrailGardenDetail.propertyProvider"),
|
||||
value:
|
||||
card.category === "litellm"
|
||||
? t("guardrails.guardrailGardenDetail.litellmContentFilter")
|
||||
: t("guardrails.guardrailGardenDetail.partnerGuardrail"),
|
||||
},
|
||||
...(card.subcategory
|
||||
? [{ property: t("guardrails.guardrailGardenDetail.propertySubcategory"), value: card.subcategory }]
|
||||
: []),
|
||||
...(card.category === "litellm"
|
||||
? [
|
||||
{
|
||||
property: t("guardrails.guardrailGardenDetail.propertyCost"),
|
||||
value: t("guardrails.guardrailGardenDetail.costFree"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(card.category === "litellm"
|
||||
? [{ property: t("guardrails.guardrailGardenDetail.propertyExternalDeps"), value: t("common.none") }]
|
||||
: []),
|
||||
...(card.category === "litellm"
|
||||
? [{ property: t("guardrails.guardrailGardenDetail.propertyLatency"), 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.guardrailGardenDetail.metricPrecision"), value: `${card.eval.precision}%` },
|
||||
{ metric: t("guardrails.guardrailGardenDetail.metricRecall"), value: `${card.eval.recall}%` },
|
||||
{ metric: t("guardrails.guardrailGardenDetail.metricF1Score"), value: `${card.eval.f1}%` },
|
||||
{ metric: t("guardrails.guardrailGardenDetail.metricTestCases"), value: String(card.eval.testCases) },
|
||||
{ metric: t("guardrails.guardrailGardenDetail.metricFalsePositives"), value: "0" },
|
||||
{ metric: t("guardrails.guardrailGardenDetail.metricFalseNegatives"), value: "0" },
|
||||
{ metric: t("guardrails.guardrailGardenDetail.metricLatencyP50"), value: card.eval.latency },
|
||||
]
|
||||
: [];
|
||||
|
||||
const tabs = [{ key: "overview", label: "Overview" }, ...(card.eval ? [{ key: "eval", label: "Eval Results" }] : [])];
|
||||
const tabs = [
|
||||
{ key: "overview", label: t("guardrails.guardrailGardenDetail.tabOverview") },
|
||||
...(card.eval ? [{ key: "eval", label: t("guardrails.guardrailGardenDetail.tabEval") }] : []),
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 960, margin: "0 auto" }}>
|
||||
|
|
@ -86,7 +110,7 @@ const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack,
|
|||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
Create Guardrail
|
||||
{t("guardrails.guardrailGardenDetail.createGuardrail")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -118,17 +142,23 @@ const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack,
|
|||
<div style={{ display: "flex", gap: 64 }}>
|
||||
{/* Left column — overview + details table */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<h2 style={{ fontSize: 18, fontWeight: 400, color: "#202124", margin: "0 0 12px 0" }}>Overview</h2>
|
||||
<h2 style={{ fontSize: 18, fontWeight: 400, color: "#202124", margin: "0 0 12px 0" }}>
|
||||
{t("guardrails.guardrailGardenDetail.tabOverview")}
|
||||
</h2>
|
||||
<p style={{ fontSize: 14, color: "#3c4043", lineHeight: 1.7, margin: "0 0 32px 0" }}>{card.description}</p>
|
||||
|
||||
<h2 style={{ fontSize: 18, fontWeight: 400, color: "#202124", margin: "0 0 4px 0" }}>Guardrail Details</h2>
|
||||
<p style={{ fontSize: 13, color: "#5f6368", margin: "0 0 16px 0" }}>Details are as follows</p>
|
||||
<h2 style={{ fontSize: 18, fontWeight: 400, color: "#202124", margin: "0 0 4px 0" }}>
|
||||
{t("guardrails.guardrailGardenDetail.guardrailDetails")}
|
||||
</h2>
|
||||
<p style={{ fontSize: 13, color: "#5f6368", margin: "0 0 16px 0" }}>
|
||||
{t("guardrails.guardrailGardenDetail.detailsSubtitle")}
|
||||
</p>
|
||||
|
||||
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 14 }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: "1px solid #dadce0" }}>
|
||||
<th style={{ textAlign: "left", padding: "12px 0", color: "#5f6368", fontWeight: 500, width: 200 }}>
|
||||
Property
|
||||
{t("guardrails.guardrailGardenDetail.propertyHeader")}
|
||||
</th>
|
||||
<th style={{ textAlign: "left", padding: "12px 0", color: "#5f6368", fontWeight: 500 }}>
|
||||
{card.name}
|
||||
|
|
@ -150,22 +180,30 @@ const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack,
|
|||
<div style={{ width: 240, flexShrink: 0 }}>
|
||||
{/* Guardrail ID */}
|
||||
<div style={{ marginBottom: 28 }}>
|
||||
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 4 }}>Guardrail ID</div>
|
||||
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 4 }}>
|
||||
{t("guardrails.guardrailGardenDetail.guardrailId")}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: "#202124", wordBreak: "break-all" }}>litellm/{card.id}</div>
|
||||
</div>
|
||||
|
||||
{/* Type */}
|
||||
<div style={{ marginBottom: 28 }}>
|
||||
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 4 }}>Type</div>
|
||||
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 4 }}>
|
||||
{t("guardrails.guardrailGardenDetail.typeLabel")}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: "#202124" }}>
|
||||
{card.category === "litellm" ? "Content Filter" : "Partner"}
|
||||
{card.category === "litellm"
|
||||
? t("guardrails.guardrailGardenDetail.typeContentFilter")
|
||||
: t("guardrails.guardrailGardenDetail.typePartner")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags — pill style like Vertex */}
|
||||
{card.tags.length > 0 && (
|
||||
<div style={{ marginBottom: 28 }}>
|
||||
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 8 }}>Tags</div>
|
||||
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 8 }}>
|
||||
{t("guardrails.guardrailGardenDetail.tagsLabel")}
|
||||
</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
||||
{card.tags.map((tag) => (
|
||||
<span
|
||||
|
|
@ -191,12 +229,18 @@ const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack,
|
|||
|
||||
{activeTab === "eval" && (
|
||||
<div>
|
||||
<h2 style={{ fontSize: 18, fontWeight: 400, color: "#202124", margin: "0 0 16px 0" }}>Eval Results</h2>
|
||||
<h2 style={{ fontSize: 18, fontWeight: 400, color: "#202124", margin: "0 0 16px 0" }}>
|
||||
{t("guardrails.guardrailGardenDetail.tabEval")}
|
||||
</h2>
|
||||
<table style={{ width: "100%", maxWidth: 560, borderCollapse: "collapse", fontSize: 14 }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: "#f8f9fa", borderBottom: "1px solid #dadce0" }}>
|
||||
<th style={{ textAlign: "left", padding: "12px 16px", color: "#5f6368", fontWeight: 500 }}>Metric</th>
|
||||
<th style={{ textAlign: "left", padding: "12px 16px", color: "#5f6368", fontWeight: 500 }}>Value</th>
|
||||
<th style={{ textAlign: "left", padding: "12px 16px", color: "#5f6368", fontWeight: 500 }}>
|
||||
{t("guardrails.guardrailGardenDetail.metricHeader")}
|
||||
</th>
|
||||
<th style={{ textAlign: "left", padding: "12px 16px", color: "#5f6368", fontWeight: 500 }}>
|
||||
{t("guardrails.guardrailGardenDetail.valueHeader")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { Badge, Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, T
|
|||
import { Button, Divider, Form, Input, Select, Tooltip } from "antd";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import ContentFilterManager, { formatContentFilterDataForAPI } from "./content_filter/ContentFilterManager";
|
||||
import CustomCodeModal, { EditGuardrailData } from "./custom_code/CustomCodeModal";
|
||||
|
|
@ -51,6 +52,7 @@ interface ProviderParamsResponse {
|
|||
}
|
||||
|
||||
const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose, accessToken, isAdmin }) => {
|
||||
const { t } = useTranslation();
|
||||
const [guardrailData, setGuardrailData] = useState<any>(null);
|
||||
const [guardrailProviderSpecificParams, setGuardrailProviderSpecificParams] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -162,7 +164,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
setSelectedPiiActions({});
|
||||
}
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to load guardrail information");
|
||||
NotificationsManager.fromBackend(t("guardrails.guardrailInfo.loadFailed"));
|
||||
console.error("Error fetching guardrail info:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -447,28 +449,28 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
|
||||
// Only proceed with update if there are actual changes
|
||||
if (Object.keys(updateData).length === 0) {
|
||||
NotificationsManager.info("No changes detected");
|
||||
NotificationsManager.info(t("guardrails.guardrailInfo.noChanges"));
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await updateGuardrailCall(accessToken, guardrailId, updateData);
|
||||
NotificationsManager.success("Guardrail updated successfully");
|
||||
NotificationsManager.success(t("guardrails.guardrailInfo.updateSuccess"));
|
||||
setHasUnsavedContentFilterChanges(false);
|
||||
fetchGuardrailInfo();
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
console.error("Error updating guardrail:", error);
|
||||
NotificationsManager.fromBackend("Failed to update guardrail");
|
||||
NotificationsManager.fromBackend(t("guardrails.guardrailInfo.updateFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-4">Loading...</div>;
|
||||
return <div className="p-4">{t("common.loading")}</div>;
|
||||
}
|
||||
|
||||
if (!guardrailData) {
|
||||
return <div className="p-4">Guardrail not found</div>;
|
||||
return <div className="p-4">{t("guardrails.guardrailInfo.notFound")}</div>;
|
||||
}
|
||||
|
||||
// Format date helper function
|
||||
|
|
@ -497,9 +499,9 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<div className="p-4">
|
||||
<div>
|
||||
<Button type="text" icon={<ArrowLeftIcon className="w-4 h-4" />} onClick={onClose} className="mb-4">
|
||||
Back to Guardrails
|
||||
{t("guardrails.guardrailInfo.backToGuardrails")}
|
||||
</Button>
|
||||
<Title>{guardrailData.guardrail_name || "Unnamed Guardrail"}</Title>
|
||||
<Title>{guardrailData.guardrail_name || t("guardrails.guardrailInfo.unnamedGuardrail")}</Title>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Text className="text-gray-500 font-mono">{guardrailData.guardrail_id}</Text>
|
||||
|
||||
|
|
@ -519,8 +521,8 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
|
||||
<TabGroup>
|
||||
<TabList className="mb-4">
|
||||
<Tab key="overview">Overview</Tab>
|
||||
{isAdmin ? <Tab key="settings">Settings</Tab> : <></>}
|
||||
<Tab key="overview">{t("guardrails.guardrailInfo.tabOverview")}</Tab>
|
||||
{isAdmin ? <Tab key="settings">{t("guardrails.guardrailInfo.tabSettings")}</Tab> : <></>}
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
|
|
@ -528,7 +530,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<TabPanel>
|
||||
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
|
||||
<Card>
|
||||
<Text>Provider</Text>
|
||||
<Text>{t("guardrails.guardrailInfo.provider")}</Text>
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
{logo && (
|
||||
<img
|
||||
|
|
@ -546,20 +548,24 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Mode</Text>
|
||||
<Text>{t("guardrails.guardrailInfo.mode")}</Text>
|
||||
<div className="mt-2">
|
||||
<Title>{guardrailData.litellm_params?.mode || "-"}</Title>
|
||||
<Badge color={guardrailData.litellm_params?.default_on ? "green" : "gray"}>
|
||||
{guardrailData.litellm_params?.default_on ? "Default On" : "Default Off"}
|
||||
{guardrailData.litellm_params?.default_on
|
||||
? t("guardrails.guardrailInfo.defaultOn")
|
||||
: t("guardrails.guardrailInfo.defaultOff")}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Created At</Text>
|
||||
<Text>{t("common.createdAt")}</Text>
|
||||
<div className="mt-2">
|
||||
<Title>{formatDate(guardrailData.created_at)}</Title>
|
||||
<Text>Last Updated: {formatDate(guardrailData.updated_at)}</Text>
|
||||
<Text>
|
||||
{t("guardrails.guardrailInfo.lastUpdated", { date: formatDate(guardrailData.updated_at) })}
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
|
@ -568,9 +574,11 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && (
|
||||
<Card className="mt-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<Text className="font-medium">PII Protection</Text>
|
||||
<Text className="font-medium">{t("guardrails.guardrailInfo.piiProtection")}</Text>
|
||||
<Badge color="blue">
|
||||
{Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities configured
|
||||
{t("guardrails.guardrailInfo.piiEntitiesConfigured", {
|
||||
count: Object.keys(guardrailData.litellm_params.pii_entities_config).length,
|
||||
})}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -579,11 +587,17 @@ 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="mt-6">
|
||||
<Text className="mb-4 text-lg font-semibold">PII Entity Configuration</Text>
|
||||
<Text className="mb-4 text-lg font-semibold">
|
||||
{t("guardrails.guardrailInfo.piiEntityConfiguration")}
|
||||
</Text>
|
||||
<div className="border rounded-lg overflow-hidden shadow-sm">
|
||||
<div className="bg-gray-50 px-5 py-3 border-b flex">
|
||||
<Text className="flex-1 font-semibold text-gray-700">Entity Type</Text>
|
||||
<Text className="flex-1 font-semibold text-gray-700">Configuration</Text>
|
||||
<Text className="flex-1 font-semibold text-gray-700">
|
||||
{t("guardrails.guardrailInfo.entityType")}
|
||||
</Text>
|
||||
<Text className="flex-1 font-semibold text-gray-700">
|
||||
{t("guardrails.guardrailInfo.configuration")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
{Object.entries(guardrailData.litellm_params?.pii_entities_config).map(([key, value]) => (
|
||||
|
|
@ -618,11 +632,11 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<CodeOutlined className="text-blue-500" />
|
||||
<Text className="font-medium text-lg">Custom Code</Text>
|
||||
<Text className="font-medium text-lg">{t("guardrails.guardrailInfo.customCode")}</Text>
|
||||
</div>
|
||||
{isAdmin && !isConfigGuardrail && (
|
||||
<Button size="small" icon={<CodeOutlined />} onClick={() => setCustomCodeModalVisible(true)}>
|
||||
Edit Code
|
||||
{t("guardrails.guardrailInfo.editCode")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -651,9 +665,9 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<TabPanel>
|
||||
<Card>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Guardrail Settings</Title>
|
||||
<Title>{t("guardrails.guardrailInfo.guardrailSettings")}</Title>
|
||||
{isConfigGuardrail && (
|
||||
<Tooltip title="Guardrail is defined in the config file and cannot be edited.">
|
||||
<Tooltip title={t("guardrails.guardrailInfo.configGuardrailTooltip")}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
|
@ -661,10 +675,10 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
!isConfigGuardrail &&
|
||||
(guardrailData.litellm_params?.guardrail === "custom_code" ? (
|
||||
<Button icon={<CodeOutlined />} onClick={() => setCustomCodeModalVisible(true)}>
|
||||
Edit Code
|
||||
{t("guardrails.guardrailInfo.editCode")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => setIsEditing(true)}>Edit Settings</Button>
|
||||
<Button onClick={() => setIsEditing(true)}>{t("guardrails.guardrailInfo.editSettings")}</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
@ -697,47 +711,47 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
label="Guardrail Name"
|
||||
label={t("guardrails.guardrailInfo.guardrailName")}
|
||||
name="guardrail_name"
|
||||
rules={[{ required: true, message: "Please input a guardrail name" }]}
|
||||
rules={[{ required: true, message: t("guardrails.guardrailInfo.guardrailNameRequired") }]}
|
||||
>
|
||||
<Input placeholder="Enter guardrail name" />
|
||||
<Input placeholder={t("guardrails.guardrailInfo.guardrailNamePlaceholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Default On" name="default_on">
|
||||
<Form.Item label={t("guardrails.guardrailInfo.defaultOnLabel")} name="default_on">
|
||||
<Select>
|
||||
<Select.Option value={true}>Yes</Select.Option>
|
||||
<Select.Option value={false}>No</Select.Option>
|
||||
<Select.Option value={true}>{t("common.yes")}</Select.Option>
|
||||
<Select.Option value={false}>{t("common.no")}</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Skip system messages in guardrail"
|
||||
label={t("guardrails.guardrailInfo.skipSystemMsg")}
|
||||
name="skip_system_message_choice"
|
||||
tooltip="Unified guardrails: omit role: system from guardrail input (LLM still gets full messages). Use global default follows litellm_settings.skip_system_message_in_guardrail."
|
||||
tooltip={t("guardrails.guardrailInfo.skipSystemMsgTooltip")}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value="inherit">Use global default</Select.Option>
|
||||
<Select.Option value="yes">Yes — exclude from guardrail scan</Select.Option>
|
||||
<Select.Option value="no">No — always include in scan</Select.Option>
|
||||
<Select.Option value="inherit">{t("guardrails.guardrailInfo.useGlobalDefault")}</Select.Option>
|
||||
<Select.Option value="yes">{t("guardrails.guardrailInfo.yesExcludeFromScan")}</Select.Option>
|
||||
<Select.Option value="no">{t("guardrails.guardrailInfo.noAlwaysInclude")}</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Skip tool messages in guardrail"
|
||||
label={t("guardrails.guardrailInfo.skipToolMsg")}
|
||||
name="skip_tool_message_choice"
|
||||
tooltip="Unified guardrails: omit role: tool from guardrail input (LLM still gets full messages). Use global default follows litellm_settings.skip_tool_message_in_guardrail."
|
||||
tooltip={t("guardrails.guardrailInfo.skipToolMsgTooltip")}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value="inherit">Use global default</Select.Option>
|
||||
<Select.Option value="yes">Yes — exclude from guardrail scan</Select.Option>
|
||||
<Select.Option value="no">No — always include in scan</Select.Option>
|
||||
<Select.Option value="inherit">{t("guardrails.guardrailInfo.useGlobalDefault")}</Select.Option>
|
||||
<Select.Option value="yes">{t("guardrails.guardrailInfo.yesExcludeFromScan")}</Select.Option>
|
||||
<Select.Option value="no">{t("guardrails.guardrailInfo.noAlwaysInclude")}</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{guardrailData.litellm_params?.guardrail === "presidio" && (
|
||||
<>
|
||||
<Divider orientation="left">PII Protection</Divider>
|
||||
<Divider orientation="left">{t("guardrails.guardrailInfo.piiProtection")}</Divider>
|
||||
<div className="mb-6">
|
||||
{guardrailSettings && (
|
||||
<PiiConfiguration
|
||||
|
|
@ -764,7 +778,9 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
/>
|
||||
|
||||
{(guardrailData.litellm_params?.guardrail === "tool_permission" ||
|
||||
guardrailProviderSpecificParams) && <Divider orientation="left">Provider Settings</Divider>}
|
||||
guardrailProviderSpecificParams) && (
|
||||
<Divider orientation="left">{t("guardrails.guardrailInfo.providerSettings")}</Divider>
|
||||
)}
|
||||
|
||||
{guardrailData.litellm_params?.guardrail === "tool_permission" ? (
|
||||
<ToolPermissionRulesEditor value={toolPermissionConfig} onChange={setToolPermissionConfig} />
|
||||
|
|
@ -806,8 +822,8 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
</>
|
||||
)}
|
||||
|
||||
<Divider orientation="left">Advanced Settings</Divider>
|
||||
<Form.Item label="Guardrail Information" name="guardrail_info">
|
||||
<Divider orientation="left">{t("guardrails.guardrailInfo.advancedSettings")}</Divider>
|
||||
<Form.Item label={t("guardrails.guardrailInfo.guardrailInformation")} name="guardrail_info">
|
||||
<Input.TextArea rows={5} />
|
||||
</Form.Item>
|
||||
|
||||
|
|
@ -819,57 +835,58 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
resetToolPermissionEditor();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save Changes
|
||||
{t("guardrails.guardrailInfo.saveChanges")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">Guardrail ID</Text>
|
||||
<Text className="font-medium">{t("guardrails.guardrailInfo.guardrailIdLabel")}</Text>
|
||||
<div className="font-mono">{guardrailData.guardrail_id}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Guardrail Name</Text>
|
||||
<div>{guardrailData.guardrail_name || "Unnamed Guardrail"}</div>
|
||||
<Text className="font-medium">{t("guardrails.guardrailInfo.guardrailName")}</Text>
|
||||
<div>{guardrailData.guardrail_name || t("guardrails.guardrailInfo.unnamedGuardrail")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Provider</Text>
|
||||
<Text className="font-medium">{t("guardrails.guardrailInfo.provider")}</Text>
|
||||
<div>{displayName}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Mode</Text>
|
||||
<Text className="font-medium">{t("guardrails.guardrailInfo.mode")}</Text>
|
||||
<div>{guardrailData.litellm_params?.mode || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Default On</Text>
|
||||
<Text className="font-medium">{t("guardrails.guardrailInfo.defaultOnLabel")}</Text>
|
||||
<Badge color={guardrailData.litellm_params?.default_on ? "green" : "gray"}>
|
||||
{guardrailData.litellm_params?.default_on ? "Yes" : "No"}
|
||||
{guardrailData.litellm_params?.default_on ? t("common.yes") : t("common.no")}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{guardrailData.litellm_params?.pii_entities_config &&
|
||||
Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && (
|
||||
<div>
|
||||
<Text className="font-medium">PII Protection</Text>
|
||||
<Text className="font-medium">{t("guardrails.guardrailInfo.piiProtection")}</Text>
|
||||
<div className="mt-2">
|
||||
<Badge color="blue">
|
||||
{Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities
|
||||
configured
|
||||
{t("guardrails.guardrailInfo.piiEntitiesConfigured", {
|
||||
count: Object.keys(guardrailData.litellm_params.pii_entities_config).length,
|
||||
})}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Created At</Text>
|
||||
<Text className="font-medium">{t("common.createdAt")}</Text>
|
||||
<div>{formatDate(guardrailData.created_at)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Last Updated</Text>
|
||||
<Text className="font-medium">{t("guardrails.guardrailInfo.lastUpdatedLabel")}</Text>
|
||||
<div>{formatDate(guardrailData.updated_at)}</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -2862,6 +2862,92 @@
|
|||
"invalidJsonConfig": "Invalid JSON in configuration",
|
||||
"loadSettingsFailed": "Failed to load guardrail settings",
|
||||
"updateFailed": "Failed to update guardrail: {{error}}"
|
||||
},
|
||||
"guardrailGarden": {
|
||||
"searchPlaceholder": "Search guardrails",
|
||||
"litellmSectionTitle": "LiteLLM Content Filter",
|
||||
"showLess": "Show less",
|
||||
"showAll": "Show all ({{count}})",
|
||||
"litellmSectionDesc": "Built-in guardrails powered by LiteLLM. Zero latency, no external dependencies, no additional cost.",
|
||||
"partnerSectionTitle": "Partner Guardrails",
|
||||
"partnerSectionDesc": "Third-party guardrail integrations from leading AI security providers."
|
||||
},
|
||||
"guardrailGardenCard": {
|
||||
"evalBadge": "F1: {{f1}}% · {{testCases}} test cases"
|
||||
},
|
||||
"guardrailGardenDetail": {
|
||||
"propertyProvider": "Provider",
|
||||
"litellmContentFilter": "LiteLLM Content Filter",
|
||||
"partnerGuardrail": "Partner Guardrail",
|
||||
"propertySubcategory": "Subcategory",
|
||||
"propertyCost": "Cost",
|
||||
"costFree": "$0 / request",
|
||||
"propertyExternalDeps": "External Dependencies",
|
||||
"propertyLatency": "Latency",
|
||||
"metricPrecision": "Precision",
|
||||
"metricRecall": "Recall",
|
||||
"metricF1Score": "F1 Score",
|
||||
"metricTestCases": "Test Cases",
|
||||
"metricFalsePositives": "False Positives",
|
||||
"metricFalseNegatives": "False Negatives",
|
||||
"metricLatencyP50": "Latency (p50)",
|
||||
"tabOverview": "Overview",
|
||||
"tabEval": "Eval Results",
|
||||
"createGuardrail": "Create Guardrail",
|
||||
"guardrailDetails": "Guardrail Details",
|
||||
"detailsSubtitle": "Details are as follows",
|
||||
"propertyHeader": "Property",
|
||||
"guardrailId": "Guardrail ID",
|
||||
"typeLabel": "Type",
|
||||
"typeContentFilter": "Content Filter",
|
||||
"typePartner": "Partner",
|
||||
"tagsLabel": "Tags",
|
||||
"metricHeader": "Metric",
|
||||
"valueHeader": "Value"
|
||||
},
|
||||
"guardrailInfo": {
|
||||
"backToGuardrails": "Back to Guardrails",
|
||||
"unnamedGuardrail": "Unnamed Guardrail",
|
||||
"tabOverview": "Overview",
|
||||
"tabSettings": "Settings",
|
||||
"provider": "Provider",
|
||||
"mode": "Mode",
|
||||
"defaultOn": "Default On",
|
||||
"defaultOff": "Default Off",
|
||||
"lastUpdated": "Last Updated: {{date}}",
|
||||
"piiProtection": "PII Protection",
|
||||
"piiEntitiesConfigured_one": "{{count}} PII entity configured",
|
||||
"piiEntitiesConfigured_other": "{{count}} PII entities configured",
|
||||
"piiEntityConfiguration": "PII Entity Configuration",
|
||||
"entityType": "Entity Type",
|
||||
"configuration": "Configuration",
|
||||
"customCode": "Custom Code",
|
||||
"editCode": "Edit Code",
|
||||
"guardrailSettings": "Guardrail Settings",
|
||||
"configGuardrailTooltip": "Guardrail is defined in the config file and cannot be edited.",
|
||||
"editSettings": "Edit Settings",
|
||||
"guardrailName": "Guardrail Name",
|
||||
"guardrailNameRequired": "Please input a guardrail name",
|
||||
"guardrailNamePlaceholder": "Enter guardrail name",
|
||||
"defaultOnLabel": "Default On",
|
||||
"useGlobalDefault": "Use global default",
|
||||
"yesExcludeFromScan": "Yes — exclude from guardrail scan",
|
||||
"noAlwaysInclude": "No — always include in scan",
|
||||
"skipSystemMsg": "Skip system messages in guardrail",
|
||||
"skipSystemMsgTooltip": "Unified guardrails: omit role: system from guardrail input (LLM still gets full messages). Use global default follows litellm_settings.skip_system_message_in_guardrail.",
|
||||
"skipToolMsg": "Skip tool messages in guardrail",
|
||||
"skipToolMsgTooltip": "Unified guardrails: omit role: tool from guardrail input (LLM still gets full messages). Use global default follows litellm_settings.skip_tool_message_in_guardrail.",
|
||||
"providerSettings": "Provider Settings",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"guardrailInformation": "Guardrail Information",
|
||||
"saveChanges": "Save Changes",
|
||||
"guardrailIdLabel": "Guardrail ID",
|
||||
"lastUpdatedLabel": "Last Updated",
|
||||
"loadFailed": "Failed to load guardrail information",
|
||||
"noChanges": "No changes detected",
|
||||
"updateSuccess": "Guardrail updated successfully",
|
||||
"updateFailed": "Failed to update guardrail",
|
||||
"notFound": "Guardrail not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2862,6 +2862,92 @@
|
|||
"invalidJsonConfig": "配置中的 JSON 格式无效",
|
||||
"loadSettingsFailed": "加载护栏设置失败",
|
||||
"updateFailed": "更新护栏失败:{{error}}"
|
||||
},
|
||||
"guardrailGarden": {
|
||||
"searchPlaceholder": "搜索护栏",
|
||||
"litellmSectionTitle": "LiteLLM 内容过滤器",
|
||||
"showLess": "收起",
|
||||
"showAll": "显示全部({{count}})",
|
||||
"litellmSectionDesc": "由 LiteLLM 内置的护栏,零延迟,无外部依赖,无额外费用。",
|
||||
"partnerSectionTitle": "合作伙伴护栏",
|
||||
"partnerSectionDesc": "来自领先 AI 安全提供商的第三方护栏集成。"
|
||||
},
|
||||
"guardrailGardenCard": {
|
||||
"evalBadge": "F1:{{f1}}% · {{testCases}} 个测试用例"
|
||||
},
|
||||
"guardrailGardenDetail": {
|
||||
"propertyProvider": "提供商",
|
||||
"litellmContentFilter": "LiteLLM 内容过滤器",
|
||||
"partnerGuardrail": "合作伙伴护栏",
|
||||
"propertySubcategory": "子类别",
|
||||
"propertyCost": "费用",
|
||||
"costFree": "$0 / 次请求",
|
||||
"propertyExternalDeps": "外部依赖",
|
||||
"propertyLatency": "延迟",
|
||||
"metricPrecision": "精确率",
|
||||
"metricRecall": "召回率",
|
||||
"metricF1Score": "F1 分数",
|
||||
"metricTestCases": "测试用例",
|
||||
"metricFalsePositives": "误报",
|
||||
"metricFalseNegatives": "漏报",
|
||||
"metricLatencyP50": "延迟(p50)",
|
||||
"tabOverview": "概览",
|
||||
"tabEval": "评估结果",
|
||||
"createGuardrail": "创建护栏",
|
||||
"guardrailDetails": "护栏详情",
|
||||
"detailsSubtitle": "详细信息如下",
|
||||
"propertyHeader": "属性",
|
||||
"guardrailId": "护栏 ID",
|
||||
"typeLabel": "类型",
|
||||
"typeContentFilter": "内容过滤器",
|
||||
"typePartner": "合作伙伴",
|
||||
"tagsLabel": "标签",
|
||||
"metricHeader": "指标",
|
||||
"valueHeader": "值"
|
||||
},
|
||||
"guardrailInfo": {
|
||||
"backToGuardrails": "返回护栏列表",
|
||||
"unnamedGuardrail": "未命名护栏",
|
||||
"tabOverview": "概览",
|
||||
"tabSettings": "设置",
|
||||
"provider": "提供商",
|
||||
"mode": "模式",
|
||||
"defaultOn": "默认启用",
|
||||
"defaultOff": "默认关闭",
|
||||
"lastUpdated": "最近更新:{{date}}",
|
||||
"piiProtection": "PII 保护",
|
||||
"piiEntitiesConfigured_one": "已配置 {{count}} 个 PII 实体",
|
||||
"piiEntitiesConfigured_other": "已配置 {{count}} 个 PII 实体",
|
||||
"piiEntityConfiguration": "PII 实体配置",
|
||||
"entityType": "实体类型",
|
||||
"configuration": "配置",
|
||||
"customCode": "自定义代码",
|
||||
"editCode": "编辑代码",
|
||||
"guardrailSettings": "护栏设置",
|
||||
"configGuardrailTooltip": "护栏在配置文件中定义,无法编辑。",
|
||||
"editSettings": "编辑设置",
|
||||
"guardrailName": "护栏名称",
|
||||
"guardrailNameRequired": "请输入护栏名称",
|
||||
"guardrailNamePlaceholder": "输入护栏名称",
|
||||
"defaultOnLabel": "默认启用",
|
||||
"useGlobalDefault": "使用全局默认",
|
||||
"yesExcludeFromScan": "是 — 从护栏扫描中排除",
|
||||
"noAlwaysInclude": "否 — 始终包含在扫描中",
|
||||
"skipSystemMsg": "在护栏中跳过系统消息",
|
||||
"skipSystemMsgTooltip": "统一护栏:从护栏输入中省略 role: system(LLM 仍接收完整消息)。使用全局默认值遵循 litellm_settings.skip_system_message_in_guardrail。",
|
||||
"skipToolMsg": "在护栏中跳过工具消息",
|
||||
"skipToolMsgTooltip": "统一护栏:从护栏输入中省略 role: tool(LLM 仍接收完整消息)。使用全局默认值遵循 litellm_settings.skip_tool_message_in_guardrail。",
|
||||
"providerSettings": "提供商设置",
|
||||
"advancedSettings": "高级设置",
|
||||
"guardrailInformation": "护栏信息",
|
||||
"saveChanges": "保存更改",
|
||||
"guardrailIdLabel": "护栏 ID",
|
||||
"lastUpdatedLabel": "最近更新",
|
||||
"loadFailed": "加载护栏信息失败",
|
||||
"noChanges": "未检测到更改",
|
||||
"updateSuccess": "护栏已成功更新",
|
||||
"updateFailed": "更新护栏失败",
|
||||
"notFound": "未找到护栏"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue