style(usage): show first-token and total duration together

Split the request-log latency cell into labeled TTFT and elapsed rows, colored green, amber, or red by duration.
This commit is contained in:
lzt404 committed 2026-10-06 01:22:41 +08:00
1 parent d5ce1010e5
commit c24bb1556b
5 files changed
+80 -5

No files matched your search

+2
View File
@@ -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': '推論',
+2
View File
@@ -804,6 +804,8 @@ export const en: Record<MessageKey, string> = {
'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',
+2
View File
@@ -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': '思考',
+48
View File
@@ -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;
+26 -5
View File
@@ -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 <UsageResultCell record={record} />;
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 (
<td className="usage-td-latency usage-stacked-cell align-left" title={`${record.latency_ms} ms`}>
<strong>{compactDuration(record.latency_ms)}</strong>
<small title={record.ttft_ms == null ? undefined : `${record.ttft_ms} ms`}>{t('usage.column.ttft')} {record.ttft_ms == null ? '—' : compactDuration(record.ttft_ms)}</small>
<td className="usage-td-latency align-left" title={latencyTitle}>
<span className="usage-latency-line">
<span className="usage-latency-label">{t('usage.latency.ttft')}</span>
<span className={ttftTone ? `usage-latency-value ${ttftTone}` : 'usage-latency-value'}>{record.ttft_ms == null ? '—' : compactDuration(record.ttft_ms)}</span>
</span>
<span className="usage-latency-line">
<span className="usage-latency-label">{t('usage.latency.elapsed')}</span>
<span className={`usage-latency-value ${latencyTone}`}>{compactDuration(record.latency_ms)}</span>
</span>
</td>
);
}
case 'speed': {
const value = formatGenerationSpeed({
outputTokens: record.tokens.output_tokens,