feat(ui): i18n for compliance UI

This commit is contained in:
吹雪 2026-06-10 22:14:47 +09:00
parent e1d3ea9aa5
commit 2932904741
4 changed files with 319 additions and 99 deletions

View file

@ -1602,11 +1602,6 @@
"count": 1
}
},
"src/components/playground/complianceUI/ComplianceUI.tsx": {
"react-hooks/preserve-manual-memoization": {
"count": 3
}
},
"src/components/playground/llm_calls/a2a_send_message.tsx": {
"max-params": {
"count": 2

View file

@ -10,6 +10,7 @@ import { getGuardrailsList, testPoliciesAndGuardrails } from "@/components/netwo
import PolicySelector, { getPolicyOptionEntries } from "@/components/policies/PolicySelector";
import { Policy } from "@/components/policies/types";
import { makeOpenAIChatCompletionRequest } from "../llm_calls/chat_completion";
import { useTranslation } from "react-i18next";
import {
AlertTriangle,
BarChart3,
@ -123,6 +124,7 @@ export default function ComplianceUI({
fixedModel,
proxySettings,
}: ComplianceUIProps) {
const { t } = useTranslation();
const frameworks = getFrameworks();
const [policyValueToLabel, setPolicyValueToLabel] = useState<Map<string, string>>(new Map());
@ -194,7 +196,7 @@ export default function ComplianceUI({
const customFrameworks: ComplianceFramework[] = Array.from(fwMap.entries()).map(([fwName, catMap]) => ({
name: fwName,
icon: customPrompts.find((p) => p.framework === fwName)?.categoryIcon ?? "file-text",
description: `Custom prompts — ${fwName}.`,
description: t("playground.complianceUi.customPromptsDesc", { name: fwName }),
categories: Array.from(catMap.entries()).map(([catName, prompts]) => ({
name: catName,
icon: prompts[0]?.categoryIcon ?? "file-text",
@ -275,7 +277,7 @@ export default function ComplianceUI({
framework: "Custom",
category: "Custom Prompts",
categoryIcon: "pencil",
categoryDescription: "Custom prompts added this session.",
categoryDescription: t("playground.complianceUi.customPromptsSessionDesc"),
prompt: newPromptText.trim(),
expectedResult: newPromptExpected,
};
@ -330,11 +332,11 @@ export default function ComplianceUI({
setCsvError(null);
if (!file.name.endsWith(".csv") && file.type !== "text/csv") {
setCsvError("Please upload a .csv file.");
setCsvError(t("playground.complianceUi.csvErrorNotCsv"));
return;
}
if (file.size > 5 * 1024 * 1024) {
setCsvError("File too large (max 5 MB).");
setCsvError(t("playground.complianceUi.csvErrorTooLarge"));
return;
}
@ -343,16 +345,14 @@ export default function ComplianceUI({
skipEmptyLines: true,
complete: (results) => {
if (!results.data || results.data.length === 0) {
setCsvError("CSV file is empty.");
setCsvError(t("playground.complianceUi.csvErrorEmpty"));
return;
}
const headers = results.meta.fields ?? [];
const missing = EXPECTED_CSV_COLUMNS.filter((col) => !headers.includes(col));
if (missing.length > 0) {
setCsvError(
`Missing required columns: ${missing.join(", ")}. Expected: prompt, expected_result. Optional: framework, category.`,
);
setCsvError(t("playground.complianceUi.csvErrorMissingColumns", { columns: missing.join(", ") }));
return;
}
@ -365,11 +365,13 @@ export default function ComplianceUI({
const expected = row.expected_result?.trim().toLowerCase();
if (!prompt) {
errors.push(`Row ${rowNum}: missing prompt text`);
errors.push(t("playground.complianceUi.csvRowMissingPrompt", { row: rowNum }));
return;
}
if (expected !== "fail" && expected !== "pass") {
errors.push(`Row ${rowNum}: expected_result must be "fail" or "pass", got "${row.expected_result ?? ""}"`);
errors.push(
t("playground.complianceUi.csvRowInvalidExpected", { row: rowNum, value: row.expected_result ?? "" }),
);
return;
}
@ -381,7 +383,7 @@ export default function ComplianceUI({
framework,
category,
categoryIcon: "file-text",
categoryDescription: `Prompts uploaded from CSV — ${category}.`,
categoryDescription: t("playground.complianceUi.csvPromptsDesc", { category }),
prompt,
expectedResult: expected as "fail" | "pass",
});
@ -389,13 +391,16 @@ export default function ComplianceUI({
if (errors.length > 0) {
setCsvError(
errors.slice(0, 5).join("\n") + (errors.length > 5 ? `\n...and ${errors.length - 5} more errors` : ""),
errors.slice(0, 5).join("\n") +
(errors.length > 5
? "\n" + t("playground.complianceUi.csvMoreErrors", { count: errors.length - 5 })
: ""),
);
return;
}
if (newPrompts.length === 0) {
setCsvError("No valid prompts found in CSV.");
setCsvError(t("playground.complianceUi.csvErrorNoValidPrompts"));
return;
}
@ -418,7 +423,7 @@ export default function ComplianceUI({
setCsvError(null);
},
error: () => {
setCsvError("Failed to parse CSV file.");
setCsvError(t("playground.complianceUi.csvErrorParseFailed"));
},
});
@ -470,7 +475,7 @@ export default function ComplianceUI({
const sysMsg: QuickTestMessage = {
id: `msg-${Date.now()}-sys`,
type: "system",
text: "Allowed — model response received.",
text: t("playground.complianceUi.quickTestAllowedModel"),
result: "allowed",
returnedText: fullResponse,
timestamp: new Date(),
@ -492,8 +497,10 @@ export default function ComplianceUI({
const returnedText = Array.isArray(inputs?.texts) && inputs.texts.length > 0 ? inputs.texts[0] : undefined;
const displayText =
result === "blocked"
? `Blocked — ${triggeredBy ?? "content filter"}`
: "Allowed — no policy or guardrail violations detected.";
? t("playground.complianceUi.quickTestBlocked", {
triggeredBy: triggeredBy ?? t("playground.complianceUi.contentFilter"),
})
: t("playground.complianceUi.quickTestAllowedClean");
const sysMsg: QuickTestMessage = {
id: `msg-${Date.now()}-sys`,
type: "system",
@ -510,7 +517,7 @@ export default function ComplianceUI({
const sysMsg: QuickTestMessage = {
id: `msg-${Date.now()}-sys`,
type: "system",
text: `Error: ${errorMessage}`,
text: t("playground.complianceUi.quickTestError", { errorMessage }),
result: "blocked",
triggeredBy: errorMessage,
timestamp: new Date(),
@ -608,7 +615,7 @@ export default function ComplianceUI({
...row,
actualResult: "blocked" as const,
isMatch: false,
triggeredBy: `Error: ${errorMessage}`,
triggeredBy: t("playground.complianceUi.quickTestError", { errorMessage }),
status: "complete" as const,
})),
);
@ -687,11 +694,11 @@ export default function ComplianceUI({
const testButtonLabel = (() => {
const parts: string[] = [];
if (selectedPolicies.length > 0)
parts.push(`${selectedPolicies.length} ${selectedPolicies.length === 1 ? "policy" : "policies"}`);
parts.push(t("playground.complianceUi.policyCount", { count: selectedPolicies.length }));
if (selectedGuardrails.length > 0)
parts.push(`${selectedGuardrails.length} ${selectedGuardrails.length === 1 ? "guardrail" : "guardrails"}`);
if (parts.length === 0) return "Test";
return `Test ${parts.join(" & ")}`;
parts.push(t("playground.complianceUi.guardrailCount", { count: selectedGuardrails.length }));
if (parts.length === 0) return t("playground.complianceUi.testButton");
return t("playground.complianceUi.testButtonWithConfig", { config: parts.join(" & ") });
})();
return (
@ -700,14 +707,14 @@ export default function ComplianceUI({
{/* Top config */}
<div className="flex-shrink-0 border-b border-gray-200 px-6 py-4">
<div className="mb-3">
<h3 className="text-sm font-semibold text-gray-900">Test Configuration</h3>
<p className="text-xs text-gray-500 mt-0.5">Select policies, guardrails, or both to test against.</p>
<h3 className="text-sm font-semibold text-gray-900">{t("playground.complianceUi.testConfiguration")}</h3>
<p className="text-xs text-gray-500 mt-0.5">{t("playground.complianceUi.testConfigurationDesc")}</p>
</div>
<div className="flex items-start gap-3 flex-wrap">
<div className="flex-1 min-w-[200px]">
<label className="text-[11px] font-medium text-gray-500 uppercase tracking-wide mb-1.5 block">
Policies
{t("playground.complianceUi.policiesLabel")}
</label>
{accessToken && (
<PolicySelector
@ -721,13 +728,15 @@ export default function ComplianceUI({
<div className="flex flex-col items-center pt-6 flex-shrink-0">
<div className="w-px h-4 bg-gray-200" />
<span className="text-[10px] font-medium text-gray-400 my-1">or</span>
<span className="text-[10px] font-medium text-gray-400 my-1">
{t("playground.complianceUi.orSeparator")}
</span>
<div className="w-px h-4 bg-gray-200" />
</div>
<div className="flex-1 min-w-[200px]">
<label className="text-[11px] font-medium text-gray-500 uppercase tracking-wide mb-1.5 block">
Guardrails
{t("playground.complianceUi.guardrailsLabel")}
</label>
<div className="relative">
<button
@ -736,7 +745,9 @@ export default function ComplianceUI({
className="w-full flex items-center justify-between border border-gray-200 rounded-lg px-3 py-2 text-sm text-left hover:border-gray-300 transition-colors"
>
<span className={selectedGuardrails.length > 0 ? "text-gray-700" : "text-gray-400"}>
{selectedGuardrails.length > 0 ? `${selectedGuardrails.length} selected` : "None selected"}
{selectedGuardrails.length > 0
? t("playground.complianceUi.guardrailsSelected", { count: selectedGuardrails.length })
: t("playground.complianceUi.guardrailsNoneSelected")}
</span>
<ChevronDown className="w-4 h-4 text-gray-400" />
</button>
@ -744,7 +755,7 @@ export default function ComplianceUI({
<div className="absolute z-30 top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg py-1 max-h-52 overflow-y-auto">
{guardrailOptions.length === 0 ? (
<div className="px-3 py-2 text-xs text-gray-500">
No guardrails available. Create guardrails in the Guardrails page.
{t("playground.complianceUi.noGuardrailsAvailable")}
</div>
) : (
guardrailOptions.map((g) => (
@ -783,7 +794,7 @@ export default function ComplianceUI({
type="button"
onClick={() => toggleGuardrail(id)}
className="hover:text-indigo-900"
aria-label="Remove"
aria-label={t("common.remove")}
>
<X className="w-2.5 h-2.5" />
</button>
@ -801,7 +812,7 @@ export default function ComplianceUI({
onClick={() => batchAbortControllerRef.current?.abort()}
className="flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap bg-red-600 text-white hover:bg-red-700"
>
<Square className="w-3.5 h-3.5" /> Stop
<Square className="w-3.5 h-3.5" /> {t("playground.complianceUi.stopButton")}
</button>
) : (
<button
@ -810,12 +821,13 @@ export default function ComplianceUI({
disabled={selectedPromptIds.size === 0 || disabledPersonalKeyCreation}
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${selectedPromptIds.size === 0 || disabledPersonalKeyCreation ? "bg-gray-100 text-gray-400 cursor-not-allowed" : "bg-blue-600 text-white hover:bg-blue-700"}`}
>
<Play className="w-3.5 h-3.5" /> Simulate ({selectedPromptIds.size})
<Play className="w-3.5 h-3.5" />{" "}
{t("playground.complianceUi.simulateButton", { count: selectedPromptIds.size })}
</button>
)}
{isRunning && (
<span className="text-[11px] text-gray-500 flex items-center gap-1">
<Loader2 className="w-3 h-3 animate-spin" /> Running...
<Loader2 className="w-3 h-3 animate-spin" /> {t("playground.complianceUi.runningStatus")}
</span>
)}
<button
@ -828,7 +840,7 @@ export default function ComplianceUI({
}}
className="flex items-center justify-center gap-1.5 px-4 py-1.5 rounded-lg text-xs font-medium text-gray-500 hover:bg-gray-100 transition-colors"
>
<RotateCcw className="w-3 h-3" /> Reset
<RotateCcw className="w-3 h-3" /> {t("common.reset")}
</button>
</div>
</div>
@ -841,7 +853,7 @@ export default function ComplianceUI({
<div className="flex-1 overflow-y-auto min-h-0">
<div className="px-4 pt-4 pb-2">
<div className="flex items-center justify-between mb-2.5">
<h3 className="text-sm font-semibold text-gray-900">Test Prompts</h3>
<h3 className="text-sm font-semibold text-gray-900">{t("playground.complianceUi.testPrompts")}</h3>
<span className="text-[11px] text-gray-400 tabular-nums">
{selectedPromptIds.size}/{totalPromptCount}
</span>
@ -853,7 +865,7 @@ export default function ComplianceUI({
type="text"
value={searchPrompt}
onChange={(e) => setSearchPrompt(e.target.value)}
placeholder="Search prompts..."
placeholder={t("playground.complianceUi.searchPromptsPlaceholder")}
className="w-full border border-gray-200 rounded-lg pl-8 pr-3 py-1.5 text-xs placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400"
/>
</div>
@ -865,7 +877,7 @@ export default function ComplianceUI({
onClick={selectAll}
className="text-[11px] font-medium text-blue-600 hover:text-blue-700"
>
Select All
{t("playground.complianceUi.selectAll")}
</button>
<span className="text-gray-300 text-[10px]">·</span>
<button
@ -873,7 +885,7 @@ export default function ComplianceUI({
onClick={deselectAll}
className="text-[11px] font-medium text-gray-500 hover:text-gray-700"
>
Clear
{t("common.clear")}
</button>
</div>
<div className="flex items-center gap-1">
@ -885,7 +897,7 @@ export default function ComplianceUI({
}}
className={`flex items-center gap-1 text-[11px] font-medium px-2 py-0.5 rounded transition-colors ${showAddPrompt ? "bg-blue-50 text-blue-600" : "text-gray-500 hover:bg-gray-100"}`}
>
<Plus className="w-3 h-3" /> Add
<Plus className="w-3 h-3" /> {t("common.add")}
</button>
<button
type="button"
@ -895,7 +907,7 @@ export default function ComplianceUI({
}}
className={`flex items-center gap-1 text-[11px] font-medium px-2 py-0.5 rounded transition-colors ${showCsvUpload ? "bg-blue-50 text-blue-600" : "text-gray-500 hover:bg-gray-100"}`}
>
<Upload className="w-3 h-3" /> CSV
<Upload className="w-3 h-3" /> {t("playground.complianceUi.csvButton")}
</button>
</div>
</div>
@ -906,7 +918,7 @@ export default function ComplianceUI({
<textarea
value={newPromptText}
onChange={(e) => setNewPromptText(e.target.value)}
placeholder="Enter your test prompt..."
placeholder={t("playground.complianceUi.enterPromptPlaceholder")}
rows={2}
className="w-full border border-gray-200 rounded px-2.5 py-1.5 text-xs text-gray-700 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 resize-none bg-white"
/>
@ -917,14 +929,14 @@ export default function ComplianceUI({
onClick={() => setNewPromptExpected("fail")}
className={`text-[10px] font-semibold px-2 py-0.5 rounded ${newPromptExpected === "fail" ? "bg-red-100 text-red-700" : "bg-gray-100 text-gray-500"}`}
>
Should Fail
{t("playground.complianceUi.shouldFail")}
</button>
<button
type="button"
onClick={() => setNewPromptExpected("pass")}
className={`text-[10px] font-semibold px-2 py-0.5 rounded ${newPromptExpected === "pass" ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-500"}`}
>
Should Pass
{t("playground.complianceUi.shouldPass")}
</button>
</div>
<div className="flex items-center gap-1.5">
@ -936,7 +948,7 @@ export default function ComplianceUI({
}}
className="text-[11px] text-gray-500 px-2 py-1"
>
Cancel
{t("common.cancel")}
</button>
<button
type="button"
@ -944,7 +956,7 @@ export default function ComplianceUI({
disabled={!newPromptText.trim()}
className={`text-[11px] font-medium px-2.5 py-1 rounded ${newPromptText.trim() ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-400"}`}
>
Add
{t("common.add")}
</button>
</div>
</div>
@ -954,25 +966,31 @@ export default function ComplianceUI({
{showCsvUpload && (
<div className="mx-4 mb-2 border border-blue-200 bg-blue-50/30 rounded-lg p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-semibold text-gray-700">Upload CSV Dataset</span>
<span className="text-[11px] font-semibold text-gray-700">
{t("playground.complianceUi.uploadCsvDataset")}
</span>
<button
type="button"
onClick={downloadCsvTemplate}
className="flex items-center gap-1 text-[10px] font-medium text-blue-600 hover:text-blue-700"
>
<Download className="w-3 h-3" /> Download Template
<Download className="w-3 h-3" /> {t("playground.complianceUi.downloadTemplate")}
</button>
</div>
<div className="mb-2 p-2 bg-white rounded border border-gray-200">
<p className="text-[10px] text-gray-500 leading-relaxed">
<span className="font-semibold text-gray-600">Required columns:</span>{" "}
<span className="font-semibold text-gray-600">
{t("playground.complianceUi.csvRequiredColumns")}
</span>{" "}
<code className="bg-gray-100 px-1 rounded text-[10px]">prompt</code>,{" "}
<code className="bg-gray-100 px-1 rounded text-[10px]">expected_result</code>{" "}
<span className="text-gray-400">(fail or pass)</span>
<span className="text-gray-400">{t("playground.complianceUi.csvFailOrPass")}</span>
</p>
<p className="text-[10px] text-gray-500 leading-relaxed mt-0.5">
<span className="font-semibold text-gray-600">Optional columns:</span>{" "}
<span className="font-semibold text-gray-600">
{t("playground.complianceUi.csvOptionalColumns")}
</span>{" "}
<code className="bg-gray-100 px-1 rounded text-[10px]">framework</code>,{" "}
<code className="bg-gray-100 px-1 rounded text-[10px]">category</code>
</p>
@ -993,7 +1011,7 @@ export default function ComplianceUI({
onClick={() => csvInputRef.current?.click()}
className="w-full flex items-center justify-center gap-1.5 py-2 border-2 border-dashed border-gray-300 rounded-lg text-xs text-gray-500 hover:border-blue-400 hover:text-blue-600 transition-colors"
>
<Upload className="w-3.5 h-3.5" /> Choose CSV file
<Upload className="w-3.5 h-3.5" /> {t("playground.complianceUi.chooseCsvFile")}
</button>
{csvError && (
@ -1011,7 +1029,7 @@ export default function ComplianceUI({
}}
className="text-[11px] text-gray-500 px-2 py-1"
>
Cancel
{t("common.cancel")}
</button>
</div>
</div>
@ -1040,7 +1058,9 @@ export default function ComplianceUI({
<CategoryIcon iconKey={fw.icon} className="w-4 h-4 text-gray-500 flex-shrink-0" />
<div className="flex-1 min-w-0">
<span className="text-xs font-semibold text-gray-900">{fw.name}</span>
<span className="text-[10px] text-gray-400 ml-1.5">{fwPromptCount} prompts</span>
<span className="text-[10px] text-gray-400 ml-1.5">
{t("playground.complianceUi.promptCount", { count: fwPromptCount })}
</span>
</div>
{fwSelectedCount > 0 && (
<span className="text-[10px] font-medium bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded-full">
@ -1055,7 +1075,9 @@ export default function ComplianceUI({
}}
className="text-[10px] font-medium text-blue-600 hover:text-blue-700 px-1.5 py-0.5 rounded hover:bg-blue-50 flex-shrink-0"
>
{fwSelectedCount === fwPromptCount ? "Clear" : "All"}
{fwSelectedCount === fwPromptCount
? t("common.clear")
: t("playground.complianceUi.allButton")}
</button>
</button>
@ -1107,7 +1129,9 @@ export default function ComplianceUI({
onClick={() => toggleCategoryPrompts(category)}
className="text-[10px] font-medium text-blue-600 hover:text-blue-700 flex-shrink-0 whitespace-nowrap"
>
{allCatSelected ? "Clear" : "Select all"}
{allCatSelected
? t("common.clear")
: t("playground.complianceUi.selectAllCategory")}
</button>
</div>
{category.prompts.map((prompt) => (
@ -1126,7 +1150,9 @@ export default function ComplianceUI({
<span
className={`inline-block mt-0.5 text-[9px] font-semibold px-1 py-0.5 rounded ${prompt.expectedResult === "fail" ? "bg-red-50 text-red-600" : "bg-green-50 text-green-600"}`}
>
{prompt.expectedResult === "fail" ? "Should Fail" : "Should Pass"}
{prompt.expectedResult === "fail"
? t("playground.complianceUi.shouldFail")
: t("playground.complianceUi.shouldPass")}
</span>
</div>
{isCustom && (
@ -1138,7 +1164,7 @@ export default function ComplianceUI({
deleteCustomPrompt(prompt.id);
}}
className="opacity-0 group-hover:opacity-100 p-0.5 text-gray-400 hover:text-red-500 transition-all flex-shrink-0"
aria-label="Delete"
aria-label={t("common.delete")}
>
<Trash2 className="w-3 h-3" />
</button>
@ -1168,7 +1194,7 @@ export default function ComplianceUI({
onClick={() => setRightTab("quick-test")}
className={`relative flex items-center gap-1.5 px-3 py-2.5 text-xs font-medium transition-colors ${rightTab === "quick-test" ? "text-blue-600" : "text-gray-500 hover:text-gray-700"}`}
>
<MessageSquare className="w-3.5 h-3.5" /> Quick Test
<MessageSquare className="w-3.5 h-3.5" /> {t("playground.complianceUi.quickTestTab")}
{rightTab === "quick-test" && (
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600 rounded-t" />
)}
@ -1178,7 +1204,7 @@ export default function ComplianceUI({
onClick={() => setRightTab("batch-results")}
className={`relative flex items-center gap-1.5 px-3 py-2.5 text-xs font-medium transition-colors ${rightTab === "batch-results" ? "text-blue-600" : "text-gray-500 hover:text-gray-700"}`}
>
<ListChecks className="w-3.5 h-3.5" /> Batch Results
<ListChecks className="w-3.5 h-3.5" /> {t("playground.complianceUi.batchResultsTab")}
{testResults.length > 0 && (
<span className="text-[10px] bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded-full">
{testResults.length}
@ -1196,7 +1222,9 @@ export default function ComplianceUI({
<div className="px-5 pt-4 pb-2 flex-shrink-0">
{hasAnyConfig ? (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-[11px] font-medium text-gray-500">Testing against:</span>
<span className="text-[11px] font-medium text-gray-500">
{t("playground.complianceUi.testingAgainst")}
</span>
{selectedPolicies.map((id) => (
<span key={id} className="text-[11px] bg-blue-50 text-blue-700 px-2 py-0.5 rounded font-medium">
{policyValueToLabel.get(id) ?? id}
@ -1215,9 +1243,7 @@ export default function ComplianceUI({
})}
</div>
) : (
<p className="text-[11px] text-gray-400">
No policies or guardrails selected — select above to test against specific rules.
</p>
<p className="text-[11px] text-gray-400">{t("playground.complianceUi.noConfigSelected")}</p>
)}
</div>
@ -1228,7 +1254,7 @@ export default function ComplianceUI({
<div className="w-10 h-10 bg-gray-100 rounded-xl flex items-center justify-center mx-auto mb-3">
<MessageSquare className="w-5 h-5 text-gray-400" />
</div>
<p className="text-xs text-gray-500">Type a prompt below to quickly test it.</p>
<p className="text-xs text-gray-500">{t("playground.complianceUi.quickTestEmptyHint")}</p>
</div>
</div>
)}
@ -1247,14 +1273,16 @@ export default function ComplianceUI({
) : (
<CheckCircle2 className="w-3 h-3 inline" />
)}
{msg.result === "blocked" ? "Blocked" : "Allowed"}
{msg.result === "blocked"
? t("playground.complianceUi.blockedLabel")
: t("playground.complianceUi.allowedLabel")}
<span className="font-normal mx-0.5">—</span>
</span>
)}
{msg.text}
{msg.type === "system" && msg.returnedText != null && (
<span className="block mt-1.5 pt-1.5 border-t border-gray-200/60">
<span className="text-gray-500">Returned: </span>
<span className="text-gray-500">{t("playground.complianceUi.returnedLabel")} </span>
<span className="font-medium text-gray-700 break-all">{msg.returnedText}</span>
</span>
)}
@ -1279,16 +1307,17 @@ export default function ComplianceUI({
value={quickTestInput}
onChange={(e) => setQuickTestInput(e.target.value)}
onKeyDown={handleQuickTestKeyDown}
placeholder="Enter text to test..."
placeholder={t("playground.complianceUi.enterTextPlaceholder")}
rows={3}
className="w-full px-3 pt-3 pb-1 text-sm text-gray-700 placeholder:text-gray-400 focus:outline-none resize-none"
/>
<div className="flex items-center justify-between px-3 pb-2">
<span className="text-[10px] text-gray-400">
Press <kbd className="px-1 py-0.5 bg-gray-100 rounded text-[10px] font-mono">Enter</kbd> to
submit ·{" "}
<kbd className="px-1 py-0.5 bg-gray-100 rounded text-[10px] font-mono">Shift+Enter</kbd> for new
line
{t("playground.complianceUi.keyboardHintEnter")}{" "}
<kbd className="px-1 py-0.5 bg-gray-100 rounded text-[10px] font-mono">Enter</kbd>{" "}
{t("playground.complianceUi.keyboardHintSubmit")} ·{" "}
<kbd className="px-1 py-0.5 bg-gray-100 rounded text-[10px] font-mono">Shift+Enter</kbd>{" "}
{t("playground.complianceUi.keyboardHintNewLine")}
</span>
<span className="text-[10px] text-gray-400 tabular-nums">{quickTestInput.length}</span>
</div>
@ -1310,7 +1339,7 @@ export default function ComplianceUI({
<div className="flex-1 flex flex-col overflow-hidden bg-white min-h-0">
<div className="px-5 py-3 border-b border-gray-200 flex-shrink-0">
<div className="flex items-center justify-between mb-2">
<h2 className="text-sm font-semibold text-gray-900">Results</h2>
<h2 className="text-sm font-semibold text-gray-900">{t("playground.complianceUi.resultsTitle")}</h2>
{testResults.length > 0 && (
<div className="flex items-center gap-2">
<button
@ -1319,7 +1348,7 @@ export default function ComplianceUI({
disabled={filteredResults.length === 0}
className="flex items-center gap-1 text-[11px] font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-100 px-2 py-1 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent"
>
<Download className="w-3 h-3" /> Export CSV
<Download className="w-3 h-3" /> {t("playground.complianceUi.exportCsv")}
</button>
<div className="flex items-center gap-2.5 text-[11px]">
<span className="flex items-center gap-1 text-green-600">
@ -1328,14 +1357,14 @@ export default function ComplianceUI({
</span>
<span
className="flex items-center gap-1 text-amber-600"
title="Allowed content that should have been blocked"
title={t("playground.complianceUi.falseNegativeTitle")}
>
<AlertTriangle className="w-3 h-3" />
{falseNegativeCount} FN
</span>
<span
className="flex items-center gap-1 text-red-600"
title="Blocked content that should have been allowed"
title={t("playground.complianceUi.falsePositiveTitle")}
>
<X className="w-3 h-3" />
{falsePositiveCount} FP
@ -1361,6 +1390,12 @@ export default function ComplianceUI({
: filter === "mismatches"
? mismatchCount
: pendingCount;
const filterLabels: Record<ResultFilter, string> = {
all: t("common.all"),
matches: t("playground.complianceUi.filterMatches"),
mismatches: t("playground.complianceUi.filterMismatches"),
pending: t("playground.complianceUi.filterPending"),
};
return (
<button
key={filter}
@ -1368,7 +1403,7 @@ export default function ComplianceUI({
onClick={() => setResultFilter(filter)}
className={`text-[11px] font-medium px-2.5 py-1 rounded-md transition-colors capitalize ${resultFilter === filter ? "bg-gray-900 text-white" : "text-gray-500 hover:bg-gray-100"}`}
>
{filter} ({count})
{filterLabels[filter]} ({count})
</button>
);
})}
@ -1384,7 +1419,7 @@ export default function ComplianceUI({
<FlaskConical className="w-6 h-6 text-gray-400" />
</div>
<p className="text-xs text-gray-500 max-w-[240px]">
Select prompts and click Simulate to run batch compliance tests.
{t("playground.complianceUi.batchEmptyHint")}
</p>
</div>
</div>
@ -1395,22 +1430,22 @@ export default function ComplianceUI({
<div className="flex items-center gap-3 text-sm flex-1">
<span>
<span className="font-semibold text-gray-700">{testResults.length}</span>{" "}
<span className="text-gray-500">total</span>
<span className="text-gray-500">{t("common.total")}</span>
</span>
<div className="w-px h-4 bg-gray-200" />
<span>
<span className="font-semibold text-green-700">{matchCount}</span>{" "}
<span className="text-gray-500">correct</span>
<span className="text-gray-500">{t("playground.complianceUi.statCorrect")}</span>
</span>
<div className="w-px h-4 bg-gray-200" />
<span title="Allowed content that should have been blocked">
<span title={t("playground.complianceUi.falseNegativeTitle")}>
<span className="font-semibold text-amber-700">{falseNegativeCount}</span>{" "}
<span className="text-gray-500">false negative</span>
<span className="text-gray-500">{t("playground.complianceUi.statFalseNegative")}</span>
</span>
<div className="w-px h-4 bg-gray-200" />
<span title="Blocked content that should have been allowed">
<span title={t("playground.complianceUi.falsePositiveTitle")}>
<span className="font-semibold text-red-700">{falsePositiveCount}</span>{" "}
<span className="text-gray-500">false positive</span>
<span className="text-gray-500">{t("playground.complianceUi.statFalsePositive")}</span>
</span>
</div>
<div
@ -1422,7 +1457,9 @@ export default function ComplianceUI({
: "bg-red-50 border-red-200 text-red-700"
}`}
>
<span className="text-[10px] font-semibold uppercase tracking-wider opacity-90">Score</span>
<span className="text-[10px] font-semibold uppercase tracking-wider opacity-90">
{t("playground.complianceUi.scoreLabel")}
</span>
<span>{Math.round((matchCount / completedResults.length) * 100)}%</span>
</div>
</div>
@ -1456,13 +1493,17 @@ export default function ComplianceUI({
<span
className={`text-[9px] font-semibold px-1 py-0.5 rounded ${result.expectedResult === "fail" ? "bg-red-50 text-red-600" : "bg-green-50 text-green-600"}`}
>
{result.expectedResult === "fail" ? "Expect Block" : "Expect Allow"}
{result.expectedResult === "fail"
? t("playground.complianceUi.expectBlock")
: t("playground.complianceUi.expectAllow")}
</span>
{result.status === "complete" && (
<span
className={`text-[9px] font-bold px-1 py-0.5 rounded ${result.isMatch ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700"}`}
>
{result.isMatch ? "✓ Match" : "✗ Gap"}
{result.isMatch
? t("playground.complianceUi.matchLabel")
: t("playground.complianceUi.gapLabel")}
</span>
)}
</div>
@ -1479,7 +1520,11 @@ export default function ComplianceUI({
});
}}
className="flex-shrink-0 p-0.5 text-gray-400 hover:text-gray-600"
aria-label={isExpanded ? "Collapse" : "Expand"}
aria-label={
isExpanded
? t("playground.complianceUi.collapseAriaLabel")
: t("playground.complianceUi.expandAriaLabel")
}
>
{isExpanded ? (
<ChevronDown className="w-3.5 h-3.5" />
@ -1493,25 +1538,27 @@ export default function ComplianceUI({
<div className="mt-2 pt-2 border-t border-gray-100 text-[11px] space-y-1">
{result.triggeredBy && (
<div>
<span className="text-gray-400">Triggered by:</span>{" "}
<span className="text-gray-400">{t("playground.complianceUi.triggeredBy")}</span>{" "}
<span className="font-medium text-gray-700 bg-gray-100 px-1.5 py-0.5 rounded">
{result.triggeredBy}
</span>
</div>
)}
<div>
<span className="text-gray-400">Verdict:</span>{" "}
<span className="text-gray-400">{t("playground.complianceUi.verdictLabel")}</span>{" "}
<span className={result.isMatch ? "text-green-600" : "text-red-600"}>
{result.isMatch
? "Correctly handled"
? t("playground.complianceUi.verdictCorrect")
: result.expectedResult === "fail"
? "Gap — should have been blocked"
: "False positive — incorrectly blocked"}
? t("playground.complianceUi.verdictGap")
: t("playground.complianceUi.verdictFalsePositive")}
</span>
</div>
{result.returnedText != null && result.returnedText !== "" && (
<div className="mt-1.5">
<span className="text-gray-400 block mb-0.5">LLM response:</span>
<span className="text-gray-400 block mb-0.5">
{t("playground.complianceUi.llmResponse")}
</span>
<div className="text-gray-700 bg-gray-50 rounded px-2 py-1.5 border border-gray-100 max-h-32 overflow-y-auto whitespace-pre-wrap break-words">
{result.returnedText}
</div>

View file

@ -4105,6 +4105,95 @@
"selectModel": "Select a model",
"addCustomModel": "+ Add custom model",
"customModelPlaceholder": "Custom Model Name (Enter to add)"
},
"complianceUi": {
"testConfiguration": "Test Configuration",
"testConfigurationDesc": "Select policies, guardrails, or both to test against.",
"policiesLabel": "Policies",
"guardrailsLabel": "Guardrails",
"orSeparator": "or",
"guardrailsSelected": "{{count}} selected",
"guardrailsNoneSelected": "None selected",
"noGuardrailsAvailable": "No guardrails available. Create guardrails in the Guardrails page.",
"stopButton": "Stop",
"simulateButton": "Simulate ({{count}})",
"runningStatus": "Running...",
"testPrompts": "Test Prompts",
"searchPromptsPlaceholder": "Search prompts...",
"selectAll": "Select All",
"csvButton": "CSV",
"enterPromptPlaceholder": "Enter your test prompt...",
"shouldFail": "Should Fail",
"shouldPass": "Should Pass",
"uploadCsvDataset": "Upload CSV Dataset",
"downloadTemplate": "Download Template",
"csvRequiredColumns": "Required columns:",
"csvOptionalColumns": "Optional columns:",
"csvFailOrPass": "(fail or pass)",
"chooseCsvFile": "Choose CSV file",
"csvErrorNotCsv": "Please upload a .csv file.",
"csvErrorTooLarge": "File too large (max 5 MB).",
"csvErrorEmpty": "CSV file is empty.",
"csvErrorMissingColumns": "Missing required columns: {{columns}}. Expected: prompt, expected_result. Optional: framework, category.",
"csvRowMissingPrompt": "Row {{row}}: missing prompt text",
"csvRowInvalidExpected": "Row {{row}}: expected_result must be \"fail\" or \"pass\", got \"{{value}}\"",
"csvMoreErrors": "...and {{count}} more errors",
"csvErrorNoValidPrompts": "No valid prompts found in CSV.",
"csvErrorParseFailed": "Failed to parse CSV file.",
"csvPromptsDesc": "Prompts uploaded from CSV — {{category}}.",
"customPromptsDesc": "Custom prompts — {{name}}.",
"customPromptsSessionDesc": "Custom prompts added this session.",
"policyCount_one": "{{count}} policy",
"policyCount_other": "{{count}} policies",
"guardrailCount_one": "{{count}} guardrail",
"guardrailCount_other": "{{count}} guardrails",
"testButton": "Test",
"testButtonWithConfig": "Test {{config}}",
"quickTestTab": "Quick Test",
"batchResultsTab": "Batch Results",
"testingAgainst": "Testing against:",
"noConfigSelected": "No policies or guardrails selected — select above to test against specific rules.",
"quickTestEmptyHint": "Type a prompt below to quickly test it.",
"blockedLabel": "Blocked",
"allowedLabel": "Allowed",
"returnedLabel": "Returned:",
"enterTextPlaceholder": "Enter text to test...",
"keyboardHintEnter": "Press",
"keyboardHintSubmit": "to submit",
"keyboardHintNewLine": "for new line",
"quickTestAllowedModel": "Allowed — model response received.",
"quickTestBlocked": "Blocked — {{triggeredBy}}",
"quickTestAllowedClean": "Allowed — no policy or guardrail violations detected.",
"quickTestError": "Error: {{errorMessage}}",
"contentFilter": "content filter",
"resultsTitle": "Results",
"exportCsv": "Export CSV",
"falseNegativeTitle": "Allowed content that should have been blocked",
"falsePositiveTitle": "Blocked content that should have been allowed",
"filterMatches": "Matches",
"filterMismatches": "Mismatches",
"filterPending": "Pending",
"batchEmptyHint": "Select prompts and click Simulate to run batch compliance tests.",
"statCorrect": "correct",
"statFalseNegative": "false negative",
"statFalsePositive": "false positive",
"scoreLabel": "Score",
"promptCount_one": "{{count}} prompt",
"promptCount_other": "{{count}} prompts",
"allButton": "All",
"selectAllCategory": "Select all",
"expectBlock": "Expect Block",
"expectAllow": "Expect Allow",
"matchLabel": "✓ Match",
"gapLabel": "✗ Gap",
"collapseAriaLabel": "Collapse",
"expandAriaLabel": "Expand",
"triggeredBy": "Triggered by:",
"verdictLabel": "Verdict:",
"verdictCorrect": "Correctly handled",
"verdictGap": "Gap — should have been blocked",
"verdictFalsePositive": "False positive — incorrectly blocked",
"llmResponse": "LLM response:"
}
}
}

View file

@ -4105,6 +4105,95 @@
"selectModel": "选择模型",
"addCustomModel": "+ 添加自定义模型",
"customModelPlaceholder": "自定义模型名称(回车确认)"
},
"complianceUi": {
"testConfiguration": "测试配置",
"testConfigurationDesc": "选择策略、护栏或两者以进行测试。",
"policiesLabel": "策略",
"guardrailsLabel": "护栏",
"orSeparator": "或",
"guardrailsSelected": "已选 {{count}} 个",
"guardrailsNoneSelected": "未选择",
"noGuardrailsAvailable": "暂无护栏。请在护栏页面创建护栏。",
"stopButton": "停止",
"simulateButton": "模拟运行 ({{count}})",
"runningStatus": "运行中...",
"testPrompts": "测试提示词",
"searchPromptsPlaceholder": "搜索提示词...",
"selectAll": "全选",
"csvButton": "CSV",
"enterPromptPlaceholder": "输入测试提示词...",
"shouldFail": "应被拦截",
"shouldPass": "应通过",
"uploadCsvDataset": "上传 CSV 数据集",
"downloadTemplate": "下载模板",
"csvRequiredColumns": "必填列:",
"csvOptionalColumns": "可选列:",
"csvFailOrPass": "(fail 或 pass)",
"chooseCsvFile": "选择 CSV 文件",
"csvErrorNotCsv": "请上传 .csv 文件。",
"csvErrorTooLarge": "文件过大(最大 5 MB)。",
"csvErrorEmpty": "CSV 文件为空。",
"csvErrorMissingColumns": "缺少必填列:{{columns}}。必填:prompt、expected_result。可选:framework、category。",
"csvRowMissingPrompt": "第 {{row}} 行:缺少提示词文本",
"csvRowInvalidExpected": "第 {{row}} 行:expected_result 必须为 \"fail\" 或 \"pass\",实际值为 \"{{value}}\"",
"csvMoreErrors": "...以及 {{count}} 个更多错误",
"csvErrorNoValidPrompts": "CSV 中未找到有效的提示词。",
"csvErrorParseFailed": "CSV 文件解析失败。",
"csvPromptsDesc": "从 CSV 上传的提示词 — {{category}}。",
"customPromptsDesc": "自定义提示词 — {{name}}。",
"customPromptsSessionDesc": "本次会话添加的自定义提示词。",
"policyCount_one": "{{count}} 个策略",
"policyCount_other": "{{count}} 个策略",
"guardrailCount_one": "{{count}} 个护栏",
"guardrailCount_other": "{{count}} 个护栏",
"testButton": "测试",
"testButtonWithConfig": "测试 {{config}}",
"quickTestTab": "快速测试",
"batchResultsTab": "批量结果",
"testingAgainst": "正在测试:",
"noConfigSelected": "未选择策略或护栏 — 请在上方选择以针对特定规则进行测试。",
"quickTestEmptyHint": "在下方输入提示词以快速测试。",
"blockedLabel": "已拦截",
"allowedLabel": "已允许",
"returnedLabel": "返回内容:",
"enterTextPlaceholder": "输入要测试的文本...",
"keyboardHintEnter": "按",
"keyboardHintSubmit": "提交",
"keyboardHintNewLine": "换行",
"quickTestAllowedModel": "已允许 — 收到模型响应。",
"quickTestBlocked": "已拦截 — {{triggeredBy}}",
"quickTestAllowedClean": "已允许 — 未检测到策略或护栏违规。",
"quickTestError": "错误:{{errorMessage}}",
"contentFilter": "内容过滤器",
"resultsTitle": "结果",
"exportCsv": "导出 CSV",
"falseNegativeTitle": "应被拦截但被放行的内容",
"falsePositiveTitle": "应被放行但被拦截的内容",
"filterMatches": "匹配",
"filterMismatches": "不匹配",
"filterPending": "待处理",
"batchEmptyHint": "选择提示词并点击模拟运行以执行批量合规测试。",
"statCorrect": "正确",
"statFalseNegative": "漏报",
"statFalsePositive": "误报",
"scoreLabel": "得分",
"promptCount_one": "{{count}} 个提示词",
"promptCount_other": "{{count}} 个提示词",
"allButton": "全部",
"selectAllCategory": "全选",
"expectBlock": "期望拦截",
"expectAllow": "期望放行",
"matchLabel": "✓ 匹配",
"gapLabel": "✗ 缺口",
"collapseAriaLabel": "折叠",
"expandAriaLabel": "展开",
"triggeredBy": "触发来源:",
"verdictLabel": "判定:",
"verdictCorrect": "处理正确",
"verdictGap": "缺口 — 本应被拦截",
"verdictFalsePositive": "误报 — 被错误拦截",
"llmResponse": "模型响应:"
}
}
}