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:
1 parent
d5ce1010e5
commit
c24bb1556b
5 files changed
+80
-5
No files matched your search
@@ -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': '推論',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '思考',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user