mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for playground chat_ui batch 3
This commit is contained in:
parent
c48e2ad23f
commit
a4b623ba37
12 changed files with 287 additions and 67 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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" }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">"{resultPage.search_query}"</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">
|
||||
|
|
|
|||
|
|
@ -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" \\
|
||||
|
|
|
|||
|
|
@ -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:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 以继续会话:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue