import { useMemo, useState } from 'react'; import type { JobTimings, JobTokenUsageSummary } from '../../../shared/types/api'; import { useI18n } from '../../../shared/i18n/index'; function formatDuration(ms: number): string { if (ms >= 1000) { return `${(ms / 1000).toFixed(1)}s`; } return `${Math.round(ms)}ms`; } function formatTokens(value: number): string { if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(2)}M`; if (value >= 1_000) return `${(value / 1_000).toFixed(value >= 10_000 ? 1 : 2)}K`; return String(Math.round(value)); } interface TimingPanelProps { timings?: JobTimings; submittedAt?: string | null; finishedAt?: string | null; tokenUsage?: JobTokenUsageSummary; } export function TimingPanel({ timings, submittedAt, finishedAt, tokenUsage }: TimingPanelProps) { const { t } = useI18n(); const [isOpen, setIsOpen] = useState(false); const { total, items } = useMemo(() => { const timingLabels: Array<{ key: keyof JobTimings; label: string }> = [ { key: 'queue', label: t('timing.queue') }, { key: 'analyze', label: t('timing.analyze') }, { key: 'generate', label: t('timing.generate') }, { key: 'edit', label: t('timing.edit') }, { key: 'retry', label: t('timing.retry') }, { key: 'render', label: t('timing.render') }, { key: 'mux', label: t('timing.mux') }, { key: 'store', label: t('timing.store') }, ]; const items = timingLabels .map(({ key, label }) => ({ key, label, value: timings?.[key] })) .filter((item): item is { key: keyof JobTimings; label: string; value: number } => typeof item.value === 'number'); const submittedMs = submittedAt ? Date.parse(submittedAt) : Number.NaN; const finishedMs = finishedAt ? Date.parse(finishedAt) : Number.NaN; const endToEndTotal = Number.isFinite(submittedMs) && Number.isFinite(finishedMs) ? Math.max(0, finishedMs - submittedMs) : undefined; const measuredTotal = timings?.total; const totalIsMeasured = typeof measuredTotal === 'number'; const total = totalIsMeasured ? measuredTotal : typeof endToEndTotal === 'number' ? endToEndTotal : items.reduce((sum, item) => sum + item.value, 0); const rows: Array<{ key: keyof JobTimings | 'overhead'; label: string; value: number }> = [...items]; if (totalIsMeasured) { const measured = items.reduce((sum, item) => sum + item.value, 0); const overhead = Math.max(0, total - measured); if (overhead >= 1) { rows.push({ key: 'overhead', label: t('timing.overhead'), value: overhead }); } } return { total, items: rows }; }, [finishedAt, submittedAt, t, timings]); const tokenTotals = tokenUsage?.totals; const hasMeasuredTokens = Boolean(tokenTotals && tokenTotals.measuredCalls > 0); const measuredTotalTokens = tokenTotals ? tokenTotals.totalTokens || tokenTotals.promptTokens + tokenTotals.completionTokens : 0; const hasTokenCalls = Boolean(tokenUsage?.calls.length); if (!items.length && !Number.isFinite(total) && !hasTokenCalls) { return null; } return (