feat(ui): i18n for guardrail test playground

This commit is contained in:
吹雪 2026-06-10 17:31:31 +09:00
parent 95af75657c
commit fbb8477d23
5 changed files with 149 additions and 34 deletions

View file

@ -2,6 +2,7 @@ import React, { useState } from "react";
import { Button } from "@tremor/react";
import { Input, Typography, Tooltip } from "antd";
import { CopyOutlined, InfoCircleOutlined } from "@ant-design/icons";
import { useTranslation, Trans } from "react-i18next";
import NotificationsManager from "../molecules/notifications_manager";
import GuardrailTestResults from "./GuardrailTestResults";
@ -25,11 +26,12 @@ export function GuardrailTestPanel({
errors,
onClose,
}: GuardrailTestPanelProps) {
const { t } = useTranslation();
const [inputText, setInputText] = useState("");
const handleSubmit = () => {
if (!inputText.trim()) {
NotificationsManager.fromBackend("Please enter text to test");
NotificationsManager.fromBackend(t("guardrails.guardrailTestPanel.pleaseEnterText"));
return;
}
@ -74,9 +76,9 @@ export function GuardrailTestPanel({
const handleCopyInput = async () => {
const success = await copyToClipboard(inputText);
if (success) {
NotificationsManager.success("Input copied to clipboard");
NotificationsManager.success(t("guardrails.guardrailTestPanel.inputCopied"));
} else {
NotificationsManager.fromBackend("Failed to copy input");
NotificationsManager.fromBackend(t("guardrails.guardrailTestPanel.failedToCopyInput"));
}
};
@ -87,7 +89,7 @@ export function GuardrailTestPanel({
<div className="flex items-center space-x-3">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-1">
<h2 className="text-lg font-semibold text-gray-900">Test Guardrails:</h2>
<h2 className="text-lg font-semibold text-gray-900">{t("guardrails.guardrailTestPanel.title")}</h2>
<div className="flex flex-wrap gap-2">
{guardrailNames.map((name) => (
<div
@ -100,7 +102,7 @@ export function GuardrailTestPanel({
</div>
</div>
<p className="text-sm text-gray-500">
Test {guardrailNames.length > 1 ? "guardrails" : "guardrail"} and compare results
{t("guardrails.guardrailTestPanel.subtitle", { count: guardrailNames.length })}
</p>
</div>
</div>
@ -112,14 +114,16 @@ export function GuardrailTestPanel({
<div>
<div className="flex justify-between items-center mb-2">
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-gray-700">Input Text</label>
<Tooltip title="Press Enter to submit. Use Shift+Enter for new line.">
<label className="text-sm font-medium text-gray-700">
{t("guardrails.guardrailTestPanel.inputText")}
</label>
<Tooltip title={t("guardrails.guardrailTestPanel.inputTooltip")}>
<InfoCircleOutlined className="text-gray-400 cursor-help" />
</Tooltip>
</div>
{inputText && (
<Button size="xs" variant="secondary" icon={CopyOutlined} onClick={handleCopyInput}>
Copy Input
{t("guardrails.guardrailTestPanel.copyInput")}
</Button>
)}
</div>
@ -127,26 +131,31 @@ export function GuardrailTestPanel({
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Enter text to test with guardrails..."
placeholder={t("guardrails.guardrailTestPanel.inputPlaceholder")}
rows={8}
className="font-mono text-sm"
/>
<div className="flex justify-between items-center mt-1">
<Text className="text-xs text-gray-500">
Press <kbd className="px-1 py-0.5 bg-gray-100 border border-gray-300 rounded text-xs">Enter</kbd> to
submit •{" "}
<kbd className="px-1 py-0.5 bg-gray-100 border border-gray-300 rounded text-xs">Shift+Enter</kbd> for
new line
<Trans
i18nKey="guardrails.guardrailTestPanel.keyboardHint"
components={{
enter: <kbd className="px-1 py-0.5 bg-gray-100 border border-gray-300 rounded text-xs" />,
shiftEnter: <kbd className="px-1 py-0.5 bg-gray-100 border border-gray-300 rounded text-xs" />,
}}
/>
</Text>
<Text className="text-xs text-gray-500">
{t("guardrails.guardrailTestPanel.characters", { count: inputText.length })}
</Text>
<Text className="text-xs text-gray-500">Characters: {inputText.length}</Text>
</div>
</div>
<div className="pt-2">
<Button onClick={handleSubmit} loading={isLoading} disabled={!inputText.trim()} className="w-full">
{isLoading
? `Testing ${guardrailNames.length} guardrail${guardrailNames.length > 1 ? "s" : ""}...`
: `Test ${guardrailNames.length} guardrail${guardrailNames.length > 1 ? "s" : ""}`}
? t("guardrails.guardrailTestPanel.testingButton", { count: guardrailNames.length })
: t("guardrails.guardrailTestPanel.testButton", { count: guardrailNames.length })}
</Button>
</div>
</div>

View file

@ -1,6 +1,7 @@
import React, { useState } from "react";
import { Card, List, Empty, Spin, Input, Typography } from "antd";
import { ExperimentOutlined, SearchOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import GuardrailTestPanel from "./GuardrailTestPanel";
import { applyGuardrail } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
@ -43,6 +44,7 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
accessToken,
onClose,
}) => {
const { t } = useTranslation();
const [selectedGuardrails, setSelectedGuardrails] = useState<Set<string>>(new Set());
const [searchQuery, setSearchQuery] = useState("");
const [testResults, setTestResults] = useState<TestResult[]>([]);
@ -103,10 +105,10 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
setIsTesting(false);
if (results.length > 0) {
NotificationsManager.success(`${results.length} guardrail${results.length > 1 ? "s" : ""} applied successfully`);
NotificationsManager.success(t("guardrails.guardrailTestPlayground.appliedSuccess", { count: results.length }));
}
if (errors.length > 0) {
NotificationsManager.fromBackend(`${errors.length} guardrail${errors.length > 1 ? "s" : ""} failed`);
NotificationsManager.fromBackend(t("guardrails.guardrailTestPlayground.applyFailed", { count: errors.length }));
}
};
@ -118,10 +120,12 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
<div className="w-1/4 border-r border-gray-200 flex flex-col overflow-hidden">
<div className="p-4 border-b border-gray-200">
<div className="mb-3">
<h3 className="text-lg font-semibold mb-3">Guardrails</h3>
<h3 className="text-lg font-semibold mb-3">
{t("guardrails.guardrailTestPlayground.guardrailsHeading")}
</h3>
<Input
prefix={<SearchOutlined />}
placeholder="Search guardrails..."
placeholder={t("guardrails.guardrailTestPlayground.searchPlaceholder")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@ -135,7 +139,13 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
</div>
) : filteredGuardrails.length === 0 ? (
<div className="p-4">
<Empty description={searchQuery ? "No guardrails match your search" : "No guardrails available"} />
<Empty
description={
searchQuery
? t("guardrails.guardrailTestPlayground.noGuardrailsMatch")
: t("guardrails.guardrailTestPlayground.noGuardrailsAvailable")
}
/>
</div>
) : (
<List
@ -164,11 +174,11 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
description={
<div className="text-xs space-y-1 mt-1">
<div>
<span className="font-medium">Type: </span>
<span className="font-medium">{t("guardrails.guardrailTestPlayground.typeLabel")} </span>
<span className="text-gray-600">{guardrail.litellm_params.guardrail}</span>
</div>
<div>
<span className="font-medium">Mode: </span>
<span className="font-medium">{t("guardrails.guardrailTestPlayground.modeLabel")} </span>
<span className="text-gray-600">{guardrail.litellm_params.mode}</span>
</div>
</div>
@ -182,7 +192,10 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
<div className="p-3 border-t border-gray-200 bg-gray-50">
<Typography.Text className="text-xs text-gray-600">
{selectedGuardrails.size} of {filteredGuardrails.length} selected
{t("guardrails.guardrailTestPlayground.selectedCount", {
selected: selectedGuardrails.size,
total: filteredGuardrails.length,
})}
</Typography.Text>
</div>
</div>
@ -191,7 +204,7 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
<div className="w-3/4 flex flex-col bg-white">
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
<Typography.Title level={2} className="text-xl font-semibold mb-0">
Guardrail Testing Playground
{t("guardrails.guardrailTestPlayground.pageTitle")}
</Typography.Title>
</div>
@ -200,10 +213,10 @@ const GuardrailTestPlayground: React.FC<GuardrailTestPlaygroundProps> = ({
<div className="h-full flex flex-col items-center justify-center text-gray-400">
<ExperimentOutlined style={{ fontSize: "48px", marginBottom: "16px" }} />
<Typography.Paragraph className="text-lg font-medium text-gray-600 mb-2">
Select Guardrails to Test
{t("guardrails.guardrailTestPlayground.emptyStateTitle")}
</Typography.Paragraph>
<Typography.Paragraph className="text-center text-gray-500 max-w-md">
Choose one or more guardrails from the left sidebar to start testing and comparing results.
{t("guardrails.guardrailTestPlayground.emptyStateDesc")}
</Typography.Paragraph>
</div>
) : (

View file

@ -2,6 +2,7 @@ import React, { useState } from "react";
import { Button, Card } from "@tremor/react";
import { Typography } from "antd";
import { CopyOutlined, CheckCircleOutlined, ClockCircleOutlined, DownOutlined, RightOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import NotificationsManager from "../molecules/notifications_manager";
const { Text } = Typography;
@ -24,6 +25,7 @@ interface GuardrailTestResultsProps {
}
export function GuardrailTestResults({ results, errors }: GuardrailTestResultsProps) {
const { t } = useTranslation();
const [collapsedResults, setCollapsedResults] = useState<Set<string>>(new Set());
const toggleResultCollapse = (guardrailName: string) => {
@ -70,7 +72,7 @@ export function GuardrailTestResults({ results, errors }: GuardrailTestResultsPr
return (
<div className="space-y-3 pt-4 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900">Results</h3>
<h3 className="text-sm font-semibold text-gray-900">{t("guardrails.guardrailTestResults.resultsHeading")}</h3>
{/* Success Results */}
{results &&
@ -105,13 +107,13 @@ export function GuardrailTestResults({ results, errors }: GuardrailTestResultsPr
onClick={async () => {
const success = await copyToClipboard(result.response_text);
if (success) {
NotificationsManager.success("Result copied to clipboard");
NotificationsManager.success(t("guardrails.guardrailTestResults.resultCopied"));
} else {
NotificationsManager.fromBackend("Failed to copy result");
NotificationsManager.fromBackend(t("guardrails.guardrailTestResults.failedToCopyResult"));
}
}}
>
Copy
{t("common.copy")}
</Button>
)}
</div>
@ -119,13 +121,16 @@ export function GuardrailTestResults({ results, errors }: GuardrailTestResultsPr
{!isCollapsed && (
<>
<div className="bg-white border border-green-200 rounded p-3">
<label className="text-xs font-medium text-gray-600 mb-2 block">Output Text</label>
<label className="text-xs font-medium text-gray-600 mb-2 block">
{t("guardrails.guardrailTestResults.outputText")}
</label>
<div className="font-mono text-sm text-gray-900 whitespace-pre-wrap break-words">
{result.response_text}
</div>
</div>
<div className="text-xs text-gray-600">
<span className="font-medium">Characters:</span> {result.response_text.length}
<span className="font-medium">{t("guardrails.guardrailTestResults.characters")}</span>{" "}
{result.response_text.length}
</div>
</>
)}
@ -163,7 +168,7 @@ export function GuardrailTestResults({ results, errors }: GuardrailTestResultsPr
className="text-sm font-medium text-red-800 cursor-pointer"
onClick={() => toggleResultCollapse(errorItem.guardrailName)}
>
{errorItem.guardrailName} - Error
{t("guardrails.guardrailTestResults.errorTitle", { name: errorItem.guardrailName })}
</p>
<div className="flex items-center space-x-1 text-xs text-gray-600">
<ClockCircleOutlined />

View file

@ -2421,5 +2421,49 @@
"showJson": "Show JSON",
"copied": "Copied!"
}
},
"guardrails": {
"guardrailTestPanel": {
"title": "Test Guardrails:",
"subtitle_one": "Test guardrail and compare results",
"subtitle_other": "Test guardrails and compare results",
"inputText": "Input Text",
"inputTooltip": "Press Enter to submit. Use Shift+Enter for new line.",
"copyInput": "Copy Input",
"inputPlaceholder": "Enter text to test with guardrails...",
"keyboardHint": "Press <enter>Enter</enter> to submit • <shiftEnter>Shift+Enter</shiftEnter> for new line",
"characters": "Characters: {{count}}",
"testButton_one": "Test {{count}} guardrail",
"testButton_other": "Test {{count}} guardrails",
"testingButton_one": "Testing {{count}} guardrail...",
"testingButton_other": "Testing {{count}} guardrails...",
"pleaseEnterText": "Please enter text to test",
"inputCopied": "Input copied to clipboard",
"failedToCopyInput": "Failed to copy input"
},
"guardrailTestPlayground": {
"pageTitle": "Guardrail Testing Playground",
"guardrailsHeading": "Guardrails",
"searchPlaceholder": "Search guardrails...",
"noGuardrailsMatch": "No guardrails match your search",
"noGuardrailsAvailable": "No guardrails available",
"typeLabel": "Type:",
"modeLabel": "Mode:",
"selectedCount": "{{selected}} of {{total}} selected",
"emptyStateTitle": "Select Guardrails to Test",
"emptyStateDesc": "Choose one or more guardrails from the left sidebar to start testing and comparing results.",
"appliedSuccess_one": "{{count}} guardrail applied successfully",
"appliedSuccess_other": "{{count}} guardrails applied successfully",
"applyFailed_one": "{{count}} guardrail failed",
"applyFailed_other": "{{count}} guardrails failed"
},
"guardrailTestResults": {
"resultsHeading": "Results",
"outputText": "Output Text",
"characters": "Characters:",
"errorTitle": "{{name}} - Error",
"resultCopied": "Result copied to clipboard",
"failedToCopyResult": "Failed to copy result"
}
}
}

View file

@ -2421,5 +2421,49 @@
"showJson": "显示 JSON",
"copied": "已复制!"
}
},
"guardrails": {
"guardrailTestPanel": {
"title": "测试护栏:",
"subtitle_one": "测试护栏并比较结果",
"subtitle_other": "测试护栏并比较结果",
"inputText": "输入文本",
"inputTooltip": "按 Enter 提交,按 Shift+Enter 换行。",
"copyInput": "复制输入",
"inputPlaceholder": "输入要用护栏测试的文本...",
"keyboardHint": "按 <enter>Enter</enter> 提交 • <shiftEnter>Shift+Enter</shiftEnter> 换行",
"characters": "字符数:{{count}}",
"testButton_one": "测试 {{count}} 个护栏",
"testButton_other": "测试 {{count}} 个护栏",
"testingButton_one": "正在测试 {{count}} 个护栏...",
"testingButton_other": "正在测试 {{count}} 个护栏...",
"pleaseEnterText": "请输入要测试的文本",
"inputCopied": "输入已复制到剪贴板",
"failedToCopyInput": "复制输入失败"
},
"guardrailTestPlayground": {
"pageTitle": "护栏测试 Playground",
"guardrailsHeading": "护栏",
"searchPlaceholder": "搜索护栏...",
"noGuardrailsMatch": "没有护栏匹配您的搜索",
"noGuardrailsAvailable": "暂无护栏可用",
"typeLabel": "类型:",
"modeLabel": "模式:",
"selectedCount": "已选 {{selected}} / {{total}}",
"emptyStateTitle": "选择要测试的护栏",
"emptyStateDesc": "从左侧边栏选择一个或多个护栏,开始测试并比较结果。",
"appliedSuccess_one": "{{count}} 个护栏已成功应用",
"appliedSuccess_other": "{{count}} 个护栏已成功应用",
"applyFailed_one": "{{count}} 个护栏执行失败",
"applyFailed_other": "{{count}} 个护栏执行失败"
},
"guardrailTestResults": {
"resultsHeading": "结果",
"outputText": "输出文本",
"characters": "字符数:",
"errorTitle": "{{name}} - 错误",
"resultCopied": "结果已复制到剪贴板",
"failedToCopyResult": "复制结果失败"
}
}
}