feat(ui): i18n for guardrails content filter components

This commit is contained in:
吹雪 2026-06-10 17:58:30 +09:00
parent 59880539d3
commit 535e00626d
9 changed files with 374 additions and 110 deletions

View file

@ -1,6 +1,7 @@
import React from "react";
import { Typography, Select, Table, Tag, Button } from "antd";
import { DeleteOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
const { Text } = Typography;
const { Option } = Select;
@ -28,9 +29,10 @@ const CategoryTable: React.FC<CategoryTableProps> = ({
onRemove,
readOnly = false,
}) => {
const { t } = useTranslation();
const columns = [
{
title: "Category",
title: t("guardrails.categoryTable.colCategory"),
dataIndex: "display_name",
key: "display_name",
render: (displayName: string, record: ContentCategory) => (
@ -47,7 +49,7 @@ const CategoryTable: React.FC<CategoryTableProps> = ({
),
},
{
title: "Severity Threshold",
title: t("guardrails.categoryTable.colSeverityThreshold"),
dataIndex: "severity_threshold",
key: "severity_threshold",
width: 180,
@ -67,15 +69,15 @@ const CategoryTable: React.FC<CategoryTableProps> = ({
style={{ width: 150 }}
size="small"
>
<Option value="high">High</Option>
<Option value="medium">Medium</Option>
<Option value="low">Low</Option>
<Option value="high">{t("guardrails.categoryTable.severityHigh")}</Option>
<Option value="medium">{t("guardrails.categoryTable.severityMedium")}</Option>
<Option value="low">{t("guardrails.categoryTable.severityLow")}</Option>
</Select>
);
},
},
{
title: "Action",
title: t("guardrails.categoryTable.colAction"),
dataIndex: "action",
key: "action",
width: 150,
@ -90,8 +92,8 @@ const CategoryTable: React.FC<CategoryTableProps> = ({
style={{ width: 120 }}
size="small"
>
<Option value="BLOCK">Block</Option>
<Option value="MASK">Mask</Option>
<Option value="BLOCK">{t("guardrails.categoryTable.actionBlock")}</Option>
<Option value="MASK">{t("guardrails.categoryTable.actionMask")}</Option>
</Select>
);
},
@ -105,14 +107,18 @@ const CategoryTable: React.FC<CategoryTableProps> = ({
width: 100,
render: (_: any, record: ContentCategory) => (
<Button type="text" danger size="small" icon={<DeleteOutlined />} onClick={() => onRemove?.(record.id)}>
Delete
{t("common.delete")}
</Button>
),
} as any);
}
if (categories.length === 0) {
return <div style={{ textAlign: "center", padding: "40px 0", color: "#999" }}>No categories configured.</div>;
return (
<div style={{ textAlign: "center", padding: "40px 0", color: "#999" }}>
{t("guardrails.categoryTable.noCategories")}
</div>
);
}
return <Table dataSource={categories} columns={columns} rowKey="id" pagination={false} size="small" />;

View file

@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react";
import { Card, Typography, Select, Switch, Form, Space, InputNumber } from "antd";
import { getMajorAirlines } from "../../networking";
import { useTranslation } from "react-i18next";
const { Title, Text } = Typography;
const { Option } = Select;
@ -51,6 +52,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);
@ -117,17 +119,14 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
title={
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Title level={5} style={{ margin: 0 }}>
Competitor Intent Filter
{t("guardrails.competitorIntentConfiguration.title")}
</Title>
<Switch checked={false} onChange={handleEnabledChange} />
</div>
}
size="small"
>
<Text type="secondary">
Block or reframe competitor comparison questions. When enabled, airline type auto-loads competitors from IATA;
generic type requires manual competitor list.
</Text>
<Text type="secondary">{t("guardrails.competitorIntentConfiguration.disabledDescription")}</Text>
</Card>
);
}
@ -137,7 +136,7 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
title={
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Title level={5} style={{ margin: 0 }}>
Competitor Intent Filter
{t("guardrails.competitorIntentConfiguration.title")}
</Title>
<Switch checked={enabled} onChange={handleEnabledChange} />
</div>
@ -145,28 +144,27 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
size="small"
>
<Text type="secondary" style={{ display: "block", marginBottom: 16 }}>
Block or reframe competitor comparison questions. Airline type uses major airlines (excluding your brand);
generic requires manual competitor list.
{t("guardrails.competitorIntentConfiguration.enabledDescription")}
</Text>
<Form layout="vertical" size="small">
<Form.Item label="Type">
<Form.Item label={t("common.type")}>
<Select
value={effectiveConfig.competitor_intent_type}
onChange={(v) => handleConfigChange("competitor_intent_type", v)}
style={{ width: "100%" }}
>
<Option value="airline">Airline (auto-load competitors from IATA)</Option>
<Option value="generic">Generic (specify competitors manually)</Option>
<Option value="airline">{t("guardrails.competitorIntentConfiguration.typeAirline")}</Option>
<Option value="generic">{t("guardrails.competitorIntentConfiguration.typeGeneric")}</Option>
</Select>
</Form.Item>
<Form.Item
label="Your Brand (brand_self)"
label={t("guardrails.competitorIntentConfiguration.brandSelfLabel")}
required
help={
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.competitorIntentConfiguration.brandSelfHelpAirline")
: t("guardrails.competitorIntentConfiguration.brandSelfHelpGeneric")
}
>
<Select
@ -174,10 +172,10 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
style={{ width: "100%" }}
placeholder={
loadingAirlines
? "Loading airlines..."
? t("guardrails.competitorIntentConfiguration.loadingAirlines")
: effectiveConfig.competitor_intent_type === "airline"
? "Search or select airline, or type to add custom"
: "Type and press Enter to add"
? t("guardrails.competitorIntentConfiguration.brandSelfPlaceholderAirline")
: t("guardrails.competitorIntentConfiguration.brandSelfPlaceholderGeneric")
}
value={effectiveConfig.brand_self}
onChange={(v) =>
@ -212,13 +210,13 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
{effectiveConfig.competitor_intent_type === "airline" && (
<Form.Item
label="Locations (optional)"
help="Countries, cities, airports for disambiguation (e.g. qatar, doha)"
label={t("guardrails.competitorIntentConfiguration.locationsLabel")}
help={t("guardrails.competitorIntentConfiguration.locationsHelp")}
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Type and press Enter to add"
placeholder={t("guardrails.competitorIntentConfiguration.tagsPlaceholder")}
value={effectiveConfig.locations ?? []}
onChange={(v) => handleNestedArrayChange("locations", v ?? [])}
tokenSeparators={[","]}
@ -227,11 +225,15 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
)}
{effectiveConfig.competitor_intent_type === "generic" && (
<Form.Item label="Competitors" required help="Competitor names to detect (required for generic type)">
<Form.Item
label={t("guardrails.competitorIntentConfiguration.competitorsLabel")}
required
help={t("guardrails.competitorIntentConfiguration.competitorsHelp")}
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Type and press Enter to add"
placeholder={t("guardrails.competitorIntentConfiguration.tagsPlaceholder")}
value={effectiveConfig.competitors ?? []}
onChange={(v) => handleNestedArrayChange("competitors", v ?? [])}
tokenSeparators={[","]}
@ -239,52 +241,57 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
</Form.Item>
)}
<Form.Item label="Policy: Competitor comparison">
<Form.Item label={t("guardrails.competitorIntentConfiguration.policyCompetitorLabel")}>
<Select
value={effectiveConfig.policy?.competitor_comparison ?? "refuse"}
onChange={(v) => handlePolicyChange("competitor_comparison", v)}
style={{ width: "100%" }}
>
<Option value="refuse">Refuse (block request)</Option>
<Option value="reframe">Reframe (suggest alternative)</Option>
<Option value="refuse">{t("guardrails.competitorIntentConfiguration.policyRefuse")}</Option>
<Option value="reframe">{t("guardrails.competitorIntentConfiguration.policyReframe")}</Option>
</Select>
</Form.Item>
<Form.Item label="Policy: Possible competitor comparison">
<Form.Item label={t("guardrails.competitorIntentConfiguration.policyPossibleCompetitorLabel")}>
<Select
value={effectiveConfig.policy?.possible_competitor_comparison ?? "reframe"}
onChange={(v) => handlePolicyChange("possible_competitor_comparison", v)}
style={{ width: "100%" }}
>
<Option value="refuse">Refuse (block request)</Option>
<Option value="reframe">Reframe (suggest alternative to backend LLM)</Option>
<Option value="refuse">{t("guardrails.competitorIntentConfiguration.policyRefuse")}</Option>
<Option value="reframe">{t("guardrails.competitorIntentConfiguration.policyReframeBackend")}</Option>
</Select>
</Form.Item>
<Form.Item
label="Confidence thresholds"
label={t("guardrails.competitorIntentConfiguration.confidenceThresholdsLabel")}
help={
<>
Classify competitor intent by confidence (0–1). Higher confidence → stronger intent.
{t("guardrails.competitorIntentConfiguration.confidenceThresholdsHelp")}
<ul style={{ marginBottom: 0, marginTop: 4, paddingLeft: 20 }}>
<li>
<strong>High (≥)</strong>: Treat as full competitor comparison → uses &quot;Competitor
comparison&quot; policy
<strong>{t("guardrails.competitorIntentConfiguration.thresholdHighLabel")}</strong>:{" "}
{t("guardrails.competitorIntentConfiguration.thresholdHighDesc")}
</li>
<li>
<strong>Medium (≥)</strong>: Treat as possible comparison → uses &quot;Possible competitor
comparison&quot; policy
<strong>{t("guardrails.competitorIntentConfiguration.thresholdMediumLabel")}</strong>:{" "}
{t("guardrails.competitorIntentConfiguration.thresholdMediumDesc")}
</li>
<li>
<strong>Low (≥)</strong>: Log only; allow request. Below Low → allow with no action
<strong>{t("guardrails.competitorIntentConfiguration.thresholdLowLabel")}</strong>:{" "}
{t("guardrails.competitorIntentConfiguration.thresholdLowDesc")}
</li>
</ul>
Raise thresholds to be more permissive; lower them to be stricter.
{t("guardrails.competitorIntentConfiguration.thresholdsNote")}
</>
}
>
<Space wrap>
<Form.Item label="High" style={{ marginBottom: 0 }} help="e.g. 0.7">
<Form.Item
label={t("guardrails.competitorIntentConfiguration.thresholdHighShort")}
style={{ marginBottom: 0 }}
help={t("guardrails.competitorIntentConfiguration.thresholdHighExample")}
>
<InputNumber
min={0}
max={1}
@ -294,7 +301,11 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
style={{ width: 80 }}
/>
</Form.Item>
<Form.Item label="Medium" style={{ marginBottom: 0 }} help="e.g. 0.45">
<Form.Item
label={t("guardrails.competitorIntentConfiguration.thresholdMediumShort")}
style={{ marginBottom: 0 }}
help={t("guardrails.competitorIntentConfiguration.thresholdMediumExample")}
>
<InputNumber
min={0}
max={1}
@ -304,7 +315,11 @@ const CompetitorIntentConfiguration: React.FC<CompetitorIntentConfigurationProps
style={{ width: 80 }}
/>
</Form.Item>
<Form.Item label="Low" style={{ marginBottom: 0 }} help="e.g. 0.3">
<Form.Item
label={t("guardrails.competitorIntentConfiguration.thresholdLowShort")}
style={{ marginBottom: 0 }}
help={t("guardrails.competitorIntentConfiguration.thresholdLowExample")}
>
<InputNumber
min={0}
max={1}

View file

@ -2,6 +2,7 @@ import React from "react";
import { Card, Typography, Select, Table, Tag, Collapse, Button } from "antd";
import { DeleteOutlined, PlusOutlined, FileTextOutlined } from "@ant-design/icons";
import { getCategoryYaml } from "../../networking";
import { useTranslation } from "react-i18next";
const { Title, Text } = Typography;
const { Option } = Select;
@ -42,6 +43,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
pendingSelection,
onPendingSelectionChange,
}) => {
const { t } = useTranslation();
// Use controlled state if parent provides it, otherwise use local state
const [localSelectedCategoryName, setLocalSelectedCategoryName] = React.useState<string>("");
const selectedCategoryName = pendingSelection !== undefined ? pendingSelection : localSelectedCategoryName;
@ -166,7 +168,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
const columns = [
{
title: "Category",
title: t("guardrails.contentCategoryConfiguration.colCategory"),
dataIndex: "display_name",
key: "display_name",
render: (text: string, record: SelectedCategory) => {
@ -182,7 +184,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
},
},
{
title: "Action",
title: t("guardrails.contentCategoryConfiguration.colAction"),
dataIndex: "action",
key: "action",
width: 150,
@ -202,7 +204,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
),
},
{
title: "Severity Threshold",
title: t("guardrails.contentCategoryConfiguration.colSeverityThreshold"),
dataIndex: "severity_threshold",
key: "severity_threshold",
width: 180,
@ -212,9 +214,9 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
onChange={(value) => onCategoryUpdate(record.id, "severity_threshold", value)}
style={{ width: "100%" }}
>
<Option value="low">Low</Option>
<Option value="medium">Medium</Option>
<Option value="high">High</Option>
<Option value="low">{t("guardrails.contentCategoryConfiguration.severityLow")}</Option>
<Option value="medium">{t("guardrails.contentCategoryConfiguration.severityMedium")}</Option>
<Option value="high">{t("guardrails.contentCategoryConfiguration.severityHigh")}</Option>
</Select>
),
},
@ -224,7 +226,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
width: 80,
render: (_: any, record: SelectedCategory) => (
<Button icon={<DeleteOutlined />} onClick={() => onCategoryRemove(record.id)} size="small">
Remove
{t("common.remove")}
</Button>
),
},
@ -241,10 +243,10 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
style={{ display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 8 }}
>
<Title level={5} style={{ margin: 0 }}>
Blocked topics
{t("guardrails.contentCategoryConfiguration.title")}
</Title>
<Text type="secondary" style={{ fontSize: 12, fontWeight: 400 }}>
Select topics to block using keyword and semantic analysis
{t("guardrails.contentCategoryConfiguration.subtitle")}
</Text>
</div>
}
@ -252,7 +254,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
>
<div style={{ marginBottom: 16, display: "flex", gap: 8 }}>
<Select
placeholder="Select a content category"
placeholder={t("guardrails.contentCategoryConfiguration.selectPlaceholder")}
value={selectedCategoryName || undefined}
onChange={setSelectedCategoryName}
style={{ flex: 1 }}
@ -272,7 +274,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
))}
</Select>
<Button type="primary" onClick={handleAddCategory} disabled={!selectedCategoryName} icon={<PlusOutlined />}>
Add
{t("common.add")}
</Button>
</div>
@ -296,7 +298,9 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
)}
</div>
{loadingPreviewYaml ? (
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>Loading content...</div>
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>
{t("guardrails.contentCategoryConfiguration.loadingContent")}
</div>
) : previewYaml ? (
<pre
style={{
@ -318,7 +322,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
</pre>
) : (
<div style={{ padding: "8px", textAlign: "center", color: "#888", fontSize: "12px" }}>
Unable to load category content
{t("guardrails.contentCategoryConfiguration.unableToLoadContent")}
</div>
)}
</div>
@ -355,12 +359,17 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<FileTextOutlined />
<span>
View {fileTypeLabel} for {category.display_name}
{t("guardrails.contentCategoryConfiguration.viewFileFor", {
fileType: fileTypeLabel,
name: category.display_name,
})}
</span>
</div>
),
children: loadingYaml[category.category] ? (
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>Loading content...</div>
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>
{t("guardrails.contentCategoryConfiguration.loadingContent")}
</div>
) : categoryYaml[category.category] ? (
<pre
style={{
@ -378,7 +387,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
</pre>
) : (
<div style={{ padding: "16px", textAlign: "center", color: "#888" }}>
Content will load when expanded
{t("guardrails.contentCategoryConfiguration.contentWillLoad")}
</div>
),
};
@ -396,7 +405,7 @@ const ContentCategoryConfiguration: React.FC<ContentCategoryConfigurationProps>
borderRadius: "4px",
}}
>
No blocked topics selected. Add topics to detect and block harmful content.
{t("guardrails.contentCategoryConfiguration.noTopicsSelected")}
</div>
)}
</Card>

View file

@ -3,6 +3,7 @@ import { Typography, Space, Upload, Card, Button } from "antd";
import { PlusOutlined, UploadOutlined } from "@ant-design/icons";
import { validateBlockedWordsFile } from "../../networking";
import NotificationsManager from "../../molecules/notifications_manager";
import { useTranslation } from "react-i18next";
import PatternModal from "./PatternModal";
import CustomPatternModal from "./CustomPatternModal";
import KeywordModal from "./KeywordModal";
@ -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);
@ -118,7 +120,7 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
const handleAddPrebuiltPattern = () => {
if (!selectedPatternName) {
NotificationsManager.error("Please select a pattern");
NotificationsManager.error(t("guardrails.contentFilterConfiguration.pleaseSelectPattern"));
return;
}
@ -139,7 +141,7 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
const handleAddCustomPattern = () => {
if (!customPatternName || !customPatternRegex) {
NotificationsManager.error("Please provide pattern name and regex");
NotificationsManager.error(t("guardrails.contentFilterConfiguration.pleaseProvidePatternNameAndRegex"));
return;
}
@ -159,7 +161,7 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
const handleAddKeyword = () => {
if (!newKeyword) {
NotificationsManager.error("Please enter a keyword");
NotificationsManager.error(t("guardrails.contentFilterConfiguration.pleaseEnterKeyword"));
return;
}
@ -187,14 +189,21 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
if (onFileUpload) {
onFileUpload(content);
}
NotificationsManager.success(result.message || "File uploaded successfully");
NotificationsManager.success(
result.message || t("guardrails.contentFilterConfiguration.fileUploadedSuccessfully"),
);
} else {
const errorMessage = result.error || (result.errors && result.errors.join(", ")) || "Invalid file";
NotificationsManager.error(`Validation failed: ${errorMessage}`);
const errorMessage =
result.error ||
(result.errors && result.errors.join(", ")) ||
t("guardrails.contentFilterConfiguration.invalidFile");
NotificationsManager.error(
t("guardrails.contentFilterConfiguration.validationFailed", { error: errorMessage }),
);
}
}
} catch (error) {
NotificationsManager.error(`Failed to upload file: ${error}`);
NotificationsManager.error(t("guardrails.contentFilterConfiguration.failedToUploadFile", { error }));
} finally {
setUploadValidating(false);
}
@ -210,10 +219,7 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
<div className="space-y-6">
{!showStep && (
<div>
<Text type="secondary">
Configure patterns, keywords, and content categories to detect and filter sensitive information in requests
and responses.
</Text>
<Text type="secondary">{t("guardrails.contentFilterConfiguration.description")}</Text>
</div>
)}
@ -222,10 +228,10 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
title={
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Title level={5} style={{ margin: 0 }}>
Pattern Detection
{t("guardrails.contentFilterConfiguration.patternDetectionTitle")}
</Title>
<Text type="secondary" style={{ fontSize: 14, fontWeight: 400 }}>
Detect sensitive information using regex patterns (SSN, credit cards, API keys, etc.)
{t("guardrails.contentFilterConfiguration.patternDetectionSubtitle")}
</Text>
</div>
}
@ -234,10 +240,10 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
<div style={{ marginBottom: 16 }}>
<Space>
<Button type="primary" onClick={() => setPatternModalVisible(true)} icon={<PlusOutlined />}>
Add prebuilt pattern
{t("guardrails.contentFilterConfiguration.addPrebuiltPattern")}
</Button>
<Button onClick={() => setCustomPatternModalVisible(true)} icon={<PlusOutlined />}>
Add custom regex
{t("guardrails.contentFilterConfiguration.addCustomRegex")}
</Button>
</Space>
</div>
@ -250,10 +256,10 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
title={
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Title level={5} style={{ margin: 0 }}>
Blocked Keywords
{t("guardrails.contentFilterConfiguration.blockedKeywordsTitle")}
</Title>
<Text type="secondary" style={{ fontSize: 14, fontWeight: 400 }}>
Block or mask specific sensitive terms and phrases
{t("guardrails.contentFilterConfiguration.blockedKeywordsSubtitle")}
</Text>
</div>
}
@ -262,11 +268,11 @@ const ContentFilterConfiguration: React.FC<ContentFilterConfigurationProps> = ({
<div style={{ marginBottom: 16 }}>
<Space>
<Button type="primary" onClick={() => setKeywordModalVisible(true)} icon={<PlusOutlined />}>
Add keyword
{t("guardrails.contentFilterConfiguration.addKeyword")}
</Button>
<Upload beforeUpload={handleFileUpload} accept=".yaml,.yml" showUploadList={false}>
<Button icon={<UploadOutlined />} loading={uploadValidating}>
Upload YAML file
{t("guardrails.contentFilterConfiguration.uploadYamlFile")}
</Button>
</Upload>
</Space>

View file

@ -3,6 +3,7 @@ import { Card, Text, Badge } from "@tremor/react";
import PatternTable from "./PatternTable";
import KeywordTable from "./KeywordTable";
import CategoryTable from "./CategoryTable";
import { useTranslation } from "react-i18next";
interface Pattern {
id: string;
@ -55,6 +56,8 @@ const ContentFilterDisplay: React.FC<ContentFilterDisplayProps> = ({
onCategorySeverityChange,
onCategoryRemove,
}) => {
const { t } = useTranslation();
if (patterns.length === 0 && blockedWords.length === 0 && categories.length === 0) {
return null;
}
@ -67,8 +70,10 @@ const ContentFilterDisplay: React.FC<ContentFilterDisplayProps> = ({
{categories.length > 0 && (
<Card className="mt-6">
<div className="flex justify-between items-center mb-4">
<Text className="text-lg font-semibold">Content Categories</Text>
<Badge color="blue">{categories.length} categories configured</Badge>
<Text className="text-lg font-semibold">{t("guardrails.contentFilterDisplay.contentCategories")}</Text>
<Badge color="blue">
{t("guardrails.contentFilterDisplay.categoriesConfigured", { count: categories.length })}
</Badge>
</div>
<CategoryTable
categories={categories}
@ -83,8 +88,10 @@ const ContentFilterDisplay: React.FC<ContentFilterDisplayProps> = ({
{patterns.length > 0 && (
<Card className="mt-6">
<div className="flex justify-between items-center mb-4">
<Text className="text-lg font-semibold">Pattern Detection</Text>
<Badge color="blue">{patterns.length} patterns configured</Badge>
<Text className="text-lg font-semibold">{t("guardrails.contentFilterDisplay.patternDetection")}</Text>
<Badge color="blue">
{t("guardrails.contentFilterDisplay.patternsConfigured", { count: patterns.length })}
</Badge>
</div>
<PatternTable
patterns={patterns}
@ -97,8 +104,10 @@ const ContentFilterDisplay: React.FC<ContentFilterDisplayProps> = ({
{blockedWords.length > 0 && (
<Card className="mt-6">
<div className="flex justify-between items-center mb-4">
<Text className="text-lg font-semibold">Blocked Keywords</Text>
<Badge color="blue">{blockedWords.length} keywords configured</Badge>
<Text className="text-lg font-semibold">{t("guardrails.contentFilterDisplay.blockedKeywords")}</Text>
<Badge color="blue">
{t("guardrails.contentFilterDisplay.keywordsConfigured", { count: blockedWords.length })}
</Badge>
</div>
<KeywordTable
keywords={blockedWords}

View file

@ -3,6 +3,7 @@ import React, { useEffect, useState } from "react";
import ContentFilterConfiguration from "./ContentFilterConfiguration";
import ContentFilterDisplay from "./ContentFilterDisplay";
import type { CompetitorIntentConfig } from "./CompetitorIntentConfiguration";
import { useTranslation } from "react-i18next";
const { Text } = Typography;
@ -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[]>([]);
@ -233,18 +235,13 @@ const ContentFilterManager: React.FC<ContentFilterManagerProps> = ({
// Edit mode
return (
<>
<Divider orientation="left">Content Filter Configuration</Divider>
<Divider orientation="left">{t("guardrails.contentFilterManager.dividerTitle")}</Divider>
{hasUnsavedChanges && (
<Alert
type="warning"
showIcon
className="mb-4"
message={
<Text>
You have unsaved changes to patterns or keywords. Remember to click &quot;Save Changes&quot; at the
bottom.
</Text>
}
message={<Text>{t("guardrails.contentFilterManager.unsavedChangesWarning")}</Text>}
/>
)}
<div className="mb-6">

View file

@ -1,5 +1,6 @@
import React from "react";
import { Typography, Select, Modal, Space, Button, Input } from "antd";
import { useTranslation } from "react-i18next";
const { Text } = Typography;
const { Option } = Select;
@ -27,13 +28,20 @@ const CustomPatternModal: React.FC<CustomPatternModalProps> = ({
onAdd,
onCancel,
}) => {
const { t } = useTranslation();
return (
<Modal title="Add custom regex pattern" open={visible} onCancel={onCancel} footer={null} width={800}>
<Modal
title={t("guardrails.customPatternModal.title")}
open={visible}
onCancel={onCancel}
footer={null}
width={800}
>
<Space direction="vertical" style={{ width: "100%" }} size="large">
<div>
<Text strong>Pattern name</Text>
<Text strong>{t("guardrails.customPatternModal.patternNameLabel")}</Text>
<Input
placeholder="e.g., internal_id, employee_code"
placeholder={t("guardrails.customPatternModal.patternNamePlaceholder")}
value={patternName}
onChange={(e) => onNameChange(e.target.value)}
style={{ marginTop: 8 }}
@ -41,34 +49,34 @@ const CustomPatternModal: React.FC<CustomPatternModalProps> = ({
</div>
<div>
<Text strong>Regex pattern</Text>
<Text strong>{t("guardrails.customPatternModal.regexPatternLabel")}</Text>
<Input
placeholder="e.g., ID-[0-9]{6}"
placeholder={t("guardrails.customPatternModal.regexPatternPlaceholder")}
value={patternRegex}
onChange={(e) => onRegexChange(e.target.value)}
style={{ marginTop: 8 }}
/>
<Text type="secondary" style={{ fontSize: 12 }}>
Enter a valid regular expression to match sensitive data
{t("guardrails.customPatternModal.regexPatternHelp")}
</Text>
</div>
<div>
<Text strong>Action</Text>
<Text strong>{t("guardrails.customPatternModal.actionLabel")}</Text>
<Text type="secondary" style={{ display: "block", marginTop: 4, marginBottom: 8 }}>
Choose what action the guardrail should take when this pattern is detected
{t("guardrails.customPatternModal.actionHelp")}
</Text>
<Select value={patternAction} onChange={onActionChange} style={{ width: "100%" }}>
<Option value="BLOCK">Block</Option>
<Option value="MASK">Mask</Option>
<Option value="BLOCK">{t("guardrails.customPatternModal.actionBlock")}</Option>
<Option value="MASK">{t("guardrails.customPatternModal.actionMask")}</Option>
</Select>
</div>
</Space>
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "24px" }}>
<Button onClick={onCancel}>Cancel</Button>
<Button onClick={onCancel}>{t("common.cancel")}</Button>
<Button type="primary" onClick={onAdd}>
Add
{t("common.add")}
</Button>
</div>
</Modal>

View file

@ -2607,6 +2607,113 @@
"errorTitle": "{{name}} - Error",
"resultCopied": "Result copied to clipboard",
"failedToCopyResult": "Failed to copy result"
},
"categoryTable": {
"colCategory": "Category",
"colSeverityThreshold": "Severity Threshold",
"colAction": "Action",
"severityHigh": "High",
"severityMedium": "Medium",
"severityLow": "Low",
"actionBlock": "Block",
"actionMask": "Mask",
"noCategories": "No categories configured."
},
"competitorIntentConfiguration": {
"title": "Competitor Intent Filter",
"disabledDescription": "Block or reframe competitor comparison questions. When enabled, airline type auto-loads competitors from IATA; generic type requires manual competitor list.",
"enabledDescription": "Block or reframe competitor comparison questions. Airline type uses major airlines (excluding your brand); generic requires manual competitor list.",
"typeAirline": "Airline (auto-load competitors from IATA)",
"typeGeneric": "Generic (specify competitors manually)",
"brandSelfLabel": "Your Brand (brand_self)",
"brandSelfHelpAirline": "Select your airline from the list (excluded from competitors) or type to add a custom term",
"brandSelfHelpGeneric": "Names/codes users use for your brand",
"loadingAirlines": "Loading airlines...",
"brandSelfPlaceholderAirline": "Search or select airline, or type to add custom",
"brandSelfPlaceholderGeneric": "Type and press Enter to add",
"locationsLabel": "Locations (optional)",
"locationsHelp": "Countries, cities, airports for disambiguation (e.g. qatar, doha)",
"competitorsLabel": "Competitors",
"competitorsHelp": "Competitor names to detect (required for generic type)",
"tagsPlaceholder": "Type and press Enter to add",
"policyCompetitorLabel": "Policy: Competitor comparison",
"policyPossibleCompetitorLabel": "Policy: Possible competitor comparison",
"policyRefuse": "Refuse (block request)",
"policyReframe": "Reframe (suggest alternative)",
"policyReframeBackend": "Reframe (suggest alternative to backend LLM)",
"confidenceThresholdsLabel": "Confidence thresholds",
"confidenceThresholdsHelp": "Classify competitor intent by confidence (0–1). Higher confidence → stronger intent.",
"thresholdHighLabel": "High (≥)",
"thresholdHighDesc": "Treat as full competitor comparison → uses \"Competitor comparison\" policy",
"thresholdMediumLabel": "Medium (≥)",
"thresholdMediumDesc": "Treat as possible comparison → uses \"Possible competitor comparison\" policy",
"thresholdLowLabel": "Low (≥)",
"thresholdLowDesc": "Log only; allow request. Below Low → allow with no action",
"thresholdsNote": "Raise thresholds to be more permissive; lower them to be stricter.",
"thresholdHighShort": "High",
"thresholdHighExample": "e.g. 0.7",
"thresholdMediumShort": "Medium",
"thresholdMediumExample": "e.g. 0.45",
"thresholdLowShort": "Low",
"thresholdLowExample": "e.g. 0.3"
},
"contentCategoryConfiguration": {
"title": "Blocked topics",
"subtitle": "Select topics to block using keyword and semantic analysis",
"colCategory": "Category",
"colAction": "Action",
"colSeverityThreshold": "Severity Threshold",
"severityLow": "Low",
"severityMedium": "Medium",
"severityHigh": "High",
"selectPlaceholder": "Select a content category",
"loadingContent": "Loading content...",
"unableToLoadContent": "Unable to load category content",
"viewFileFor": "View {{fileType}} for {{name}}",
"contentWillLoad": "Content will load when expanded",
"noTopicsSelected": "No blocked topics selected. Add topics to detect and block harmful content."
},
"contentFilterConfiguration": {
"description": "Configure patterns, keywords, and content categories to detect and filter sensitive information in requests and responses.",
"patternDetectionTitle": "Pattern Detection",
"patternDetectionSubtitle": "Detect sensitive information using regex patterns (SSN, credit cards, API keys, etc.)",
"addPrebuiltPattern": "Add prebuilt pattern",
"addCustomRegex": "Add custom regex",
"blockedKeywordsTitle": "Blocked Keywords",
"blockedKeywordsSubtitle": "Block or mask specific sensitive terms and phrases",
"addKeyword": "Add keyword",
"uploadYamlFile": "Upload YAML file",
"pleaseSelectPattern": "Please select a pattern",
"pleaseProvidePatternNameAndRegex": "Please provide pattern name and regex",
"pleaseEnterKeyword": "Please enter a keyword",
"fileUploadedSuccessfully": "File uploaded successfully",
"invalidFile": "Invalid file",
"validationFailed": "Validation failed: {{error}}",
"failedToUploadFile": "Failed to upload file: {{error}}"
},
"contentFilterDisplay": {
"contentCategories": "Content Categories",
"categoriesConfigured": "{{count}} categories configured",
"patternDetection": "Pattern Detection",
"patternsConfigured": "{{count}} patterns configured",
"blockedKeywords": "Blocked Keywords",
"keywordsConfigured": "{{count}} keywords configured"
},
"contentFilterManager": {
"dividerTitle": "Content Filter Configuration",
"unsavedChangesWarning": "You have unsaved changes to patterns or keywords. Remember to click \"Save Changes\" at the bottom."
},
"customPatternModal": {
"title": "Add custom regex pattern",
"patternNameLabel": "Pattern name",
"patternNamePlaceholder": "e.g., internal_id, employee_code",
"regexPatternLabel": "Regex pattern",
"regexPatternPlaceholder": "e.g., ID-[0-9]{6}",
"regexPatternHelp": "Enter a valid regular expression to match sensitive data",
"actionLabel": "Action",
"actionHelp": "Choose what action the guardrail should take when this pattern is detected",
"actionBlock": "Block",
"actionMask": "Mask"
}
}
}

View file

@ -2607,6 +2607,113 @@
"errorTitle": "{{name}} - 错误",
"resultCopied": "结果已复制到剪贴板",
"failedToCopyResult": "复制结果失败"
},
"categoryTable": {
"colCategory": "分类",
"colSeverityThreshold": "严重程度阈值",
"colAction": "操作",
"severityHigh": "高",
"severityMedium": "中",
"severityLow": "低",
"actionBlock": "拦截",
"actionMask": "遮蔽",
"noCategories": "未配置任何分类。"
},
"competitorIntentConfiguration": {
"title": "竞争对手意图过滤器",
"disabledDescription": "拦截或重构竞争对手比较问题。启用后,航空公司类型会自动从 IATA 加载竞争对手;通用类型需手动指定竞争对手列表。",
"enabledDescription": "拦截或重构竞争对手比较问题。航空公司类型使用主要航空公司(排除您的品牌);通用类型需手动指定竞争对手列表。",
"typeAirline": "航空公司(自动从 IATA 加载竞争对手)",
"typeGeneric": "通用(手动指定竞争对手)",
"brandSelfLabel": "您的品牌(brand_self)",
"brandSelfHelpAirline": "从列表中选择您的航空公司(已排除在竞争对手之外),或输入自定义词语",
"brandSelfHelpGeneric": "用户用来指代您品牌的名称或代码",
"loadingAirlines": "正在加载航空公司...",
"brandSelfPlaceholderAirline": "搜索或选择航空公司,或输入自定义内容",
"brandSelfPlaceholderGeneric": "输入后按 Enter 添加",
"locationsLabel": "地点(可选)",
"locationsHelp": "用于消歧的国家、城市、机场(如:qatar、doha)",
"competitorsLabel": "竞争对手",
"competitorsHelp": "要检测的竞争对手名称(通用类型必填)",
"tagsPlaceholder": "输入后按 Enter 添加",
"policyCompetitorLabel": "策略:竞争对手比较",
"policyPossibleCompetitorLabel": "策略:可能的竞争对手比较",
"policyRefuse": "拒绝(拦截请求)",
"policyReframe": "重构(建议替代方案)",
"policyReframeBackend": "重构(向后端 LLM 建议替代方案)",
"confidenceThresholdsLabel": "置信度阈值",
"confidenceThresholdsHelp": "按置信度(0–1)对竞争对手意图分类。置信度越高,意图越强。",
"thresholdHighLabel": "高(≥)",
"thresholdHighDesc": "视为完整的竞争对手比较 → 使用「竞争对手比较」策略",
"thresholdMediumLabel": "中(≥)",
"thresholdMediumDesc": "视为可能的比较 → 使用「可能的竞争对手比较」策略",
"thresholdLowLabel": "低(≥)",
"thresholdLowDesc": "仅记录日志,允许请求。低于低阈值 → 无操作直接允许",
"thresholdsNote": "提高阈值以放宽限制;降低阈值以收紧限制。",
"thresholdHighShort": "高",
"thresholdHighExample": "例如:0.7",
"thresholdMediumShort": "中",
"thresholdMediumExample": "例如:0.45",
"thresholdLowShort": "低",
"thresholdLowExample": "例如:0.3"
},
"contentCategoryConfiguration": {
"title": "封锁主题",
"subtitle": "使用关键词和语义分析选择要封锁的主题",
"colCategory": "分类",
"colAction": "操作",
"colSeverityThreshold": "严重程度阈值",
"severityLow": "低",
"severityMedium": "中",
"severityHigh": "高",
"selectPlaceholder": "选择内容分类",
"loadingContent": "正在加载内容...",
"unableToLoadContent": "无法加载分类内容",
"viewFileFor": "查看 {{fileType}} 文件:{{name}}",
"contentWillLoad": "展开后将加载内容",
"noTopicsSelected": "未选择封锁主题。添加主题以检测和拦截有害内容。"
},
"contentFilterConfiguration": {
"description": "配置模式、关键词和内容分类,以检测和过滤请求及响应中的敏感信息。",
"patternDetectionTitle": "模式检测",
"patternDetectionSubtitle": "使用正则表达式模式检测敏感信息(身份证号、信用卡、API 密钥等)",
"addPrebuiltPattern": "添加预置模式",
"addCustomRegex": "添加自定义正则",
"blockedKeywordsTitle": "封锁关键词",
"blockedKeywordsSubtitle": "拦截或遮蔽特定敏感词语和短语",
"addKeyword": "添加关键词",
"uploadYamlFile": "上传 YAML 文件",
"pleaseSelectPattern": "请选择一个模式",
"pleaseProvidePatternNameAndRegex": "请提供模式名称和正则表达式",
"pleaseEnterKeyword": "请输入关键词",
"fileUploadedSuccessfully": "文件上传成功",
"invalidFile": "无效文件",
"validationFailed": "验证失败:{{error}}",
"failedToUploadFile": "上传文件失败:{{error}}"
},
"contentFilterDisplay": {
"contentCategories": "内容分类",
"categoriesConfigured": "已配置 {{count}} 个分类",
"patternDetection": "模式检测",
"patternsConfigured": "已配置 {{count}} 个模式",
"blockedKeywords": "封锁关键词",
"keywordsConfigured": "已配置 {{count}} 个关键词"
},
"contentFilterManager": {
"dividerTitle": "内容过滤器配置",
"unsavedChangesWarning": "模式或关键词有未保存的更改,请记得点击底部的「保存更改」。"
},
"customPatternModal": {
"title": "添加自定义正则模式",
"patternNameLabel": "模式名称",
"patternNamePlaceholder": "例如:internal_id、employee_code",
"regexPatternLabel": "正则表达式",
"regexPatternPlaceholder": "例如:ID-[0-9]{6}",
"regexPatternHelp": "输入有效的正则表达式以匹配敏感数据",
"actionLabel": "操作",
"actionHelp": "选择当检测到此模式时护栏应执行的操作",
"actionBlock": "拦截",
"actionMask": "遮蔽"
}
}
}