import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import {
Activity,
AlertTriangle,
HeartPulse,
Timer,
TrendingUp,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { GroupBadge } from '@/components/group-badge'
import { getPerfMetrics, type PerformanceGroup } from '../api'
import {
formatLatency,
formatUptimePct,
type UptimeDayPoint,
} from '../lib/mock-stats'
import type { PricingModel } from '../types'
import { LatencyTrendChart, UptimeBarChart } from './model-details-charts'
import { UptimeSparkline } from './model-details-uptime-sparkline'
const COMPACT_NUMBER = new Intl.NumberFormat(undefined, {
notation: 'compact',
maximumFractionDigits: 1,
})
function StatCard(props: {
icon: React.ComponentType<{ className?: string }>
label: string
value: React.ReactNode
hint?: string
intent?: 'default' | 'warning' | 'success'
}) {
const Icon = props.icon
const intent = props.intent ?? 'default'
return (
{props.label}
{props.value}
{props.hint && (
{props.hint}
)}
)
}
type PerformanceRow = {
group: string
avg_ttft_ms: number
avg_latency_ms: number
success_rate: number
request_count: number
}
function toLatencySeries(groups: PerformanceGroup[]) {
return groups.flatMap((group) =>
group.series
.filter((point) => point.ttft_count > 0 && point.avg_ttft_ms > 0)
.map((point) => ({
timestamp: new Date(point.ts * 1000).toISOString(),
group: group.group,
ttft_ms: point.avg_ttft_ms,
}))
)
}
function toUptimeSeries(groups: PerformanceGroup[]): UptimeDayPoint[] {
const byTs = new Map()
for (const group of groups) {
for (const point of group.series) {
const current = byTs.get(point.ts) ?? { count: 0, success: 0 }
current.count += point.count
current.success += point.success_count
byTs.set(point.ts, current)
}
}
return Array.from(byTs.entries())
.sort(([a], [b]) => a - b)
.map(([ts, value]) => {
const uptime = value.count > 0 ? (value.success / value.count) * 100 : 0
return {
date: new Date(ts * 1000).toISOString(),
uptime_pct: Math.round(uptime * 100) / 100,
incidents: value.success < value.count ? 1 : 0,
outage_minutes: 0,
}
})
}
function toGroupUptimeSeries(group: PerformanceGroup): UptimeDayPoint[] {
return group.series.map((point) => ({
date: new Date(point.ts * 1000).toISOString(),
uptime_pct: Math.round(point.success_rate * 100) / 100,
incidents: point.success_count < point.count ? 1 : 0,
outage_minutes: 0,
}))
}
function weightedAverage(
rows: PerformanceRow[],
field: 'avg_ttft_ms' | 'avg_latency_ms'
): number {
let total = 0
let count = 0
for (const row of rows) {
if (row[field] <= 0 || row.request_count <= 0) continue
total += row[field] * row.request_count
count += row.request_count
}
return count > 0 ? Math.round(total / count) : 0
}
export function ModelDetailsPerformance(props: { model: PricingModel }) {
const { t } = useTranslation()
const metricsQuery = useQuery({
queryKey: ['perf-metrics', props.model.model_name],
queryFn: () => getPerfMetrics(props.model.model_name, 24),
staleTime: 60 * 1000,
})
const groups = metricsQuery.data?.data.groups ?? []
const performances = useMemo(
() =>
groups.map((group) => ({
group: group.group,
avg_ttft_ms: group.avg_ttft_ms,
avg_latency_ms: group.avg_latency_ms,
success_rate: group.success_rate,
request_count: group.request_count,
})),
[groups]
)
const latencySeries = useMemo(() => toLatencySeries(groups), [groups])
const uptimeSeries = useMemo(() => toUptimeSeries(groups), [groups])
const uptimeByGroup = useMemo>(() => {
const map: Record = {}
for (const group of groups) {
map[group.group] = toGroupUptimeSeries(group)
}
return map
}, [groups])
if (metricsQuery.isLoading || performances.length === 0) {
return (
{t('Performance data is not yet available for this model.')}
)
}
const ttftValues = performances
.map((p) => p.avg_ttft_ms)
.filter((value) => value > 0)
const bestTtft = ttftValues.length > 0 ? Math.min(...ttftValues) : 0
const avgLatency = weightedAverage(performances, 'avg_latency_ms')
const totalRequests = performances.reduce((s, p) => s + p.request_count, 0)
const totalSuccess = groups.reduce((s, p) => s + p.success_count, 0)
const successRate =
totalRequests > 0 ? (totalSuccess / totalRequests) * 100 : 0
const incidentCount = uptimeSeries.reduce((s, p) => s + p.incidents, 0)
let intent: 'default' | 'warning' | 'success' = 'warning'
if (successRate >= 99.9) {
intent = 'success'
} else if (successRate >= 99) {
intent = 'default'
}
const headerCellClass =
'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase'
return (
0
? t('{{count}} incidents in the last 24 hours', {
count: incidentCount,
})
: t('No incidents in the last 24 hours')
}
intent={intent}
/>
{t('Group')}
{t('Average TTFT')}
{t('Average latency')}
{t('Success rate')}
{t('Request Count')}
{performances.map((perf) => {
const isBestTtft = perf.avg_ttft_ms === bestTtft
return (
{formatLatency(perf.avg_ttft_ms)}
{formatLatency(perf.avg_latency_ms)}
{COMPACT_NUMBER.format(perf.request_count)}
)
})}
0
? t(
'Request success rate; {{incidents}} incident buckets in the last 24 hours',
{
incidents: incidentCount,
}
)
: t('Request success rate sampled over the last 24 hours')
}
accent={
incidentCount > 0 ? (
{t('{{count}} incidents', {
count: incidentCount,
})}
) : null
}
/>
)
}
function SectionHeader(props: {
icon: React.ComponentType<{ className?: string }>
title: string
description?: string
accent?: React.ReactNode
}) {
const Icon = props.icon
return (
{props.title}
{props.description && (
{props.description}
)}
{props.accent && (
{props.accent}
)}
)
}