Fix timeline duration display from latency

This commit is contained in:
elky
2026-07-09 11:46:45 +08:00
parent 1ca83ca3fb
commit 5e1d14f19b
2 changed files with 97 additions and 10 deletions
@@ -182,20 +182,20 @@
<!-- 核心信息网格 -->
<div class="info-grid">
<div
v-if="currentAttempt.started_at"
v-if="currentAttemptTimeRange"
class="info-item"
>
<span class="info-label">时间范围</span>
<span class="info-value mono time-range-value">
{{ formatTime(currentAttempt.started_at) }}
{{ formatTime(currentAttemptTimeRange.startIso) }}
<span class="time-arrow-container">
<span
v-if="currentAttempt.finished_at"
v-if="currentAttemptTimeRange.endIso"
class="time-duration"
>+{{ formatDuration(currentAttempt.started_at, currentAttempt.finished_at) }}</span>
>+{{ currentAttemptTimeRange.durationLabel }}</span>
<span class="time-arrow"></span>
</span>
{{ currentAttempt.finished_at ? formatTime(currentAttempt.finished_at) : '进行中' }}
{{ currentAttemptTimeRange.endIso ? formatTime(currentAttemptTimeRange.endIso) : '进行中' }}
</span>
</div>
<div
@@ -570,6 +570,12 @@ interface NodeGroup {
isPoolGroup?: boolean
}
interface AttemptTimeRange {
startIso: string
endIso?: string
durationLabel?: string
}
// 用量数据类型
interface UsageData {
status?: string | null
@@ -1060,6 +1066,10 @@ const currentAttempt = computed(() => {
return selectedGroup.value.allAttempts[selectedAttemptIndex.value] || selectedGroup.value.primary
})
const currentAttemptTimeRange = computed<AttemptTimeRange | null>(() => {
return resolveAttemptTimeRange(currentAttempt.value)
})
const currentAttemptDisplayStatus = computed(() => getDisplayStatus(currentAttempt.value))
watch(currentAttempt, (attempt) => {
@@ -2189,6 +2199,68 @@ onBeforeUnmount(() => {
defineExpose({ refresh: () => loadTrace(true) })
const TERMINAL_TIME_RANGE_STATUSES = new Set([
'success',
'failed',
'cancelled',
'stream_interrupted',
])
const parseTimestampMs = (value?: string | null): number | null => {
if (!value) return null
const timestamp = new Date(value).getTime()
return Number.isFinite(timestamp) ? timestamp : null
}
const normalizeLatencyMs = (value?: number | null): number | null => {
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return null
return Math.round(value)
}
const formatDurationMs = (durationMs: number): string => {
const safeDurationMs = Math.max(0, Math.round(durationMs))
if (safeDurationMs >= 1000) {
return `${(safeDurationMs / 1000).toFixed(2)}s`
}
return `${safeDurationMs}ms`
}
const resolveAttemptTimeRange = (attempt: CandidateRecord | null | undefined): AttemptTimeRange | null => {
if (!attempt?.started_at) return null
const startMs = parseTimestampMs(attempt.started_at)
if (startMs == null) {
return {
startIso: attempt.started_at,
endIso: attempt.finished_at,
durationLabel: attempt.finished_at ? formatDuration(attempt.started_at, attempt.finished_at) : undefined,
}
}
const rawEndMs = parseTimestampMs(attempt.finished_at)
const latencyMs = normalizeLatencyMs(attempt.latency_ms)
let endMs = rawEndMs
if (
latencyMs != null &&
latencyMs > 0 &&
(endMs == null || endMs <= startMs) &&
(attempt.finished_at || TERMINAL_TIME_RANGE_STATUSES.has(attempt.status))
) {
endMs = startMs + latencyMs
}
if (endMs == null) {
return { startIso: attempt.started_at }
}
return {
startIso: attempt.started_at,
endIso: new Date(endMs).toISOString(),
durationLabel: formatDurationMs(endMs - startMs),
}
}
// 格式化时间(详细)
const formatTime = (dateStr: string) => {
const date = new Date(dateStr)
@@ -2206,11 +2278,7 @@ const formatTime = (dateStr: string) => {
const formatDuration = (startStr: string, endStr: string): string => {
const start = new Date(startStr).getTime()
const end = new Date(endStr).getTime()
const durationMs = end - start
if (durationMs >= 1000) {
return `${(durationMs / 1000).toFixed(2)}s`
}
return `${durationMs}ms`
return formatDurationMs(end - start)
}
// 获取状态标签
@@ -792,6 +792,25 @@ describe('HorizontalRequestTimeline', () => {
expect(detailText).not.toContain('未知')
})
it('uses latency to display the time range when recorded finish time equals start time', async () => {
const trace = buildTrace([
buildCandidate({
id: 'cand-success-same-time',
status: 'success',
started_at: '2026-05-06T12:00:00.000Z',
finished_at: '2026-05-06T12:00:00.000Z',
latency_ms: 22200,
}),
])
const root = mountTimeline(trace)
await nextTick()
const detailText = root.querySelector('.detail-panel')?.textContent ?? ''
expect(detailText).toContain('+22.20s')
expect(detailText).not.toContain('+0ms')
})
it('follows the active key when silent polling updates the trace', async () => {
const initialTrace = buildTrace([
buildCandidate({