feat(ui): i18n for view_logs batch 3

This commit is contained in:
吹雪 2026-06-10 14:57:20 +09:00
parent 1a1144fe76
commit 888cc88826
8 changed files with 339 additions and 73 deletions

View file

@ -1,4 +1,5 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Typography, Descriptions, Card, Tag, Tabs, Alert, Collapse, Radio, Space, Spin } from "antd";
import moment from "moment";
import { LogEntry } from "../columns";
@ -52,6 +53,7 @@ export interface LogDetailContentProps {
* be reused for both single-log and session-mode views.
*/
export function LogDetailContent({ logEntry, isLoadingDetails = false, accessToken }: LogDetailContentProps) {
const { t } = useTranslation();
const metadata = logEntry.metadata || {};
const hasError = metadata.status === "failure";
const errorInfo = hasError ? metadata.error_information : null;
@ -83,9 +85,9 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
if (hasError && errorInfo) {
return {
error: {
message: errorInfo.error_message || "An error occurred",
type: errorInfo.error_class || "error",
code: errorInfo.error_code || "unknown",
message: errorInfo.error_message || t("common.somethingWentWrong"),
type: errorInfo.error_class || t("common.error"),
code: errorInfo.error_code || t("common.unknown"),
param: null,
},
};
@ -100,7 +102,7 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
<Alert
type="error"
showIcon
message="Request Failed"
message={t("viewLogs.logDetailContent.requestFailed")}
description={<ErrorDescription errorInfo={errorInfo} />}
className="mb-6"
/>
@ -113,22 +115,33 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
{/* Request Details */}
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Card title="Request Details" size="small" bordered={false} style={{ marginBottom: 0 }}>
<Card
title={t("viewLogs.logDetailContent.requestDetails")}
size="small"
bordered={false}
style={{ marginBottom: 0 }}
>
<Descriptions column={2} size="small">
<Descriptions.Item label="Model">{logEntry.model}</Descriptions.Item>
<Descriptions.Item label="Provider">{logEntry.custom_llm_provider || "-"}</Descriptions.Item>
<Descriptions.Item label="Call Type">{logEntry.call_type}</Descriptions.Item>
<Descriptions.Item label="Model ID">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelModel")}>{logEntry.model}</Descriptions.Item>
<Descriptions.Item label={t("viewLogs.logDetailContent.labelProvider")}>
{logEntry.custom_llm_provider || "-"}
</Descriptions.Item>
<Descriptions.Item label={t("viewLogs.logDetailContent.labelCallType")}>
{logEntry.call_type}
</Descriptions.Item>
<Descriptions.Item label={t("viewLogs.logDetailContent.labelModelId")}>
<TruncatedValue value={logEntry.model_id} />
</Descriptions.Item>
<Descriptions.Item label="API Base">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelApiBase")}>
<TruncatedValue value={logEntry.api_base} maxWidth={API_BASE_MAX_WIDTH} />
</Descriptions.Item>
{logEntry.requester_ip_address && (
<Descriptions.Item label="IP Address">{logEntry.requester_ip_address}</Descriptions.Item>
<Descriptions.Item label={t("viewLogs.logDetailContent.labelIpAddress")}>
{logEntry.requester_ip_address}
</Descriptions.Item>
)}
{hasGuardrailData && (
<Descriptions.Item label="Guardrail">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelGuardrail")}>
<GuardrailLabel label={primaryGuardrailLabel} maskedCount={totalMaskedEntities} />
</Descriptions.Item>
)}
@ -165,7 +178,7 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
{isLoadingDetails ? (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6 p-8 text-center">
<Spin size="default" />
<div style={{ marginTop: 8, color: "#999" }}>Loading request &amp; response data...</div>
<div style={{ marginTop: 8, color: "#999" }}>{t("viewLogs.logDetailContent.loadingRequestResponse")}</div>
</div>
) : (
<RequestResponseSection
@ -216,16 +229,17 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
// ============================================================================
function ErrorDescription({ errorInfo }: { errorInfo: any }) {
const { t } = useTranslation();
return (
<div>
{errorInfo.error_code && (
<div>
<Text strong>Error Code:</Text> {errorInfo.error_code}
<Text strong>{t("viewLogs.logDetailContent.errorCode")}</Text> {errorInfo.error_code}
</div>
)}
{errorInfo.error_message && (
<div>
<Text strong>Message:</Text> {errorInfo.error_message}
<Text strong>{t("viewLogs.logDetailContent.errorMessage")}</Text> {errorInfo.error_message}
</div>
)}
</div>
@ -233,10 +247,11 @@ function ErrorDescription({ errorInfo }: { errorInfo: any }) {
}
function TagsSection({ tags }: { tags: Record<string, any> }) {
const { t } = useTranslation();
return (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden p-4 mb-6">
<Text strong style={{ display: "block", marginBottom: 8, fontSize: 16 }}>
Tags
{t("viewLogs.logDetailContent.tags")}
</Text>
<Space size={SPACING_MEDIUM} wrap>
{Object.entries(tags).map(([key, value]) => (
@ -250,6 +265,7 @@ function TagsSection({ tags }: { tags: Record<string, any> }) {
}
function GuardrailLabel({ label, maskedCount }: { label: string; maskedCount: number }) {
const { t } = useTranslation();
const handleClick = () => {
const el = document.getElementById("guardrail-section");
if (el) el.scrollIntoView({ behavior: "smooth" });
@ -260,7 +276,7 @@ function GuardrailLabel({ label, maskedCount }: { label: string; maskedCount: nu
<a onClick={handleClick} style={{ cursor: "pointer" }}>
{label}
</a>
{maskedCount > 0 && <Tag color="blue">{maskedCount} masked</Tag>}
{maskedCount > 0 && <Tag color="blue">{t("viewLogs.logDetailContent.masked", { count: maskedCount })}</Tag>}
</Space>
);
}
@ -279,6 +295,7 @@ function getUncachedInputTextTokens(metadata: Record<string, any>): number | und
}
function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: Record<string, any> }) {
const { t } = useTranslation();
const completionStartTime = logEntry.completionStartTime;
const ttftMs =
completionStartTime && completionStartTime !== logEntry.endTime
@ -300,17 +317,19 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
return (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Card title="Metrics" size="small" style={{ marginBottom: 0 }}>
<Card title={t("viewLogs.logDetailContent.metrics")} size="small" style={{ marginBottom: 0 }}>
<Descriptions column={2} size="small">
{showAnthropicMessagesInputOutput ? (
<>
<Descriptions.Item label="Input Tokens">{formatNumberWithCommas(uncachedInputTokens)}</Descriptions.Item>
<Descriptions.Item label="Output Tokens">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelInputTokens")}>
{formatNumberWithCommas(uncachedInputTokens)}
</Descriptions.Item>
<Descriptions.Item label={t("viewLogs.logDetailContent.labelOutputTokens")}>
{formatNumberWithCommas(logEntry.completion_tokens)}
</Descriptions.Item>
</>
) : (
<Descriptions.Item label="Tokens">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelTokens")}>
<TokenFlow
prompt={logEntry.prompt_tokens}
completion={logEntry.completion_tokens}
@ -318,26 +337,30 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
/>
</Descriptions.Item>
)}
<Descriptions.Item label="Cost">${formatNumberWithCommas(logEntry.spend || 0, 8)}</Descriptions.Item>
<Descriptions.Item label="Duration">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelCost")}>
${formatNumberWithCommas(logEntry.spend || 0, 8)}
</Descriptions.Item>
<Descriptions.Item label={t("viewLogs.logDetailContent.labelDuration")}>
{logEntry.request_duration_ms != null ? (logEntry.request_duration_ms / 1000).toFixed(3) : "-"} s
</Descriptions.Item>
{ttftMs != null && ttftMs > 0 && (
<Descriptions.Item label="Time to First Token">{(ttftMs / 1000).toFixed(3)} s</Descriptions.Item>
<Descriptions.Item label={t("viewLogs.logDetailContent.labelTimeToFirstToken")}>
{(ttftMs / 1000).toFixed(3)} s
</Descriptions.Item>
)}
{hasCacheActivity && (
<>
<Descriptions.Item label="Cache Hit">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelCacheHit")}>
<Tag color={cacheHitColor}>{cacheHitValue}</Tag>
</Descriptions.Item>
{metadata?.additional_usage_values?.cache_read_input_tokens > 0 && (
<Descriptions.Item label="Cache Read Tokens">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelCacheReadTokens")}>
{formatNumberWithCommas(metadata.additional_usage_values.cache_read_input_tokens)}
</Descriptions.Item>
)}
{metadata?.additional_usage_values?.cache_creation_input_tokens > 0 && (
<Descriptions.Item label="Cache Creation Tokens">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelCacheCreationTokens")}>
{formatNumberWithCommas(metadata.additional_usage_values.cache_creation_input_tokens)}
</Descriptions.Item>
)}
@ -345,12 +368,12 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
)}
{metadata?.litellm_overhead_time_ms !== undefined && metadata.litellm_overhead_time_ms !== null && (
<Descriptions.Item label="LiteLLM Overhead">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelLitellmOverhead")}>
{metadata.litellm_overhead_time_ms.toFixed(2)} ms
</Descriptions.Item>
)}
<Descriptions.Item label="Retries">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelRetries")}>
{metadata?.attempted_retries !== undefined && metadata?.attempted_retries !== null ? (
metadata.attempted_retries > 0 ? (
<>
@ -360,17 +383,17 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
: ""}
</>
) : (
<Tag color="green">None</Tag>
<Tag color="green">{t("viewLogs.logDetailContent.retriesNone")}</Tag>
)
) : (
"-"
)}
</Descriptions.Item>
<Descriptions.Item label="Start Time">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelStartTime")}>
{moment(logEntry.startTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")}
</Descriptions.Item>
<Descriptions.Item label="End Time">
<Descriptions.Item label={t("viewLogs.logDetailContent.labelEndTime")}>
{moment(logEntry.endTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")}
</Descriptions.Item>
</Descriptions>
@ -394,6 +417,7 @@ function RequestResponseSection({
getFormattedResponse,
logEntry,
}: RequestResponseSectionProps) {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<typeof TAB_REQUEST | typeof TAB_RESPONSE>(TAB_REQUEST);
const [viewMode, setViewMode] = useState<"pretty" | "json">("pretty");
@ -438,11 +462,11 @@ function RequestResponseSection({
}}
>
<h3 className="text-lg font-medium text-gray-900" style={{ margin: 0 }}>
Request & Response
{t("viewLogs.logDetailContent.requestAndResponse")}
</h3>
<Radio.Group size="small" value={viewMode} onChange={(e) => setViewMode(e.target.value)}>
<Radio.Button value="pretty">Pretty</Radio.Button>
<Radio.Button value="json">JSON</Radio.Button>
<Radio.Button value="pretty">{t("viewLogs.logDetailContent.viewPretty")}</Radio.Button>
<Radio.Button value="json">{t("viewLogs.logDetailContent.viewJson")}</Radio.Button>
</Radio.Group>
</div>
),
@ -467,7 +491,10 @@ function RequestResponseSection({
<Text
copyable={{
text: getCopyText(),
tooltips: ["Copy JSON", "Copied!"],
tooltips: [
t("viewLogs.logDetailContent.copyJson"),
t("viewLogs.logDetailContent.copiedJson"),
],
}}
disabled={activeTab === TAB_RESPONSE && !hasResponse && !hasError}
/>
@ -475,7 +502,7 @@ function RequestResponseSection({
items={[
{
key: TAB_REQUEST,
label: "Request",
label: t("viewLogs.logDetailContent.tabRequest"),
children: (
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
<JsonViewer data={getRawRequest()} mode="formatted" />
@ -484,14 +511,14 @@ function RequestResponseSection({
},
{
key: TAB_RESPONSE,
label: "Response",
label: t("viewLogs.logDetailContent.tabResponse"),
children: (
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
{hasResponse || hasError ? (
<JsonViewer data={getFormattedResponse()} mode="formatted" />
) : (
<div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}>
Response data not available
{t("viewLogs.logDetailContent.responseNotAvailable")}
</div>
)}
</div>
@ -510,6 +537,7 @@ function RequestResponseSection({
}
export function GuardrailJumpLink({ guardrailEntries }: { guardrailEntries: any[] }) {
const { t } = useTranslation();
const allPassed = guardrailEntries.every((e) => {
const status = e?.guardrail_status || e?.status;
return status === "pass" || status === "passed" || status === "success";
@ -538,8 +566,8 @@ export function GuardrailJumpLink({ guardrailEntries }: { guardrailEntries: any[
border: `1px solid ${allPassed ? "#bbf7d0" : "#fecaca"}`,
}}
>
{allPassed ? "\u2713" : "\u2717"} {guardrailEntries.length} guardrail{guardrailEntries.length !== 1 ? "s" : ""}{" "}
evaluated
{allPassed ? "\u2713" : "\u2717"}{" "}
{t("viewLogs.logDetailContent.guardrailsEvaluated", { count: guardrailEntries.length })}
<span style={{ fontSize: 11, opacity: 0.7 }}>{"\u2193"}</span>
</div>
</div>
@ -547,6 +575,7 @@ export function GuardrailJumpLink({ guardrailEntries }: { guardrailEntries: any[
}
function MetadataSection({ metadata }: { metadata: Record<string, any> }) {
const { t } = useTranslation();
return (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Collapse
@ -555,14 +584,17 @@ function MetadataSection({ metadata }: { metadata: Record<string, any> }) {
items={[
{
key: "1",
label: <h3 className="text-lg font-medium text-gray-900">Metadata</h3>,
label: <h3 className="text-lg font-medium text-gray-900">{t("viewLogs.logDetailContent.metadata")}</h3>,
children: (
<div>
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
<Text
copyable={{
text: JSON.stringify(metadata, null, 2),
tooltips: ["Copy Metadata", "Copied!"],
tooltips: [
t("viewLogs.logDetailContent.copyMetadata"),
t("viewLogs.logDetailContent.copiedMetadata"),
],
}}
/>
</div>

View file

@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Drawer } from "antd";
import { CheckOutlined, CopyOutlined, LeftOutlined, RightOutlined } from "@ant-design/icons";
import { Bot, Sparkles, Wrench } from "lucide-react";
@ -234,6 +235,8 @@ export function LogDetailsDrawer({
}
};
const { t } = useTranslation();
if (!currentLog || !enrichedLog) return null;
return (
@ -259,7 +262,7 @@ export function LogDetailsDrawer({
icon={<LeftOutlined />}
onClick={() => setIsSidebarCollapsed(true)}
className="absolute top-2 left-2 z-20 !bg-white !border !border-slate-200 !rounded-md"
aria-label="Collapse trace sidebar"
aria-label={t("viewLogs.logDetailsDrawer.collapseTraceSidebar")}
/>
) : (
<Button
@ -268,7 +271,7 @@ export function LogDetailsDrawer({
icon={<RightOutlined />}
onClick={() => setIsSidebarCollapsed(false)}
className="absolute top-2 left-2 z-20 !bg-white !border !border-slate-200 !rounded-md"
aria-label="Expand trace sidebar"
aria-label={t("viewLogs.logDetailsDrawer.expandTraceSidebar")}
/>
)}
{!isSidebarCollapsed && (
@ -277,7 +280,9 @@ export function LogDetailsDrawer({
<div className="flex items-start justify-between gap-2">
<div>
<div className="text-[10px] uppercase tracking-wide text-slate-500">
{isSessionMode ? "Session" : "Trace"}
{isSessionMode
? t("viewLogs.logDetailsDrawer.labelSession")
: t("viewLogs.logDetailsDrawer.labelTrace")}
</div>
<div className="font-mono text-[12px] text-slate-900 leading-tight flex items-center gap-1">
<span className="truncate">{leftPanelDisplayId}</span>
@ -285,7 +290,7 @@ export function LogDetailsDrawer({
type="button"
onClick={handleCopyLeftPanelId}
className="text-slate-400 hover:text-slate-600"
aria-label="Copy trace id"
aria-label={t("viewLogs.logDetailsDrawer.copyTraceId")}
>
{copiedLeftPanelId ? (
<CheckOutlined className="text-[11px]" />
@ -297,7 +302,7 @@ export function LogDetailsDrawer({
</div>
</div>
<div className="mt-1 text-[11px] text-slate-500 font-mono">
{logsForList.length} req
{t("viewLogs.logDetailsDrawer.labelReqCount", { count: logsForList.length })}
{[
isSessionMode
? llmCount
@ -309,7 +314,11 @@ export function LogDetailsDrawer({
: logsForList.filter((row) => AGENT_CALL_TYPES.includes(row.call_type)).length,
isSessionMode ? mcpCount : logsForList.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length,
].map((count, i) => {
const label = [" LLM", " Agent", " MCP"][i];
const label = [
t("viewLogs.logDetailsDrawer.labelLlm"),
t("viewLogs.logDetailsDrawer.labelAgent"),
t("viewLogs.logDetailsDrawer.labelMcp"),
][i];
return count > 0 ? (
<span key={label}>
<span className="mx-1.5">·</span>

View file

@ -4,6 +4,7 @@
*/
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Typography } from "antd";
import MessageManager from "@/components/molecules/message_manager";
import { ParsedMessage } from "./prettyMessagesTypes";
@ -19,6 +20,7 @@ interface OutputCardProps {
}
export function OutputCard({ message, completionTokens, outputCost }: OutputCardProps) {
const { t } = useTranslation();
const [isCollapsed, setIsCollapsed] = useState(false);
const handleCopy = () => {
@ -26,7 +28,7 @@ export function OutputCard({ message, completionTokens, outputCost }: OutputCard
const content = message.content || "";
navigator.clipboard.writeText(content);
MessageManager.success("Output copied");
MessageManager.success(t("viewLogs.outputCard.outputCopied"));
};
if (!message) {
@ -56,7 +58,7 @@ export function OutputCard({ message, completionTokens, outputCost }: OutputCard
>
<div style={{ padding: "12px 16px" }}>
<Text type="secondary" style={{ fontSize: 13, fontStyle: "italic" }}>
No response data available
{t("viewLogs.outputCard.noResponseData")}
</Text>
</div>
</div>

View file

@ -5,6 +5,7 @@
*/
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Typography, Tag, Tooltip } from "antd";
import {
SoundOutlined,
@ -90,6 +91,7 @@ export function isRealtimeResponse(response: any): boolean {
}
export function RealtimePrettyView({ response, metrics }: RealtimePrettyViewProps) {
const { t } = useTranslation();
const events: RealtimeEvent[] = response?.results || [];
const usage = response?.usage;
@ -122,7 +124,7 @@ export function RealtimePrettyView({ response, metrics }: RealtimePrettyViewProp
fontSize: 13,
}}
>
No recognized realtime events found
{t("viewLogs.realtimePrettyView.noEventsFound")}
</div>
)}
</div>
@ -130,6 +132,7 @@ export function RealtimePrettyView({ response, metrics }: RealtimePrettyViewProp
}
function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCount: number }) {
const { t } = useTranslation();
const [isCollapsed, setIsCollapsed] = useState(true);
return (
@ -170,14 +173,14 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<SettingOutlined style={{ color: "#8c8c8c", fontSize: 14 }} />
<Text style={{ fontWeight: 500, fontSize: 14 }}>Session</Text>
<Text style={{ fontWeight: 500, fontSize: 14 }}>{t("viewLogs.realtimePrettyView.labelSession")}</Text>
</div>
<Text type="secondary" style={{ fontSize: 12 }}>
{session.model}
</Text>
{turnCount > 0 && (
<Tag color="purple" style={{ margin: 0, fontWeight: 500 }}>
{turnCount} {turnCount === 1 ? "turn" : "turns"}
{t("viewLogs.realtimePrettyView.turnCount", { count: turnCount })}
</Tag>
)}
{session.voice && (
@ -214,15 +217,32 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
fontSize: 13,
}}
>
<ConfigRow label="Model" value={session.model} />
<ConfigRow label="Voice" value={session.voice} />
<ConfigRow label="Temperature" value={session.temperature} />
<ConfigRow label="Max Output Tokens" value={session.max_response_output_tokens} />
<ConfigRow label="Input Audio Format" value={session.input_audio_format} />
<ConfigRow label="Output Audio Format" value={session.output_audio_format} />
{session.turn_detection && <ConfigRow label="Turn Detection" value={session.turn_detection.type} />}
<ConfigRow label={t("viewLogs.realtimePrettyView.labelModel")} value={session.model} />
<ConfigRow label={t("viewLogs.realtimePrettyView.labelVoice")} value={session.voice} />
<ConfigRow label={t("viewLogs.realtimePrettyView.labelTemperature")} value={session.temperature} />
<ConfigRow
label={t("viewLogs.realtimePrettyView.labelMaxOutputTokens")}
value={session.max_response_output_tokens}
/>
<ConfigRow
label={t("viewLogs.realtimePrettyView.labelInputAudioFormat")}
value={session.input_audio_format}
/>
<ConfigRow
label={t("viewLogs.realtimePrettyView.labelOutputAudioFormat")}
value={session.output_audio_format}
/>
{session.turn_detection && (
<ConfigRow
label={t("viewLogs.realtimePrettyView.labelTurnDetection")}
value={session.turn_detection.type}
/>
)}
{session.tools && session.tools.length > 0 && (
<ConfigRow label="Tools" value={`${session.tools.length} tool(s)`} />
<ConfigRow
label={t("viewLogs.realtimePrettyView.labelTools")}
value={t("viewLogs.realtimePrettyView.toolCount", { count: session.tools.length })}
/>
)}
</div>
@ -238,7 +258,7 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
marginBottom: 4,
}}
>
Instructions
{t("viewLogs.realtimePrettyView.labelInstructions")}
</Text>
<div
style={{
@ -324,6 +344,7 @@ function ConversationCard({
}
function ResponseTurn({ response, index }: { response: RealtimeResponse; index: number }) {
const { t } = useTranslation();
const outputs = response.output || [];
const usage = response.usage;
@ -349,7 +370,10 @@ function ResponseTurn({ response, index }: { response: RealtimeResponse; index:
</Tag>
{usage && (
<Text type="secondary" style={{ fontSize: 11 }}>
{usage.input_tokens ?? 0} in / {usage.output_tokens ?? 0} out tokens
{t("viewLogs.realtimePrettyView.tokenUsage", {
input: usage.input_tokens ?? 0,
output: usage.output_tokens ?? 0,
})}
</Text>
)}
{response.conversation_id && (
@ -367,8 +391,18 @@ function ResponseTurn({ response, index }: { response: RealtimeResponse; index:
))}
{/* Token breakdown if available */}
{usage?.input_token_details && <TokenBreakdown label="Input" details={usage.input_token_details} />}
{usage?.output_token_details && <TokenBreakdown label="Output" details={usage.output_token_details} />}
{usage?.input_token_details && (
<TokenBreakdown
label={t("viewLogs.realtimePrettyView.inputTokenBreakdown")}
details={usage.input_token_details}
/>
)}
{usage?.output_token_details && (
<TokenBreakdown
label={t("viewLogs.realtimePrettyView.outputTokenBreakdown")}
details={usage.output_token_details}
/>
)}
</div>
);
}

View file

@ -2,6 +2,7 @@
* SectionHeader - Datadog-style header with icon, label, metrics, and copy
*/
import { useTranslation } from "react-i18next";
import { Typography, Button, Tooltip } from "antd";
import { MessageOutlined, CopyOutlined, DownOutlined, UpOutlined } from "@ant-design/icons";
@ -26,6 +27,7 @@ export function SectionHeader({
onToggleCollapse,
turnCount,
}: SectionHeaderProps) {
const { t } = useTranslation();
return (
<div
onClick={onToggleCollapse}
@ -67,33 +69,35 @@ export function SectionHeader({
) : (
<span style={{ fontSize: 14, filter: "grayscale(1)", opacity: 0.6 }}>✨</span>
)}
<Text style={{ fontWeight: 500, fontSize: 14 }}>{type === "input" ? "Input" : "Output"}</Text>
<Text style={{ fontWeight: 500, fontSize: 14 }}>
{type === "input" ? t("viewLogs.sectionHeader.labelInput") : t("viewLogs.sectionHeader.labelOutput")}
</Text>
</div>
{/* Tokens */}
{tokens !== undefined && (
<Text type="secondary" style={{ fontSize: 12 }}>
Tokens: {tokens.toLocaleString()}
{t("viewLogs.sectionHeader.tokens", { count: tokens.toLocaleString() })}
</Text>
)}
{/* Cost */}
{cost !== undefined && (
<Text type="secondary" style={{ fontSize: 12 }}>
Cost: ${cost.toFixed(6)}
{t("viewLogs.sectionHeader.cost", { amount: cost.toFixed(6) })}
</Text>
)}
{/* Turn count */}
{turnCount !== undefined && turnCount > 0 && (
<Text type="secondary" style={{ fontSize: 12 }}>
Turns: {turnCount}
{t("viewLogs.sectionHeader.turns", { count: turnCount })}
</Text>
)}
</div>
{/* Copy Button */}
<Tooltip title="Copy">
<Tooltip title={t("viewLogs.sectionHeader.copy")}>
<Button
type="text"
size="small"

View file

@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { Typography } from "antd";
const { Text } = Typography;
@ -13,10 +14,14 @@ interface TokenFlowProps {
* Shows total with breakdown of prompt and completion tokens.
*/
export function TokenFlow({ prompt = 0, completion = 0, total = 0 }: TokenFlowProps) {
const { t } = useTranslation();
return (
<Text>
{total.toLocaleString()} ({prompt.toLocaleString()} prompt tokens + {completion.toLocaleString()} completion
tokens)
{t("viewLogs.tokenFlow.breakdown", {
total: total.toLocaleString(),
prompt: prompt.toLocaleString(),
completion: completion.toLocaleString(),
})}
</Text>
);
}

View file

@ -2174,6 +2174,96 @@
"historyTree": {
"history_one": "HISTORY ({{count}} message)",
"history_other": "HISTORY ({{count}} messages)"
},
"logDetailContent": {
"requestFailed": "Request Failed",
"tags": "Tags",
"requestDetails": "Request Details",
"labelModel": "Model",
"labelProvider": "Provider",
"labelCallType": "Call Type",
"labelModelId": "Model ID",
"labelApiBase": "API Base",
"labelIpAddress": "IP Address",
"labelGuardrail": "Guardrail",
"masked_one": "{{count}} masked",
"masked_other": "{{count}} masked",
"metrics": "Metrics",
"labelInputTokens": "Input Tokens",
"labelOutputTokens": "Output Tokens",
"labelTokens": "Tokens",
"labelCost": "Cost",
"labelDuration": "Duration",
"labelTimeToFirstToken": "Time to First Token",
"labelCacheHit": "Cache Hit",
"labelCacheReadTokens": "Cache Read Tokens",
"labelCacheCreationTokens": "Cache Creation Tokens",
"labelLitellmOverhead": "LiteLLM Overhead",
"labelRetries": "Retries",
"labelStartTime": "Start Time",
"labelEndTime": "End Time",
"retriesNone": "None",
"loadingRequestResponse": "Loading request & response data...",
"requestAndResponse": "Request & Response",
"viewPretty": "Pretty",
"viewJson": "JSON",
"copyJson": "Copy JSON",
"copiedJson": "Copied!",
"tabRequest": "Request",
"tabResponse": "Response",
"responseNotAvailable": "Response data not available",
"metadata": "Metadata",
"copyMetadata": "Copy Metadata",
"copiedMetadata": "Copied!",
"guardrailsEvaluated_one": "{{count}} guardrail evaluated",
"guardrailsEvaluated_other": "{{count}} guardrails evaluated",
"errorCode": "Error Code:",
"errorMessage": "Message:"
},
"logDetailsDrawer": {
"collapseTraceSidebar": "Collapse trace sidebar",
"expandTraceSidebar": "Expand trace sidebar",
"copyTraceId": "Copy trace id",
"labelSession": "Session",
"labelTrace": "Trace",
"labelReqCount": "{{count}} req",
"labelLlm": " LLM",
"labelAgent": " Agent",
"labelMcp": " MCP"
},
"outputCard": {
"outputCopied": "Output copied",
"noResponseData": "No response data available"
},
"realtimePrettyView": {
"noEventsFound": "No recognized realtime events found",
"labelSession": "Session",
"labelModel": "Model",
"labelVoice": "Voice",
"labelTemperature": "Temperature",
"labelMaxOutputTokens": "Max Output Tokens",
"labelInputAudioFormat": "Input Audio Format",
"labelOutputAudioFormat": "Output Audio Format",
"labelTurnDetection": "Turn Detection",
"labelTools": "Tools",
"toolCount": "{{count}} tool(s)",
"labelInstructions": "Instructions",
"tokenUsage": "{{input}} in / {{output}} out tokens",
"turnCount_one": "{{count}} turn",
"turnCount_other": "{{count}} turns",
"inputTokenBreakdown": "Input Token Breakdown",
"outputTokenBreakdown": "Output Token Breakdown"
},
"sectionHeader": {
"labelInput": "Input",
"labelOutput": "Output",
"tokens": "Tokens: {{count}}",
"cost": "Cost: ${{amount}}",
"turns": "Turns: {{count}}",
"copy": "Copy"
},
"tokenFlow": {
"breakdown": "{{total}} ({{prompt}} prompt tokens + {{completion}} completion tokens)"
}
}
}

View file

@ -2174,6 +2174,96 @@
"historyTree": {
"history_one": "历史({{count}} 条消息)",
"history_other": "历史({{count}} 条消息)"
},
"logDetailContent": {
"requestFailed": "请求失败",
"tags": "标签",
"requestDetails": "请求详情",
"labelModel": "模型",
"labelProvider": "提供商",
"labelCallType": "调用类型",
"labelModelId": "模型 ID",
"labelApiBase": "API Base",
"labelIpAddress": "IP 地址",
"labelGuardrail": "护栏",
"masked_one": "{{count}} 个已脱敏",
"masked_other": "{{count}} 个已脱敏",
"metrics": "指标",
"labelInputTokens": "输入 Token",
"labelOutputTokens": "输出 Token",
"labelTokens": "Token",
"labelCost": "费用",
"labelDuration": "耗时",
"labelTimeToFirstToken": "首 Token 延迟",
"labelCacheHit": "缓存命中",
"labelCacheReadTokens": "缓存读取 Token",
"labelCacheCreationTokens": "缓存创建 Token",
"labelLitellmOverhead": "LiteLLM 开销",
"labelRetries": "重试次数",
"labelStartTime": "开始时间",
"labelEndTime": "结束时间",
"retriesNone": "无",
"loadingRequestResponse": "正在加载请求和响应数据...",
"requestAndResponse": "请求与响应",
"viewPretty": "格式化",
"viewJson": "JSON",
"copyJson": "复制 JSON",
"copiedJson": "已复制!",
"tabRequest": "请求",
"tabResponse": "响应",
"responseNotAvailable": "响应数据不可用",
"metadata": "元数据",
"copyMetadata": "复制元数据",
"copiedMetadata": "已复制!",
"guardrailsEvaluated_one": "已评估 {{count}} 个护栏",
"guardrailsEvaluated_other": "已评估 {{count}} 个护栏",
"errorCode": "错误代码:",
"errorMessage": "消息:"
},
"logDetailsDrawer": {
"collapseTraceSidebar": "收起追踪侧边栏",
"expandTraceSidebar": "展开追踪侧边栏",
"copyTraceId": "复制追踪 ID",
"labelSession": "会话",
"labelTrace": "追踪",
"labelReqCount": "{{count}} 个请求",
"labelLlm": " LLM",
"labelAgent": " Agent",
"labelMcp": " MCP"
},
"outputCard": {
"outputCopied": "输出已复制",
"noResponseData": "暂无响应数据"
},
"realtimePrettyView": {
"noEventsFound": "未发现已知的实时事件",
"labelSession": "会话",
"labelModel": "模型",
"labelVoice": "语音",
"labelTemperature": "温度",
"labelMaxOutputTokens": "最大输出 Token",
"labelInputAudioFormat": "输入音频格式",
"labelOutputAudioFormat": "输出音频格式",
"labelTurnDetection": "轮次检测",
"labelTools": "工具",
"toolCount": "{{count}} 个工具",
"labelInstructions": "指令",
"tokenUsage": "{{input}} 输入 / {{output}} 输出 Token",
"turnCount_one": "{{count}} 轮",
"turnCount_other": "{{count}} 轮",
"inputTokenBreakdown": "输入 Token 明细",
"outputTokenBreakdown": "输出 Token 明细"
},
"sectionHeader": {
"labelInput": "输入",
"labelOutput": "输出",
"tokens": "Token:{{count}}",
"cost": "费用:${{amount}}",
"turns": "轮次:{{count}}",
"copy": "复制"
},
"tokenFlow": {
"breakdown": "{{total}}({{prompt}} 提示词 Token + {{completion}} 补全 Token)"
}
}
}