Files
new-api/web/default/src/features/dashboard/components/models/performance-overview.tsx
T
t0ng7u 262ab93123 style(web): unify design system across default frontend
🔤 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
2026-07-11 02:02:11 +08:00

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>
)
}