feat(usage): highlight latency by fixed thresholds (#412)

Color first-output and total latency independently using fixed thresholds.
Keep the existing layout and details popover without a side indicator.

Closes #407

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Jiahao
2026-10-13 11:45:38 +08:00
committed by GitHub
co-authored by Codex
parent a9a86071bf
commit f0cf8e7198
2 changed files with 35 additions and 2 deletions
@@ -10,6 +10,14 @@ const props = defineProps<{
}>()
const latencyDetails = computed(() => usageLatencyDetails(props.record))
const latencyTones = {
good: 'text-cp-success-text',
warn: 'text-cp-warning-text',
// 主题的 Orange 与 Warning 共用色种,混入 Error 让相邻两档可区分
slow: 'latency-slow',
critical: 'text-cp-error-text',
}
</script>
<template>
@@ -18,9 +26,13 @@ const latencyDetails = computed(() => usageLatencyDetails(props.record))
class="grid grid-cols-[auto_auto] items-center justify-end gap-x-2 gap-y-1.5 whitespace-nowrap font-mono text-cp-sm leading-none font-heavy tabular-nums"
>
<span class="text-cp-xs text-cp-text-quaternary">首字</span>
<span class="text-cp-text-secondary">{{ latencyDetails.firstOutputDisplay }}</span>
<span :class="latencyDetails.firstOutputSeverity === null ? 'text-cp-text-quaternary' : latencyTones[latencyDetails.firstOutputSeverity]">
{{ latencyDetails.firstOutputDisplay }}
</span>
<span class="text-cp-xs text-cp-text-quaternary">总耗时</span>
<span class="text-cp-text">{{ latencyDetails.totalDisplay }}</span>
<span :class="latencyDetails.totalSeverity === null ? 'text-cp-text-quaternary' : latencyTones[latencyDetails.totalSeverity]">
{{ latencyDetails.totalDisplay }}
</span>
</div>
<UsageDetailPopover trigger-label="查看延迟明细">
@@ -58,3 +70,9 @@ const latencyDetails = computed(() => usageLatencyDetails(props.record))
</UsageDetailPopover>
</div>
</template>
<style scoped>
.latency-slow {
color: color-mix(in oklab, var(--cp-color-warning-text) 65%, var(--cp-color-error-text));
}
</style>
@@ -224,11 +224,26 @@ export function usageLatencyDetails(record: UsageLatencyRecord) {
return {
upstreamDisplay,
firstOutputDisplay: formatDuration(firstTokenMs),
firstOutputSeverity: latencySeverity(firstTokenMs, [10_000, 30_000, 60_000]),
totalDisplay: formatDuration(totalMs),
totalSeverity: latencySeverity(totalMs, [60_000, 180_000, 300_000]),
sections,
}
}
function latencySeverity(ms: number | null, thresholds: readonly [number, number, number]): 'good' | 'warn' | 'slow' | 'critical' | null {
if (ms === null)
return null
// 沿用 sub2api 的固定分档,边界值进入下一档,不改变请求状态
if (ms >= thresholds[2])
return 'critical'
if (ms >= thresholds[1])
return 'slow'
if (ms >= thresholds[0])
return 'warn'
return 'good'
}
export function usageBillingText(record: Pick<UsageCommonRecord, 'billing'>) {
return record.billing?.totalAmountDisplay || '—'
}