diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 532e247..47457a5 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -816,6 +816,8 @@ export const jaOverrides = { 'usage.column.result': '結果', 'usage.column.latency': '合計所要時間', 'usage.column.ttft': '初回トークン遅延', + 'usage.latency.ttft': '初字', + 'usage.latency.elapsed': '所要', 'usage.column.input': '入力', 'usage.column.output': '出力', 'usage.column.reasoning': '推論', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 9a5fd8d..dd3da98 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -804,6 +804,8 @@ export const en: Record = { 'usage.column.result': 'Result', 'usage.column.latency': 'Total Duration', 'usage.column.ttft': 'First Token Latency', + 'usage.latency.ttft': 'TTFT', + 'usage.latency.elapsed': 'Elapsed', 'usage.column.input': 'Input', 'usage.column.output': 'Output', 'usage.column.reasoning': 'Reasoning', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 76d0251..0732a32 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -803,6 +803,8 @@ export const zhCN = { 'usage.column.result': '结果', 'usage.column.latency': '总耗时', 'usage.column.ttft': '首字延迟', + 'usage.latency.ttft': '首字', + 'usage.latency.elapsed': '耗时', 'usage.column.input': '输入', 'usage.column.output': '输出', 'usage.column.reasoning': '思考', diff --git a/src/pages/UsageEventsView.css b/src/pages/UsageEventsView.css index c8f9926..0219f9b 100644 --- a/src/pages/UsageEventsView.css +++ b/src/pages/UsageEventsView.css @@ -12,6 +12,9 @@ --usage-log-cache-read: #0f766e; --usage-log-cache-write: #b45309; --usage-log-response-model: #9a5808; + --usage-log-duration-good: #16a34a; + --usage-log-duration-warn: #d97706; + --usage-log-duration-bad: #dc2626; display: flex; min-width: 0; min-height: 0; @@ -194,6 +197,48 @@ font-variant-numeric: tabular-nums; } + +.usage-request-log .usage-td-latency { + font-weight: 600; +} + +.usage-request-log .usage-latency-line { + display: grid; + grid-template-columns: max-content max-content; + column-gap: 6px; + align-items: baseline; + line-height: 1.35; + white-space: nowrap; +} + +.usage-request-log .usage-latency-line + .usage-latency-line { + margin-top: 2px; +} + +.usage-request-log .usage-latency-label { + color: var(--usage-log-muted); + font-size: var(--font-size-caption); + font-weight: 400; +} + +.usage-request-log .usage-latency-value { + font-variant-numeric: tabular-nums; + font-weight: 700; +} + +.usage-request-log .usage-latency-value.tone-good { + color: var(--usage-log-duration-good); +} + +.usage-request-log .usage-latency-value.tone-warn { + color: var(--usage-log-duration-warn); +} + +.usage-request-log .usage-latency-value.tone-bad { + color: var(--usage-log-duration-bad); +} + + .usage-request-log .usage-events-table .usage-td-key > strong { font-family: var(--font-family-mono); font-size: var(--font-size-caption); @@ -381,6 +426,9 @@ --usage-log-cache-read: #5eead4; --usage-log-cache-write: #fbbf24; --usage-log-response-model: #f0bc73; + --usage-log-duration-good: #4ade80; + --usage-log-duration-warn: #fbbf24; + --usage-log-duration-bad: #f87171; border-color: var(--usage-log-line); background: var(--usage-log-surface); box-shadow: none; diff --git a/src/pages/UsageEventsView.tsx b/src/pages/UsageEventsView.tsx index a03a4e1..00d106f 100644 --- a/src/pages/UsageEventsView.tsx +++ b/src/pages/UsageEventsView.tsx @@ -12,6 +12,14 @@ import { usageModelDetails } from '../services/usageModel'; const compactNumber = (value: number) => formatUsageNumber(value, getCurrentLocale()); const compactDuration = (value: number) => formatDuration(value, getCurrentLocale()); + +const durationTone = (value: number | null | undefined) => { + if (value == null || !Number.isFinite(value) || value < 0) return undefined; + if (value >= 30_000) return 'tone-bad'; + if (value >= 15_000) return 'tone-warn'; + return 'tone-good'; +}; + const formatTime = (value: string) => { const date = new Date(value); return Number.isNaN(date.getTime()) ? value : new Intl.DateTimeFormat(getCurrentLocale(), { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(date); @@ -91,7 +99,7 @@ const EVENT_COLUMNS: readonly EventColumnDef[] = [ { key: 'result', labelKey: 'usage.column.result', defaultWidth: 78, minWidth: 68, align: 'left' }, { key: 'total', labelKey: 'usage.column.tokens', defaultWidth: 112, minWidth: 100, align: 'left' }, { key: 'cache', labelKey: 'usage.column.cache', defaultWidth: 94, minWidth: 84, align: 'left' }, - { key: 'latency', labelKey: 'usage.column.latency', defaultWidth: 104, minWidth: 92, align: 'left' }, + { key: 'latency', labelKey: 'usage.column.latency', defaultWidth: 112, minWidth: 100, align: 'left' }, { key: 'speed', labelKey: 'usage.column.speed', defaultWidth: 82, minWidth: 72, align: 'left' }, { key: 'cost', labelKey: 'usage.column.cost', defaultWidth: 96, minWidth: 86, align: 'left' }, { key: 'key', labelKey: 'usage.column.key', defaultWidth: 112, minWidth: 96, align: 'left' }, @@ -453,13 +461,26 @@ function UsageEventCell({ ); case 'result': return ; - case 'latency': + case 'latency': { + const latencyTone = durationTone(record.latency_ms); + const ttftTone = durationTone(record.ttft_ms); + const latencyTitle = [ + `${t('usage.latency.ttft')}: ${record.ttft_ms == null ? '—' : `${record.ttft_ms} ms`}`, + `${t('usage.latency.elapsed')}: ${record.latency_ms} ms`, + ].join('\n'); return ( - - {compactDuration(record.latency_ms)} - {t('usage.column.ttft')} {record.ttft_ms == null ? '—' : compactDuration(record.ttft_ms)} + + + {t('usage.latency.ttft')} + {record.ttft_ms == null ? '—' : compactDuration(record.ttft_ms)} + + + {t('usage.latency.elapsed')} + {compactDuration(record.latency_ms)} + ); + } case 'speed': { const value = formatGenerationSpeed({ outputTokens: record.tokens.output_tokens,