feat(performance): update performance metrics handling and UI components

This commit is contained in:
CaIon
2026-05-12 16:04:15 +08:00
parent ba474393fb
commit 19fc384e67
37 changed files with 936 additions and 443 deletions
@@ -18,19 +18,10 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Activity, Gauge, HeartPulse, Timer } from 'lucide-react'
import { 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,
@@ -40,7 +31,7 @@ import {
import type { PerfModelSummary } from '@/features/performance-metrics/types'
const PERFORMANCE_WINDOW_HOURS = 24
const TOP_MODEL_LIMIT = 8
const TOP_MODEL_LIMIT = 5
type WeightedMetric = 'avg_latency_ms' | 'avg_tps' | 'success_rate'
@@ -51,121 +42,55 @@ type PerformanceSummary = {
successRate: number
}
function weightedAverage(
function simpleAverage(
rows: PerfModelSummary[],
metric: WeightedMetric,
isValid: (value: number) => boolean
): number {
let total = 0
let weight = 0
let count = 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
if (!isValid(value)) continue
total += value
count++
}
return weight > 0 ? total / weight : 0
return count > 0 ? total / count : NaN
}
function buildPerformanceSummary(rows: PerfModelSummary[]): PerformanceSummary {
const totalRequests = rows.reduce(
(sum, row) => sum + (Number(row.request_count) || 0),
0
)
return {
totalRequests,
totalRequests: rows.length,
avgLatencyMs: Math.round(
weightedAverage(
simpleAverage(
rows,
'avg_latency_ms',
(value) => Number.isFinite(value) && value > 0
)
),
avgTps: weightedAverage(
avgTps: simpleAverage(
rows,
'avg_tps',
(value) => Number.isFinite(value) && value > 0
),
successRate: weightedAverage(rows, 'success_rate', Number.isFinite),
successRate: simpleAverage(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'
if (!Number.isFinite(successRate)) return 'text-muted-foreground'
if (successRate >= 99.9) return 'text-success'
if (successRate >= 99) return 'text-warning'
return 'text-destructive'
}
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 (
<div className='px-3 py-2.5 sm:px-5 sm:py-4'>
<div className='flex items-center gap-2'>
<Icon
className='text-muted-foreground/60 size-3.5 shrink-0'
aria-hidden='true'
/>
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
{props.label}
</div>
</div>
{props.loading ? (
<div className='mt-2 space-y-1.5'>
<Skeleton className='h-7 w-20' />
<Skeleton className='h-3.5 w-28' />
</div>
) : (
<>
<div
className={cn(
'text-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl',
props.valueClassName
)}
>
{props.value}
</div>
<div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
{props.hint}
</div>
</>
)}
</div>
)
}
function PerformanceTableHeader(props: { description: string }) {
const { t } = useTranslation()
return (
<div className='flex flex-col gap-1.5 border-b px-3 py-2 sm:px-5 sm:py-3 lg:flex-row lg:items-center lg:justify-between'>
<div className='flex items-center gap-2'>
<Activity className='text-muted-foreground/60 size-4' />
<div className='text-sm font-semibold'>
{t('Model performance metrics')}
</div>
</div>
<span className='text-muted-foreground text-xs'>{props.description}</span>
</div>
)
if (!Number.isFinite(successRate)) return 'bg-muted-foreground'
if (successRate >= 99.9) return 'bg-success'
if (successRate >= 99) return 'bg-warning'
return 'bg-destructive'
}
export function PerformanceOverview() {
@@ -178,139 +103,136 @@ export function PerformanceOverview() {
})
const models = useMemo(
() =>
[...(metricsQuery.data?.data.models ?? [])]
.filter((model) => Number(model.request_count) > 0)
.sort((a, b) => b.request_count - a.request_count),
() => 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
const description = t('Performance metrics for the last 24 hours')
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 (
<section className='space-y-3 sm:space-y-4'>
<div className='overflow-hidden rounded-lg border'>
<div className='divide-border/60 grid grid-cols-2 divide-x sm:grid-cols-4'>
<PerformanceMetricItem
icon={Activity}
label={t('Requests (24h)')}
value={formatNumber(summary.totalRequests)}
hint={t('Monitored relay requests')}
loading={loading}
/>
<PerformanceMetricItem
icon={Timer}
label={t('Average latency')}
value={formatLatency(summary.avgLatencyMs)}
hint={t('Weighted by request count')}
loading={loading}
/>
<PerformanceMetricItem
icon={Gauge}
label={t('Throughput')}
value={formatThroughput(summary.avgTps)}
hint='TPS'
loading={loading}
/>
<PerformanceMetricItem
icon={HeartPulse}
label={t('Success rate')}
value={formatUptimePct(summary.successRate)}
hint={t('Weighted by request count')}
loading={loading}
valueClassName={successRateClassName(summary.successRate)}
<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>
</div>
<div className='overflow-hidden rounded-lg border'>
<PerformanceTableHeader description={description} />
{!loading && !hasData ? (
<div className='text-muted-foreground p-6 text-center text-sm'>
{t('No performance data available')}
{/* 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='overflow-x-auto'>
<Table className='text-sm'>
<TableHeader>
<TableRow className='hover:bg-transparent'>
<TableHead>{t('Model')}</TableHead>
<TableHead className='text-right'>
{t('Requests (24h)')}
</TableHead>
<TableHead className='text-right'>
{t('Average latency')}
</TableHead>
<TableHead className='text-right'>
{t('Throughput')}
</TableHead>
<TableHead className='text-right'>
{t('Success rate')}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading
? Array.from({ length: 4 }).map((_, index) => (
<TableRow key={index}>
<TableCell>
<Skeleton className='h-4 w-40' />
</TableCell>
<TableCell className='text-right'>
<Skeleton className='ml-auto h-4 w-16' />
</TableCell>
<TableCell className='text-right'>
<Skeleton className='ml-auto h-4 w-16' />
</TableCell>
<TableCell className='text-right'>
<Skeleton className='ml-auto h-4 w-16' />
</TableCell>
<TableCell className='text-right'>
<Skeleton className='ml-auto h-4 w-20' />
</TableCell>
</TableRow>
))
: topModels.map((model) => (
<TableRow key={model.model_name}>
<TableCell className='max-w-[220px] truncate font-mono'>
{model.model_name}
</TableCell>
<TableCell className='text-right font-mono tabular-nums'>
{formatNumber(model.request_count)}
</TableCell>
<TableCell className='text-right font-mono tabular-nums'>
{formatLatency(model.avg_latency_ms)}
</TableCell>
<TableCell className='text-right font-mono tabular-nums'>
{formatThroughput(model.avg_tps)}
</TableCell>
<TableCell
className={cn(
'text-right font-mono font-semibold tabular-nums',
successRateClassName(model.success_rate)
)}
>
<span className='inline-flex items-center justify-end gap-1.5'>
<span
className={cn(
'size-2 rounded-full',
successDotClassName(model.success_rate)
)}
aria-hidden='true'
/>
{formatUptimePct(model.success_rate)}
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<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={successRateClassName(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>
</section>
</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-[11px]'>{props.label}</span>
<span
className={cn(
'font-mono 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-[11px]'>
{model.model_name}
</span>
<span
className={cn(
'size-1.5 rounded-full',
successDotClassName(model.success_rate)
)}
aria-hidden='true'
/>
<span
className={cn(
'font-mono text-[11px] font-semibold tabular-nums',
successRateClassName(model.success_rate)
)}
>
{formatUptimePct(model.success_rate)}
</span>
</span>
)
}
@@ -56,6 +56,7 @@ import { useApiInfo } from '../../hooks/use-status-data'
import { AnnouncementsPanel } from './announcements-panel'
import { ApiInfoPanel } from './api-info-panel'
import { FAQPanel } from './faq-panel'
import { PerformanceHealthPanel } from './performance-health-panel'
import { SummaryCards } from './summary-cards'
import { UptimePanel } from './uptime-panel'
@@ -716,6 +717,11 @@ export function OverviewDashboard() {
<CardStaggerContainer className='grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,1fr)_22rem]'>
<div className='grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2'>
{isAdmin && (
<CardStaggerItem className='lg:col-span-2'>
<PerformanceHealthPanel />
</CardStaggerItem>
)}
<CardStaggerItem>
<ApiInfoPanel />
</CardStaggerItem>
@@ -0,0 +1,207 @@
/*
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 { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Gauge, HeartPulse, Timer } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { Skeleton } from '@/components/ui/skeleton'
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 = 5
type WeightedMetric = 'avg_latency_ms' | 'avg_tps' | 'success_rate'
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 rateTextClass(rate: number): string {
if (!Number.isFinite(rate)) return 'text-muted-foreground'
if (rate >= 99.9) return 'text-success'
if (rate >= 99) return 'text-warning'
return 'text-destructive'
}
function rateDotClass(rate: number): string {
if (!Number.isFinite(rate)) return 'bg-muted-foreground'
if (rate >= 99.9) return 'bg-success'
if (rate >= 99) return 'bg-warning'
return 'bg-destructive'
}
export function PerformanceHealthPanel() {
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(() => {
return {
avgLatencyMs: Math.round(
simpleAverage(models, 'avg_latency_ms', (v) => Number.isFinite(v) && v > 0)
),
avgTps: simpleAverage(models, 'avg_tps', (v) => Number.isFinite(v) && v > 0),
successRate: simpleAverage(models, 'success_rate', Number.isFinite),
}
}, [models])
const topModels = useMemo(() => models.slice(0, TOP_MODEL_LIMIT), [models])
const loading = metricsQuery.isLoading
const hasData = models.length > 0
return (
<section className='bg-card h-full overflow-hidden rounded-2xl border shadow-xs'>
<div className='flex items-center gap-2 border-b px-4 py-3 sm:px-5'>
<HeartPulse className='text-muted-foreground/60 size-4 shrink-0' aria-hidden='true' />
<h3 className='text-sm font-semibold'>{t('Performance health')}</h3>
<span className='text-muted-foreground ml-auto text-xs'>
{t('Performance metrics for the last 24 hours')}
</span>
</div>
<div className='grid gap-3 p-4 sm:p-5 lg:grid-cols-[minmax(0,1fr)_14rem]'>
{/* KPI metrics */}
<div className='grid grid-cols-3 gap-2'>
<MetricCell
icon={HeartPulse}
label={t('Success rate')}
value={formatUptimePct(summary.successRate)}
loading={loading}
valueClassName={rateTextClass(summary.successRate)}
/>
<MetricCell
icon={Timer}
label={t('Average latency')}
value={formatLatency(summary.avgLatencyMs)}
loading={loading}
/>
<MetricCell
icon={Gauge}
label={t('Throughput')}
value={formatThroughput(summary.avgTps)}
loading={loading}
/>
</div>
{/* Top models */}
<div className='flex flex-col'>
<span className='text-muted-foreground mb-1 text-[11px] font-medium'>
{t('Top models by traffic')}
</span>
{loading ? (
<div className='space-y-1'>
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className='h-5 w-full rounded' />
))}
</div>
) : !hasData ? (
<span className='text-muted-foreground/60 text-xs'>
{t('No performance data available')}
</span>
) : (
<div className='space-y-0.5'>
{topModels.map((model) => (
<div
key={model.model_name}
className='flex items-center justify-between gap-2 rounded px-1.5 py-1'
>
<span className='min-w-0 flex-1 truncate font-mono text-[11px]'>
{model.model_name}
</span>
<span className='inline-flex shrink-0 items-center gap-1'>
<span
className={cn('size-1.5 rounded-full', rateDotClass(model.success_rate))}
aria-hidden='true'
/>
<span
className={cn(
'font-mono text-[11px] font-semibold tabular-nums',
rateTextClass(model.success_rate)
)}
>
{formatUptimePct(model.success_rate)}
</span>
</span>
</div>
))}
</div>
)}
</div>
</div>
</section>
)
}
function MetricCell(props: {
icon: React.ComponentType<{ className?: string }>
label: string
value: string
loading: boolean
valueClassName?: string
}) {
const Icon = props.icon
return (
<div className='bg-muted/40 rounded-xl px-3 py-2.5'>
<div className='text-muted-foreground flex items-center gap-1.5 text-[11px] font-medium'>
<Icon className='size-3 shrink-0' aria-hidden='true' />
<span className='truncate'>{props.label}</span>
</div>
{props.loading ? (
<Skeleton className='mt-1.5 h-5 w-16' />
) : (
<div
className={cn(
'mt-1.5 font-mono text-sm font-semibold tabular-nums',
props.valueClassName
)}
>
{props.value}
</div>
)}
</div>
)
}
@@ -19,12 +19,18 @@ For commercial licensing, please contact support@quantumnous.com
import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import { ArrowRight, CreditCard } from 'lucide-react'
import {
ArrowRight,
Flame,
ShieldCheck,
TrendingDown,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useAuthStore } from '@/stores/auth-store'
import { getCurrencyLabel, isCurrencyDisplayEnabled } from '@/lib/currency'
import { formatNumber, formatQuota } from '@/lib/format'
import { computeTimeRange } from '@/lib/time'
import { cn } from '@/lib/utils'
import { useStatus } from '@/hooks/use-status'
import { Button } from '@/components/ui/button'
import { StaggerContainer, StaggerItem } from '@/components/page-transition'
@@ -87,12 +93,62 @@ function buildSummarySparklines(
}
}
function getSummarySparkline(
key: string,
sparklineData: Record<SummarySparklineKey, number[]>
): number[] | undefined {
if (key === 'usage') return sparklineData.usage
if (key === 'requests') return sparklineData.requests
return undefined
}
function getRunwayDays(remainQuota: number, recentUsage: number): number | null {
if (remainQuota <= 0 || recentUsage <= 0) return null
const days = remainQuota / recentUsage
if (!Number.isFinite(days)) return null
return days
}
type HealthLevel = 'healthy' | 'caution' | 'critical'
function getHealthLevel(
remainQuota: number,
recentUsage: number
): HealthLevel {
if (remainQuota <= 0) return 'critical'
const days = getRunwayDays(remainQuota, recentUsage)
if (days !== null && days < 3) return 'caution'
return 'healthy'
}
const HEALTH_CONFIG: Record<
HealthLevel,
{ dotClass: string; labelKey: string }
> = {
healthy: {
dotClass: 'bg-success',
labelKey: 'Healthy',
},
caution: {
dotClass: 'bg-warning',
labelKey: 'Low balance',
},
critical: {
dotClass: 'bg-destructive',
labelKey: 'Balance depleted',
},
}
export function SummaryCards() {
const { t } = useTranslation()
const user = useAuthStore((state) => state.auth.user)
const { status, loading } = useStatus()
const summaryTimeRange = useMemo(() => computeTimeRange(1), [])
const remainQuota = Number(user?.quota ?? 0)
const usedQuota = Number(user?.used_quota ?? 0)
const requestCount = Number(user?.request_count ?? 0)
const usageTrendQuery = useQuery({
queryKey: [
@@ -112,16 +168,11 @@ export function SummaryCards() {
})
const summaryValues = useMemo(() => {
const remainQuota = Number(user?.quota ?? 0)
const usedQuota = Number(user?.used_quota ?? 0)
const requestCount = Number(user?.request_count ?? 0)
return {
remainDisplay: formatQuota(remainQuota),
usedDisplay: formatQuota(usedQuota),
requestCountDisplay: formatNumber(requestCount),
}
}, [user])
}, [requestCount, usedQuota])
const currencyEnabledFromStore = isCurrencyDisplayEnabled()
const statusCurrencyFlag =
@@ -138,37 +189,53 @@ export function SummaryCards() {
() =>
buildSummarySparklines(
usageTrendQuery.data?.data ?? [],
Number(user?.quota ?? 0),
remainQuota,
summaryTimeRange.start_timestamp,
summaryTimeRange.end_timestamp
),
[
remainQuota,
summaryTimeRange.end_timestamp,
summaryTimeRange.start_timestamp,
usageTrendQuery.data?.data,
user?.quota,
]
)
const recentUsage = useMemo(
() =>
(usageTrendQuery.data?.data ?? []).reduce(
(total, item) => total + (Number(item.quota) || 0),
0
),
[usageTrendQuery.data?.data]
)
const healthLevel = getHealthLevel(remainQuota, recentUsage)
const healthCfg = HEALTH_CONFIG[healthLevel]
const runwayDays = getRunwayDays(remainQuota, recentUsage)
const todayUsageDisplay = formatQuota(recentUsage)
const items = useSummaryCardsConfig({
...summaryValues,
todayUsageDisplay,
currencyEnabled,
currencyLabel,
}).map((config, index) => {
const tones = ['rose', 'teal', 'gray'] as const
return {
key: config.key,
title: config.title,
value: config.value,
desc: config.description,
icon: config.icon,
tone: tones[index] ?? 'gray',
sparkline:
config.key === 'balance'
? sparklineData.balance
: config.key === 'usage'
? sparklineData.usage
: sparklineData.requests,
config.key === 'todayUsage'
? sparklineData.usage
: getSummarySparkline(config.key, sparklineData),
sparklineVariant: 'line' as const,
}
})
@@ -189,7 +256,7 @@ export function SummaryCards() {
<StaggerContainer className='grid gap-3 md:grid-cols-3'>
{items.map((it) => (
<StaggerItem
key={it.title}
key={it.key}
className='bg-background/60 rounded-xl border p-3'
>
<StatCard
@@ -199,6 +266,7 @@ export function SummaryCards() {
icon={it.icon}
tone={it.tone}
sparkline={it.sparkline}
sparklineVariant={it.sparklineVariant}
loading={loading}
/>
</StaggerItem>
@@ -206,28 +274,78 @@ export function SummaryCards() {
</StaggerContainer>
</div>
<div className='bg-warning/10 flex flex-col justify-between gap-5 border-t p-4 sm:p-5 xl:border-t-0 xl:border-l'>
<div className='flex flex-col gap-2'>
<div className='text-muted-foreground text-sm'>
{t('Credit remaining')}
</div>
<div className='flex items-center gap-2'>
<span className='font-mono text-2xl font-semibold tracking-tight'>
{summaryValues.remainDisplay}
<div className='bg-warning/10 flex flex-col justify-between gap-4 border-t p-4 sm:p-5 xl:border-t-0 xl:border-l'>
<div className='flex flex-col gap-3'>
<div className='flex items-center justify-between'>
<span className='text-muted-foreground text-xs font-medium'>
{t('Credit remaining')}
</span>
<span className='flex items-center gap-1.5'>
<span
className={cn('size-1.5 rounded-full', healthCfg.dotClass)}
aria-hidden='true'
/>
<span className='text-muted-foreground text-[11px] font-medium'>
{t(healthCfg.labelKey)}
</span>
</span>
<CreditCard
className='text-muted-foreground size-4'
aria-hidden='true'
/>
</div>
<p className='text-muted-foreground text-sm leading-relaxed'>
{currencyEnabled
? `${t('Displayed in')} ${currencyLabel}`
: t('Balance is shown in quota units')}
</p>
<div className='font-mono text-2xl font-semibold tracking-tight'>
{formatQuota(remainQuota)}
</div>
<div className='grid grid-cols-2 gap-2'>
<div className='bg-background/60 rounded-lg px-2.5 py-2'>
<div className='text-muted-foreground flex items-center gap-1 text-[11px] leading-none font-medium'>
<Flame className='size-3 shrink-0' aria-hidden='true' />
<span className='truncate'>{t('Last 24h usage')}</span>
</div>
<div className='text-foreground mt-1.5 truncate text-xs font-semibold tabular-nums'>
{formatQuota(recentUsage)}
</div>
</div>
<div className='bg-background/60 rounded-lg px-2.5 py-2'>
<div className='text-muted-foreground flex items-center gap-1 text-[11px] leading-none font-medium'>
{runwayDays !== null && runwayDays < 3 ? (
<TrendingDown
className='size-3 shrink-0'
aria-hidden='true'
/>
) : (
<ShieldCheck
className='size-3 shrink-0'
aria-hidden='true'
/>
)}
<span className='truncate'>{t('Runway')}</span>
</div>
<div
className={cn(
'mt-1.5 truncate text-xs font-semibold tabular-nums',
healthLevel === 'critical' && 'text-destructive',
healthLevel === 'caution' && 'text-warning'
)}
>
{runwayDays !== null
? runwayDays < 1
? t('Less than 1 day left')
: runwayDays > 999
? `999+ ${t('days')}`
: `~${formatNumber(Math.floor(runwayDays))} ${t('days')}`
: remainQuota <= 0
? t('Balance depleted')
: t('No recent usage')}
</div>
</div>
</div>
</div>
<Button className='justify-between' render={<Link to='/wallet' />}>
<span>{t('Recharge')}</span>
<Button
className='justify-between'
render={<Link to='/wallet' />}
>
<span>{t('Wallet')}</span>
<ArrowRight data-icon='inline-end' />
</Button>
</div>
+161 -15
View File
@@ -16,12 +16,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { ReactNode } from 'react'
import { useId, type ReactNode } from 'react'
import { type LucideIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Skeleton } from '@/components/ui/skeleton'
type StatCardTone = 'rose' | 'teal' | 'gray'
type StatCardSparklineVariant = 'bars' | 'line'
type StatCardDetailTone =
| 'default'
| 'muted'
| 'success'
| 'warning'
| 'destructive'
export interface StatCardDetail {
label: string
value: string
tone?: StatCardDetailTone
}
interface StatCardProps {
title: string
@@ -29,6 +42,8 @@ interface StatCardProps {
description: string
icon: LucideIcon
sparkline?: number[]
sparklineVariant?: StatCardSparklineVariant
details?: StatCardDetail[]
tone?: StatCardTone
loading?: boolean
error?: boolean
@@ -41,6 +56,20 @@ const TONE_CLASSES: Record<StatCardTone, string> = {
gray: 'from-muted-foreground/50 via-muted-foreground/20 to-transparent dark:from-muted-foreground/40 dark:via-muted-foreground/20',
}
const LINE_TONE_CLASSES: Record<StatCardTone, string> = {
rose: 'text-warning',
teal: 'text-primary',
gray: 'text-muted-foreground',
}
const DETAIL_TONE_CLASSES: Record<StatCardDetailTone, string> = {
default: 'text-foreground',
muted: 'text-muted-foreground',
success: 'text-success',
warning: 'text-warning',
destructive: 'text-destructive',
}
function normalizeSparkline(values?: number[]): number[] {
if (!values?.length) return []
@@ -51,10 +80,133 @@ function normalizeSparkline(values?: number[]): number[] {
return sanitized.map((value) => Math.max(8, (value / max) * 100))
}
function buildLineSparkline(values?: number[]) {
if (!values?.length) return null
const sanitized = values.map((value) => Math.max(0, Number(value) || 0))
const width = 160
const height = 36
const padding = 3
const max = Math.max(...sanitized)
const min = Math.min(...sanitized)
const range = max - min
const points = sanitized.map((value, index) => {
const x =
sanitized.length === 1
? width / 2
: (index / (sanitized.length - 1)) * width
const normalized =
range > 0 ? (value - min) / range : max > 0 ? 0.5 : 0
const y = height - padding - normalized * (height - padding * 2)
return { x, y }
})
const linePath = points
.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`)
.join(' ')
const firstPoint = points[0]
const lastPoint = points[points.length - 1]
const areaPath = `${linePath} L ${lastPoint.x} ${height} L ${firstPoint.x} ${height} Z`
return {
areaPath,
linePath,
}
}
function LineSparkline(props: { values?: number[]; tone: StatCardTone }) {
const rawGradientId = useId()
const gradientId = `stat-card-line-${rawGradientId.replace(/:/g, '')}`
const paths = buildLineSparkline(props.values)
if (!paths) return <div className='h-8' aria-hidden='true' />
return (
<div
className={cn(
'relative h-8 overflow-hidden rounded-lg',
LINE_TONE_CLASSES[props.tone]
)}
aria-hidden='true'
>
<svg
viewBox='0 0 160 36'
preserveAspectRatio='none'
className='size-full'
>
<defs>
<linearGradient id={gradientId} x1='0' x2='0' y1='0' y2='1'>
<stop offset='0%' stopColor='currentColor' stopOpacity='0.24' />
<stop offset='100%' stopColor='currentColor' stopOpacity='0' />
</linearGradient>
</defs>
<path d={paths.areaPath} fill={`url(#${gradientId})`} />
<path
d={paths.linePath}
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2.25'
vectorEffect='non-scaling-stroke'
/>
</svg>
</div>
)
}
function BarSparkline(props: { values?: number[]; tone: StatCardTone }) {
const sparkline = normalizeSparkline(props.values)
return (
<div className='flex h-8 items-end gap-1' aria-hidden='true'>
{sparkline.map((height, index) => (
<span
key={`spark-${index}`}
className={cn(
'flex-1 rounded-t-sm bg-linear-to-t',
height <= 0 && 'opacity-20',
TONE_CLASSES[props.tone]
)}
style={{ height: `${height}%` }}
/>
))}
</div>
)
}
function StatCardDetails(props: { details: StatCardDetail[] }) {
return (
<div className='grid grid-cols-2 gap-2'>
{props.details.map((detail) => (
<div
key={detail.label}
className='bg-muted/40 rounded-lg border border-transparent px-2.5 py-2'
>
<div className='text-muted-foreground truncate text-[11px] leading-none font-medium'>
{detail.label}
</div>
<div
className={cn(
'mt-1.5 truncate text-xs font-semibold tabular-nums',
DETAIL_TONE_CLASSES[detail.tone ?? 'default']
)}
title={detail.value}
>
{detail.value}
</div>
</div>
))}
</div>
)
}
export function StatCard(props: StatCardProps) {
const Icon = props.icon
const tone = props.tone ?? 'gray'
const sparkline = normalizeSparkline(props.sparkline)
const sparklineVariant = props.sparklineVariant ?? 'bars'
return (
<div className='group flex min-h-32 flex-col justify-between gap-3'>
@@ -94,19 +246,13 @@ export function StatCard(props: StatCardProps) {
</div>
)}
<div className='flex h-8 items-end gap-1' aria-hidden='true'>
{sparkline.map((height, index) => (
<span
key={`${props.title}-spark-${index}`}
className={cn(
'flex-1 rounded-t-sm bg-linear-to-t',
height <= 0 && 'opacity-20',
TONE_CLASSES[tone]
)}
style={{ height: `${height}%` }}
/>
))}
</div>
{props.details?.length ? (
<StatCardDetails details={props.details} />
) : sparklineVariant === 'line' ? (
<LineSparkline values={props.sparkline} tone={tone} />
) : (
<BarSparkline values={props.sparkline} tone={tone} />
)}
</div>
)
}
@@ -22,7 +22,7 @@ import {
Layers,
Gauge,
Zap,
Wallet,
Flame,
TrendingUp,
Activity,
type LucideIcon,
@@ -83,7 +83,7 @@ export function useModelStatCardsConfig(): StatCardConfig[] {
}
export function useSummaryCardsConfig(totals: {
remainDisplay: string
todayUsageDisplay: string
usedDisplay: string
requestCountDisplay: string
currencyLabel: string
@@ -93,13 +93,13 @@ export function useSummaryCardsConfig(totals: {
return [
{
key: 'balance',
title: t('Current Balance'),
value: totals.remainDisplay,
key: 'todayUsage',
title: t('Last 24h usage'),
value: totals.todayUsageDisplay,
description: totals.currencyEnabled
? `${t('Remaining quota')} (${totals.currencyLabel})`
: t('Remaining quota units'),
icon: Wallet,
? `${t('Consumed in the last 24 hours')} (${totals.currencyLabel})`
: t('Consumed in the last 24 hours'),
icon: Flame,
},
{
key: 'usage',
+22 -22
View File
@@ -109,25 +109,23 @@ function ModelChartsFallback() {
function PerformanceOverviewFallback() {
return (
<div className='space-y-3 sm:space-y-4'>
<div className='overflow-hidden rounded-lg border'>
<div className='divide-border/60 grid grid-cols-2 divide-x sm:grid-cols-4'>
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className='px-3 py-2.5 sm:px-5 sm:py-4'>
<Skeleton className='h-4 w-24' />
<Skeleton className='mt-2 h-7 w-20' />
<Skeleton className='mt-1.5 h-3.5 w-28' />
</div>
<div className='overflow-hidden rounded-lg border'>
<div className='flex flex-wrap items-center gap-x-6 gap-y-2 px-4 py-3 sm:px-5'>
<div className='flex items-center gap-2'>
<Skeleton className='h-4 w-24' />
</div>
{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 className='ml-auto flex items-center gap-2'>
{Array.from({ length: 2 }).map((_, i) => (
<Skeleton key={i} className='h-5 w-28 rounded-full' />
))}
</div>
</div>
<div className='overflow-hidden rounded-lg border'>
<div className='flex items-center justify-between border-b px-4 py-3 sm:px-5'>
<Skeleton className='h-5 w-40' />
<Skeleton className='h-4 w-48' />
</div>
<Skeleton className='h-44 w-full' />
</div>
</div>
)
}
@@ -267,12 +265,14 @@ export function Dashboard() {
/>
</Suspense>
</FadeIn>
{isAdmin && (
<FadeIn delay={0.05}>
<Suspense fallback={<PerformanceOverviewFallback />}>
<LazyPerformanceOverview />
</Suspense>
</FadeIn>
)}
<FadeIn delay={0.1}>
<Suspense fallback={<PerformanceOverviewFallback />}>
<LazyPerformanceOverview />
</Suspense>
</FadeIn>
<FadeIn delay={0.15}>
<Suspense fallback={<ModelChartsFallback />}>
<LazyConsumptionDistributionChart
data={modelData}
@@ -286,7 +286,7 @@ export function Dashboard() {
/>
</Suspense>
</FadeIn>
<FadeIn delay={0.2}>
<FadeIn delay={0.15}>
<Suspense fallback={<ModelChartsFallback />}>
<LazyModelCharts
data={modelData}
+1 -1
View File
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
export function formatThroughput(tps: number): string {
if (tps <= 0) return '—'
if (!Number.isFinite(tps) || tps <= 0) return '—'
if (tps >= 1_000) return `${(tps / 1_000).toFixed(1)}K t/s`
return `${tps.toFixed(tps < 10 ? 2 : 1)} t/s`
}
+1 -1
View File
@@ -48,7 +48,7 @@ export type PerfModelSummary = {
avg_latency_ms: number
success_rate: number
avg_tps: number
request_count: number
request_count?: number
}
export type PerfSummaryAllData = {
@@ -59,9 +59,7 @@ export function ModelCardGrid(props: ModelCardGridProps) {
const perfMap = useMemo(() => {
const map = new Map<string, ModelPerfBadgeData>()
for (const model of perfQuery.data?.data?.models ?? []) {
if (model.request_count > 0) {
map.set(model.model_name, model)
}
map.set(model.model_name, model)
}
return map
}, [perfQuery.data])
@@ -475,14 +475,14 @@ export function RechargeFormCard({
</div>
{topupLink && (
<p className='text-muted-foreground text-xs'>
{t('Need a code?')}{' '}
{t('Need a redemption code?')}{' '}
<a
href={topupLink}
target='_blank'
rel='noopener noreferrer'
className='inline-flex items-center gap-1 underline-offset-4 hover:underline'
>
{t('Purchase here')}
{t('Get one here')}
<ExternalLink className='h-3 w-3' />
</a>
</p>
@@ -254,7 +254,7 @@ export function SubscriptionPlansCard({
<>
<TitledCard
title={t('Subscription Plans')}
description={t('Purchase a plan to enjoy model benefits')}
description={t('Subscribe to a plan for model access')}
icon={<Crown className='h-4 w-4' />}
contentClassName='space-y-4 sm:space-y-5'
>
@@ -499,7 +499,7 @@ export function SubscriptionPlansCard({
{!hasAny && (
<p className='text-muted-foreground mt-2 text-xs'>
{t('Purchase a plan to enjoy model benefits')}
{t('Subscribe to a plan for model access')}
</p>
)}
</div>