feat(performance): implement success rate grading and enhance UI representation
This commit is contained in:
@@ -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'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -722,106 +718,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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
/*
|
||||
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: {
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
/*
|
||||
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>
|
||||
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
/*
|
||||
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>
|
||||
)
|
||||
}
|
||||
+7
-18
@@ -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)}%
|
||||
|
||||
+341
-98
@@ -19,7 +19,18 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { useMemo } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate, useParams, useSearch } from '@tanstack/react-router'
|
||||
import { ArrowLeft, Code2, HeartPulse, Info, Timer } from 'lucide-react'
|
||||
import {
|
||||
ArrowLeft,
|
||||
CalendarClock,
|
||||
Code2,
|
||||
FileText,
|
||||
HeartPulse,
|
||||
Info,
|
||||
Layers,
|
||||
Maximize2,
|
||||
Sparkles,
|
||||
Timer,
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getLobeIcon } from '@/lib/lobe-icon'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -43,6 +54,7 @@ import {
|
||||
formatLatency,
|
||||
formatThroughput,
|
||||
formatUptimePct,
|
||||
getSuccessRateTextClass,
|
||||
} from '@/features/performance-metrics/lib/format'
|
||||
import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants'
|
||||
import { usePricingData } from '../hooks/use-pricing-data'
|
||||
@@ -54,20 +66,16 @@ import {
|
||||
} from '../lib/dynamic-price'
|
||||
import { parseTags } from '../lib/filters'
|
||||
import { getAvailableGroups, isTokenBasedModel } from '../lib/model-helpers'
|
||||
import { inferModelMetadata } from '../lib/model-metadata'
|
||||
import { formatFixedPrice, formatGroupPrice } from '../lib/price'
|
||||
import type {
|
||||
Modality,
|
||||
ModelCapability,
|
||||
PriceType,
|
||||
PricingModel,
|
||||
TokenUnit,
|
||||
} from '../types'
|
||||
import { DynamicPricingBreakdown } from './dynamic-pricing-breakdown'
|
||||
import { ModelDetailsApi, ModelDetailsProviderInfo } from './model-details-api'
|
||||
import { ModalityIcons } from './model-details-modalities'
|
||||
import { ModelDetailsApi } from './model-details-api'
|
||||
import { ModelDetailsPerformance } from './model-details-performance'
|
||||
import { ModelDetailsQuickStats } from './model-details-quick-stats'
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Local UI helpers
|
||||
@@ -96,80 +104,51 @@ const CAPABILITY_LABEL_KEYS: Record<ModelCapability, string> = {
|
||||
embeddings: 'Embeddings',
|
||||
}
|
||||
|
||||
function CompactCapabilityList(props: { capabilities: ModelCapability[] }) {
|
||||
const { t } = useTranslation()
|
||||
const MODALITY_LABEL_KEYS: Record<string, string> = {
|
||||
text: 'Text',
|
||||
image: 'Image',
|
||||
audio: 'Audio',
|
||||
video: 'Video',
|
||||
file: 'File',
|
||||
}
|
||||
|
||||
if (props.capabilities.length === 0) {
|
||||
return (
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
{t('No capabilities reported for this model.')}
|
||||
</span>
|
||||
)
|
||||
const TOKEN_FORMAT = new Intl.NumberFormat(undefined, {
|
||||
maximumFractionDigits: 1,
|
||||
})
|
||||
|
||||
function formatCatalogTokenCount(tokens: number): string {
|
||||
if (!Number.isFinite(tokens) || tokens <= 0) return ''
|
||||
if (tokens >= 1_000_000) {
|
||||
return `${TOKEN_FORMAT.format(tokens / 1_000_000)}M`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap gap-1.5'>
|
||||
{props.capabilities.map((capability) => (
|
||||
<span
|
||||
key={capability}
|
||||
className='bg-muted text-muted-foreground rounded-md px-2 py-1 text-xs font-medium'
|
||||
>
|
||||
{t(CAPABILITY_LABEL_KEYS[capability] ?? capability)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
if (tokens >= 1_000) {
|
||||
return `${TOKEN_FORMAT.format(tokens / 1_000)}K`
|
||||
}
|
||||
return TOKEN_FORMAT.format(tokens)
|
||||
}
|
||||
|
||||
function CompactModalities(props: { input: Modality[]; output: Modality[] }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='grid gap-2 sm:grid-cols-2'>
|
||||
<div className='flex items-center justify-between gap-3 rounded-lg border px-3 py-2'>
|
||||
<span className='text-muted-foreground text-xs font-medium'>
|
||||
{t('Input')}
|
||||
</span>
|
||||
<ModalityIcons modalities={props.input} />
|
||||
</div>
|
||||
<div className='flex items-center justify-between gap-3 rounded-lg border px-3 py-2'>
|
||||
<span className='text-muted-foreground text-xs font-medium'>
|
||||
{t('Output')}
|
||||
</span>
|
||||
<ModalityIcons modalities={props.output} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
function formatCatalogYearMonth(value?: string): string {
|
||||
if (!value) return ''
|
||||
const [yearStr, monthStr] = value.split('-')
|
||||
const year = Number(yearStr)
|
||||
const month = Number(monthStr)
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month)) return value
|
||||
const date = new Date(Date.UTC(year, month - 1, 1))
|
||||
return date.toLocaleString(undefined, { year: 'numeric', month: 'short' })
|
||||
}
|
||||
|
||||
function ModelSignalsSection(props: {
|
||||
capabilities: ModelCapability[]
|
||||
input: Modality[]
|
||||
output: Modality[]
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle>
|
||||
{t('Capabilities')} / {t('Supported modalities')}
|
||||
</SectionTitle>
|
||||
<div className='grid gap-3 rounded-xl border p-3 @2xl/details:grid-cols-[minmax(0,1.5fr)_minmax(260px,1fr)]'>
|
||||
<CompactCapabilityList capabilities={props.capabilities} />
|
||||
<CompactModalities input={props.input} output={props.output} />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
function normalizeCatalogItems(items?: readonly string[]): string[] {
|
||||
if (!items) return []
|
||||
return items.filter((item) => item.trim().length > 0)
|
||||
}
|
||||
|
||||
function OverviewMetric(props: {
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
label: string
|
||||
value: React.ReactNode
|
||||
intent?: 'default' | 'warning' | 'success'
|
||||
valueClassName?: string
|
||||
}) {
|
||||
const Icon = props.icon
|
||||
const intent = props.intent ?? 'default'
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 items-center gap-2 px-3 py-2'>
|
||||
@@ -181,8 +160,7 @@ function OverviewMetric(props: {
|
||||
<div
|
||||
className={cn(
|
||||
'text-foreground truncate font-mono text-sm 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}
|
||||
@@ -208,12 +186,6 @@ function OverviewSummaryGrid(props: { model: PricingModel }) {
|
||||
successRates.length > 0
|
||||
? successRates.reduce((sum, rate) => sum + rate, 0) / successRates.length
|
||||
: Number.NaN
|
||||
let successIntent: 'default' | 'warning' | 'success' = 'warning'
|
||||
if (successRate >= 99.9) {
|
||||
successIntent = 'success'
|
||||
} else if (successRate >= 99) {
|
||||
successIntent = 'default'
|
||||
}
|
||||
const tpsValues = groups
|
||||
.map((group) => group.avg_tps)
|
||||
.filter((value) => value > 0)
|
||||
@@ -248,12 +220,305 @@ function OverviewSummaryGrid(props: { model: PricingModel }) {
|
||||
icon={HeartPulse}
|
||||
label={t('Success rate')}
|
||||
value={formatUptimePct(successRate)}
|
||||
intent={successIntent}
|
||||
valueClassName={getSuccessRateTextClass(successRate)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CatalogPillList(props: { items: string[] }) {
|
||||
return (
|
||||
<div className='flex min-w-0 flex-wrap gap-1.5'>
|
||||
{props.items.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
className='bg-muted text-muted-foreground rounded-md px-2 py-1 text-xs font-medium'
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CatalogTextValue(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<span className='text-foreground min-w-0 truncate text-sm font-semibold'>
|
||||
{props.children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function CatalogInfoCell(props: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className='bg-card flex min-w-0 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>
|
||||
)
|
||||
}
|
||||
|
||||
function ModalityLabels(props: { items: string[] }) {
|
||||
const { t } = useTranslation()
|
||||
if (props.items.length === 0) return null
|
||||
|
||||
return (
|
||||
<span className='inline-flex items-center gap-1 align-middle'>
|
||||
{props.items.map((item) => (
|
||||
<span key={item} className='font-medium'>
|
||||
{t(MODALITY_LABEL_KEYS[item] ?? item)}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelBackendQuickStats(props: { model: PricingModel }) {
|
||||
const { t } = useTranslation()
|
||||
const model = props.model
|
||||
const inputModalities = normalizeCatalogItems(model.input_modalities)
|
||||
const outputModalities = normalizeCatalogItems(model.output_modalities)
|
||||
const contextLength = model.context_length ?? 0
|
||||
const maxOutput = model.max_output_tokens ?? 0
|
||||
const knowledgeCutoff = formatCatalogYearMonth(model.knowledge_cutoff)
|
||||
const releaseDate = formatCatalogYearMonth(model.release_date)
|
||||
|
||||
const stats: {
|
||||
key: string
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
label: string
|
||||
value: React.ReactNode
|
||||
hint?: string
|
||||
}[] = []
|
||||
|
||||
if (contextLength > 0) {
|
||||
stats.push({
|
||||
key: 'context',
|
||||
icon: Layers,
|
||||
label: t('Context'),
|
||||
value: formatCatalogTokenCount(contextLength),
|
||||
hint: t('Maximum input window'),
|
||||
})
|
||||
}
|
||||
|
||||
if (maxOutput > 0) {
|
||||
stats.push({
|
||||
key: 'max-output',
|
||||
icon: Maximize2,
|
||||
label: t('Max output'),
|
||||
value: formatCatalogTokenCount(maxOutput),
|
||||
hint: t('Maximum tokens per response'),
|
||||
})
|
||||
}
|
||||
|
||||
if (inputModalities.length > 0 || outputModalities.length > 0) {
|
||||
stats.push({
|
||||
key: 'modalities',
|
||||
icon: FileText,
|
||||
label: t('Modalities'),
|
||||
value: (
|
||||
<span className='inline-flex items-center gap-1'>
|
||||
<ModalityLabels items={inputModalities} />
|
||||
{inputModalities.length > 0 && outputModalities.length > 0 && (
|
||||
<span className='text-muted-foreground/40'>→</span>
|
||||
)}
|
||||
<ModalityLabels items={outputModalities} />
|
||||
</span>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
if (knowledgeCutoff) {
|
||||
stats.push({
|
||||
key: 'knowledge',
|
||||
icon: Sparkles,
|
||||
label: t('Knowledge cutoff'),
|
||||
value: knowledgeCutoff,
|
||||
})
|
||||
}
|
||||
|
||||
if (releaseDate) {
|
||||
stats.push({
|
||||
key: 'release',
|
||||
icon: CalendarClock,
|
||||
label: t('Released'),
|
||||
value: releaseDate,
|
||||
})
|
||||
}
|
||||
|
||||
if (stats.length === 0) return null
|
||||
|
||||
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='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>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelBackendSignalsSection(props: { model: PricingModel }) {
|
||||
const { t } = useTranslation()
|
||||
const capabilities = normalizeCatalogItems(props.model.capabilities)
|
||||
const inputModalities = normalizeCatalogItems(props.model.input_modalities)
|
||||
const outputModalities = normalizeCatalogItems(props.model.output_modalities)
|
||||
|
||||
if (
|
||||
capabilities.length === 0 &&
|
||||
inputModalities.length === 0 &&
|
||||
outputModalities.length === 0
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle>
|
||||
{t('Capabilities')} / {t('Supported modalities')}
|
||||
</SectionTitle>
|
||||
<div className='grid gap-3 rounded-xl border p-3 @2xl/details:grid-cols-[minmax(0,1.5fr)_minmax(260px,1fr)]'>
|
||||
{capabilities.length > 0 ? (
|
||||
<CatalogPillList
|
||||
items={capabilities.map((capability) =>
|
||||
t(
|
||||
CAPABILITY_LABEL_KEYS[capability as ModelCapability] ??
|
||||
capability
|
||||
)
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
{(inputModalities.length > 0 || outputModalities.length > 0) && (
|
||||
<div className='grid gap-2 sm:grid-cols-2'>
|
||||
{inputModalities.length > 0 && (
|
||||
<div className='flex items-center justify-between gap-3 rounded-lg border px-3 py-2'>
|
||||
<span className='text-muted-foreground text-xs font-medium'>
|
||||
{t('Input')}
|
||||
</span>
|
||||
<CatalogTextValue>
|
||||
<ModalityLabels items={inputModalities} />
|
||||
</CatalogTextValue>
|
||||
</div>
|
||||
)}
|
||||
{outputModalities.length > 0 && (
|
||||
<div className='flex items-center justify-between gap-3 rounded-lg border px-3 py-2'>
|
||||
<span className='text-muted-foreground text-xs font-medium'>
|
||||
{t('Output')}
|
||||
</span>
|
||||
<CatalogTextValue>
|
||||
<ModalityLabels items={outputModalities} />
|
||||
</CatalogTextValue>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelBackendProviderSection(props: { model: PricingModel }) {
|
||||
const { t } = useTranslation()
|
||||
const model = props.model
|
||||
const groups = normalizeCatalogItems(model.enable_groups)
|
||||
const endpoints = normalizeCatalogItems(model.supported_endpoint_types)
|
||||
const tags = parseTags(model.tags)
|
||||
const cells: React.ReactNode[] = []
|
||||
|
||||
if (model.vendor_name) {
|
||||
cells.push(
|
||||
<CatalogInfoCell key='provider' label={t('Provider')}>
|
||||
<CatalogTextValue>{model.vendor_name}</CatalogTextValue>
|
||||
</CatalogInfoCell>
|
||||
)
|
||||
}
|
||||
|
||||
cells.push(
|
||||
<CatalogInfoCell key='type' label={t('Type')}>
|
||||
<CatalogTextValue>
|
||||
{model.quota_type === QUOTA_TYPE_VALUES.TOKEN
|
||||
? t('Token-based')
|
||||
: t('Per Request')}
|
||||
</CatalogTextValue>
|
||||
</CatalogInfoCell>
|
||||
)
|
||||
|
||||
if (groups.length > 0) {
|
||||
cells.push(
|
||||
<CatalogInfoCell key='groups' label={t('Groups')}>
|
||||
<CatalogPillList items={groups} />
|
||||
</CatalogInfoCell>
|
||||
)
|
||||
}
|
||||
|
||||
if (endpoints.length > 0) {
|
||||
cells.push(
|
||||
<CatalogInfoCell key='endpoints' label={t('Endpoints')}>
|
||||
<CatalogPillList items={endpoints} />
|
||||
</CatalogInfoCell>
|
||||
)
|
||||
}
|
||||
|
||||
if (tags.length > 0) {
|
||||
cells.push(
|
||||
<CatalogInfoCell key='tags' label={t('Tags')}>
|
||||
<CatalogPillList items={tags} />
|
||||
</CatalogInfoCell>
|
||||
)
|
||||
}
|
||||
|
||||
if (model.parameter_count) {
|
||||
cells.push(
|
||||
<CatalogInfoCell key='parameters' label={t('Parameters')}>
|
||||
<CatalogTextValue>{model.parameter_count}</CatalogTextValue>
|
||||
</CatalogInfoCell>
|
||||
)
|
||||
}
|
||||
|
||||
if (cells.length === 0) return null
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle>{t('Model')}</SectionTitle>
|
||||
<div className='border-border/60 bg-border/60 grid grid-cols-1 gap-px overflow-hidden rounded-lg border sm:grid-cols-2'>
|
||||
{cells}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelBackendDetailsSection(props: { model: PricingModel }) {
|
||||
return (
|
||||
<>
|
||||
<ModelBackendQuickStats model={props.model} />
|
||||
<ModelBackendSignalsSection model={props.model} />
|
||||
<ModelBackendProviderSection model={props.model} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Model header (always visible above the detail sections)
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -264,7 +529,6 @@ function ModelHeader(props: { model: PricingModel }) {
|
||||
const modelIconKey = model.icon || model.vendor_icon
|
||||
const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 20) : null
|
||||
const description = model.description || model.vendor_description || null
|
||||
const tags = parseTags(model.tags)
|
||||
const isSpecialExpression =
|
||||
model.billing_mode === 'tiered_expr' &&
|
||||
Boolean(model.billing_expr) &&
|
||||
@@ -312,18 +576,6 @@ function ModelHeader(props: { model: PricingModel }) {
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{tags.length > 0 && (
|
||||
<div className='mt-2.5 flex flex-wrap gap-1'>
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className='bg-muted text-muted-foreground rounded px-2 py-0.5 text-[11px] font-medium'
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -901,7 +1153,6 @@ export interface ModelDetailsContentProps {
|
||||
export function ModelDetailsContent(props: ModelDetailsContentProps) {
|
||||
const { t } = useTranslation()
|
||||
const showRechargePrice = props.showRechargePrice ?? false
|
||||
const metadata = useMemo(() => inferModelMetadata(props.model), [props.model])
|
||||
|
||||
const isDynamic =
|
||||
props.model.billing_mode === 'tiered_expr' &&
|
||||
@@ -955,15 +1206,7 @@ export function ModelDetailsContent(props: ModelDetailsContentProps) {
|
||||
/>
|
||||
</section>
|
||||
|
||||
<ModelDetailsQuickStats metadata={metadata} />
|
||||
|
||||
<ModelSignalsSection
|
||||
capabilities={metadata.capabilities}
|
||||
input={metadata.input_modalities}
|
||||
output={metadata.output_modalities}
|
||||
/>
|
||||
|
||||
<ModelDetailsProviderInfo model={props.model} />
|
||||
<ModelBackendDetailsSection model={props.model} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='performance' className='outline-none'>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user