Commit a95655a2 by CaIon

feat(performance): implement success rate grading and enhance UI representation

parent 3cc2b1be
......@@ -27,6 +27,8 @@ import {
formatLatency,
formatThroughput,
formatUptimePct,
getSuccessRateDotClass,
getSuccessRateTextClass,
} from '@/features/performance-metrics/lib/format'
import type { PerfModelSummary } from '@/features/performance-metrics/types'
......@@ -79,20 +81,6 @@ function buildPerformanceSummary(rows: PerfModelSummary[]): PerformanceSummary {
}
}
function successRateClassName(successRate: number): string {
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 (!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() {
const { t } = useTranslation()
const metricsQuery = useQuery({
......@@ -152,7 +140,7 @@ export function PerformanceOverview() {
icon={HeartPulse}
label={t('Success rate')}
value={formatUptimePct(summary.successRate)}
valueClassName={successRateClassName(summary.successRate)}
valueClassName={getSuccessRateTextClass(summary.successRate)}
/>
<InlineMetric
icon={Timer}
......@@ -221,14 +209,14 @@ function ModelBadge(props: { model: PerfModelSummary }) {
<span
className={cn(
'size-1.5 rounded-full',
successDotClassName(model.success_rate)
getSuccessRateDotClass(model.success_rate)
)}
aria-hidden='true'
/>
<span
className={cn(
'font-mono text-[11px] font-semibold tabular-nums',
successRateClassName(model.success_rate)
getSuccessRateTextClass(model.success_rate)
)}
>
{formatUptimePct(model.success_rate)}
......
......@@ -27,6 +27,8 @@ import {
formatLatency,
formatThroughput,
formatUptimePct,
getSuccessRateDotClass,
getSuccessRateTextClass,
} from '@/features/performance-metrics/lib/format'
import type { PerfModelSummary } from '@/features/performance-metrics/types'
......@@ -51,20 +53,6 @@ function simpleAverage(
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({
......@@ -121,7 +109,7 @@ export function PerformanceHealthPanel() {
label={t('Success rate')}
value={formatUptimePct(summary.successRate)}
loading={loading}
valueClassName={rateTextClass(summary.successRate)}
valueClassName={getSuccessRateTextClass(summary.successRate)}
/>
<MetricCell
icon={Timer}
......@@ -162,14 +150,14 @@ export function PerformanceHealthPanel() {
<span
className={cn(
'size-1.5 rounded-full',
rateDotClass(model.success_rate)
getSuccessRateDotClass(model.success_rate)
)}
aria-hidden='true'
/>
<span
className={cn(
'font-mono text-[11px] font-semibold tabular-nums',
rateTextClass(model.success_rate)
getSuccessRateTextClass(model.success_rate)
)}
>
{formatUptimePct(model.success_rate)}
......
......@@ -32,3 +32,67 @@ export function formatUptimePct(pct: number): string {
if (!Number.isFinite(pct)) return '—'
return `${pct.toFixed(2)}%`
}
export type SuccessRateLevel =
| 'excellent'
| 'good'
| 'warning'
| 'critical'
| 'unknown'
const SUCCESS_RATE_EXCELLENT_MIN = 100
const SUCCESS_RATE_GOOD_MIN = 90
const SUCCESS_RATE_WARNING_MIN = 70
/**
* Single source of truth for grading a success rate (0-100).
* - excellent: 100% (full green)
* - good: >= 90% (slightly lighter green)
* - warning: >= 70%
* - critical: below 70%
* - unknown: non-finite values
*/
export function getSuccessRateLevel(rate: number): SuccessRateLevel {
if (!Number.isFinite(rate)) return 'unknown'
if (rate >= SUCCESS_RATE_EXCELLENT_MIN) return 'excellent'
if (rate >= SUCCESS_RATE_GOOD_MIN) return 'good'
if (rate >= SUCCESS_RATE_WARNING_MIN) return 'warning'
return 'critical'
}
const SUCCESS_RATE_TEXT_CLASS: Record<SuccessRateLevel, string> = {
excellent: 'text-success',
good: 'text-success/70',
warning: 'text-warning',
critical: 'text-destructive',
unknown: 'text-muted-foreground',
}
const SUCCESS_RATE_DOT_CLASS: Record<SuccessRateLevel, string> = {
excellent: 'bg-success',
good: 'bg-success/60',
warning: 'bg-warning',
critical: 'bg-destructive',
unknown: 'bg-muted-foreground',
}
// Hex colors for non-CSS contexts (e.g. chart libraries that need raw values).
const SUCCESS_RATE_HEX_COLOR: Record<SuccessRateLevel, string> = {
excellent: '#10b981', // emerald-500 (full green)
good: '#34d399', // emerald-400 (slightly lighter green)
warning: '#f59e0b', // amber-500
critical: '#ef4444', // red-500
unknown: '#9ca3af', // gray-400
}
export function getSuccessRateTextClass(rate: number): string {
return SUCCESS_RATE_TEXT_CLASS[getSuccessRateLevel(rate)]
}
export function getSuccessRateDotClass(rate: number): string {
return SUCCESS_RATE_DOT_CLASS[getSuccessRateLevel(rate)]
}
export function getSuccessRateColor(rate: number): string {
return SUCCESS_RATE_HEX_COLOR[getSuccessRateLevel(rate)]
}
......@@ -19,17 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
import { useMemo, useState } from 'react'
import {
ChevronRight,
ExternalLink,
Gauge,
KeyRound,
ScrollText,
ShieldCheck,
Sigma,
Zap,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import type { BundledLanguage } from 'shiki/bundle/web'
import { cn } from '@/lib/utils'
import { useStatus } from '@/hooks/use-status'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
......@@ -48,7 +45,6 @@ import {
type SupportedParameter,
} from '../lib/mock-stats'
import { replaceModelInPath } from '../lib/model-helpers'
import { inferApiInfo } from '../lib/model-metadata'
import type { PricingModel } from '../types'
// ---------------------------------------------------------------------------
......@@ -723,106 +719,6 @@ function RateLimitsSection(props: { model: PricingModel }) {
}
// ---------------------------------------------------------------------------
// Provider info card (vendor / tokenizer / license / privacy)
// ---------------------------------------------------------------------------
//
// Exported separately so the Overview tab can render it alongside capabilities
// and modalities (i.e. "what is this model?" rather than "how do I call it?").
export function ModelDetailsProviderInfo(props: { model: PricingModel }) {
const { t } = useTranslation()
const info = useMemo(() => inferApiInfo(props.model), [props.model])
return (
<section>
<SectionTitle icon={ShieldCheck}>
{t('Provider & data privacy')}
</SectionTitle>
<div className='border-border/60 bg-border/60 grid grid-cols-1 gap-px overflow-hidden rounded-lg border sm:grid-cols-2'>
<InfoCell label={t('Provider')}>
<div className='flex items-center gap-1.5'>
<span className='text-sm font-medium'>{info.vendor_label}</span>
{info.homepage && (
<a
href={info.homepage}
target='_blank'
rel='noopener noreferrer'
className='text-muted-foreground hover:text-foreground inline-flex items-center gap-0.5 text-[11px]'
>
{t('Docs')}
<ExternalLink className='size-3' />
</a>
)}
</div>
</InfoCell>
<InfoCell label={t('Tokenizer')}>
<div className='flex flex-col gap-0.5'>
<code className='font-mono text-xs'>{info.tokenizer}</code>
{info.tokenizer_note && (
<span className='text-muted-foreground text-[10px]'>
{info.tokenizer_note}
</span>
)}
</div>
</InfoCell>
<InfoCell label={t('License')}>
<div className='flex flex-col gap-1'>
<span className='text-sm'>{info.license}</span>
<Badge
variant='outline'
className={cn(
'h-4 w-fit px-1.5 text-[9px] font-medium',
info.license_kind === 'open' &&
'border-emerald-500/40 text-emerald-600 dark:text-emerald-400',
info.license_kind === 'open-weight' &&
'border-sky-500/40 text-sky-600 dark:text-sky-400',
info.license_kind === 'proprietary' &&
'border-amber-500/40 text-amber-600 dark:text-amber-400'
)}
>
{info.license_kind === 'open'
? t('Open source')
: info.license_kind === 'open-weight'
? t('Open weights')
: info.license_kind === 'proprietary'
? t('Proprietary')
: t('Unknown')}
</Badge>
</div>
</InfoCell>
<InfoCell label={t('Data retention')}>
<span className='text-sm'>
{info.data_retention_days === 0
? t('Zero retention')
: `${info.data_retention_days} ${t('days')}`}
</span>
<span className='text-muted-foreground text-[10px]'>
{info.training_opt_out
? t('Not used for upstream training by default')
: t('May be used for training by upstream provider')}
</span>
</InfoCell>
</div>
</section>
)
}
function InfoCell(props: { label: string; children: React.ReactNode }) {
return (
<div className='bg-card flex flex-col gap-1 px-3 py-2.5'>
<span className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
{props.label}
</span>
{props.children}
</div>
)
}
// ---------------------------------------------------------------------------
// Authentication preview
// ---------------------------------------------------------------------------
......
/*
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 {
BookOpenCheck,
Braces,
Code2,
Database,
FileCode,
Globe,
type LucideIcon,
PanelTopOpen,
ScanEye,
Settings2,
Sparkles,
Workflow,
Zap,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import type { ModelCapability } from '../types'
type CapabilityMeta = {
icon: LucideIcon
labelKey: string
descriptionKey: string
}
const CAPABILITY_META: Record<ModelCapability, CapabilityMeta> = {
function_calling: {
icon: Workflow,
labelKey: 'Function calling',
descriptionKey:
'Invoke developer-defined functions with structured arguments',
},
streaming: {
icon: Zap,
labelKey: 'Streaming',
descriptionKey: 'Stream tokens incrementally as they are generated',
},
vision: {
icon: ScanEye,
labelKey: 'Vision',
descriptionKey: 'Understand image inputs alongside text',
},
json_mode: {
icon: Braces,
labelKey: 'JSON mode',
descriptionKey: 'Force a syntactically valid JSON response',
},
structured_output: {
icon: FileCode,
labelKey: 'Structured output',
descriptionKey: 'Return data conforming to a JSON schema',
},
reasoning: {
icon: Sparkles,
labelKey: 'Reasoning',
descriptionKey: 'Multi-step thinking before final answer',
},
tools: {
icon: Settings2,
labelKey: 'Tools',
descriptionKey: 'Use external tools to extend capabilities',
},
system_prompt: {
icon: PanelTopOpen,
labelKey: 'System prompt',
descriptionKey: 'Steer behaviour with a system instruction',
},
web_search: {
icon: Globe,
labelKey: 'Web search',
descriptionKey: 'Search the public web at inference time',
},
code_interpreter: {
icon: Code2,
labelKey: 'Code interpreter',
descriptionKey: 'Execute code in a sandbox during the response',
},
caching: {
icon: Database,
labelKey: 'Prompt caching',
descriptionKey: 'Cache repeated prompt prefixes for cheaper, faster reuse',
},
embeddings: {
icon: BookOpenCheck,
labelKey: 'Embeddings',
descriptionKey: 'Return vector embeddings for inputs',
},
}
/**
* Order capabilities for display. We put the most user-facing capabilities
* first, then the rest. Anything not listed sinks to the bottom in a stable
* order so the layout looks tidy across models.
*/
const CAPABILITY_ORDER: ModelCapability[] = [
'streaming',
'function_calling',
'tools',
'json_mode',
'structured_output',
'vision',
'reasoning',
'caching',
'system_prompt',
'web_search',
'code_interpreter',
'embeddings',
]
function orderCapabilities(capabilities: ModelCapability[]): ModelCapability[] {
const set = new Set(capabilities)
const ordered = CAPABILITY_ORDER.filter((c) => set.has(c))
for (const c of capabilities) {
if (!ordered.includes(c)) ordered.push(c)
}
return ordered
}
export function ModelDetailsCapabilities(props: {
capabilities: ModelCapability[]
}) {
const { t } = useTranslation()
const ordered = orderCapabilities(props.capabilities)
if (ordered.length === 0) {
return (
<p className='text-muted-foreground text-sm'>
{t('No capabilities reported for this model.')}
</p>
)
}
return (
<div className='grid grid-cols-2 gap-2 @md/details:grid-cols-3 @2xl/details:grid-cols-4'>
{ordered.map((capability) => {
const meta = CAPABILITY_META[capability]
if (!meta) return null
const Icon = meta.icon
return (
<div
key={capability}
className={cn(
'group flex items-start gap-2 rounded-lg border p-3 transition-colors',
'hover:bg-muted/30'
)}
>
<span className='bg-muted text-foreground inline-flex size-7 shrink-0 items-center justify-center rounded-md transition-colors group-hover:bg-emerald-100 group-hover:text-emerald-700 dark:group-hover:bg-emerald-500/20 dark:group-hover:text-emerald-300'>
<Icon className='size-3.5' />
</span>
<div className='min-w-0 flex-1'>
<div className='text-foreground truncate text-xs font-semibold'>
{t(meta.labelKey)}
</div>
<p className='text-muted-foreground mt-0.5 line-clamp-2 text-[11px] leading-snug'>
{t(meta.descriptionKey)}
</p>
</div>
</div>
)
})}
</div>
)
}
......@@ -24,6 +24,7 @@ import { useChartTheme } from '@/lib/use-chart-theme'
import { cn } from '@/lib/utils'
import { VCHART_OPTION } from '@/lib/vchart'
import { useThemeCustomization } from '@/context/theme-customization-provider'
import { getSuccessRateColor } from '@/features/performance-metrics/lib/format'
import type { LatencyTimePoint, UptimeDayPoint } from '../lib/mock-stats'
function formatHourLabel(iso: string): string {
......@@ -229,11 +230,7 @@ export function UptimeTrendChart(props: {
size: 5,
stroke: '#ffffff',
lineWidth: 1.5,
fill: (datum: { uptime: number }) => {
if (datum.uptime >= 99.9) return '#10b981'
if (datum.uptime >= 99.0) return '#f59e0b'
return '#ef4444'
},
fill: (datum: { uptime: number }) => getSuccessRateColor(datum.uptime),
},
},
tooltip: {
......
/*
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 {
FileText,
Image as ImageIcon,
Mic2,
Type as TypeIcon,
Video,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { StaticDataTable } from '@/components/data-table'
import type { Modality } from '../types'
type IconComponent = React.ComponentType<{ className?: string }>
const MODALITY_META: Record<
Modality,
{ icon: IconComponent; labelKey: string }
> = {
text: { icon: TypeIcon, labelKey: 'Text' },
image: { icon: ImageIcon, labelKey: 'Image' },
audio: { icon: Mic2, labelKey: 'Audio' },
video: { icon: Video, labelKey: 'Video' },
file: { icon: FileText, labelKey: 'File' },
}
const ALL_MODALITIES: Modality[] = ['text', 'image', 'audio', 'video', 'file']
/** Inline modality icons (used by the quick-stats flow). */
export function ModalityIcons(props: {
modalities: Modality[]
className?: string
}) {
const { t } = useTranslation()
if (props.modalities.length === 0) {
return <span className='text-muted-foreground text-xs'></span>
}
return (
<span className='inline-flex items-center gap-1'>
{props.modalities.map((modality) => {
const meta = MODALITY_META[modality]
const Icon = meta.icon
return (
<Tooltip key={modality}>
<TooltipTrigger
render={
<span
aria-label={t(meta.labelKey)}
className='text-foreground/80 inline-flex'
/>
}
>
<Icon className={cn('size-3.5', props.className)} />
</TooltipTrigger>
<TooltipContent side='top' className='text-xs'>
{t(meta.labelKey)}
</TooltipContent>
</Tooltip>
)
})}
</span>
)
}
/**
* 2 × N matrix showing which modalities are supported as input vs output.
* Cells with a checkmark indicate support; empty cells show a dash.
*/
export function ModalitiesMatrix(props: {
input: Modality[]
output: Modality[]
}) {
const { t } = useTranslation()
const inputSet = new Set(props.input)
const outputSet = new Set(props.output)
return (
<StaticDataTable
className='rounded-lg'
tableClassName='text-sm'
headerRowClassName='bg-muted/40'
data={[
{ label: t('Input'), set: inputSet },
{ label: t('Output'), set: outputSet },
]}
getRowKey={(row) => row.label}
columns={[
{
id: 'modality',
header: t('Modality'),
className:
'text-muted-foreground px-3 py-2 text-left text-[11px] font-medium tracking-wider uppercase',
cellClassName:
'text-muted-foreground bg-muted/30 px-3 py-2 text-left text-[11px] font-medium tracking-wider uppercase',
cell: (row) => row.label,
},
...ALL_MODALITIES.map((modality) => ({
id: modality,
header: t(MODALITY_META[modality].labelKey),
className:
'text-muted-foreground border-l px-3 py-2 text-center text-[11px] font-medium tracking-wider uppercase',
cellClassName: (row: { label: string; set: Set<Modality> }) =>
cn(
'border-l px-3 py-2 text-center',
row.set.has(modality)
? 'bg-emerald-50/40 dark:bg-emerald-500/10'
: 'bg-background'
),
cell: (row: { label: string; set: Set<Modality> }) => {
const enabled = row.set.has(modality)
const Icon = MODALITY_META[modality].icon
return (
<span
className={cn(
'inline-flex items-center justify-center',
enabled
? 'text-emerald-700 dark:text-emerald-300'
: 'text-muted-foreground/40'
)}
aria-label={
enabled
? t('{{modality}} supported', {
modality: t(MODALITY_META[modality].labelKey),
})
: t('{{modality}} not supported', {
modality: t(MODALITY_META[modality].labelKey),
})
}
>
<Icon className='size-4' />
</span>
)
},
})),
]}
/>
)
}
......@@ -31,6 +31,7 @@ import {
formatLatency,
formatThroughput,
formatUptimePct,
getSuccessRateTextClass,
} from '@/features/performance-metrics/lib/format'
import type { PerformanceGroup } from '@/features/performance-metrics/types'
import { type UptimeDayPoint } from '../lib/mock-stats'
......@@ -43,10 +44,9 @@ function StatCard(props: {
label: string
value: React.ReactNode
hint?: string
intent?: 'default' | 'warning' | 'success'
valueClassName?: string
}) {
const Icon = props.icon
const intent = props.intent ?? 'default'
return (
<div className='bg-background flex flex-col gap-1 rounded-lg border p-3'>
<span className='text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider uppercase'>
......@@ -56,8 +56,7 @@ function StatCard(props: {
<span
className={cn(
'text-foreground font-mono text-lg font-semibold tabular-nums',
intent === 'warning' && 'text-amber-600 dark:text-amber-400',
intent === 'success' && 'text-emerald-600 dark:text-emerald-400'
props.valueClassName
)}
>
{props.value}
......@@ -217,12 +216,6 @@ export function ModelDetailsPerformance(props: { model: PricingModel }) {
successRates.length
: 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'
}
return (
<div className='flex flex-col gap-4'>
......@@ -249,7 +242,7 @@ export function ModelDetailsPerformance(props: { model: PricingModel }) {
})
: t('No incidents in the last 24 hours')
}
intent={intent}
valueClassName={getSuccessRateTextClass(successRate)}
/>
</div>
......
/*
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 {
CalendarClock,
FileText,
Layers,
Maximize2,
Sparkles,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import {
formatTokenCount,
formatYearMonth,
type ModelMetadata,
} from '../lib/model-metadata'
import type { Modality } from '../types'
import { ModalityIcons } from './model-details-modalities'
type QuickStatsProps = {
metadata: ModelMetadata
}
type Stat = {
key: string
icon: React.ComponentType<{ className?: string }>
label: string
value: React.ReactNode
hint?: string
}
function buildStats(
metadata: ModelMetadata,
t: (key: string) => string
): Stat[] {
const stats: Stat[] = [
{
key: 'context',
icon: Layers,
label: t('Context'),
value: formatTokenCount(metadata.context_length),
hint: t('Maximum input window'),
},
]
if (metadata.max_output_tokens > 0) {
stats.push({
key: 'max-output',
icon: Maximize2,
label: t('Max output'),
value: formatTokenCount(metadata.max_output_tokens),
hint: t('Maximum tokens per response'),
})
}
stats.push({
key: 'modalities',
icon: FileText,
label: t('Modalities'),
value: (
<ModalityFlow
input={metadata.input_modalities}
output={metadata.output_modalities}
/>
),
})
if (metadata.knowledge_cutoff) {
stats.push({
key: 'knowledge',
icon: Sparkles,
label: t('Knowledge cutoff'),
value: formatYearMonth(metadata.knowledge_cutoff),
})
}
if (metadata.release_date) {
stats.push({
key: 'release',
icon: CalendarClock,
label: t('Released'),
value: formatYearMonth(metadata.release_date),
})
}
return stats
}
function ModalityFlow(props: { input: Modality[]; output: Modality[] }) {
return (
<span className='inline-flex items-center gap-1 align-middle'>
<ModalityIcons modalities={props.input} className='size-3.5' />
<span className='text-muted-foreground/40'></span>
<ModalityIcons modalities={props.output} className='size-3.5' />
</span>
)
}
export function ModelDetailsQuickStats(props: QuickStatsProps) {
const { t } = useTranslation()
const stats = buildStats(props.metadata, t)
return (
<div className='bg-muted/20 grid grid-cols-2 gap-px overflow-hidden rounded-lg border @md/details:grid-cols-3 @2xl/details:grid-cols-5'>
{stats.map((stat) => {
const Icon = stat.icon
return (
<div
key={stat.key}
className={cn(
'bg-background flex min-w-0 flex-col gap-0.5 px-3 py-2.5'
)}
>
<span className='text-muted-foreground inline-flex min-w-0 items-center gap-1 text-[10px] font-medium tracking-wider uppercase'>
<Icon className='size-3 shrink-0' />
<span className='truncate'>{stat.label}</span>
</span>
<span className='text-foreground truncate text-sm font-semibold tabular-nums'>
{stat.value}
</span>
{stat.hint && (
<span className='text-muted-foreground/60 truncate text-[10px]'>
{stat.hint}
</span>
)}
</div>
)
})}
</div>
)
}
......@@ -25,7 +25,11 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { formatUptimePct } from '@/features/performance-metrics/lib/format'
import {
formatUptimePct,
getSuccessRateDotClass,
getSuccessRateTextClass,
} from '@/features/performance-metrics/lib/format'
import { aggregateUptime, type UptimeDayPoint } from '../lib/mock-stats'
// ---------------------------------------------------------------------------
......@@ -50,14 +54,6 @@ type UptimeSparklineProps = {
className?: string
}
function colourFor(uptime: number): string {
if (uptime >= 99.9) return 'bg-emerald-500'
if (uptime >= 99.0) return 'bg-emerald-400'
if (uptime >= 95.0) return 'bg-amber-500'
if (uptime >= 90.0) return 'bg-amber-600'
return 'bg-rose-500'
}
function heightFor(uptime: number): string {
if (uptime >= 99.9) return 'h-full'
if (uptime >= 99.0) return 'h-[88%]'
......@@ -66,13 +62,6 @@ function heightFor(uptime: number): string {
return 'h-[40%]'
}
function overallTextColour(pct: number): string {
if (pct >= 99.9) return 'text-emerald-600 dark:text-emerald-400'
if (pct >= 99.0) return 'text-emerald-600 dark:text-emerald-400'
if (pct >= 95.0) return 'text-amber-600 dark:text-amber-400'
return 'text-rose-600 dark:text-rose-400'
}
export function UptimeSparkline(props: UptimeSparklineProps) {
const size = props.size ?? 'md'
const showOverall = props.showOverall ?? true
......@@ -116,7 +105,7 @@ export function UptimeSparkline(props: UptimeSparklineProps) {
<div
className={cn(
'w-full rounded-sm',
colourFor(day.uptime_pct),
getSuccessRateDotClass(day.uptime_pct),
heightFor(day.uptime_pct)
)}
aria-hidden
......@@ -138,7 +127,7 @@ export function UptimeSparkline(props: UptimeSparklineProps) {
<span
className={cn(
'font-mono text-sm font-semibold tabular-nums',
overallTextColour(overall)
getSuccessRateTextClass(overall)
)}
>
{overall.toFixed(1)}%
......
......@@ -22,6 +22,7 @@ import { cn } from '@/lib/utils'
import {
formatLatency,
formatThroughput,
getSuccessRateDotClass,
} from '@/features/performance-metrics/lib/format'
export type ModelPerfBadgeData = {
......@@ -49,12 +50,7 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
const { avg_latency_ms, avg_tps, success_rate } = props.perf
let statusColor = 'bg-emerald-500'
if (success_rate < 99) {
statusColor = 'bg-red-500'
} else if (success_rate < 99.9) {
statusColor = 'bg-amber-500'
}
const statusColor = getSuccessRateDotClass(success_rate)
return (
<div
......
......@@ -25,6 +25,5 @@ export * from './price'
export * from './model-helpers'
export * from './billing-expr'
export * from './tier-expr'
export * from './model-metadata'
export * from './mock-stats'
export * from './seed'
......@@ -57,10 +57,8 @@ export type PricingModel = {
/** Pricing version returned by backend, useful for cache busting */
pricing_version?: string
/**
* Optional model metadata fields. These are not yet returned by the backend
* and are populated client-side from {@link inferModelMetadata}.
* When the backend ships these fields, the inference layer becomes a
* fallback rather than the source of truth.
* Optional model metadata fields reserved for backend-provided catalog data.
* Keep them data-driven; do not synthesize display values on the client.
*/
context_length?: number
max_output_tokens?: number
......
......@@ -16,12 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useEffect, useState } from 'react'
import { type FormEvent, useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { getCurrencyDisplay, getCurrencyLabel } from '@/lib/currency'
import { formatQuota, parseQuotaFromDollars } from '@/lib/format'
import { addTimeToDate } from '@/lib/time'
import { Button } from '@/components/ui/button'
import {
......@@ -135,6 +136,18 @@ export function RedemptionsMutateDrawer({
}
}
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
if (!isUpdate) {
const name = form.getValues('name')
if (!name?.trim()) {
const quota = parseQuotaFromDollars(form.getValues('quota_dollars'))
form.setValue('name', formatQuota(quota), { shouldValidate: true })
}
}
void form.handleSubmit(onSubmit)(event)
}
const handleSetExpiry = (months: number, days: number, hours: number) => {
const newDate = addTimeToDate(months, days, hours)
form.setValue('expired_time', newDate)
......@@ -177,7 +190,7 @@ export function RedemptionsMutateDrawer({
<Form {...form}>
<form
id='redemption-form'
onSubmit={form.handleSubmit(onSubmit)}
onSubmit={handleSubmit}
className={sideDrawerFormClassName()}
>
<SideDrawerSection>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment