🔤 Typography - Fix --font-sans stack: 'Public Sans Variable' was never applied (wrong family name), add CJK fallbacks (PingFang SC / Microsoft YaHei UI / Noto Sans SC) - Sync <html lang> with i18n language for correct CJK glyph selection - Remove dead fake font system (font-provider.tsx, config/fonts.ts, --font-inter/--font-manrope) - Typography contract: 3-tier headings, arbitrary text-[10px]/[11px] → text-xs (46 files), no uppercase table headers, font-bold → font-semibold - Numbers use tabular-nums only; font-mono reserved for keys/IDs/code (29 files) 🎨 Color discipline - Retire StatusBadge string-hash rainbow (autoColor → neutral); all variants resolve to 5 semantic voices - Add tintedBadgeClassMap as the single tinted-pill recipe - Replace hardcoded emerald/amber/rose/sky classes with semantic tokens across ~30 files (timing badges, progress bars, system panels, pricing, rankings, log row tints) - Drop all !important text overrides and [font-family:var(--font-body)] hacks 🧘 Motion restraint - Remove admin entrance animations: table row stagger, card hover lift/shadow, button press scale, dashboard shimmer line - Neuter page-transition stagger components into plain wrappers; page transitions reduced to pure opacity fade - Decorative motion stays landing-only 📐 Shape - Single radius source: --radius 1rem → 0.625rem - Sparklines use solid chart tokens instead of decorative gradients - PanelWrapper: border-only (no shadow), align radius with Card 📏 Guardrails - Add "Design System Discipline" section (3.10.1) to web/default/AGENTS.md ✅ Verified: tsgo typecheck clean, rsbuild production build passes
228 lines
6.5 KiB
TypeScript
Vendored
228 lines
6.5 KiB
TypeScript
Vendored
/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { Gauge, HeartPulse, Timer } from 'lucide-react'
|
|
import { useMemo } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
import { getPerfMetricsSummary } from '@/features/performance-metrics/api'
|
|
import {
|
|
formatLatency,
|
|
formatThroughput,
|
|
formatUptimePct,
|
|
getSuccessRateDotClass,
|
|
getSuccessRateTextClass,
|
|
} from '@/features/performance-metrics/lib/format'
|
|
import type { PerfModelSummary } from '@/features/performance-metrics/types'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
const PERFORMANCE_WINDOW_HOURS = 24
|
|
const TOP_MODEL_LIMIT = 6
|
|
|
|
type WeightedMetric = 'avg_latency_ms' | 'avg_tps' | 'success_rate'
|
|
|
|
type PerformanceSummary = {
|
|
totalRequests: number
|
|
avgLatencyMs: number
|
|
avgTps: number
|
|
successRate: number
|
|
}
|
|
|
|
function simpleAverage(
|
|
rows: PerfModelSummary[],
|
|
metric: WeightedMetric,
|
|
isValid: (value: number) => boolean
|
|
): number {
|
|
let total = 0
|
|
let count = 0
|
|
|
|
for (const row of rows) {
|
|
const value = Number(row[metric])
|
|
if (!isValid(value)) continue
|
|
total += value
|
|
count++
|
|
}
|
|
|
|
return count > 0 ? total / count : NaN
|
|
}
|
|
|
|
function buildPerformanceSummary(rows: PerfModelSummary[]): PerformanceSummary {
|
|
return {
|
|
totalRequests: rows.length,
|
|
avgLatencyMs: Math.round(
|
|
simpleAverage(
|
|
rows,
|
|
'avg_latency_ms',
|
|
(value) => Number.isFinite(value) && value > 0
|
|
)
|
|
),
|
|
avgTps: simpleAverage(
|
|
rows,
|
|
'avg_tps',
|
|
(value) => Number.isFinite(value) && value > 0
|
|
),
|
|
successRate: simpleAverage(rows, 'success_rate', Number.isFinite),
|
|
}
|
|
}
|
|
|
|
export function PerformanceOverview() {
|
|
const { t } = useTranslation()
|
|
const metricsQuery = useQuery({
|
|
queryKey: ['perf-metrics-summary', PERFORMANCE_WINDOW_HOURS],
|
|
queryFn: () => getPerfMetricsSummary(PERFORMANCE_WINDOW_HOURS),
|
|
staleTime: 60 * 1000,
|
|
retry: false,
|
|
})
|
|
|
|
const models = useMemo(
|
|
() => metricsQuery.data?.data.models ?? [],
|
|
[metricsQuery.data]
|
|
)
|
|
const summary = useMemo(() => buildPerformanceSummary(models), [models])
|
|
const topModels = useMemo(() => models.slice(0, TOP_MODEL_LIMIT), [models])
|
|
const loading = metricsQuery.isLoading
|
|
const hasData = models.length > 0
|
|
|
|
if (!loading && !hasData) {
|
|
return (
|
|
<div className='text-muted-foreground overflow-hidden rounded-lg border px-4 py-3 text-center text-xs'>
|
|
{t('No performance data available')}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className='overflow-hidden rounded-lg border'>
|
|
<div className='flex flex-wrap items-center gap-x-5 gap-y-2.5 px-4 py-2.5 sm:px-5 sm:py-3'>
|
|
{/* Title */}
|
|
<div className='flex items-center gap-1.5'>
|
|
<HeartPulse
|
|
className='text-muted-foreground/60 size-3.5 shrink-0'
|
|
aria-hidden='true'
|
|
/>
|
|
<span className='text-xs font-semibold whitespace-nowrap'>
|
|
{t('Performance health')}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Separator */}
|
|
<div className='bg-border hidden h-4 w-px sm:block' />
|
|
|
|
{/* 3 KPI inline metrics */}
|
|
{loading ? (
|
|
<div className='flex flex-wrap items-center gap-x-5 gap-y-2'>
|
|
{Array.from({ length: 3 }).map((_, i) => (
|
|
<div key={i} className='flex items-center gap-1.5'>
|
|
<Skeleton className='h-3 w-14' />
|
|
<Skeleton className='h-4 w-16' />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className='flex flex-wrap items-center gap-x-5 gap-y-2'>
|
|
<InlineMetric
|
|
icon={HeartPulse}
|
|
label={t('Success rate')}
|
|
value={formatUptimePct(summary.successRate)}
|
|
valueClassName={getSuccessRateTextClass(summary.successRate)}
|
|
/>
|
|
<InlineMetric
|
|
icon={Timer}
|
|
label={t('Average latency')}
|
|
value={formatLatency(summary.avgLatencyMs)}
|
|
/>
|
|
<InlineMetric
|
|
icon={Gauge}
|
|
label={t('Throughput')}
|
|
value={formatThroughput(summary.avgTps)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Separator */}
|
|
<div className='bg-border hidden h-4 w-px lg:block' />
|
|
|
|
{/* Top models inline badges */}
|
|
{!loading && hasData && (
|
|
<div className='flex flex-wrap items-center gap-1.5'>
|
|
{topModels.map((model) => (
|
|
<ModelBadge key={model.model_name} model={model} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function InlineMetric(props: {
|
|
icon: React.ComponentType<{ className?: string }>
|
|
label: string
|
|
value: string
|
|
valueClassName?: string
|
|
}) {
|
|
const Icon = props.icon
|
|
|
|
return (
|
|
<div className='flex items-center gap-1.5'>
|
|
<Icon
|
|
className='text-muted-foreground/50 size-3 shrink-0'
|
|
aria-hidden='true'
|
|
/>
|
|
<span className='text-muted-foreground text-xs'>{props.label}</span>
|
|
<span
|
|
className={cn(
|
|
'text-xs font-semibold tabular-nums',
|
|
props.valueClassName
|
|
)}
|
|
>
|
|
{props.value}
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ModelBadge(props: { model: PerfModelSummary }) {
|
|
const model = props.model
|
|
|
|
return (
|
|
<span className='bg-muted/50 inline-flex items-center gap-1.5 rounded-full px-2.5 py-1'>
|
|
<span className='max-w-[10rem] truncate font-mono text-xs'>
|
|
{model.model_name}
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
'size-1.5 rounded-full',
|
|
getSuccessRateDotClass(model.success_rate)
|
|
)}
|
|
aria-hidden='true'
|
|
/>
|
|
<span
|
|
className={cn(
|
|
'text-xs font-semibold tabular-nums',
|
|
getSuccessRateTextClass(model.success_rate)
|
|
)}
|
|
>
|
|
{formatUptimePct(model.success_rate)}
|
|
</span>
|
|
</span>
|
|
)
|
|
}
|