feat(ui): i18n for playground compare UI

This commit is contained in:
吹雪 2026-06-10 21:59:23 +09:00
parent a4b623ba37
commit e1d3ea9aa5
7 changed files with 158 additions and 33 deletions

View file

@ -4,6 +4,7 @@ import NotificationsManager from "@/components/molecules/notifications_manager";
import { ClearOutlined, DeleteOutlined, FilePdfOutlined, PlusOutlined } from "@ant-design/icons";
import { Button, Input, Select, Tooltip } from "antd";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { v4 as uuidv4 } from "uuid";
import ChatImageUpload from "../chat_ui/ChatImageUpload";
import { createChatDisplayMessage, createChatMultimodalMessage } from "../chat_ui/ChatImageUtils";
@ -53,6 +54,7 @@ const GENERIC_FOLLOW_UPS = [
const SUGGESTED_PROMPTS = ["Write me a poem", "Explain quantum computing", "Draft a polite email requesting a meeting"];
const DEFAULT_ENDPOINT = EndpointId.CHAT_COMPLETIONS;
export default function CompareUI({ accessToken, disabledPersonalKeyCreation }: CompareUIProps) {
const { t } = useTranslation();
const [comparisons, setComparisons] = useState<ComparisonInstance[]>([
{
id: "1",
@ -483,7 +485,7 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
return;
}
if (!effectiveApiKey) {
NotificationsManager.fromBackend("Please provide a Virtual Key or select Current UI Session");
NotificationsManager.fromBackend(t("playground.compareUi.provideVirtualKey"));
return;
}
const targetComparisons = comparisons;
@ -643,13 +645,13 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
messages[messages.length - 1] = {
...last,
content: assistantContent
? `${assistantContent}\nError fetching response: ${errorMessage}`
: `Error fetching response: ${errorMessage}`,
? `${assistantContent}\n${t("playground.compareUi.errorFetchingResponse", { errorMessage })}`
: t("playground.compareUi.errorFetchingResponse", { errorMessage }),
};
} else {
messages.push({
role: "assistant",
content: `Error fetching response: ${errorMessage}`,
content: t("playground.compareUi.errorFetchingResponse", { errorMessage }),
});
}
return {
@ -693,7 +695,9 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
<div className="border-b px-4 py-2">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-600">Virtual Key Source</span>
<span className="text-sm font-medium text-gray-600">
{t("playground.compareUi.virtualKeySourceLabel")}
</span>
<Select
value={apiKeySource}
onChange={(value) => setApiKeySource(value as "session" | "custom")}
@ -701,21 +705,21 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
className="w-48"
>
<Select.Option value="session" disabled={!canUseSessionKey}>
Current UI Session
{t("playground.compareUi.currentUiSession")}
</Select.Option>
<Select.Option value="custom">Virtual Key</Select.Option>
<Select.Option value="custom">{t("playground.compareUi.virtualKeyOption")}</Select.Option>
</Select>
{apiKeySource === "custom" && (
<Input.Password
value={customApiKey}
onChange={(event) => setCustomApiKey(event.target.value)}
placeholder="Enter Virtual Key"
placeholder={t("playground.compareUi.enterVirtualKeyPlaceholder")}
className="w-56"
/>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-600">Endpoint</span>
<span className="text-sm font-medium text-gray-600">{t("playground.compareUi.endpointLabel")}</span>
<Select
value={selectedEndpoint}
onChange={(value) => setSelectedEndpoint(value as EndpointIdType)}
@ -730,15 +734,17 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
</div>
<div className="flex items-center gap-3">
<Button onClick={clearAllChats} disabled={!hasMessages} icon={<ClearOutlined />}>
Clear All Chats
{t("playground.compareUi.clearAllChats")}
</Button>
<Tooltip
title={
comparisons.length >= maxComparisons ? "Compare up to 3 models at a time" : "Add another comparison"
comparisons.length >= maxComparisons
? t("playground.compareUi.compareUpToThreeModels")
: t("playground.compareUi.addAnotherComparison")
}
>
<Button onClick={addComparison} disabled={comparisons.length >= maxComparisons} icon={<PlusOutlined />}>
Add Comparison
{t("playground.compareUi.addComparison")}
</Button>
</Tooltip>
</div>
@ -770,7 +776,7 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
<div className="border border-gray-200 shadow-lg rounded-xl bg-white p-4">
<div className="flex items-center justify-between gap-4 mb-3 min-h-8">
{hasAttachment ? (
<span className="text-sm text-gray-500">Attachment ready to send</span>
<span className="text-sm text-gray-500">{t("playground.compareUi.attachmentReadyToSend")}</span>
) : showSuggestedPrompts ? (
<div className="flex items-center gap-2 overflow-x-auto">
{SUGGESTED_PROMPTS.map((prompt) => (
@ -817,14 +823,16 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
) : (
<img
src={uploadedFilePreviewUrl || ""}
alt="Upload preview"
alt={t("playground.compareUi.uploadPreviewAlt")}
className="w-10 h-10 rounded-md border border-gray-200 object-cover"
/>
)}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 truncate">{uploadedFile.name}</div>
<div className="text-xs text-gray-500">{isUploadedFilePdf ? "PDF" : "Image"}</div>
<div className="text-xs text-gray-500">
{isUploadedFilePdf ? t("playground.compareUi.pdfLabel") : t("playground.compareUi.imageLabel")}
</div>
</div>
<button
className="flex items-center justify-center w-6 h-6 text-gray-400 hover:text-gray-600 hover:bg-gray-200 rounded-full transition-colors"

View file

@ -1,5 +1,6 @@
import { Settings, X } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ComparisonInstance } from "../CompareUI";
import { MessageDisplay } from "./MessageDisplay";
import { UnifiedSelector } from "./UnifiedSelector";
@ -32,6 +33,7 @@ export function ComparisonPanel({
endpointConfig,
apiKey,
}: ComparisonPanelProps) {
const { t } = useTranslation();
const isA2AMode = isAgentEndpoint(endpointConfig.id);
const currentSelection = getComparisonSelection(comparison, endpointConfig.id);
const [popoverVisible, setPopoverVisible] = useState(false);
@ -104,7 +106,7 @@ export function ComparisonPanel({
{/* Sync Checkbox */}
<div className="flex items-center gap-2">
<Checkbox checked={comparison.applyAcrossModels} onChange={(e) => handleSyncChange(e.target.checked)}>
<span className="text-xs font-medium">Sync Settings Across Models</span>
<span className="text-xs font-medium">{t("playground.comparisonPanel.syncSettingsAcrossModels")}</span>
</Checkbox>
</div>
@ -112,10 +114,14 @@ export function ComparisonPanel({
{/* General Settings */}
<div>
<h4 className="text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wide">General Settings</h4>
<h4 className="text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wide">
{t("playground.comparisonPanel.generalSettings")}
</h4>
<div className="space-y-2">
<div>
<label className="text-xs font-medium text-gray-600 block mb-0.5">Tags</label>
<label className="text-xs font-medium text-gray-600 block mb-0.5">
{t("playground.comparisonPanel.tagsLabel")}
</label>
<TagSelector
value={comparison.tags}
onChange={(value) => handleSettingChange("tags", value)}
@ -123,7 +129,9 @@ export function ComparisonPanel({
/>
</div>
<div>
<label className="text-xs font-medium text-gray-600 block mb-0.5">Vector Stores</label>
<label className="text-xs font-medium text-gray-600 block mb-0.5">
{t("playground.comparisonPanel.vectorStoresLabel")}
</label>
<VectorStoreSelector
value={comparison.vectorStores}
onChange={(value) => handleSettingChange("vectorStores", value)}
@ -131,7 +139,9 @@ export function ComparisonPanel({
/>
</div>
<div>
<label className="text-xs font-medium text-gray-600 block mb-0.5">Guardrails</label>
<label className="text-xs font-medium text-gray-600 block mb-0.5">
{t("playground.comparisonPanel.guardrailsLabel")}
</label>
<GuardrailSelector
value={comparison.guardrails}
onChange={(value) => handleSettingChange("guardrails", value)}
@ -142,20 +152,24 @@ export function ComparisonPanel({
</div>
{/* Advanced Settings */}
<div>
<h4 className="text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wide">Advanced Settings</h4>
<h4 className="text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wide">
{t("playground.comparisonPanel.advancedSettings")}
</h4>
<div className="space-y-2">
<div className="flex items-center gap-2 pb-1">
<Checkbox
checked={comparison.useAdvancedParams}
onChange={(e) => handleAdvancedParamsChange(e.target.checked)}
>
<span className="text-sm font-medium">Use Advanced Parameters</span>
<span className="text-sm font-medium">{t("playground.comparisonPanel.useAdvancedParameters")}</span>
</Checkbox>
</div>
<div className="space-y-2 transition-opacity duration-200" style={{ opacity: disabledOpacity }}>
<div>
<div className="flex items-center justify-between mb-1">
<label className={`text-xs font-medium ${disabledTextColor}`}>Temperature</label>
<label className={`text-xs font-medium ${disabledTextColor}`}>
{t("playground.comparisonPanel.temperatureLabel")}
</label>
<span className={`text-xs ${disabledTextColor}`}>{comparison.temperature.toFixed(2)}</span>
</div>
<Slider
@ -173,7 +187,9 @@ export function ComparisonPanel({
</div>
<div>
<div className="flex items-center justify-between mb-1">
<label className={`text-xs font-medium ${disabledTextColor}`}>Max Tokens</label>
<label className={`text-xs font-medium ${disabledTextColor}`}>
{t("playground.comparisonPanel.maxTokensLabel")}
</label>
<span className={`text-xs ${disabledTextColor}`}>{comparison.maxTokens}</span>
</div>
<Slider

View file

@ -1,5 +1,6 @@
import { Bot, Loader2, UserRound } from "lucide-react";
import React from "react";
import { useTranslation } from "react-i18next";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
@ -15,6 +16,8 @@ interface MessageDisplayProps {
}
export function MessageDisplay({ messages, isLoading }: MessageDisplayProps) {
const { t } = useTranslation();
if (messages.length === 0) {
return <div className="h-full" />;
}
@ -101,7 +104,7 @@ export function MessageDisplay({ messages, isLoading }: MessageDisplayProps) {
<div className="flex flex-col gap-6 min-w-0 w-full p-4">
{conversationBlocks.map((block, blockIndex) => {
const assistantMessage = block.assistant;
const displayModel = assistantMessage?.model || "Assistant";
const displayModel = assistantMessage?.model || t("playground.messageDisplay.assistantFallback");
return (
<div key={blockIndex} className="space-y-4">
{block.user && (
@ -110,7 +113,7 @@ export function MessageDisplay({ messages, isLoading }: MessageDisplayProps) {
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-600">
<UserRound size={16} />
</div>
<div className="text-sm font-semibold text-gray-700">You</div>
<div className="text-sm font-semibold text-gray-700">{t("playground.messageDisplay.youLabel")}</div>
</div>
{renderMessageBody(block.user)}
</div>
@ -152,10 +155,10 @@ export function MessageDisplay({ messages, isLoading }: MessageDisplayProps) {
) : isLoading && blockIndex === conversationBlocks.length - 1 ? (
<div className="flex items-center gap-2 text-sm text-gray-500">
<Loader2 size={18} className="animate-spin" />
<span>Generating response...</span>
<span>{t("playground.messageDisplay.generatingResponse")}</span>
</div>
) : (
<div className="text-sm text-gray-500">Waiting for a response...</div>
<div className="text-sm text-gray-500">{t("playground.messageDisplay.waitingForResponse")}</div>
)}
</div>
);
@ -163,7 +166,7 @@ export function MessageDisplay({ messages, isLoading }: MessageDisplayProps) {
{isLoading && conversationBlocks.length === 0 && (
<div className="flex items-center gap-2 text-gray-500">
<Loader2 size={18} className="animate-spin" />
<span>Generating response...</span>
<span>{t("playground.messageDisplay.generatingResponse")}</span>
</div>
)}
</div>

View file

@ -1,6 +1,7 @@
import React from "react";
import { Input, Button } from "antd";
import { ArrowUpOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
const { TextArea } = Input;
@ -14,6 +15,7 @@ interface MessageInputProps {
}
export function MessageInput({ value, onChange, onSend, disabled, hasAttachment, uploadComponent }: MessageInputProps) {
const { t } = useTranslation();
const canSend = !disabled && (value.trim().length > 0 || Boolean(hasAttachment));
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@ -33,7 +35,7 @@ export function MessageInput({ value, onChange, onSend, disabled, hasAttachment,
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type your message... (Shift+Enter for new line)"
placeholder={t("playground.messageInput.placeholder")}
disabled={disabled}
className="flex-1"
autoSize={{ minRows: 1, maxRows: 4 }}

View file

@ -1,6 +1,7 @@
import React, { useMemo, useState } from "react";
import { Select } from "antd";
import { TextInput } from "@tremor/react";
import { useTranslation } from "react-i18next";
interface ModelSelectorProps {
value: string;
onChange: (value: string) => void;
@ -9,6 +10,7 @@ interface ModelSelectorProps {
disabled?: boolean;
}
export function ModelSelector({ value, onChange, models, loading, disabled }: ModelSelectorProps) {
const { t } = useTranslation();
const [isAddingCustom, setIsAddingCustom] = useState(false);
const [customValue, setCustomValue] = useState("");
@ -55,7 +57,11 @@ export function ModelSelector({ value, onChange, models, loading, disabled }: Mo
onChange={handleSelectChange}
disabled={disabled}
loading={loading}
placeholder={loading ? "Loading models..." : "Select a model"}
placeholder={
loading
? t("playground.compareModelSelector.loadingModels")
: t("playground.compareModelSelector.selectModel")
}
className="w-full rounded-md"
showSearch
optionFilterProp="children"
@ -65,12 +71,12 @@ export function ModelSelector({ value, onChange, models, loading, disabled }: Mo
{model}
</Select.Option>
))}
<Select.Option value="__custom__">+ Add custom model</Select.Option>
<Select.Option value="__custom__">{t("playground.compareModelSelector.addCustomModel")}</Select.Option>
</Select>
{isAddingCustom && (
<TextInput
className="mt-2"
placeholder="Custom Model Name (Enter to add)"
placeholder={t("playground.compareModelSelector.customModelPlaceholder")}
value={customValue}
onValueChange={setCustomValue}
onKeyDown={(event) => {

View file

@ -4061,5 +4061,50 @@
"uiSessionActiveDesc": "UI session active - context maintained client-side",
"copyTooltipTitle": "Copy response ID to continue session:"
}
},
"playground": {
"compareUi": {
"virtualKeySourceLabel": "Virtual Key Source",
"currentUiSession": "Current UI Session",
"virtualKeyOption": "Virtual Key",
"enterVirtualKeyPlaceholder": "Enter Virtual Key",
"endpointLabel": "Endpoint",
"clearAllChats": "Clear All Chats",
"compareUpToThreeModels": "Compare up to 3 models at a time",
"addAnotherComparison": "Add another comparison",
"addComparison": "Add Comparison",
"attachmentReadyToSend": "Attachment ready to send",
"uploadPreviewAlt": "Upload preview",
"pdfLabel": "PDF",
"imageLabel": "Image",
"errorFetchingResponse": "Error fetching response: {{errorMessage}}",
"provideVirtualKey": "Please provide a Virtual Key or select Current UI Session"
},
"comparisonPanel": {
"syncSettingsAcrossModels": "Sync Settings Across Models",
"generalSettings": "General Settings",
"tagsLabel": "Tags",
"vectorStoresLabel": "Vector Stores",
"guardrailsLabel": "Guardrails",
"advancedSettings": "Advanced Settings",
"useAdvancedParameters": "Use Advanced Parameters",
"temperatureLabel": "Temperature",
"maxTokensLabel": "Max Tokens"
},
"messageDisplay": {
"youLabel": "You",
"generatingResponse": "Generating response...",
"waitingForResponse": "Waiting for a response...",
"assistantFallback": "Assistant"
},
"messageInput": {
"placeholder": "Type your message... (Shift+Enter for new line)"
},
"compareModelSelector": {
"loadingModels": "Loading models...",
"selectModel": "Select a model",
"addCustomModel": "+ Add custom model",
"customModelPlaceholder": "Custom Model Name (Enter to add)"
}
}
}

View file

@ -4061,5 +4061,50 @@
"uiSessionActiveDesc": "UI 会话活跃 - 上下文在客户端维护",
"copyTooltipTitle": "复制响应 ID 以继续会话:"
}
},
"playground": {
"compareUi": {
"virtualKeySourceLabel": "虚拟密钥来源",
"currentUiSession": "当前 UI 会话",
"virtualKeyOption": "虚拟密钥",
"enterVirtualKeyPlaceholder": "输入虚拟密钥",
"endpointLabel": "端点",
"clearAllChats": "清空所有对话",
"compareUpToThreeModels": "最多同时对比 3 个模型",
"addAnotherComparison": "添加新对比项",
"addComparison": "添加对比",
"attachmentReadyToSend": "附件已准备好发送",
"uploadPreviewAlt": "上传预览",
"pdfLabel": "PDF",
"imageLabel": "图片",
"errorFetchingResponse": "获取响应失败:{{errorMessage}}",
"provideVirtualKey": "请提供虚拟密钥或选择当前 UI 会话"
},
"comparisonPanel": {
"syncSettingsAcrossModels": "跨模型同步设置",
"generalSettings": "常规设置",
"tagsLabel": "标签",
"vectorStoresLabel": "向量存储",
"guardrailsLabel": "护栏",
"advancedSettings": "高级设置",
"useAdvancedParameters": "使用高级参数",
"temperatureLabel": "Temperature",
"maxTokensLabel": "最大 Token 数"
},
"messageDisplay": {
"youLabel": "你",
"generatingResponse": "正在生成响应...",
"waitingForResponse": "等待响应...",
"assistantFallback": "Assistant"
},
"messageInput": {
"placeholder": "输入消息...(Shift+Enter 换行)"
},
"compareModelSelector": {
"loadingModels": "加载模型中...",
"selectModel": "选择模型",
"addCustomModel": "+ 添加自定义模型",
"customModelPlaceholder": "自定义模型名称(回车确认)"
}
}
}