feat(ui): i18n for policies batch 5

This commit is contained in:
吹雪 2026-06-11 00:24:12 +09:00
parent aa883b580e
commit 13276aea0a
7 changed files with 312 additions and 107 deletions

View file

@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from "react";
import { Card, Badge, Button } from "@tremor/react";
import { ArrowLeftIcon, PencilIcon } from "@heroicons/react/outline";
import { Descriptions, Tag, Spin, Divider, Typography, Alert } from "antd";
import { useTranslation } from "react-i18next";
import { Policy } from "./types";
import { PipelineInfoDisplay } from "./pipeline_flow_builder";
import { getResolvedGuardrails } from "../networking";
@ -25,6 +26,7 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
isAdmin,
getPolicy,
}) => {
const { t } = useTranslation();
const [policy, setPolicy] = useState<Policy | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [resolvedGuardrails, setResolvedGuardrails] = useState<string[]>([]);
@ -70,10 +72,10 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
if (!policy) {
return (
<Card>
<Text type="danger">Policy not found</Text>
<Text type="danger">{t("policies.policyInfo.notFound")}</Text>
<br />
<Button onClick={onClose} className="mt-4">
Go Back
{t("common.back")}
</Button>
</Card>
);
@ -84,11 +86,11 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
<div className="space-y-6">
<div className="flex justify-between items-center">
<Button variant="secondary" icon={ArrowLeftIcon} onClick={onClose}>
Back to Policies
{t("policies.policyInfo.backToPolicies")}
</Button>
{isAdmin && (
<Button icon={PencilIcon} onClick={() => onEdit(policy)}>
Edit Policy
{t("policies.policyInfo.editPolicy")}
</Button>
)}
</div>
@ -96,25 +98,25 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
<Title level={4}>{policy.policy_name}</Title>
<Descriptions bordered column={1}>
<Descriptions.Item label="Policy ID">
<Descriptions.Item label={t("policies.policyInfo.policyId")}>
<code className="text-xs bg-gray-100 px-2 py-1 rounded">{policy.policy_id}</code>
</Descriptions.Item>
<Descriptions.Item label="Description">
{policy.description || <Text type="secondary">No description</Text>}
<Descriptions.Item label={t("common.description")}>
{policy.description || <Text type="secondary">{t("policies.policyInfo.noDescription")}</Text>}
</Descriptions.Item>
<Descriptions.Item label="Inherits From">
<Descriptions.Item label={t("policies.policyInfo.inheritsFrom")}>
{policy.inherit ? (
<Badge color="blue" size="sm">
{policy.inherit}
</Badge>
) : (
<Text type="secondary">None</Text>
<Text type="secondary">{t("common.none")}</Text>
)}
</Descriptions.Item>
<Descriptions.Item label="Created At">
<Descriptions.Item label={t("common.createdAt")}>
{policy.created_at ? new Date(policy.created_at).toLocaleString() : "-"}
</Descriptions.Item>
<Descriptions.Item label="Updated At">
<Descriptions.Item label={t("common.updatedAt")}>
{policy.updated_at ? new Date(policy.updated_at).toLocaleString() : "-"}
</Descriptions.Item>
</Descriptions>
@ -122,10 +124,13 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
{policy.pipeline && (
<>
<Divider orientation="left">
<Text strong>Pipeline Flow</Text>
<Text strong>{t("policies.policyInfo.pipelineFlow")}</Text>
</Divider>
<Alert
message={`Pipeline (${policy.pipeline.mode} mode, ${policy.pipeline.steps.length} step${policy.pipeline.steps.length !== 1 ? "s" : ""})`}
message={t("policies.policyInfo.pipelineInfo", {
count: policy.pipeline.steps.length,
mode: policy.pipeline.mode,
})}
type="info"
showIcon
style={{ marginBottom: 16 }}
@ -135,16 +140,16 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
)}
<Divider orientation="left">
<Text strong>Guardrails Configuration</Text>
<Text strong>{t("policies.policyInfo.guardrailsConfiguration")}</Text>
</Divider>
{resolvedGuardrails.length > 0 && (
<Alert
message="Resolved Guardrails"
message={t("policies.policyInfo.resolvedGuardrails")}
description={
<div>
<Text type="secondary" style={{ display: "block", marginBottom: 8 }}>
Final guardrails that will be applied (including inheritance):
{t("policies.policyInfo.resolvedGuardrailsDesc")}
</Text>
<div className="flex flex-wrap gap-1">
{resolvedGuardrails.map((g) => (
@ -162,7 +167,7 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
)}
<Descriptions bordered column={1}>
<Descriptions.Item label="Guardrails to Add">
<Descriptions.Item label={t("policies.policyInfo.guardrailsToAdd")}>
<div className="flex flex-wrap gap-1">
{policy.guardrails_add && policy.guardrails_add.length > 0 ? (
policy.guardrails_add.map((g) => (
@ -171,11 +176,11 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
</Tag>
))
) : (
<Text type="secondary">None</Text>
<Text type="secondary">{t("common.none")}</Text>
)}
</div>
</Descriptions.Item>
<Descriptions.Item label="Guardrails to Remove">
<Descriptions.Item label={t("policies.policyInfo.guardrailsToRemove")}>
<div className="flex flex-wrap gap-1">
{policy.guardrails_remove && policy.guardrails_remove.length > 0 ? (
policy.guardrails_remove.map((g) => (
@ -184,18 +189,18 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
</Tag>
))
) : (
<Text type="secondary">None</Text>
<Text type="secondary">{t("common.none")}</Text>
)}
</div>
</Descriptions.Item>
</Descriptions>
<Divider orientation="left">
<Text strong>Conditions</Text>
<Text strong>{t("policies.policyInfo.conditions")}</Text>
</Divider>
<Descriptions bordered column={1}>
<Descriptions.Item label="Model Condition">
<Descriptions.Item label={t("policies.policyInfo.modelCondition")}>
{policy.condition?.model ? (
<Tag color="purple">
{typeof policy.condition.model === "string"
@ -203,7 +208,7 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
: JSON.stringify(policy.condition.model)}
</Tag>
) : (
<Text type="secondary">No model condition (applies to all models)</Text>
<Text type="secondary">{t("policies.policyInfo.noModelCondition")}</Text>
)}
</Descriptions.Item>
</Descriptions>

View file

@ -2,6 +2,7 @@ import React, { useMemo, useState } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Button, Badge } from "@tremor/react";
import { TrashIcon, PencilIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
import { Tooltip, Tag } from "antd";
import { useTranslation } from "react-i18next";
import {
ColumnDef,
flexRender,
@ -55,6 +56,7 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
onViewClick,
isAdmin = false,
}) => {
const { t } = useTranslation();
const [sorting, setSorting] = useState<SortingState>([{ id: "policy_name", desc: false }]);
const rows = useMemo(() => groupPoliciesByName(policies), [policies]);
@ -67,14 +69,14 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
const columns: ColumnDef<PolicyRow>[] = [
{
header: "Name",
header: t("common.name"),
accessorKey: "policy_name",
cell: ({ row }) => {
const { primaryPolicy, versionCount } = row.original;
return (
<div className="flex items-center gap-2">
<Tooltip
title={`${primaryPolicy.policy_name || "-"}${versionCount > 1 ? ` (${versionCount} versions)` : ""}`}
title={`${primaryPolicy.policy_name || "-"}${versionCount > 1 ? ` (${t("policies.policyTable.versionCount", { count: versionCount })})` : ""}`}
>
<Button
size="xs"
@ -87,7 +89,7 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
</Tooltip>
{versionCount > 1 && (
<Badge color="gray" size="xs">
{versionCount} version{versionCount !== 1 ? "s" : ""}
{t("policies.policyTable.versionCount", { count: versionCount })}
</Badge>
)}
</div>
@ -95,7 +97,7 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
},
},
{
header: "Description",
header: t("common.description"),
accessorFn: (row) => row.primaryPolicy.description ?? "",
cell: ({ row }) => {
const policy = row.original.primaryPolicy;
@ -107,7 +109,7 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
},
},
{
header: "Inherits From",
header: t("policies.policyTable.inheritsFrom"),
accessorFn: (row) => row.primaryPolicy.inherit ?? "",
cell: ({ row }) => {
const policy = row.original.primaryPolicy;
@ -121,7 +123,7 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
},
},
{
header: "Guardrails (Add)",
header: t("policies.policyTable.guardrailsAdd"),
accessorFn: (row) => (row.primaryPolicy.guardrails_add ?? []).join(", "),
cell: ({ row }) => {
const policy = row.original.primaryPolicy;
@ -146,7 +148,7 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
},
},
{
header: "Guardrails (Remove)",
header: t("policies.policyTable.guardrailsRemove"),
accessorFn: (row) => (row.primaryPolicy.guardrails_remove ?? []).join(", "),
cell: ({ row }) => {
const policy = row.original.primaryPolicy;
@ -171,7 +173,7 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
},
},
{
header: "Model Condition",
header: t("policies.policyTable.modelCondition"),
accessorFn: (row) => {
const m = row.primaryPolicy.condition?.model;
return typeof m === "string" ? m : JSON.stringify(m ?? "");
@ -189,14 +191,14 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
? modelCondition.length > 20
? modelCondition.slice(0, 20) + "..."
: modelCondition
: "Multiple"}
: t("policies.policyTable.multiple")}
</code>
</Tooltip>
);
},
},
{
header: "Created At",
header: t("common.createdAt"),
id: "created_at",
accessorFn: (row) => row.primaryPolicy.created_at ?? "",
cell: ({ row }) => {
@ -210,7 +212,7 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
},
{
id: "actions",
header: "Actions",
header: t("common.actions"),
cell: ({ row }) => {
const { primaryPolicy } = row.original;
const policy = primaryPolicy;
@ -218,7 +220,7 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
<div className="flex space-x-2">
{isAdmin && (
<>
<Tooltip title="Edit policy">
<Tooltip title={t("policies.policyTable.editPolicy")}>
<Icon
icon={PencilIcon}
size="sm"
@ -226,12 +228,13 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
className="cursor-pointer hover:text-blue-500"
/>
</Tooltip>
<Tooltip title="Delete policy">
<Tooltip title={t("policies.policyTable.deletePolicy")}>
<Icon
icon={TrashIcon}
size="sm"
onClick={() =>
policy.policy_id && onDeleteClick(policy.policy_id, policy.policy_name || "Unnamed Policy")
policy.policy_id &&
onDeleteClick(policy.policy_id, policy.policy_name || t("policies.policyTable.unnamedPolicy"))
}
className="cursor-pointer hover:text-red-500"
/>
@ -298,7 +301,7 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>Loading...</p>
<p>{t("common.loading")}</p>
</div>
</TableCell>
</TableRow>
@ -323,7 +326,7 @@ const PolicyTable: React.FC<PolicyTableProps> = ({
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No policies found</p>
<p>{t("policies.policyTable.noPoliciesFound")}</p>
</div>
</TableCell>
</TableRow>

View file

@ -1,5 +1,6 @@
import React, { useState, useEffect, useMemo } from "react";
import { Card, Button, Spin, Checkbox } from "antd";
import { useTranslation } from "react-i18next";
import MessageManager from "@/components/molecules/message_manager";
import {
ShieldCheckIcon,
@ -35,6 +36,8 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
complexity,
onUseTemplate,
}) => {
const { t } = useTranslation();
const getComplexityStyle = () => {
switch (complexity) {
case "Low":
@ -56,7 +59,7 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
<Icon className={`h-6 w-6 ${iconColor}`} />
</div>
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium border ${getComplexityStyle()}`}>
{complexity} Complexity
{t("policies.policyTemplates.complexityLabel", { complexity })}
</span>
</div>
@ -78,14 +81,14 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
{inherits && (
<div className="mb-4 text-xs">
<span className="text-gray-500">Inherits from: </span>
<span className="text-gray-500">{t("policies.policyTemplates.inheritsFrom")} </span>
<span className="font-medium text-gray-700 bg-gray-100 px-2 py-0.5 rounded">{inherits}</span>
</div>
)}
<div className="mb-6">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider block mb-2">
Included Guardrails
{t("policies.policyTemplates.includedGuardrails")}
</span>
<div className="flex flex-wrap gap-2">
{guardrails.map((g) => (
@ -100,7 +103,7 @@ const PolicyTemplateCard: React.FC<PolicyTemplateCardProps> = ({
</div>
<Button type="primary" block className="mt-auto" onClick={onUseTemplate}>
Use Template
{t("policies.policyTemplates.useTemplate")}
</Button>
</Card>
);
@ -128,6 +131,7 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({
onTemplatesLoaded,
accessToken,
}) => {
const { t } = useTranslation();
const [templates, setTemplates] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
@ -181,7 +185,7 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({
onTemplatesLoaded?.(data);
} catch (error) {
console.error("Error fetching policy templates:", error);
MessageManager.error("Failed to fetch policy templates");
MessageManager.error(t("policies.policyTemplates.fetchFailed"));
} finally {
setIsLoading(false);
}
@ -193,7 +197,7 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({
if (isLoading) {
return (
<div className="flex justify-center items-center py-20">
<Spin size="large" tip="Loading policy templates..." />
<Spin size="large" tip={t("policies.policyTemplates.loadingTemplates")} />
</div>
);
}
@ -202,16 +206,14 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({
<div className="space-y-6">
<div className="flex justify-between items-end">
<div>
<h2 className="text-lg font-medium text-gray-900">Policy Templates</h2>
<p className="text-sm text-gray-500 mt-1">
Start with a pre-configured policy template to quickly set up guardrails for your organization.
</p>
<h2 className="text-lg font-medium text-gray-900">{t("policies.policyTemplates.title")}</h2>
<p className="text-sm text-gray-500 mt-1">{t("policies.policyTemplates.subtitle")}</p>
</div>
<Button type="default" onClick={onOpenAiSuggestion} className="flex items-center gap-1.5">
<svg className="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 1l1.5 3.5L13 6l-3.5 1.5L8 11 6.5 7.5 3 6l3.5-1.5L8 1zm4 7l.75 1.75L14.5 10.5l-1.75.75L12 13l-.75-1.75L9.5 10.5l1.75-.75L12 8zM4 9l.75 1.75L6.5 11.5l-1.75.75L4 14l-.75-1.75L1.5 11.5l1.75-.75L4 9z" />
</svg>
Use AI to find templates
{t("policies.policyTemplates.useAiToFind")}
</Button>
</div>
@ -221,10 +223,10 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({
<div className="w-52 flex-shrink-0">
<div className="sticky top-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-semibold text-gray-900">Categories</span>
<span className="text-sm font-semibold text-gray-900">{t("policies.policyTemplates.categories")}</span>
{selectedTags.size > 0 && (
<button onClick={handleClearAll} className="text-xs text-blue-600 hover:text-blue-800">
Clear all
{t("common.clear")}
</button>
)}
</div>
@ -252,7 +254,10 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({
<div className="flex-1">
{selectedTags.size > 0 && (
<div className="mb-4 text-sm text-gray-500">
Showing {filteredTemplates.length} of {templates.length} templates
{t("policies.policyTemplates.showingCount", {
filtered: filteredTemplates.length,
total: templates.length,
})}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
@ -275,9 +280,9 @@ const PolicyTemplates: React.FC<PolicyTemplatesProps> = ({
{filteredTemplates.length === 0 && (
<div className="text-center py-12 text-gray-500">
<p>No templates match the selected filters.</p>
<p>{t("policies.policyTemplates.noTemplatesMatch")}</p>
<button onClick={handleClearAll} className="text-blue-600 hover:text-blue-800 mt-2 text-sm">
Clear all filters
{t("policies.policyTemplates.clearAllFilters")}
</button>
</div>
)}

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Form, Select, Alert, Tag, Empty, Typography } from "antd";
import { Button } from "@tremor/react";
import { useTranslation } from "react-i18next";
import { resolvePoliciesCall, teamListCall, keyListCall, modelAvailableCall } from "../networking";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
@ -22,6 +23,7 @@ interface ResolveResult {
}
const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [isLoading, setIsLoading] = useState(false);
const [result, setResult] = useState<ResolveResult | null>(null);
@ -99,46 +101,43 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
<div>
<div className="bg-white border rounded-lg p-6 mb-6">
<div className="mb-5">
<h3 className="text-base font-semibold mb-1">Policy Simulator</h3>
<Text type="secondary">
Simulate a request to see which policies and guardrails would apply. Select a team, key, model, or tags
below and click &quot;Simulate&quot; to see the results.
</Text>
<h3 className="text-base font-semibold mb-1">{t("policies.policyTestPanel.title")}</h3>
<Text type="secondary">{t("policies.policyTestPanel.subtitle")}</Text>
</div>
<Form form={form} layout="vertical">
<div className="grid grid-cols-2 gap-4">
<Form.Item name="team_alias" label="Team Alias" className="mb-3">
<Form.Item name="team_alias" label={t("policies.policyTestPanel.teamAliasLabel")} className="mb-3">
<Select
showSearch
allowClear
placeholder="Select or type a team alias"
options={availableTeams.map((t) => ({ label: t, value: t }))}
placeholder={t("policies.policyTestPanel.teamAliasPlaceholder")}
options={availableTeams.map((team) => ({ label: team, value: team }))}
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
/>
</Form.Item>
<Form.Item name="key_alias" label="Key Alias" className="mb-3">
<Form.Item name="key_alias" label={t("policies.policyTestPanel.keyAliasLabel")} className="mb-3">
<Select
showSearch
allowClear
placeholder="Select or type a key alias"
placeholder={t("policies.policyTestPanel.keyAliasPlaceholder")}
options={availableKeys.map((k) => ({ label: k, value: k }))}
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
/>
</Form.Item>
<Form.Item name="model" label="Model" className="mb-3">
<Form.Item name="model" label={t("policies.policyTestPanel.modelLabel")} className="mb-3">
<Select
showSearch
allowClear
placeholder="Select or type a model"
placeholder={t("policies.policyTestPanel.modelPlaceholder")}
options={availableModels.map((m) => ({ label: m, value: m }))}
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
/>
</Form.Item>
<Form.Item name="tags" label="Tags" className="mb-3">
<Form.Item name="tags" label={t("policies.policyTestPanel.tagsLabel")} className="mb-3">
<Select
mode="tags"
placeholder="Type a tag and press Enter"
placeholder={t("policies.policyTestPanel.tagsPlaceholder")}
tokenSeparators={[",", " "]}
notFoundContent={null}
suffixIcon={null}
@ -148,10 +147,10 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
</div>
<div className="flex space-x-2">
<Button onClick={handleTest} loading={isLoading} disabled={!accessToken}>
Simulate
{t("policies.policyTestPanel.simulate")}
</Button>
<Button variant="secondary" onClick={handleReset}>
Reset
{t("common.reset")}
</Button>
</div>
</Form>
@ -175,22 +174,19 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
/>
</svg>
</div>
<p className="text-sm font-medium text-gray-600 mb-1">No simulation run yet</p>
<p className="text-xs text-gray-400">
Fill in one or more fields above and click &quot;Simulate&quot; to see which policies and guardrails would
apply to that request.
</p>
<p className="text-sm font-medium text-gray-600 mb-1">{t("policies.policyTestPanel.noSimulationYet")}</p>
<p className="text-xs text-gray-400">{t("policies.policyTestPanel.noSimulationHint")}</p>
</div>
)}
{hasSearched && result && (
<div className="bg-white border rounded-lg p-6">
{result.matched_policies.length === 0 ? (
<Empty description="No policies matched this context" />
<Empty description={t("policies.policyTestPanel.noPoliciesMatched")} />
) : (
<>
<div className="mb-4">
<p className="text-sm font-semibold mb-2">Effective Guardrails</p>
<p className="text-sm font-semibold mb-2">{t("policies.policyTestPanel.effectiveGuardrails")}</p>
<div className="flex flex-wrap gap-1">
{result.effective_guardrails.length > 0 ? (
result.effective_guardrails.map((g) => (
@ -199,19 +195,19 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
</Tag>
))
) : (
<span className="text-gray-400 text-sm">None</span>
<span className="text-gray-400 text-sm">{t("common.none")}</span>
)}
</div>
</div>
<div>
<p className="text-sm font-semibold mb-2">Matched Policies</p>
<p className="text-sm font-semibold mb-2">{t("policies.policyTestPanel.matchedPolicies")}</p>
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-2 pr-4">Policy</th>
<th className="text-left py-2 pr-4">Matched Via</th>
<th className="text-left py-2">Guardrails Added</th>
<th className="text-left py-2 pr-4">{t("policies.policyTestPanel.colPolicy")}</th>
<th className="text-left py-2 pr-4">{t("policies.policyTestPanel.colMatchedVia")}</th>
<th className="text-left py-2">{t("policies.policyTestPanel.colGuardrailsAdded")}</th>
</tr>
</thead>
<tbody>
@ -231,7 +227,7 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
))}
</div>
) : (
<span className="text-gray-400">None</span>
<span className="text-gray-400">{t("common.none")}</span>
)}
</td>
</tr>
@ -245,7 +241,12 @@ const PolicyTestPanel: React.FC<PolicyTestPanelProps> = ({ accessToken }) => {
)}
{hasSearched && !result && !isLoading && (
<Alert message="Error" description="Failed to resolve policies. Check the proxy logs." type="error" showIcon />
<Alert
message={t("common.error")}
description={t("policies.policyTestPanel.resolveFailed")}
type="error"
showIcon
/>
)}
</div>
);

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Modal, Spin, Radio, Select } from "antd";
import { Button, TextInput } from "@tremor/react";
import { useTranslation } from "react-i18next";
import { modelHubCall, enrichPolicyTemplateStream } from "../networking";
interface TemplateParameter {
@ -31,6 +32,7 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
isLoading = false,
accessToken,
}) => {
const { t } = useTranslation();
const [parameterValues, setParameterValues] = useState<Record<string, string>>({});
const [competitorMode, setCompetitorMode] = useState<"ai" | "manual">("ai");
const [selectedModel, setSelectedModel] = useState<string | undefined>(undefined);
@ -190,7 +192,7 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
title={
<div>
<h3 className="text-lg font-semibold mb-1">{template?.title}</h3>
<p className="text-sm text-gray-500 font-normal">Configure competitor blocking for your brand</p>
<p className="text-sm text-gray-500 font-normal">{t("policies.templateParameterModal.subtitle")}</p>
</div>
}
open={visible}
@ -198,10 +200,12 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
width={700}
footer={[
<Button key="cancel" variant="secondary" onClick={onCancel} disabled={isLoading}>
Cancel
{t("common.cancel")}
</Button>,
<Button key="confirm" onClick={handleConfirm} loading={isLoading} disabled={!canContinue || isLoading}>
{isLoading ? "Creating guardrails..." : "Continue"}
{isLoading
? t("policies.templateParameterModal.creatingGuardrails")
: t("policies.templateParameterModal.continue")}
</Button>,
]}
>
@ -228,7 +232,9 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
{hasEnrichment && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Competitor Discovery</label>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t("policies.templateParameterModal.competitorDiscovery")}
</label>
<Radio.Group
value={competitorMode}
onChange={(e) => setCompetitorMode(e.target.value)}
@ -236,10 +242,10 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
>
<div className="flex gap-3">
<Radio.Button value="ai" className="flex-1 text-center">
✨ Use AI
✨ {t("policies.templateParameterModal.useAi")}
</Radio.Button>
<Radio.Button value="manual" className="flex-1 text-center">
Enter Manually
{t("policies.templateParameterModal.enterManually")}
</Radio.Button>
</div>
</Radio.Group>
@ -248,11 +254,11 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
{/* Brand Name */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Your Brand Name
{t("policies.templateParameterModal.brandName")}
<span className="text-red-500 ml-1">*</span>
</label>
<TextInput
placeholder="e.g. Acme Airlines"
placeholder={t("policies.templateParameterModal.brandNamePlaceholder")}
value={parameterValues[enrichmentParam || "brand_name"] || ""}
onChange={(e) =>
setParameterValues((prev) => ({
@ -267,11 +273,11 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Select Model
{t("policies.templateParameterModal.selectModel")}
<span className="text-red-500 ml-1">*</span>
</label>
<Select
placeholder="Select a model to generate names"
placeholder={t("policies.templateParameterModal.selectModelPlaceholder")}
value={selectedModel}
onChange={(value) => setSelectedModel(value)}
loading={isLoadingModels}
@ -288,7 +294,9 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
disabled={!selectedModel || !brandNameFilled || isGenerating}
className="w-full"
>
{isGenerating ? "✨ Generating names..." : "✨ Generate Competitor Names"}
{isGenerating
? `✨ ${t("policies.templateParameterModal.generatingNames")}`
: `✨ ${t("policies.templateParameterModal.generateCompetitorNames")}`}
</Button>
</>
)}
@ -296,7 +304,7 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
{/* Competitor Tags */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Competitor Names
{t("policies.templateParameterModal.competitorNames")}
{competitorTags.length > 0 && (
<span className="text-gray-400 font-normal ml-2">({competitorTags.length})</span>
)}
@ -304,14 +312,14 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Type a name and press Enter to add"
placeholder={t("policies.templateParameterModal.competitorNamesPlaceholder")}
value={competitorTags}
onChange={(values) => setCompetitorTags(values)}
tokenSeparators={[","]}
open={false}
suffixIcon={null}
/>
<p className="text-xs text-gray-500 mt-1">Type a name and press Enter to add. Click ✕ to remove.</p>
<p className="text-xs text-gray-500 mt-1">{t("policies.templateParameterModal.competitorNamesHint")}</p>
{statusMessage && (
<div className="flex items-center gap-2 mt-2 p-2 bg-blue-50 rounded border border-blue-100">
<Spin size="small" />
@ -320,8 +328,9 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
)}
{Object.keys(variationsMap).length > 0 && !statusMessage && (
<p className="text-xs text-green-600 mt-1">
✓ {Object.values(variationsMap).flat().length} alternate spellings & variations auto-generated for
guardrail matching
{t("policies.templateParameterModal.variationsGenerated", {
count: Object.values(variationsMap).flat().length,
})}
</p>
)}
</div>
@ -329,10 +338,12 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
{/* Refinement input — shown after initial generation in AI mode */}
{competitorMode === "ai" && hasGenerated && competitorTags.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Refine List</label>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t("policies.templateParameterModal.refineList")}
</label>
<div className="flex gap-2">
<TextInput
placeholder="e.g. add 10 more from Asia, increase to 50 total..."
placeholder={t("policies.templateParameterModal.refinePlaceholder")}
value={refinementInput}
onChange={(e) => setRefinementInput(e.target.value)}
onKeyDown={(e) => {
@ -348,12 +359,10 @@ const TemplateParameterModal: React.FC<TemplateParameterModalProps> = ({
disabled={!refinementInput.trim() || isRefining}
size="xs"
>
{isRefining ? "..." : "Send"}
{isRefining ? "..." : t("policies.templateParameterModal.send")}
</Button>
</div>
<p className="text-xs text-gray-400 mt-1">
Give instructions to add, remove, or change competitors. Press Enter to send.
</p>
<p className="text-xs text-gray-400 mt-1">{t("policies.templateParameterModal.refineHint")}</p>
</div>
)}
</>

View file

@ -4978,6 +4978,97 @@
"updatePolicy": "Update Policy",
"savePolicy": "Save Policy",
"addDescriptionPlaceholder": "Add a description (optional)..."
},
"policyInfo": {
"notFound": "Policy not found",
"backToPolicies": "Back to Policies",
"editPolicy": "Edit Policy",
"policyId": "Policy ID",
"noDescription": "No description",
"inheritsFrom": "Inherits From",
"pipelineFlow": "Pipeline Flow",
"pipelineInfo_one": "Pipeline ({{mode}} mode, {{count}} step)",
"pipelineInfo_other": "Pipeline ({{mode}} mode, {{count}} steps)",
"guardrailsConfiguration": "Guardrails Configuration",
"resolvedGuardrails": "Resolved Guardrails",
"resolvedGuardrailsDesc": "Final guardrails that will be applied (including inheritance):",
"guardrailsToAdd": "Guardrails to Add",
"guardrailsToRemove": "Guardrails to Remove",
"conditions": "Conditions",
"modelCondition": "Model Condition",
"noModelCondition": "No model condition (applies to all models)"
},
"policyTable": {
"versionCount_one": "{{count}} version",
"versionCount_other": "{{count}} versions",
"inheritsFrom": "Inherits From",
"guardrailsAdd": "Guardrails (Add)",
"guardrailsRemove": "Guardrails (Remove)",
"modelCondition": "Model Condition",
"multiple": "Multiple",
"editPolicy": "Edit policy",
"deletePolicy": "Delete policy",
"unnamedPolicy": "Unnamed Policy",
"noPoliciesFound": "No policies found"
},
"policyTemplates": {
"title": "Policy Templates",
"subtitle": "Start with a pre-configured policy template to quickly set up guardrails for your organization.",
"useAiToFind": "Use AI to find templates",
"categories": "Categories",
"showingCount": "Showing {{filtered}} of {{total}} templates",
"noTemplatesMatch": "No templates match the selected filters.",
"clearAllFilters": "Clear all filters",
"complexityLabel": "{{complexity}} Complexity",
"inheritsFrom": "Inherits from:",
"includedGuardrails": "Included Guardrails",
"useTemplate": "Use Template",
"loadingTemplates": "Loading policy templates...",
"fetchFailed": "Failed to fetch policy templates"
},
"policyTestPanel": {
"title": "Policy Simulator",
"subtitle": "Simulate a request to see which policies and guardrails would apply. Select a team, key, model, or tags below and click \"Simulate\" to see the results.",
"teamAliasLabel": "Team Alias",
"teamAliasPlaceholder": "Select or type a team alias",
"keyAliasLabel": "Key Alias",
"keyAliasPlaceholder": "Select or type a key alias",
"modelLabel": "Model",
"modelPlaceholder": "Select or type a model",
"tagsLabel": "Tags",
"tagsPlaceholder": "Type a tag and press Enter",
"simulate": "Simulate",
"noSimulationYet": "No simulation run yet",
"noSimulationHint": "Fill in one or more fields above and click \"Simulate\" to see which policies and guardrails would apply to that request.",
"noPoliciesMatched": "No policies matched this context",
"effectiveGuardrails": "Effective Guardrails",
"matchedPolicies": "Matched Policies",
"colPolicy": "Policy",
"colMatchedVia": "Matched Via",
"colGuardrailsAdded": "Guardrails Added",
"resolveFailed": "Failed to resolve policies. Check the proxy logs."
},
"templateParameterModal": {
"subtitle": "Configure competitor blocking for your brand",
"creatingGuardrails": "Creating guardrails...",
"continue": "Continue",
"competitorDiscovery": "Competitor Discovery",
"useAi": "Use AI",
"enterManually": "Enter Manually",
"brandName": "Your Brand Name",
"brandNamePlaceholder": "e.g. Acme Airlines",
"selectModel": "Select Model",
"selectModelPlaceholder": "Select a model to generate names",
"generatingNames": "Generating names...",
"generateCompetitorNames": "Generate Competitor Names",
"competitorNames": "Competitor Names",
"competitorNamesPlaceholder": "Type a name and press Enter to add",
"competitorNamesHint": "Type a name and press Enter to add. Click ✕ to remove.",
"variationsGenerated": "✓ {{count}} alternate spellings & variations auto-generated for guardrail matching",
"refineList": "Refine List",
"refinePlaceholder": "e.g. add 10 more from Asia, increase to 50 total...",
"send": "Send",
"refineHint": "Give instructions to add, remove, or change competitors. Press Enter to send."
}
}
}

View file

@ -4978,6 +4978,97 @@
"updatePolicy": "更新策略",
"savePolicy": "保存策略",
"addDescriptionPlaceholder": "添加描述(可选)..."
},
"policyInfo": {
"notFound": "未找到策略",
"backToPolicies": "返回策略列表",
"editPolicy": "编辑策略",
"policyId": "策略 ID",
"noDescription": "无描述",
"inheritsFrom": "继承自",
"pipelineFlow": "流水线流程",
"pipelineInfo_one": "流水线({{mode}} 模式,{{count}} 个步骤)",
"pipelineInfo_other": "流水线({{mode}} 模式,{{count}} 个步骤)",
"guardrailsConfiguration": "护栏配置",
"resolvedGuardrails": "已解析护栏",
"resolvedGuardrailsDesc": "最终将应用的护栏(含继承):",
"guardrailsToAdd": "要添加的护栏",
"guardrailsToRemove": "要移除的护栏",
"conditions": "条件",
"modelCondition": "模型条件",
"noModelCondition": "无模型条件(应用于所有模型)"
},
"policyTable": {
"versionCount_one": "{{count}} 个版本",
"versionCount_other": "{{count}} 个版本",
"inheritsFrom": "继承自",
"guardrailsAdd": "护栏(添加)",
"guardrailsRemove": "护栏(移除)",
"modelCondition": "模型条件",
"multiple": "多个",
"editPolicy": "编辑策略",
"deletePolicy": "删除策略",
"unnamedPolicy": "未命名策略",
"noPoliciesFound": "未找到策略"
},
"policyTemplates": {
"title": "策略模板",
"subtitle": "从预配置的策略模板开始,快速为您的组织设置护栏。",
"useAiToFind": "使用 AI 查找模板",
"categories": "分类",
"showingCount": "显示 {{filtered}} / {{total}} 个模板",
"noTemplatesMatch": "没有模板匹配所选筛选条件。",
"clearAllFilters": "清除所有筛选",
"complexityLabel": "{{complexity}} 复杂度",
"inheritsFrom": "继承自:",
"includedGuardrails": "包含的护栏",
"useTemplate": "使用模板",
"loadingTemplates": "正在加载策略模板...",
"fetchFailed": "获取策略模板失败"
},
"policyTestPanel": {
"title": "策略模拟器",
"subtitle": "模拟请求,查看哪些策略和护栏将被应用。在下方选择团队、密钥、模型或标签,然后点击「模拟」查看结果。",
"teamAliasLabel": "团队别名",
"teamAliasPlaceholder": "选择或输入团队别名",
"keyAliasLabel": "密钥别名",
"keyAliasPlaceholder": "选择或输入密钥别名",
"modelLabel": "模型",
"modelPlaceholder": "选择或输入模型",
"tagsLabel": "标签",
"tagsPlaceholder": "输入标签后按 Enter",
"simulate": "模拟",
"noSimulationYet": "尚未运行模拟",
"noSimulationHint": "在上方填写一个或多个字段,然后点击「模拟」,查看哪些策略和护栏将应用于该请求。",
"noPoliciesMatched": "没有策略匹配此上下文",
"effectiveGuardrails": "生效护栏",
"matchedPolicies": "已匹配策略",
"colPolicy": "策略",
"colMatchedVia": "匹配方式",
"colGuardrailsAdded": "已添加护栏",
"resolveFailed": "解析策略失败,请检查代理日志。"
},
"templateParameterModal": {
"subtitle": "为您的品牌配置竞品屏蔽",
"creatingGuardrails": "正在创建护栏...",
"continue": "继续",
"competitorDiscovery": "竞品发现",
"useAi": "使用 AI",
"enterManually": "手动输入",
"brandName": "您的品牌名称",
"brandNamePlaceholder": "例如:Acme Airlines",
"selectModel": "选择模型",
"selectModelPlaceholder": "选择用于生成名称的模型",
"generatingNames": "正在生成名称...",
"generateCompetitorNames": "生成竞品名称",
"competitorNames": "竞品名称",
"competitorNamesPlaceholder": "输入名称后按 Enter 添加",
"competitorNamesHint": "输入名称后按 Enter 添加,点击 ✕ 删除。",
"variationsGenerated": "✓ 已为护栏匹配自动生成 {{count}} 个替代拼写及变体",
"refineList": "精炼列表",
"refinePlaceholder": "例如:再添加 10 个亚洲竞品,增加到 50 个...",
"send": "发送",
"refineHint": "给出添加、移除或修改竞品的指令,按 Enter 发送。"
}
}
}