diff --git a/web/default/src/features/dashboard/components/models/performance-overview.tsx b/web/default/src/features/dashboard/components/models/performance-overview.tsx new file mode 100644 index 00000000..98745c01 --- /dev/null +++ b/web/default/src/features/dashboard/components/models/performance-overview.tsx @@ -0,0 +1,298 @@ +import { useMemo } from 'react' +import { useQuery } from '@tanstack/react-query' +import { Activity, Gauge, HeartPulse, Timer } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { formatNumber } from '@/lib/format' +import { cn } from '@/lib/utils' +import { Skeleton } from '@/components/ui/skeleton' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { getPerfMetricsSummary } from '@/features/performance-metrics/api' +import { + formatLatency, + formatThroughput, + formatUptimePct, +} from '@/features/performance-metrics/lib/format' +import type { PerfModelSummary } from '@/features/performance-metrics/types' + +const PERFORMANCE_WINDOW_HOURS = 24 +const TOP_MODEL_LIMIT = 8 + +type WeightedMetric = 'avg_latency_ms' | 'avg_tps' | 'success_rate' + +type PerformanceSummary = { + totalRequests: number + avgLatencyMs: number + avgTps: number + successRate: number +} + +function weightedAverage( + rows: PerfModelSummary[], + metric: WeightedMetric, + isValid: (value: number) => boolean +): number { + let total = 0 + let weight = 0 + + for (const row of rows) { + const value = Number(row[metric]) + const requestCount = Number(row.request_count) || 0 + if (requestCount <= 0 || !isValid(value)) continue + + total += value * requestCount + weight += requestCount + } + + return weight > 0 ? total / weight : 0 +} + +function buildPerformanceSummary(rows: PerfModelSummary[]): PerformanceSummary { + const totalRequests = rows.reduce( + (sum, row) => sum + (Number(row.request_count) || 0), + 0 + ) + + return { + totalRequests, + avgLatencyMs: Math.round( + weightedAverage( + rows, + 'avg_latency_ms', + (value) => Number.isFinite(value) && value > 0 + ) + ), + avgTps: weightedAverage( + rows, + 'avg_tps', + (value) => Number.isFinite(value) && value > 0 + ), + successRate: weightedAverage(rows, 'success_rate', Number.isFinite), + } +} + +function successRateClassName(successRate: number): string { + if (successRate >= 99.9) return 'text-emerald-600 dark:text-emerald-400' + if (successRate >= 99) return 'text-amber-600 dark:text-amber-400' + return 'text-rose-600 dark:text-rose-400' +} + +function successDotClassName(successRate: number): string { + if (successRate >= 99.9) return 'bg-emerald-500' + if (successRate >= 99) return 'bg-amber-500' + return 'bg-rose-500' +} + +function PerformanceMetricItem(props: { + icon: React.ComponentType<{ className?: string }> + label: string + value: string + hint: string + loading?: boolean + valueClassName?: string +}) { + const Icon = props.icon + + return ( +
{t('Page {{current}} of {{total}}', { - current: page, + current: currentPage, total: totalPages, })}
@@ -86,7 +83,7 @@ export function ModelCardGrid(props: ModelCardGridProps) { variant='outline' size='sm' onClick={() => setPage((current) => Math.max(1, current - 1))} - disabled={page <= 1} + disabled={currentPage <= 1} className='gap-1.5' >