feat(ui): i18n for playground chat_ui batch 3

This commit is contained in:
吹雪 2026-06-10 21:52:46 +09:00
parent c48e2ad23f
commit a4b623ba37
12 changed files with 287 additions and 67 deletions

View file

@ -10,6 +10,7 @@ import {
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import { useTranslation } from "react-i18next";
interface ContainerFileCitation {
type: "container_file_citation";
@ -33,6 +34,7 @@ const CodeInterpreterOutput: React.FC<CodeInterpreterOutputProps> = ({
annotations = [],
accessToken,
}) => {
const { t } = useTranslation();
const [imageUrls, setImageUrls] = useState<Record<string, string>>({});
const [loadingImages, setLoadingImages] = useState<Record<string, boolean>>({});
const proxyBaseUrl = getProxyBaseUrl();
@ -144,7 +146,7 @@ const CodeInterpreterOutput: React.FC<CodeInterpreterOutputProps> = ({
key: "code",
label: (
<span className="flex items-center gap-2 text-sm text-gray-600">
<CodeOutlined /> Python Code Executed
<CodeOutlined /> {t("playground.codeInterpreterOutput.pythonCodeExecuted")}
</span>
),
children: (
@ -173,13 +175,13 @@ const CodeInterpreterOutput: React.FC<CodeInterpreterOutputProps> = ({
{loadingImages[annotation.file_id] ? (
<div className="flex items-center justify-center p-8 bg-gray-50">
<Spin indicator={<LoadingOutlined spin />} />
<span className="ml-2 text-sm text-gray-500">Loading image...</span>
<span className="ml-2 text-sm text-gray-500">{t("playground.codeInterpreterOutput.loadingImage")}</span>
</div>
) : imageUrls[annotation.file_id] ? (
<div>
<img
src={imageUrls[annotation.file_id]}
alt={annotation.filename || "Generated chart"}
alt={annotation.filename || t("playground.codeInterpreterOutput.generatedChart")}
className="max-w-full"
style={{ maxHeight: "400px" }}
/>
@ -191,13 +193,13 @@ const CodeInterpreterOutput: React.FC<CodeInterpreterOutputProps> = ({
onClick={() => handleDownload(annotation)}
className="text-xs text-blue-500 hover:text-blue-700 flex items-center gap-1"
>
<DownloadOutlined /> Download
<DownloadOutlined /> {t("common.download")}
</button>
</div>
</div>
) : (
<div className="flex items-center justify-center p-4 bg-gray-50">
<span className="text-sm text-gray-400">Image not available</span>
<span className="text-sm text-gray-400">{t("playground.codeInterpreterOutput.imageNotAvailable")}</span>
</div>
)}
</div>

View file

@ -3,6 +3,7 @@ import { Switch, Tooltip } from "antd";
import MessageManager from "@/components/molecules/message_manager";
import { CodeOutlined, InfoCircleOutlined, ExclamationCircleOutlined } from "@ant-design/icons";
import { Text } from "@tremor/react";
import { useTranslation } from "react-i18next";
interface CodeInterpreterToolProps {
accessToken: string;
@ -34,12 +35,13 @@ const CodeInterpreterTool: React.FC<CodeInterpreterToolProps> = ({
selectedModel,
disabled = false,
}) => {
const { t } = useTranslation();
const isOpenAI = isOpenAIModel(selectedModel);
const isDisabled = disabled || !isOpenAI;
const handleToggle = (checked: boolean) => {
if (checked && !isOpenAI) {
MessageManager.warning("Code Interpreter is only available for OpenAI models");
MessageManager.warning(t("playground.codeInterpreterTool.onlyOpenAIWarning"));
return;
}
onEnabledChange(checked);
@ -50,8 +52,8 @@ const CodeInterpreterTool: React.FC<CodeInterpreterToolProps> = ({
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<CodeOutlined className="text-blue-500" />
<Text className="font-medium text-gray-700">Code Interpreter</Text>
<Tooltip title="Run Python code to generate files, charts, and analyze data. Container is created automatically.">
<Text className="font-medium text-gray-700">{t("playground.codeInterpreterTool.title")}</Text>
<Tooltip title={t("playground.codeInterpreterTool.tooltipDesc")}>
<InfoCircleOutlined className="text-gray-400 text-xs" />
</Tooltip>
</div>
@ -69,14 +71,14 @@ const CodeInterpreterTool: React.FC<CodeInterpreterToolProps> = ({
<div className="flex items-start gap-2">
<ExclamationCircleOutlined className="text-amber-500 mt-0.5" />
<div className="text-xs text-gray-600">
<span>Code Interpreter is currently only supported for OpenAI models. </span>
<span>{t("playground.codeInterpreterTool.onlyOpenAINotice")} </span>
<a
href={GITHUB_FEATURE_REQUEST_URL}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 underline"
>
Request support for other providers
{t("playground.codeInterpreterTool.requestSupportLink")}
</a>
</div>
</div>

View file

@ -1,4 +1,5 @@
import { DeleteOutlined, FilePdfOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
interface FilePreviewCardProps {
file: File;
@ -7,6 +8,7 @@ interface FilePreviewCardProps {
}
function FilePreviewCard({ file, previewUrl, onRemove }: FilePreviewCardProps) {
const { t } = useTranslation();
const isPdf = file.name.toLowerCase().endsWith(".pdf");
return (
@ -20,14 +22,16 @@ function FilePreviewCard({ file, previewUrl, onRemove }: FilePreviewCardProps) {
) : (
<img
src={previewUrl || ""}
alt="Upload preview"
alt={t("playground.filePreviewCard.uploadPreview")}
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">{file.name}</div>
<div className="text-xs text-gray-500">{isPdf ? "PDF" : "Image"}</div>
<div className="text-xs text-gray-500">
{isPdf ? t("playground.filePreviewCard.pdf") : t("playground.filePreviewCard.image")}
</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,6 +1,7 @@
import React from "react";
import { Typography, Collapse } from "antd";
import type { MCPEvent } from "../../mcp_tools/types";
import { useTranslation } from "react-i18next";
const { Text } = Typography;
const { Panel } = Collapse;
@ -11,6 +12,7 @@ interface MCPEventsDisplayProps {
}
const MCPEventsDisplay: React.FC<MCPEventsDisplayProps> = ({ events, className }) => {
const { t } = useTranslation();
console.log("MCPEventsDisplay: Received events:", events);
if (!events || events.length === 0) {
@ -173,7 +175,7 @@ const MCPEventsDisplay: React.FC<MCPEventsDisplayProps> = ({ events, className }
>
{/* List Tools Panel */}
{toolsEvent && (
<Panel header="List tools" key="list-tools">
<Panel header={t("playground.mcpEventsDisplay.listTools")} key="list-tools">
<div>
{toolsEvent.item?.tools?.map((tool, index) => (
<div key={index} className="tool-item">
@ -186,11 +188,11 @@ const MCPEventsDisplay: React.FC<MCPEventsDisplayProps> = ({ events, className }
{/* MCP Call Panels */}
{mcpCallEvents.map((callEvent, index) => (
<Panel header={callEvent.item?.name || "Tool call"} key={`mcp-call-${index}`}>
<Panel header={callEvent.item?.name || t("playground.mcpEventsDisplay.toolCall")} key={`mcp-call-${index}`}>
<div>
{/* Request section */}
<div className="mcp-section">
<div className="mcp-section-header">Request</div>
<div className="mcp-section-header">{t("playground.mcpEventsDisplay.request")}</div>
<div className="mcp-code-block">
{callEvent.item?.arguments && (
<pre className="mcp-json">
@ -209,14 +211,14 @@ const MCPEventsDisplay: React.FC<MCPEventsDisplayProps> = ({ events, className }
{/* Approved section */}
<div className="mcp-section">
<div className="mcp-approved">
<span className="mcp-checkmark">✓</span> Approved
<span className="mcp-checkmark">✓</span> {t("playground.mcpEventsDisplay.approved")}
</div>
</div>
{/* Response section */}
{callEvent.item?.output && (
<div className="mcp-section">
<div className="mcp-section-header">Response</div>
<div className="mcp-section-header">{t("playground.mcpEventsDisplay.response")}</div>
<div className="mcp-response-content">{callEvent.item.output}</div>
</div>
)}

View file

@ -5,6 +5,7 @@ import { Button, Input, Select, Typography } from "antd";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { getProxyBaseUrl } from "../../networking";
import { OPEN_AI_VOICE_SELECT_OPTIONS } from "./chatConstants";
import { useTranslation } from "react-i18next";
const { Text } = Typography;
@ -27,6 +28,7 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
customProxyBaseUrl,
selectedGuardrails,
}) => {
const { t } = useTranslation();
const [messages, setMessages] = useState<RealtimeMessage[]>([]);
const [inputText, setInputText] = useState("");
const [isConnected, setIsConnected] = useState(false);
@ -90,7 +92,7 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
const connect = useCallback(async () => {
if (wsRef.current) return;
if (!selectedModel) {
addMessage("status", "Please select a model first");
addMessage("status", t("playground.realtimePlayground.selectModelFirst"));
return;
}
setIsConnecting(true);
@ -110,7 +112,7 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
ws.onopen = () => {
setIsConnected(true);
setIsConnecting(false);
addMessage("status", "Connected to realtime API");
addMessage("status", t("playground.realtimePlayground.connectedToApi"));
};
ws.onmessage = async (event) => {
@ -181,7 +183,10 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
return prev;
});
} else if (type === "error") {
addMessage("status", `Error: ${data.error?.message || JSON.stringify(data.error)}`);
addMessage(
"status",
t("playground.realtimePlayground.wsError", { error: data.error?.message || JSON.stringify(data.error) }),
);
}
} catch {
// ignore parse errors
@ -189,13 +194,13 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
};
ws.onerror = () => {
addMessage("status", "WebSocket error");
addMessage("status", t("playground.realtimePlayground.websocketError"));
setIsConnected(false);
setIsConnecting(false);
};
ws.onclose = () => {
addMessage("status", "Disconnected");
addMessage("status", t("playground.realtimePlayground.disconnected"));
setIsConnected(false);
setIsConnecting(false);
wsRef.current = null;
@ -203,7 +208,7 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
wsRef.current = ws;
} catch (err: any) {
addMessage("status", `Connection failed: ${err.message}`);
addMessage("status", t("playground.realtimePlayground.connectionFailed", { error: err.message }));
setIsConnecting(false);
}
}, [
@ -297,9 +302,9 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
source.connect(processor);
processor.connect(ctx.destination);
setIsRecording(true);
addMessage("status", "🎙️ Listening...");
addMessage("status", t("playground.realtimePlayground.listening"));
} catch (err: any) {
addMessage("status", `Microphone error: ${err.message}`);
addMessage("status", t("playground.realtimePlayground.microphoneError", { error: err.message }));
}
}, [addMessage]);
@ -367,10 +372,14 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-gray-50">
<div className="flex items-center gap-3">
<SoundOutlined className="text-lg text-blue-500" />
<Text className="font-semibold text-gray-800">Realtime Voice Chat</Text>
<Text className="font-semibold text-gray-800">{t("playground.realtimePlayground.title")}</Text>
<span className={`inline-block w-2 h-2 rounded-full ${isConnected ? "bg-green-500" : "bg-gray-300"}`} />
<Text className="text-xs text-gray-500">
{isConnected ? "Connected" : isConnecting ? "Connecting..." : "Disconnected"}
{isConnected
? t("playground.realtimePlayground.connected")
: isConnecting
? t("playground.realtimePlayground.connecting")
: t("playground.realtimePlayground.disconnectedStatus")}
</Text>
</div>
<div className="flex items-center gap-2">
@ -384,11 +393,11 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
/>
{!isConnected ? (
<Button type="primary" onClick={connect} loading={isConnecting} size="small">
Connect
{t("playground.realtimePlayground.connect")}
</Button>
) : (
<Button danger onClick={disconnect} size="small" icon={<CloseCircleOutlined />}>
Disconnect
{t("playground.realtimePlayground.disconnect")}
</Button>
)}
</div>
@ -399,10 +408,9 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
{messages.length === 0 && !isConnected && (
<div className="flex flex-col items-center justify-center h-full text-gray-400 gap-3">
<SoundOutlined style={{ fontSize: 48 }} />
<Text className="text-lg text-gray-500">Realtime Voice Playground</Text>
<Text className="text-lg text-gray-500">{t("playground.realtimePlayground.emptyTitle")}</Text>
<Text className="text-sm text-gray-400 text-center max-w-md">
Click <b>Connect</b> to start a realtime session. You can speak using your microphone or type messages.
The AI will respond with voice and text.
{t("playground.realtimePlayground.emptyHint")}
</Text>
</div>
)}
@ -421,7 +429,9 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
: "bg-gray-100 text-gray-800 rounded-bl-md"
}`}
>
<div className="text-xs font-medium mb-0.5 opacity-70">{msg.role === "user" ? "You" : "AI"}</div>
<div className="text-xs font-medium mb-0.5 opacity-70">
{msg.role === "user" ? t("playground.realtimePlayground.you") : t("playground.realtimePlayground.ai")}
</div>
<div className="text-sm whitespace-pre-wrap">{msg.content}</div>
</div>
)}
@ -441,11 +451,15 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
danger={isRecording}
icon={isRecording ? <AudioMutedOutlined /> : <AudioOutlined />}
onClick={isRecording ? stopRecording : startRecording}
title={isRecording ? "Stop recording" : "Start recording"}
title={
isRecording
? t("playground.realtimePlayground.stopRecording")
: t("playground.realtimePlayground.startRecording")
}
className={isRecording ? "animate-pulse" : ""}
/>
<Input
placeholder="Type a message or use the mic..."
placeholder={t("playground.realtimePlayground.inputPlaceholder")}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onPressEnter={sendTextMessage}
@ -463,7 +477,7 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
{isRecording && (
<div className="mt-2 flex items-center gap-2 text-red-500 text-xs">
<span className="inline-block w-2 h-2 rounded-full bg-red-500 animate-pulse" />
Listening — speak into your microphone. Server VAD will detect when you stop.
{t("playground.realtimePlayground.listeningHint")}
</div>
)}
</div>

View file

@ -9,6 +9,7 @@ import {
ToolOutlined,
DollarOutlined,
} from "@ant-design/icons";
import { useTranslation } from "react-i18next";
export interface TokenUsage {
completionTokens?: number;
@ -26,66 +27,69 @@ interface ResponseMetricsProps {
}
const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, totalLatency, usage, toolName }) => {
const { t } = useTranslation();
if (!timeToFirstToken && !totalLatency && !usage) return null;
return (
<div className="response-metrics mt-2 pt-2 border-t border-gray-100 text-xs text-gray-500 flex flex-wrap gap-3">
{timeToFirstToken !== undefined && (
<Tooltip title="Time to first token">
<Tooltip title={t("playground.responseMetrics.timeToFirstToken")}>
<div className="flex items-center">
<ClockCircleOutlined className="mr-1" />
<span>TTFT: {(timeToFirstToken / 1000).toFixed(2)}s</span>
<span>{t("playground.responseMetrics.ttft", { value: (timeToFirstToken / 1000).toFixed(2) })}</span>
</div>
</Tooltip>
)}
{totalLatency !== undefined && (
<Tooltip title="Total latency">
<Tooltip title={t("playground.responseMetrics.totalLatency")}>
<div className="flex items-center">
<ClockCircleOutlined className="mr-1" />
<span>Total Latency: {(totalLatency / 1000).toFixed(2)}s</span>
<span>
{t("playground.responseMetrics.totalLatencyValue", { value: (totalLatency / 1000).toFixed(2) })}
</span>
</div>
</Tooltip>
)}
{usage?.promptTokens !== undefined && (
<Tooltip title="Prompt tokens">
<Tooltip title={t("playground.responseMetrics.promptTokens")}>
<div className="flex items-center">
<ImportOutlined className="mr-1" />
<span>In: {usage.promptTokens}</span>
<span>{t("playground.responseMetrics.in", { count: usage.promptTokens })}</span>
</div>
</Tooltip>
)}
{usage?.completionTokens !== undefined && (
<Tooltip title="Completion tokens">
<Tooltip title={t("playground.responseMetrics.completionTokens")}>
<div className="flex items-center">
<ExportOutlined className="mr-1" />
<span>Out: {usage.completionTokens}</span>
<span>{t("playground.responseMetrics.out", { count: usage.completionTokens })}</span>
</div>
</Tooltip>
)}
{usage?.reasoningTokens !== undefined && (
<Tooltip title="Reasoning tokens">
<Tooltip title={t("playground.responseMetrics.reasoningTokens")}>
<div className="flex items-center">
<BulbOutlined className="mr-1" />
<span>Reasoning: {usage.reasoningTokens}</span>
<span>{t("playground.responseMetrics.reasoning", { count: usage.reasoningTokens })}</span>
</div>
</Tooltip>
)}
{usage?.totalTokens !== undefined && (
<Tooltip title="Total tokens">
<Tooltip title={t("playground.responseMetrics.totalTokens")}>
<div className="flex items-center">
<NumberOutlined className="mr-1" />
<span>Total: {usage.totalTokens}</span>
<span>{t("playground.responseMetrics.total", { count: usage.totalTokens })}</span>
</div>
</Tooltip>
)}
{usage?.cost !== undefined && (
<Tooltip title="Cost">
<Tooltip title={t("playground.responseMetrics.cost")}>
<div className="flex items-center">
<DollarOutlined className="mr-1" />
<span>${usage.cost.toFixed(6)}</span>
@ -94,10 +98,10 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, tot
)}
{toolName && (
<Tooltip title="Tool used">
<Tooltip title={t("playground.responseMetrics.toolUsed")}>
<div className="flex items-center">
<ToolOutlined className="mr-1" />
<span>Tool: {toolName}</span>
<span>{t("playground.responseMetrics.toolName", { name: toolName })}</span>
</div>
</Tooltip>
)}

View file

@ -2,12 +2,14 @@ import React from "react";
import { MessageType } from "./types";
import { shouldShowAttachedImage } from "./ResponsesImageUtils";
import { FilePdfOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
interface ResponsesImageRendererProps {
message: MessageType;
}
const ResponsesImageRenderer: React.FC<ResponsesImageRendererProps> = ({ message }) => {
const { t } = useTranslation();
if (!shouldShowAttachedImage(message)) {
return null;
}
@ -23,7 +25,7 @@ const ResponsesImageRenderer: React.FC<ResponsesImageRendererProps> = ({ message
) : (
<img
src={message.imagePreviewUrl}
alt="User uploaded image"
alt={t("playground.chatImageRenderer.uploadedImageAlt")}
className="max-w-64 rounded-md border border-gray-200 shadow-sm"
style={{ maxHeight: "200px" }}
/>

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 ResponsesImageUpload: React.FC<ResponsesImageUploadProps> = ({
onImageUpload,
onRemoveImage,
}) => {
const { t } = useTranslation();
return (
<>
{/* Subtle upload button - only show when no image */}
@ -28,7 +30,7 @@ const ResponsesImageUpload: React.FC<ResponsesImageUploadProps> = ({
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

@ -2,12 +2,14 @@ import React, { useState } from "react";
import { Button } from "antd";
import { VectorStoreSearchResponse } from "./types";
import { DatabaseOutlined, FileTextOutlined, DownOutlined, RightOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
interface SearchResultsDisplayProps {
searchResults: VectorStoreSearchResponse[];
}
export function SearchResultsDisplay({ searchResults }: SearchResultsDisplayProps) {
const { t } = useTranslation();
const [isExpanded, setIsExpanded] = useState(true);
const [expandedResults, setExpandedResults] = useState<Record<string, boolean>>({});
@ -33,7 +35,9 @@ export function SearchResultsDisplay({ searchResults }: SearchResultsDisplayProp
onClick={() => setIsExpanded(!isExpanded)}
icon={<DatabaseOutlined />}
>
{isExpanded ? "Hide sources" : `Show sources (${totalResults})`}
{isExpanded
? t("playground.searchResultsDisplay.hideSources")
: t("playground.searchResultsDisplay.showSources", { count: totalResults })}
{isExpanded ? <DownOutlined className="ml-1" /> : <RightOutlined className="ml-1" />}
</Button>
@ -43,11 +47,11 @@ export function SearchResultsDisplay({ searchResults }: SearchResultsDisplayProp
{searchResults.map((resultPage, pageIndex) => (
<div key={pageIndex}>
<div className="text-xs text-gray-600 mb-2 flex items-center gap-2">
<span className="font-medium">Query:</span>
<span className="font-medium">{t("playground.searchResultsDisplay.query")}</span>
<span className="italic">&quot;{resultPage.search_query}&quot;</span>
<span className="text-gray-400">•</span>
<span className="text-gray-500">
{resultPage.data.length} result{resultPage.data.length !== 1 ? "s" : ""}
{t("playground.searchResultsDisplay.resultCount", { count: resultPage.data.length })}
</span>
</div>
@ -93,7 +97,9 @@ export function SearchResultsDisplay({ searchResults }: SearchResultsDisplayProp
{result.attributes && Object.keys(result.attributes).length > 0 && (
<div className="mt-2 pt-2 border-t border-gray-100">
<div className="text-xs text-gray-500 mb-1 font-medium">Metadata:</div>
<div className="text-xs text-gray-500 mb-1 font-medium">
{t("playground.searchResultsDisplay.metadata")}
</div>
<div className="space-y-1">
{Object.entries(result.attributes).map(([key, value]) => (
<div key={key} className="text-xs flex gap-2">

View file

@ -3,6 +3,7 @@ import { Switch, Tooltip } from "antd";
import { InfoCircleOutlined, CopyOutlined } from "@ant-design/icons";
import { EndpointType } from "./mode_endpoint_mapping";
import NotificationsManager from "../../molecules/notifications_manager";
import { useTranslation } from "react-i18next";
interface SessionManagementProps {
endpointType: string;
@ -17,6 +18,7 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
useApiSessionManagement,
onToggleSessionManagement,
}) => {
const { t } = useTranslation();
if (endpointType !== EndpointType.RESPONSES) {
return null;
}
@ -24,16 +26,20 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
const handleCopySessionId = () => {
if (responsesSessionId) {
navigator.clipboard.writeText(responsesSessionId);
NotificationsManager.success("Response ID copied to clipboard!");
NotificationsManager.success(t("playground.sessionManagement.copiedToClipboard"));
}
};
const getSessionDisplay = () => {
if (!responsesSessionId) {
return useApiSessionManagement ? "API Session: Ready" : "UI Session: Ready";
return useApiSessionManagement
? t("playground.sessionManagement.apiSessionReady")
: t("playground.sessionManagement.uiSessionReady");
}
const sessionPrefix = useApiSessionManagement ? "Response ID" : "UI Session";
const sessionPrefix = useApiSessionManagement
? t("playground.sessionManagement.responseIdPrefix")
: t("playground.sessionManagement.uiSessionPrefix");
const truncatedId = responsesSessionId.slice(0, 10);
return `${sessionPrefix}: ${truncatedId}...`;
};
@ -41,13 +47,13 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
const getSessionDescription = () => {
if (!responsesSessionId) {
return useApiSessionManagement
? "LiteLLM will manage session using previous_response_id"
: "UI will manage session using chat history";
? t("playground.sessionManagement.apiSessionReadyDesc")
: t("playground.sessionManagement.uiSessionReadyDesc");
}
return useApiSessionManagement
? "LiteLLM API session active - context maintained server-side"
: "UI session active - context maintained client-side";
? t("playground.sessionManagement.apiSessionActiveDesc")
: t("playground.sessionManagement.uiSessionActiveDesc");
};
return (
@ -55,8 +61,8 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
{/* Session Management Toggle */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-700">Session Management</span>
<Tooltip title="Choose between LiteLLM API session management (using previous_response_id) or UI-based session management (using chat history)">
<span className="text-sm font-medium text-gray-700">{t("playground.sessionManagement.title")}</span>
<Tooltip title={t("playground.sessionManagement.toggleTooltip")}>
<InfoCircleOutlined className="text-gray-400" style={{ fontSize: "12px" }} />
</Tooltip>
</div>
@ -86,7 +92,7 @@ const SessionManagement: React.FC<SessionManagementProps> = ({
<Tooltip
title={
<div className="text-xs">
<div className="mb-1">Copy response ID to continue session:</div>
<div className="mb-1">{t("playground.sessionManagement.copyTooltipTitle")}</div>
<div className="bg-gray-800 text-gray-100 p-2 rounded font-mono text-xs whitespace-pre-wrap">
{`curl -X POST "your-proxy-url/v1/responses" \\
-H "Authorization: Bearer your-api-key" \\

View file

@ -3972,6 +3972,94 @@
"toolsetsExample": "<strong>Example:</strong> A \"GitHub Read-only\" toolset might include only <code>list_repos</code> and <code>get_file</code> from a GitHub MCP server — preventing agents from making writes.",
"toolsetsCreatingTitle": "Creating toolsets:",
"toolsetsCreatingDesc": "Admins can create and manage toolsets from the <strong>MCP</strong> page → <strong>Toolsets</strong> tab. Toolsets can then be assigned to keys and teams to scope their tool access."
},
"codeInterpreterOutput": {
"pythonCodeExecuted": "Python Code Executed",
"loadingImage": "Loading image...",
"generatedChart": "Generated chart",
"imageNotAvailable": "Image not available"
},
"codeInterpreterTool": {
"title": "Code Interpreter",
"tooltipDesc": "Run Python code to generate files, charts, and analyze data. Container is created automatically.",
"onlyOpenAIWarning": "Code Interpreter is only available for OpenAI models",
"onlyOpenAINotice": "Code Interpreter is currently only supported for OpenAI models.",
"requestSupportLink": "Request support for other providers"
},
"filePreviewCard": {
"uploadPreview": "Upload preview",
"pdf": "PDF",
"image": "Image"
},
"mcpEventsDisplay": {
"listTools": "List tools",
"toolCall": "Tool call",
"request": "Request",
"approved": "Approved",
"response": "Response"
},
"realtimePlayground": {
"title": "Realtime Voice Chat",
"selectModelFirst": "Please select a model first",
"connectedToApi": "Connected to realtime API",
"wsError": "Error: {{error}}",
"websocketError": "WebSocket error",
"disconnected": "Disconnected",
"connectionFailed": "Connection failed: {{error}}",
"listening": "🎙️ Listening...",
"microphoneError": "Microphone error: {{error}}",
"connected": "Connected",
"connecting": "Connecting...",
"disconnectedStatus": "Disconnected",
"connect": "Connect",
"disconnect": "Disconnect",
"emptyTitle": "Realtime Voice Playground",
"emptyHint": "Click Connect to start a realtime session. You can speak using your microphone or type messages. The AI will respond with voice and text.",
"you": "You",
"ai": "AI",
"stopRecording": "Stop recording",
"startRecording": "Start recording",
"inputPlaceholder": "Type a message or use the mic...",
"listeningHint": "Listening — speak into your microphone. Server VAD will detect when you stop."
},
"responseMetrics": {
"timeToFirstToken": "Time to first token",
"totalLatency": "Total latency",
"promptTokens": "Prompt tokens",
"completionTokens": "Completion tokens",
"reasoningTokens": "Reasoning tokens",
"totalTokens": "Total tokens",
"cost": "Cost",
"toolUsed": "Tool used",
"ttft": "TTFT: {{value}}s",
"totalLatencyValue": "Total Latency: {{value}}s",
"in": "In: {{count}}",
"out": "Out: {{count}}",
"reasoning": "Reasoning: {{count}}",
"total": "Total: {{count}}",
"toolName": "Tool: {{name}}"
},
"searchResultsDisplay": {
"hideSources": "Hide sources",
"showSources": "Show sources ({{count}})",
"query": "Query:",
"resultCount_one": "{{count}} result",
"resultCount_other": "{{count}} results",
"metadata": "Metadata:"
},
"sessionManagement": {
"title": "Session Management",
"toggleTooltip": "Choose between LiteLLM API session management (using previous_response_id) or UI-based session management (using chat history)",
"copiedToClipboard": "Response ID copied to clipboard!",
"apiSessionReady": "API Session: Ready",
"uiSessionReady": "UI Session: Ready",
"responseIdPrefix": "Response ID",
"uiSessionPrefix": "UI Session",
"apiSessionReadyDesc": "LiteLLM will manage session using previous_response_id",
"uiSessionReadyDesc": "UI will manage session using chat history",
"apiSessionActiveDesc": "LiteLLM API session active - context maintained server-side",
"uiSessionActiveDesc": "UI session active - context maintained client-side",
"copyTooltipTitle": "Copy response ID to continue session:"
}
}
}

View file

@ -3972,6 +3972,94 @@
"toolsetsExample": "<strong>示例:</strong>一个「GitHub 只读」工具集可能只包含来自 GitHub MCP 服务器的 <code>list_repos</code> 和 <code>get_file</code>——防止 Agent 进行写操作。",
"toolsetsCreatingTitle": "创建工具集:",
"toolsetsCreatingDesc": "管理员可以从 <strong>MCP</strong> 页面 → <strong>工具集</strong>标签页创建和管理工具集。工具集随后可分配给密钥和团队以限定其工具访问范围。"
},
"codeInterpreterOutput": {
"pythonCodeExecuted": "已执行的 Python 代码",
"loadingImage": "图片加载中...",
"generatedChart": "生成的图表",
"imageNotAvailable": "图片不可用"
},
"codeInterpreterTool": {
"title": "代码解释器",
"tooltipDesc": "运行 Python 代码以生成文件、图表并分析数据。容器将自动创建。",
"onlyOpenAIWarning": "代码解释器仅适用于 OpenAI 模型",
"onlyOpenAINotice": "代码解释器目前仅支持 OpenAI 模型。",
"requestSupportLink": "申请支持其他提供商"
},
"filePreviewCard": {
"uploadPreview": "上传预览",
"pdf": "PDF",
"image": "图片"
},
"mcpEventsDisplay": {
"listTools": "列出工具",
"toolCall": "工具调用",
"request": "请求",
"approved": "已批准",
"response": "响应"
},
"realtimePlayground": {
"title": "实时语音对话",
"selectModelFirst": "请先选择一个模型",
"connectedToApi": "已连接到实时 API",
"wsError": "错误:{{error}}",
"websocketError": "WebSocket 错误",
"disconnected": "已断开",
"connectionFailed": "连接失败:{{error}}",
"listening": "🎙️ 正在监听...",
"microphoneError": "麦克风错误:{{error}}",
"connected": "已连接",
"connecting": "连接中...",
"disconnectedStatus": "已断开",
"connect": "连接",
"disconnect": "断开",
"emptyTitle": "实时语音 Playground",
"emptyHint": "点击「连接」开始实时会话。您可以使用麦克风说话或输入文字,AI 将以语音和文字回复。",
"you": "您",
"ai": "AI",
"stopRecording": "停止录音",
"startRecording": "开始录音",
"inputPlaceholder": "输入消息或使用麦克风...",
"listeningHint": "正在监听 — 请对麦克风说话,服务器 VAD 将检测您何时停止说话。"
},
"responseMetrics": {
"timeToFirstToken": "首 Token 时间",
"totalLatency": "总延迟",
"promptTokens": "提示词 Token",
"completionTokens": "补全 Token",
"reasoningTokens": "推理 Token",
"totalTokens": "总 Token",
"cost": "费用",
"toolUsed": "使用的工具",
"ttft": "TTFT:{{value}}s",
"totalLatencyValue": "总延迟:{{value}}s",
"in": "输入:{{count}}",
"out": "输出:{{count}}",
"reasoning": "推理:{{count}}",
"total": "总计:{{count}}",
"toolName": "工具:{{name}}"
},
"searchResultsDisplay": {
"hideSources": "隐藏来源",
"showSources": "显示来源({{count}})",
"query": "查询:",
"resultCount_one": "{{count}} 条结果",
"resultCount_other": "{{count}} 条结果",
"metadata": "元数据:"
},
"sessionManagement": {
"title": "会话管理",
"toggleTooltip": "在 LiteLLM API 会话管理(使用 previous_response_id)和基于 UI 的会话管理(使用聊天历史)之间选择",
"copiedToClipboard": "响应 ID 已复制到剪贴板!",
"apiSessionReady": "API 会话:就绪",
"uiSessionReady": "UI 会话:就绪",
"responseIdPrefix": "响应 ID",
"uiSessionPrefix": "UI 会话",
"apiSessionReadyDesc": "LiteLLM 将使用 previous_response_id 管理会话",
"uiSessionReadyDesc": "UI 将使用聊天历史管理会话",
"apiSessionActiveDesc": "LiteLLM API 会话活跃 - 上下文在服务端维护",
"uiSessionActiveDesc": "UI 会话活跃 - 上下文在客户端维护",
"copyTooltipTitle": "复制响应 ID 以继续会话:"
}
}
}