feat(ui): i18n for playground chat_ui batch 1

This commit is contained in:
吹雪 2026-06-10 21:31:05 +09:00
parent cd8fbb5709
commit da509c009a
9 changed files with 333 additions and 100 deletions

View file

@ -12,6 +12,7 @@ import {
FileTextOutlined,
RobotOutlined,
} from "@ant-design/icons";
import { useTranslation } from "react-i18next";
export interface A2ATaskMetadata {
taskId?: string;
@ -80,6 +81,7 @@ const copyToClipboard = (text: string) => {
};
const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken, totalLatency }) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
if (!a2aMetadata && !timeToFirstToken && !totalLatency) return null;
@ -92,7 +94,7 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
{/* A2A Metadata Header */}
<div className="flex items-center mb-2 text-gray-600">
<RobotOutlined className="mr-1.5 text-blue-500" />
<span className="font-medium text-gray-700">A2A Metadata</span>
<span className="font-medium text-gray-700">{t("playground.a2AMetrics.header")}</span>
</div>
{/* Main metrics row */}
@ -119,7 +121,7 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
{/* Latency */}
{totalLatency !== undefined && (
<Tooltip title="Total latency">
<Tooltip title={t("playground.a2AMetrics.totalLatency")}>
<span className="flex items-center text-blue-600">
<ClockCircleOutlined className="mr-1" />
{(totalLatency / 1000).toFixed(2)}s
@ -129,7 +131,7 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
{/* Time to first token */}
{timeToFirstToken !== undefined && (
<Tooltip title="Time to first token">
<Tooltip title={t("playground.a2AMetrics.timeToFirstToken")}>
<span className="flex items-center text-green-600">TTFT: {(timeToFirstToken / 1000).toFixed(2)}s</span>
</Tooltip>
)}
@ -139,13 +141,14 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
<div className="flex flex-wrap items-center gap-3 text-gray-500 ml-4 mt-1.5">
{/* Task ID */}
{taskId && (
<Tooltip title={`Click to copy: ${taskId}`}>
<Tooltip title={t("playground.a2AMetrics.clickToCopy", { id: taskId })}>
<span
className="flex items-center cursor-pointer hover:text-gray-700"
onClick={() => copyToClipboard(taskId)}
>
<FileTextOutlined className="mr-1" />
Task: {truncateId(taskId)}
{t("playground.a2AMetrics.taskPrefix")}
{truncateId(taskId)}
<CopyOutlined className="ml-1 text-gray-400 hover:text-gray-600" />
</span>
</Tooltip>
@ -153,13 +156,14 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
{/* Context/Session ID */}
{contextId && (
<Tooltip title={`Click to copy: ${contextId}`}>
<Tooltip title={t("playground.a2AMetrics.clickToCopy", { id: contextId })}>
<span
className="flex items-center cursor-pointer hover:text-gray-700"
onClick={() => copyToClipboard(contextId)}
>
<LinkOutlined className="mr-1" />
Session: {truncateId(contextId)}
{t("playground.a2AMetrics.sessionPrefix")}
{truncateId(contextId)}
<CopyOutlined className="ml-1 text-gray-400 hover:text-gray-600" />
</span>
</Tooltip>
@ -174,7 +178,7 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
onClick={() => setShowDetails(!showDetails)}
>
{showDetails ? <DownOutlined /> : <RightOutlined />}
<span className="ml-1">Details</span>
<span className="ml-1">{t("playground.a2AMetrics.details")}</span>
</Button>
)}
</div>
@ -185,7 +189,7 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
{/* Status message */}
{status?.message && (
<div className="mb-2">
<span className="font-medium text-gray-700">Status Message:</span>
<span className="font-medium text-gray-700">{t("playground.a2AMetrics.statusMessage")}</span>
<span className="ml-2">{status.message}</span>
</div>
)}
@ -193,7 +197,7 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
{/* Full IDs */}
{taskId && (
<div className="mb-1.5 flex items-center">
<span className="font-medium text-gray-700 w-24">Task ID:</span>
<span className="font-medium text-gray-700 w-24">{t("playground.a2AMetrics.taskId")}</span>
<code className="ml-2 px-2 py-1 bg-white border border-gray-200 rounded text-xs font-mono">{taskId}</code>
<CopyOutlined
className="ml-2 cursor-pointer text-gray-400 hover:text-blue-500"
@ -204,7 +208,7 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
{contextId && (
<div className="mb-1.5 flex items-center">
<span className="font-medium text-gray-700 w-24">Session ID:</span>
<span className="font-medium text-gray-700 w-24">{t("playground.a2AMetrics.sessionId")}</span>
<code className="ml-2 px-2 py-1 bg-white border border-gray-200 rounded text-xs font-mono">
{contextId}
</code>
@ -218,7 +222,7 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
{/* Metadata fields */}
{metadata && Object.keys(metadata).length > 0 && (
<div className="mt-3">
<span className="font-medium text-gray-700">Custom Metadata:</span>
<span className="font-medium text-gray-700">{t("playground.a2AMetrics.customMetadata")}</span>
<pre className="mt-1.5 p-2 bg-white border border-gray-200 rounded text-xs font-mono overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(metadata, null, 2)}
</pre>

View file

@ -2,6 +2,7 @@ import { InfoCircleOutlined } from "@ant-design/icons";
import { Text } from "@tremor/react";
import { Checkbox, InputNumber, Popover, Slider, Tooltip, Typography } from "antd";
import React, { useEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
interface AdditionalModelSettingsProps {
temperature?: number;
@ -51,6 +52,7 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
onMaxTokensChange?.(newValue);
};
const { t } = useTranslation();
const disabledOpacity = useAdvancedParams ? 1 : 0.4;
const disabledTextColor = useAdvancedParams ? "text-gray-700" : "text-gray-400";
@ -65,13 +67,13 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
return (
<div className="space-y-4 p-4 w-80">
<Checkbox checked={useAdvancedParams} onChange={(e) => handleUseAdvancedParamsChange(e.target.checked)}>
<span className="font-medium">Use Advanced Parameters</span>
<span className="font-medium">{t("playground.additionalModelSettings.useAdvancedParams")}</span>
</Checkbox>
{onMockTestFallbacksChange && (
<div className="flex items-center gap-1">
<Checkbox checked={mockTestFallbacks ?? false} onChange={(e) => onMockTestFallbacksChange(e.target.checked)}>
<span className="font-medium">Simulate failure to test fallbacks</span>
<span className="font-medium">{t("playground.additionalModelSettings.simulateFailure")}</span>
</Checkbox>
<Popover
trigger="hover"
@ -79,26 +81,29 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
content={
<div style={{ maxWidth: 340 }}>
<Typography.Paragraph className="text-sm" style={{ marginBottom: 8 }}>
Causes the first request to fail so the router tries fallbacks (if configured). Use this to verify
your fallback setup.
{t("playground.additionalModelSettings.simulateFailureDesc")}
</Typography.Paragraph>
<Typography.Paragraph className="text-sm" style={{ marginBottom: 0 }}>
Behavior can differ when keys, teams, or router settings are configured.{" "}
<a
href="https://docs.litellm.ai/docs/proxy/keys_teams_router_settings"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800"
>
Learn more
</a>
<Trans
i18nKey="playground.additionalModelSettings.simulateFailureNote"
components={{
learnMoreLink: (
<a
href="https://docs.litellm.ai/docs/proxy/keys_teams_router_settings"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800"
/>
),
}}
/>
</Typography.Paragraph>
</div>
}
>
<InfoCircleOutlined
className="text-xs text-gray-400 cursor-pointer shrink-0 hover:text-gray-600"
aria-label="Help: Simulate failure to test fallbacks"
aria-label={t("playground.additionalModelSettings.simulateFailureAriaLabel")}
/>
</Popover>
</div>
@ -108,8 +113,10 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
<div>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1">
<Text className={`text-sm ${disabledTextColor}`}>Temperature</Text>
<Tooltip title="Controls randomness. Lower values make output more deterministic, higher values more creative.">
<Text className={`text-sm ${disabledTextColor}`}>
{t("playground.additionalModelSettings.temperature")}
</Text>
<Tooltip title={t("playground.additionalModelSettings.temperatureTooltip")}>
<InfoCircleOutlined className={`text-xs ${disabledTextColor} cursor-help`} />
</Tooltip>
</div>
@ -142,8 +149,10 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
<div>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1">
<Text className={`text-sm ${disabledTextColor}`}>Max Tokens</Text>
<Tooltip title="Maximum number of tokens to generate in the response.">
<Text className={`text-sm ${disabledTextColor}`}>
{t("playground.additionalModelSettings.maxTokens")}
</Text>
<Tooltip title={t("playground.additionalModelSettings.maxTokensTooltip")}>
<InfoCircleOutlined className={`text-xs ${disabledTextColor} cursor-help`} />
</Tooltip>
</div>

View file

@ -11,6 +11,7 @@ import {
} from "@ant-design/icons";
import { Button, Input, Modal, Select, Spin, Tabs } from "antd";
import React, { useCallback, useEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import CodeBlock from "@/app/(dashboard)/api-reference/components/CodeBlock";
import NotificationsManager from "../../molecules/notifications_manager";
import { keyCreateCall, modelCreateCall, modelDeleteCall, modelPatchUpdateCall, proxyBaseUrl } from "../../networking";
@ -71,6 +72,7 @@ function ConnectTabContent({
createdKeyValue,
onCreateKey,
}: ConnectTabContentProps) {
const { t } = useTranslation();
const baseUrl = proxyBaseUrl ?? getConnectTabBaseUrl(proxySettings, customProxyBaseUrl);
const apiKeyForCurl = createdKeyValue
? createdKeyValue.startsWith("Bearer ")
@ -95,31 +97,32 @@ function ConnectTabContent({
return (
<div className="mx-auto max-w-3xl space-y-6">
<div>
<h3 className="text-sm font-semibold text-gray-900 mb-1">Proxy base URL</h3>
<h3 className="text-sm font-semibold text-gray-900 mb-1">{t("playground.agentBuilderView.proxyBaseUrl")}</h3>
<p className="text-sm text-gray-600 font-mono bg-gray-50 px-2 py-1.5 rounded border border-gray-200 break-all">
{baseUrl}
</p>
</div>
<div>
<h3 className="text-sm font-semibold text-gray-900 mb-2">Call your agent (cURL)</h3>
<h3 className="text-sm font-semibold text-gray-900 mb-2">{t("playground.agentBuilderView.callYourAgent")}</h3>
<CodeBlock code={curlExample} language="bash" />
</div>
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
<h3 className="text-sm font-semibold text-gray-900 mb-2">Create a key for this agent</h3>
<h3 className="text-sm font-semibold text-gray-900 mb-2">{t("playground.agentBuilderView.createKeyTitle")}</h3>
<p className="text-sm text-gray-600 mb-3">
Create a virtual key that can only call this agent. The key will be scoped to you (user_id) and restricted to
the model <span className="font-mono text-gray-800">{agentName}</span>.
<Trans
i18nKey="playground.agentBuilderView.createKeyDesc"
values={{ agentName }}
components={{ agentCode: <span className="font-mono text-gray-800" /> }}
/>
</p>
<Button type="primary" onClick={onCreateKey} loading={creatingKey} disabled={disabledPersonalKeyCreation}>
Create key for this agent
{t("playground.agentBuilderView.createKeyButton")}
</Button>
{disabledPersonalKeyCreation && (
<p className="text-xs text-amber-600 mt-2">Key creation is disabled for your account.</p>
<p className="text-xs text-amber-600 mt-2">{t("playground.agentBuilderView.keyCreationDisabled")}</p>
)}
{createdKeyValue && (
<p className="text-xs text-green-700 mt-2">
Key created. It is shown in the cURL example above — copy the snippet to use it.
</p>
<p className="text-xs text-green-700 mt-2">{t("playground.agentBuilderView.keyCreated")}</p>
)}
</div>
</div>
@ -180,6 +183,7 @@ export default function AgentBuilderView({
apiKey,
customProxyBaseUrl,
}: AgentBuilderViewProps) {
const { t } = useTranslation();
const [agentModels, setAgentModels] = useState<AgentModel[]>([]);
const [modelGroups, setModelGroups] = useState<ModelGroup[]>([]);
const [loadingAgents, setLoadingAgents] = useState(true);
@ -220,7 +224,7 @@ export default function AgentBuilderView({
return list;
} catch (e) {
console.error(e);
NotificationsManager.fromBackend("Failed to load agents");
NotificationsManager.fromBackend(t("playground.agentBuilderView.failedToLoadAgents"));
return [];
} finally {
setLoadingAgents(false);
@ -303,7 +307,7 @@ export default function AgentBuilderView({
const handleAddAgent = () => {
setSelectedId(NEW_AGENT_ID);
setDraftName("");
setDraftSystemPrompt("You are a helpful assistant.");
setDraftSystemPrompt(t("playground.agentBuilderView.defaultSystemPrompt"));
setDraftUnderlyingModel(modelGroups[0]?.model_group);
setDraftTemperature(0.7);
setDraftMaxTokens(4096);
@ -313,7 +317,7 @@ export default function AgentBuilderView({
const handleSaveAgent = async () => {
if (!accessToken || !draftName?.trim() || !draftUnderlyingModel) {
NotificationsManager.fromBackend("Name and underlying model are required");
NotificationsManager.fromBackend(t("playground.agentBuilderView.nameAndModelRequired"));
return;
}
setSaving(true);
@ -340,7 +344,7 @@ export default function AgentBuilderView({
setSelectedId(created ? getAgentSelectionKey(created) : list[0] ? getAgentSelectionKey(list[0]) : null);
setActiveTab("chat");
} catch (e) {
NotificationsManager.fromBackend("Failed to save agent");
NotificationsManager.fromBackend(t("playground.agentBuilderView.failedToSaveAgent"));
} finally {
setSaving(false);
}
@ -348,7 +352,7 @@ export default function AgentBuilderView({
const handleUpdateAgent = async () => {
if (!accessToken || !selectedAgent || !selectedAgentModelId || !draftName?.trim() || !draftUnderlyingModel) {
NotificationsManager.fromBackend("Name and underlying model are required");
NotificationsManager.fromBackend(t("playground.agentBuilderView.nameAndModelRequired"));
return;
}
setSaving(true);
@ -368,13 +372,13 @@ export default function AgentBuilderView({
},
selectedAgentModelId,
);
NotificationsManager.success("Agent updated successfully");
NotificationsManager.success(t("playground.agentBuilderView.agentUpdated"));
const list = await loadAgents();
const stillSelected = list.find((a) => getAgentModelId(a) === selectedAgentModelId);
const target = stillSelected ?? list[0];
setSelectedId(target ? getAgentSelectionKey(target) : null);
} catch (e) {
NotificationsManager.fromBackend("Failed to update agent");
NotificationsManager.fromBackend(t("playground.agentBuilderView.failedToUpdateAgent"));
} finally {
setSaving(false);
}
@ -392,12 +396,12 @@ export default function AgentBuilderView({
const keyValue = response?.key ?? null;
if (keyValue) {
setCreatedKeyValue(keyValue);
NotificationsManager.success("Virtual key created. Use it in the curl example below.");
NotificationsManager.success(t("playground.agentBuilderView.virtualKeyCreated"));
} else {
NotificationsManager.fromBackend("Key created but value not returned");
NotificationsManager.fromBackend(t("playground.agentBuilderView.keyCreatedNoValue"));
}
} catch (e) {
NotificationsManager.fromBackend("Failed to create key for agent");
NotificationsManager.fromBackend(t("playground.agentBuilderView.failedToCreateKey"));
} finally {
setCreatingKey(false);
}
@ -406,21 +410,21 @@ export default function AgentBuilderView({
const handleDeleteAgent = () => {
if (!selectedAgent || !selectedAgentModelId || !accessToken) return;
Modal.confirm({
title: "Delete agent",
content: `Are you sure you want to delete "${selectedAgent.model_name}"? This cannot be undone.`,
okText: "Delete",
title: t("playground.agentBuilderView.deleteAgentTitle"),
content: t("playground.agentBuilderView.deleteAgentConfirm", { name: selectedAgent.model_name }),
okText: t("common.delete"),
okType: "danger",
cancelText: "Cancel",
cancelText: t("common.cancel"),
onOk: async () => {
setDeleting(true);
try {
await modelDeleteCall(accessToken, selectedAgentModelId);
NotificationsManager.success("Agent deleted");
NotificationsManager.success(t("playground.agentBuilderView.agentDeleted"));
const list = await loadAgents();
const remaining = list.filter((a) => getAgentModelId(a) !== selectedAgentModelId);
setSelectedId(remaining.length > 0 ? getAgentSelectionKey(remaining[0]) : null);
} catch (e) {
NotificationsManager.fromBackend("Failed to delete agent");
NotificationsManager.fromBackend(t("playground.agentBuilderView.failedToDeleteAgent"));
} finally {
setDeleting(false);
}
@ -430,7 +434,9 @@ export default function AgentBuilderView({
if (!accessToken || !userID || !userRole) {
return (
<div className="flex h-full items-center justify-center p-8 text-gray-500">Sign in to use Agent Builder.</div>
<div className="flex h-full items-center justify-center p-8 text-gray-500">
{t("playground.agentBuilderView.signInRequired")}
</div>
);
}
@ -438,7 +444,7 @@ export default function AgentBuilderView({
<div className="flex h-full flex-col bg-white text-gray-900">
<div className="flex flex-shrink-0 flex-col border-b border-gray-200">
<div className="flex h-12 items-center justify-between px-4">
<span className="text-sm font-medium text-gray-900">Agent Builder</span>
<span className="text-sm font-medium text-gray-900">{t("playground.agentBuilderView.title")}</span>
{isNewAgent ? (
<Button
type="primary"
@ -447,21 +453,26 @@ export default function AgentBuilderView({
loading={saving}
disabled={!draftName?.trim() || !draftUnderlyingModel}
>
Save Agent
{t("playground.agentBuilderView.saveAgent")}
</Button>
) : (
<span className="text-xs text-gray-500">Build Agents that pass your compliance requirements.</span>
<span className="text-xs text-gray-500">{t("playground.agentBuilderView.buildAgentsSubtitle")}</span>
)}
</div>
<div className="flex items-center gap-2 border-t border-amber-200 bg-amber-50 px-4 py-2 text-xs text-amber-800">
<ExperimentOutlined className="flex-shrink-0 text-amber-600" />
<span>
Agent Builder is experimental and may change or be removed without notice. We’d love your feedback—email us
at{" "}
<a href="mailto:product@berri.ai" className="font-medium text-amber-900 underline hover:text-amber-700">
product@berri.ai
</a>
.
<Trans
i18nKey="playground.agentBuilderView.experimentalBanner"
components={{
emailLink: (
<a
href="mailto:product@berri.ai"
className="font-medium text-amber-900 underline hover:text-amber-700"
/>
),
}}
/>
</span>
</div>
</div>
@ -470,8 +481,16 @@ export default function AgentBuilderView({
{/* Roster */}
<div className="w-60 flex-shrink-0 border-r border-gray-200 bg-white flex flex-col">
<div className="flex items-center justify-between border-b border-gray-200 p-3">
<span className="text-xs font-semibold uppercase tracking-wide text-gray-500">Agents</span>
<Button type="text" size="small" icon={<PlusOutlined />} onClick={handleAddAgent} aria-label="Add agent" />
<span className="text-xs font-semibold uppercase tracking-wide text-gray-500">
{t("playground.agentBuilderView.agentsLabel")}
</span>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={handleAddAgent}
aria-label={t("playground.agentBuilderView.addAgentAriaLabel")}
/>
</div>
<div className="flex-1 overflow-y-auto p-2">
{loadingAgents ? (
@ -503,7 +522,7 @@ export default function AgentBuilderView({
onClick={handleAddAgent}
className="mb-1 w-full rounded-md border border-dashed border-gray-300 px-3 py-2 text-left text-sm text-gray-500 hover:border-blue-400 hover:bg-blue-50/50 hover:text-gray-700"
>
<PlusOutlined className="mr-1" /> New agent
<PlusOutlined className="mr-1" /> {t("playground.agentBuilderView.newAgent")}
</button>
</>
)}
@ -514,7 +533,7 @@ export default function AgentBuilderView({
<div className="flex flex-1 flex-col overflow-hidden">
{selectedId === null && !isNewAgent && agentModels.length === 0 && !loadingAgents && (
<div className="flex flex-1 items-center justify-center p-8 text-gray-500">
No agents yet. Add an agent to get started.
{t("playground.agentBuilderView.noAgentsYet")}
</div>
)}
{(selectedId !== null || isNewAgent) && (
@ -528,7 +547,7 @@ export default function AgentBuilderView({
key: "configure",
label: (
<span>
<RobotOutlined className="mr-1" /> Configure
<RobotOutlined className="mr-1" /> {t("playground.agentBuilderView.tabConfigure")}
</span>
),
children: (
@ -537,40 +556,47 @@ export default function AgentBuilderView({
<div className="mx-auto max-w-xl space-y-4">
{!selectedAgentModelId && selectedAgent && (
<div className="rounded border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
This agent cannot be updated or deleted here (missing model id). Manage it from Models
&amp; Endpoints.
{t("playground.agentBuilderView.missingModelIdWarning")}
</div>
)}
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">Agent name</label>
<label className="mb-1 block text-sm font-medium text-gray-700">
{t("playground.agentBuilderView.agentNameLabel")}
</label>
<Input
value={draftName}
onChange={(e) => setDraftName(e.target.value)}
placeholder="My Agent"
placeholder={t("playground.agentBuilderView.agentNamePlaceholder")}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">System prompt</label>
<label className="mb-1 block text-sm font-medium text-gray-700">
{t("playground.agentBuilderView.systemPromptLabel")}
</label>
<TextArea
value={draftSystemPrompt}
onChange={(e) => setDraftSystemPrompt(e.target.value)}
placeholder="You are a helpful assistant..."
placeholder={t("playground.agentBuilderView.systemPromptPlaceholder")}
rows={6}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">Underlying LLM</label>
<label className="mb-1 block text-sm font-medium text-gray-700">
{t("playground.agentBuilderView.underlyingLlmLabel")}
</label>
<Select
value={draftUnderlyingModel}
onChange={setDraftUnderlyingModel}
className="w-full"
options={modelGroups.map((m) => ({ value: m.model_group, label: m.model_group }))}
placeholder="Select model"
placeholder={t("common.select")}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">Temperature</label>
<label className="mb-1 block text-sm font-medium text-gray-700">
{t("playground.agentBuilderView.temperatureLabel")}
</label>
<Input
type="number"
min={0}
@ -581,7 +607,9 @@ export default function AgentBuilderView({
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">Max tokens</label>
<label className="mb-1 block text-sm font-medium text-gray-700">
{t("playground.agentBuilderView.maxTokensLabel")}
</label>
<Input
type="number"
min={1}
@ -591,10 +619,12 @@ export default function AgentBuilderView({
</div>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">MCP servers</label>
<label className="mb-1 block text-sm font-medium text-gray-700">
{t("playground.agentBuilderView.mcpServersLabel")}
</label>
<Select
mode="multiple"
placeholder="Select MCP servers to attach (same format as chat completions API)"
placeholder={t("playground.agentBuilderView.mcpServersPlaceholder")}
value={selectedMCPServerIds}
onChange={handleMCPServerChange}
loading={loadingMCPServers}
@ -609,9 +639,11 @@ export default function AgentBuilderView({
/>
{selectedAgent && draftTools.length > 0 && (
<p className="mt-1 text-xs text-gray-500">
{draftTools.length} MCP server{draftTools.length !== 1 ? "s" : ""} saved. Use the same{" "}
<code className="rounded bg-gray-100 px-1">tools</code> array in chat completions when
calling this agent.
<Trans
i18nKey="playground.agentBuilderView.mcpServersSaved"
count={draftTools.length}
components={{ toolsCode: <code className="rounded bg-gray-100 px-1" /> }}
/>
</p>
)}
</div>
@ -626,7 +658,7 @@ export default function AgentBuilderView({
loading={saving}
disabled={!draftName?.trim() || !draftUnderlyingModel}
>
Update Agent
{t("playground.agentBuilderView.updateAgent")}
</Button>
<Button
type="default"
@ -635,12 +667,12 @@ export default function AgentBuilderView({
onClick={handleDeleteAgent}
loading={deleting}
>
Delete
{t("common.delete")}
</Button>
</>
)}
<Button type="primary" icon={<CommentOutlined />} onClick={() => setActiveTab("chat")}>
Test in Chat
{t("playground.agentBuilderView.testInChat")}
</Button>
</div>
)}
@ -653,7 +685,7 @@ export default function AgentBuilderView({
key: "chat",
label: (
<span>
<CommentOutlined className="mr-1" /> Chat
<CommentOutlined className="mr-1" /> {t("playground.agentBuilderView.tabChat")}
</span>
),
disabled: isNewAgent,
@ -673,7 +705,7 @@ export default function AgentBuilderView({
/>
) : (
<div className="flex flex-1 items-center justify-center text-gray-500">
Save an agent first to test in Chat.
{t("playground.agentBuilderView.saveAgentFirst")}
</div>
)}
</div>
@ -683,7 +715,7 @@ export default function AgentBuilderView({
key: "test",
label: (
<span>
<ExperimentOutlined className="mr-1" /> Batch Test
<ExperimentOutlined className="mr-1" /> {t("playground.agentBuilderView.tabBatchTest")}
</span>
),
disabled: isNewAgent,
@ -699,7 +731,7 @@ export default function AgentBuilderView({
/>
) : (
<div className="flex flex-1 items-center justify-center text-gray-500">
Select an agent to run batch tests.
{t("playground.agentBuilderView.selectAgentForBatchTest")}
</div>
)}
</div>
@ -709,7 +741,7 @@ export default function AgentBuilderView({
key: "connect",
label: (
<span>
<LinkOutlined className="mr-1" /> Connect
<LinkOutlined className="mr-1" /> {t("playground.agentBuilderView.tabConnect")}
</span>
),
disabled: isNewAgent,
@ -729,7 +761,7 @@ export default function AgentBuilderView({
/>
) : (
<div className="flex flex-1 items-center justify-center text-gray-500">
Select an agent to see how to connect.
{t("playground.agentBuilderView.selectAgentToConnect")}
</div>
)}
</div>

View file

@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { MessageType } from "./types";
interface AudioRendererProps {
@ -6,6 +7,7 @@ interface AudioRendererProps {
}
const AudioRenderer: React.FC<AudioRendererProps> = ({ message }) => {
const { t } = useTranslation();
// Check if this message contains audio
if (!message.isAudio || typeof message.content !== "string") {
return null;
@ -14,7 +16,7 @@ const AudioRenderer: React.FC<AudioRendererProps> = ({ message }) => {
return (
<div className="mb-2">
<audio controls src={message.content} className="max-w-full" style={{ maxWidth: "500px" }}>
Your browser does not support the audio element.
{t("playground.audioRenderer.browserNotSupported")}
</audio>
</div>
);

View file

@ -1,5 +1,6 @@
import React from "react";
import Image from "next/image";
import { useTranslation } from "react-i18next";
import { MessageType } from "./types";
import { shouldShowChatAttachedImage } from "./ChatImageUtils";
import { FilePdfOutlined } from "@ant-design/icons";
@ -9,6 +10,7 @@ interface ChatImageRendererProps {
}
const ChatImageRenderer: React.FC<ChatImageRendererProps> = ({ message }) => {
const { t } = useTranslation();
if (!shouldShowChatAttachedImage(message)) {
return null;
}
@ -24,7 +26,7 @@ const ChatImageRenderer: React.FC<ChatImageRendererProps> = ({ message }) => {
) : (
<Image
src={message.imagePreviewUrl || ""}
alt="User uploaded image"
alt={t("playground.chatImageRenderer.uploadedImageAlt")}
width={256}
height={200}
className="max-w-64 rounded-md border border-gray-200 shadow-sm"

View file

@ -1,6 +1,7 @@
import React from "react";
import { Upload, Tooltip } from "antd";
import { PaperClipOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
const { Dragger } = Upload;
@ -17,6 +18,7 @@ const ChatImageUpload: React.FC<ChatImageUploadProps> = ({
onImageUpload,
onRemoveImage,
}) => {
const { t } = useTranslation();
return (
<>
{/* Subtle upload button - only show when no image */}
@ -28,7 +30,7 @@ const ChatImageUpload: React.FC<ChatImageUploadProps> = ({
className="inline-block"
style={{ padding: 0, border: "none", background: "none" }}
>
<Tooltip title="Attach image or PDF">
<Tooltip title={t("playground.chatImageUpload.attachTooltip")}>
<button
type="button"
className="flex items-center justify-center w-8 h-8 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md transition-colors"

View file

@ -1,5 +1,6 @@
import { RobotOutlined, UserOutlined } from "@ant-design/icons";
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";
@ -38,6 +39,7 @@ function ChatMessageBubble({
codeInterpreterResult,
accessToken,
}: ChatMessageBubbleProps) {
const { t } = useTranslation();
const isUser = message.role === "user";
return (
@ -114,7 +116,7 @@ function ChatMessageBubble({
{message.isImage ? (
<img
src={typeof message.content === "string" ? message.content : ""}
alt="Generated image"
alt={t("playground.chatMessageBubble.generatedImageAlt")}
className="max-w-full rounded-md border border-gray-200 shadow-sm"
style={{ maxHeight: "500px" }}
/>
@ -172,7 +174,7 @@ function ChatMessageBubble({
<div className="mt-3">
<img
src={message.image.url}
alt="Generated image"
alt={t("playground.chatMessageBubble.generatedImageAlt")}
className="max-w-full rounded-md border border-gray-200 shadow-sm"
style={{ maxHeight: "500px" }}
/>

View file

@ -3768,5 +3768,95 @@
"emptyNoMatch": "No servers match the current filters or search.",
"deleteSuccess": "Deleted MCP Server successfully"
}
},
"playground": {
"a2AMetrics": {
"header": "A2A Metadata",
"totalLatency": "Total latency",
"timeToFirstToken": "Time to first token",
"clickToCopy": "Click to copy: {{id}}",
"taskPrefix": "Task: ",
"sessionPrefix": "Session: ",
"details": "Details",
"statusMessage": "Status Message:",
"taskId": "Task ID:",
"sessionId": "Session ID:",
"customMetadata": "Custom Metadata:"
},
"additionalModelSettings": {
"useAdvancedParams": "Use Advanced Parameters",
"simulateFailure": "Simulate failure to test fallbacks",
"simulateFailureDesc": "Causes the first request to fail so the router tries fallbacks (if configured). Use this to verify your fallback setup.",
"simulateFailureNote": "Behavior can differ when keys, teams, or router settings are configured. <learnMoreLink>Learn more</learnMoreLink>",
"simulateFailureAriaLabel": "Help: Simulate failure to test fallbacks",
"temperature": "Temperature",
"temperatureTooltip": "Controls randomness. Lower values make output more deterministic, higher values more creative.",
"maxTokens": "Max Tokens",
"maxTokensTooltip": "Maximum number of tokens to generate in the response."
},
"agentBuilderView": {
"title": "Agent Builder",
"saveAgent": "Save Agent",
"buildAgentsSubtitle": "Build Agents that pass your compliance requirements.",
"experimentalBanner": "Agent Builder is experimental and may change or be removed without notice. We'd love your feedback—email us at <emailLink>product@berri.ai</emailLink>.",
"agentsLabel": "Agents",
"addAgentAriaLabel": "Add agent",
"newAgent": "New agent",
"noAgentsYet": "No agents yet. Add an agent to get started.",
"signInRequired": "Sign in to use Agent Builder.",
"tabConfigure": "Configure",
"tabChat": "Chat",
"tabBatchTest": "Batch Test",
"tabConnect": "Connect",
"missingModelIdWarning": "This agent cannot be updated or deleted here (missing model id). Manage it from Models & Endpoints.",
"agentNameLabel": "Agent name",
"agentNamePlaceholder": "My Agent",
"systemPromptLabel": "System prompt",
"systemPromptPlaceholder": "You are a helpful assistant...",
"defaultSystemPrompt": "You are a helpful assistant.",
"underlyingLlmLabel": "Underlying LLM",
"temperatureLabel": "Temperature",
"maxTokensLabel": "Max tokens",
"mcpServersLabel": "MCP servers",
"mcpServersPlaceholder": "Select MCP servers to attach (same format as chat completions API)",
"mcpServersSaved_one": "{{count}} MCP server saved. Use the same <toolsCode>tools</toolsCode> array in chat completions when calling this agent.",
"mcpServersSaved_other": "{{count}} MCP servers saved. Use the same <toolsCode>tools</toolsCode> array in chat completions when calling this agent.",
"updateAgent": "Update Agent",
"testInChat": "Test in Chat",
"saveAgentFirst": "Save an agent first to test in Chat.",
"selectAgentForBatchTest": "Select an agent to run batch tests.",
"selectAgentToConnect": "Select an agent to see how to connect.",
"proxyBaseUrl": "Proxy base URL",
"callYourAgent": "Call your agent (cURL)",
"createKeyTitle": "Create a key for this agent",
"createKeyDesc": "Create a virtual key that can only call this agent. The key will be scoped to you (user_id) and restricted to the model <agentCode>{{agentName}}</agentCode>.",
"createKeyButton": "Create key for this agent",
"keyCreationDisabled": "Key creation is disabled for your account.",
"keyCreated": "Key created. It is shown in the cURL example above — copy the snippet to use it.",
"failedToLoadAgents": "Failed to load agents",
"nameAndModelRequired": "Name and underlying model are required",
"failedToSaveAgent": "Failed to save agent",
"agentUpdated": "Agent updated successfully",
"failedToUpdateAgent": "Failed to update agent",
"virtualKeyCreated": "Virtual key created. Use it in the curl example below.",
"keyCreatedNoValue": "Key created but value not returned",
"failedToCreateKey": "Failed to create key for agent",
"deleteAgentTitle": "Delete agent",
"deleteAgentConfirm": "Are you sure you want to delete \"{{name}}\"? This cannot be undone.",
"agentDeleted": "Agent deleted",
"failedToDeleteAgent": "Failed to delete agent"
},
"audioRenderer": {
"browserNotSupported": "Your browser does not support the audio element."
},
"chatImageRenderer": {
"uploadedImageAlt": "User uploaded image"
},
"chatImageUpload": {
"attachTooltip": "Attach image or PDF"
},
"chatMessageBubble": {
"generatedImageAlt": "Generated image"
}
}
}

View file

@ -3768,5 +3768,95 @@
"emptyNoMatch": "没有服务器匹配当前过滤条件或搜索。",
"deleteSuccess": "MCP 服务器已成功删除"
}
},
"playground": {
"a2AMetrics": {
"header": "A2A 元数据",
"totalLatency": "总延迟",
"timeToFirstToken": "首 Token 时间",
"clickToCopy": "点击复制:{{id}}",
"taskPrefix": "任务:",
"sessionPrefix": "会话:",
"details": "详情",
"statusMessage": "状态消息:",
"taskId": "任务 ID:",
"sessionId": "会话 ID:",
"customMetadata": "自定义元数据:"
},
"additionalModelSettings": {
"useAdvancedParams": "使用高级参数",
"simulateFailure": "模拟失败以测试回退",
"simulateFailureDesc": "使第一个请求失败,以便路由器尝试回退(如已配置)。用于验证您的回退设置。",
"simulateFailureNote": "当配置了密钥、团队或路由器设置时,行为可能有所不同。<learnMoreLink>了解更多</learnMoreLink>",
"simulateFailureAriaLabel": "帮助:模拟失败以测试回退",
"temperature": "Temperature",
"temperatureTooltip": "控制随机性。值越低输出越确定,值越高输出越有创意。",
"maxTokens": "最大 Token 数",
"maxTokensTooltip": "响应中生成的最大 Token 数。"
},
"agentBuilderView": {
"title": "Agent Builder",
"saveAgent": "保存 Agent",
"buildAgentsSubtitle": "构建符合合规要求的 Agent。",
"experimentalBanner": "Agent Builder 为实验性功能,可能随时更改或移除。欢迎反馈——请发送邮件至 <emailLink>product@berri.ai</emailLink>。",
"agentsLabel": "Agents",
"addAgentAriaLabel": "添加 Agent",
"newAgent": "新建 Agent",
"noAgentsYet": "暂无 Agent。添加一个 Agent 以开始使用。",
"signInRequired": "请登录以使用 Agent Builder。",
"tabConfigure": "配置",
"tabChat": "聊天",
"tabBatchTest": "批量测试",
"tabConnect": "连接",
"missingModelIdWarning": "此 Agent 无法在此处更新或删除(缺少模型 ID)。请从模型与端点页面管理。",
"agentNameLabel": "Agent 名称",
"agentNamePlaceholder": "我的 Agent",
"systemPromptLabel": "系统提示词",
"systemPromptPlaceholder": "你是一个有帮助的助手...",
"defaultSystemPrompt": "You are a helpful assistant.",
"underlyingLlmLabel": "底层 LLM",
"temperatureLabel": "Temperature",
"maxTokensLabel": "最大 Token 数",
"mcpServersLabel": "MCP 服务器",
"mcpServersPlaceholder": "选择要附加的 MCP 服务器(格式与 chat completions API 相同)",
"mcpServersSaved_one": "已保存 {{count}} 个 MCP 服务器。调用此 Agent 时,请在 chat completions 中使用相同的 <toolsCode>tools</toolsCode> 数组。",
"mcpServersSaved_other": "已保存 {{count}} 个 MCP 服务器。调用此 Agent 时,请在 chat completions 中使用相同的 <toolsCode>tools</toolsCode> 数组。",
"updateAgent": "更新 Agent",
"testInChat": "在聊天中测试",
"saveAgentFirst": "请先保存 Agent 以在聊天中测试。",
"selectAgentForBatchTest": "请选择一个 Agent 以运行批量测试。",
"selectAgentToConnect": "请选择一个 Agent 以查看连接方式。",
"proxyBaseUrl": "代理基础 URL",
"callYourAgent": "调用您的 Agent(cURL)",
"createKeyTitle": "为此 Agent 创建密钥",
"createKeyDesc": "创建一个只能调用此 Agent 的虚拟密钥。该密钥将绑定到您(user_id),并限制为模型 <agentCode>{{agentName}}</agentCode>。",
"createKeyButton": "为此 Agent 创建密钥",
"keyCreationDisabled": "您的账户已禁用密钥创建。",
"keyCreated": "密钥已创建。它显示在上方的 cURL 示例中——复制代码片段以使用。",
"failedToLoadAgents": "加载 Agent 失败",
"nameAndModelRequired": "名称和底层模型为必填项",
"failedToSaveAgent": "保存 Agent 失败",
"agentUpdated": "Agent 已成功更新",
"failedToUpdateAgent": "更新 Agent 失败",
"virtualKeyCreated": "虚拟密钥已创建。请在下方的 curl 示例中使用。",
"keyCreatedNoValue": "密钥已创建但未返回值",
"failedToCreateKey": "为 Agent 创建密钥失败",
"deleteAgentTitle": "删除 Agent",
"deleteAgentConfirm": "确定要删除 \"{{name}}\"?此操作无法撤销。",
"agentDeleted": "Agent 已删除",
"failedToDeleteAgent": "删除 Agent 失败"
},
"audioRenderer": {
"browserNotSupported": "您的浏览器不支持音频元素。"
},
"chatImageRenderer": {
"uploadedImageAlt": "用户上传的图片"
},
"chatImageUpload": {
"attachTooltip": "附加图片或 PDF"
},
"chatMessageBubble": {
"generatedImageAlt": "生成的图片"
}
}
}