mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for view_logs batch 3
This commit is contained in:
parent
1a1144fe76
commit
888cc88826
8 changed files with 339 additions and 73 deletions
|
|
@ -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 & 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue