feat(default): add real rankings data

This commit is contained in:
CaIon
2026-05-06 18:20:02 +08:00
parent 0f9f094a48
commit f8cf9c57c4
41 changed files with 1498 additions and 1912 deletions
+223 -134
View File
@@ -1,16 +1,10 @@
import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useNavigate, useParams, useSearch } from '@tanstack/react-router'
import {
ArrowLeft,
Boxes,
Code2,
HeartPulse,
Info,
ReceiptText,
Rocket,
} from 'lucide-react'
import { ArrowLeft, Code2, HeartPulse, Info, Timer } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { getLobeIcon } from '@/lib/lobe-icon'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
Sheet,
@@ -32,6 +26,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { CopyButton } from '@/components/copy-button'
import { GroupBadge } from '@/components/group-badge'
import { PublicLayout } from '@/components/layout'
import { getPerfMetrics } from '../api'
import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants'
import { usePricingData } from '../hooks/use-pricing-data'
import {
@@ -42,18 +37,23 @@ import {
} from '../lib/dynamic-price'
import { parseTags } from '../lib/filters'
import {
getAvailableGroups,
isTokenBasedModel,
replaceModelInPath,
} from '../lib/model-helpers'
formatLatency,
formatThroughput,
formatUptimePct,
} from '../lib/mock-stats'
import { getAvailableGroups, isTokenBasedModel } from '../lib/model-helpers'
import { inferModelMetadata } from '../lib/model-metadata'
import { formatFixedPrice, formatGroupPrice } from '../lib/price'
import type { PriceType, PricingModel, TokenUnit } from '../types'
import type {
Modality,
ModelCapability,
PriceType,
PricingModel,
TokenUnit,
} from '../types'
import { DynamicPricingBreakdown } from './dynamic-pricing-breakdown'
import { ModelDetailsApi, ModelDetailsProviderInfo } from './model-details-api'
import { ModelDetailsApps } from './model-details-apps'
import { ModelDetailsCapabilities } from './model-details-capabilities'
import { ModalitiesMatrix } from './model-details-modalities'
import { ModalityIcons } from './model-details-modalities'
import { ModelDetailsPerformance } from './model-details-performance'
import { ModelDetailsQuickStats } from './model-details-quick-stats'
@@ -69,8 +69,181 @@ function SectionTitle(props: { children: React.ReactNode }) {
)
}
const CAPABILITY_LABEL_KEYS: Record<ModelCapability, string> = {
function_calling: 'Function calling',
streaming: 'Streaming',
vision: 'Vision',
json_mode: 'JSON mode',
structured_output: 'Structured output',
reasoning: 'Reasoning',
tools: 'Tools',
system_prompt: 'System prompt',
web_search: 'Web search',
code_interpreter: 'Code interpreter',
caching: 'Prompt caching',
embeddings: 'Embeddings',
}
function CompactCapabilityList(props: { capabilities: ModelCapability[] }) {
const { t } = useTranslation()
if (props.capabilities.length === 0) {
return (
<span className='text-muted-foreground text-xs'>
{t('No capabilities reported for this model.')}
</span>
)
}
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>
)
}
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 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 OverviewMetric(props: {
icon: React.ComponentType<{ className?: string }>
label: string
value: React.ReactNode
intent?: 'default' | 'warning' | 'success'
}) {
const Icon = props.icon
const intent = props.intent ?? 'default'
return (
<div className='flex min-w-0 items-center gap-2 px-3 py-2'>
<Icon className='text-muted-foreground/70 size-3.5 shrink-0' />
<div className='min-w-0 flex-1'>
<div className='text-muted-foreground truncate text-[10px] font-medium tracking-wider uppercase'>
{props.label}
</div>
<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.value}
</div>
</div>
</div>
)
}
function OverviewSummaryGrid(props: { model: PricingModel }) {
const { t } = useTranslation()
const metricsQuery = useQuery({
queryKey: ['perf-metrics', props.model.model_name],
queryFn: () => getPerfMetrics(props.model.model_name, 24),
staleTime: 60 * 1000,
})
const groups = metricsQuery.data?.data.groups ?? []
const successRates = groups
.map((group) => group.success_rate)
.filter((rate) => Number.isFinite(rate))
const successRate =
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)
const avgTps =
tpsValues.length > 0
? tpsValues.reduce((sum, value) => sum + value, 0) / tpsValues.length
: 0
const latencyValues = groups
.map((group) => group.avg_latency_ms)
.filter((value) => value > 0)
const avgLatency =
latencyValues.length > 0
? Math.round(
latencyValues.reduce((sum, value) => sum + value, 0) /
latencyValues.length
)
: 0
return (
<div className='bg-muted/20 grid overflow-hidden rounded-lg border sm:grid-cols-3 sm:divide-x'>
<OverviewMetric
icon={Timer}
label='TPS'
value={formatThroughput(avgTps)}
/>
<OverviewMetric
icon={Timer}
label={t('Average latency')}
value={formatLatency(avgLatency)}
/>
<OverviewMetric
icon={HeartPulse}
label={t('Success rate')}
value={formatUptimePct(successRate)}
intent={successIntent}
/>
</div>
)
}
// ----------------------------------------------------------------------------
// Model header (always visible above the tabs)
// Model header (always visible above the detail sections)
// ----------------------------------------------------------------------------
function ModelHeader(props: { model: PricingModel }) {
@@ -362,55 +535,6 @@ function PriceSection(props: {
)
}
// ----------------------------------------------------------------------------
// API endpoints list
// ----------------------------------------------------------------------------
function EndpointsSection(props: {
model: PricingModel
endpointMap: Record<string, { path?: string; method?: string }>
}) {
const { t } = useTranslation()
const endpoints = useMemo(() => {
const types = props.model.supported_endpoint_types || []
return types.map((type) => {
const info = props.endpointMap[type] || {}
let path = info.path || ''
if (path.includes('{model}')) {
path = replaceModelInPath(path, props.model.model_name || '')
}
return { type, path, method: info.method || 'POST' }
})
}, [props.model, props.endpointMap])
if (endpoints.length === 0) return null
return (
<section>
<SectionTitle>{t('API Endpoints')}</SectionTitle>
<div className='space-y-1'>
{endpoints.map(({ type, path, method }) => (
<div key={type} className='flex items-center justify-between py-1'>
<div className='flex items-center gap-2'>
<span className='text-sm font-medium'>{type}</span>
{path && (
<code className='text-muted-foreground/60 text-xs break-all'>
{path}
</code>
)}
</div>
{path && (
<span className='bg-muted text-muted-foreground rounded px-1.5 py-0.5 font-mono text-[10px] font-medium uppercase'>
{method}
</span>
)}
</div>
))}
</div>
</section>
)
}
// ----------------------------------------------------------------------------
// Auto group chain (used inside group pricing section)
// ----------------------------------------------------------------------------
@@ -740,17 +864,7 @@ function GroupPricingSection(props: {
)
}
// ----------------------------------------------------------------------------
// Tabbed details content
// ----------------------------------------------------------------------------
const TAB_VALUES = [
'overview',
'pricing',
'performance',
'api',
'apps',
] as const
const TAB_VALUES = ['overview', 'performance', 'api'] as const
type TabValue = (typeof TAB_VALUES)[number]
const TAB_META: Record<
@@ -758,10 +872,8 @@ const TAB_META: Record<
{ icon: React.ComponentType<{ className?: string }>; labelKey: string }
> = {
overview: { icon: Info, labelKey: 'Overview' },
pricing: { icon: ReceiptText, labelKey: 'Pricing' },
performance: { icon: HeartPulse, labelKey: 'Performance' },
api: { icon: Code2, labelKey: 'API' },
apps: { icon: Rocket, labelKey: 'Apps' },
}
export interface ModelDetailsContentProps {
@@ -789,8 +901,6 @@ export function ModelDetailsContent(props: ModelDetailsContentProps) {
<div className='@container/details space-y-4'>
<ModelHeader model={props.model} />
<ModelDetailsQuickStats metadata={metadata} />
<Tabs defaultValue='overview' className='gap-4'>
<TabsList className='bg-muted/60 h-auto w-full justify-start gap-1 overflow-x-auto rounded-lg p-1'>
{TAB_VALUES.map((value) => {
@@ -808,59 +918,42 @@ export function ModelDetailsContent(props: ModelDetailsContentProps) {
})}
</TabsList>
<TabsContent value='overview' className='space-y-5 outline-none'>
<section>
<div className='mb-3 flex items-center gap-2'>
<Boxes className='text-muted-foreground/70 size-3.5' />
<h3 className='text-foreground text-sm font-semibold'>
{t('Capabilities')}
</h3>
</div>
<ModelDetailsCapabilities capabilities={metadata.capabilities} />
</section>
<TabsContent value='overview' className='space-y-6 outline-none'>
<OverviewSummaryGrid model={props.model} />
<section>
<div className='mb-3 flex items-center gap-2'>
<h3 className='text-foreground text-sm font-semibold'>
{t('Supported modalities')}
</h3>
</div>
<ModalitiesMatrix
input={metadata.input_modalities}
output={metadata.output_modalities}
<section className='bg-card/60 space-y-5 rounded-xl border p-4 shadow-sm'>
<SectionTitle>{t('Pricing')}</SectionTitle>
<PriceSection
model={props.model}
priceRate={props.priceRate}
usdExchangeRate={props.usdExchangeRate}
tokenUnit={props.tokenUnit}
showRechargePrice={showRechargePrice}
/>
{isDynamic && (
<DynamicPricingBreakdown billingExpr={props.model.billing_expr} />
)}
<GroupPricingSection
model={props.model}
groupRatio={props.groupRatio}
usableGroup={props.usableGroup}
autoGroups={props.autoGroups}
priceRate={props.priceRate}
usdExchangeRate={props.usdExchangeRate}
tokenUnit={props.tokenUnit}
showRechargePrice={showRechargePrice}
/>
</section>
<ModelDetailsQuickStats metadata={metadata} />
<ModelSignalsSection
capabilities={metadata.capabilities}
input={metadata.input_modalities}
output={metadata.output_modalities}
/>
<ModelDetailsProviderInfo model={props.model} />
<PriceSection
model={props.model}
priceRate={props.priceRate}
usdExchangeRate={props.usdExchangeRate}
tokenUnit={props.tokenUnit}
showRechargePrice={showRechargePrice}
/>
<EndpointsSection
model={props.model}
endpointMap={props.endpointMap}
/>
</TabsContent>
<TabsContent value='pricing' className='space-y-5 outline-none'>
{isDynamic && (
<DynamicPricingBreakdown billingExpr={props.model.billing_expr} />
)}
<GroupPricingSection
model={props.model}
groupRatio={props.groupRatio}
usableGroup={props.usableGroup}
autoGroups={props.autoGroups}
priceRate={props.priceRate}
usdExchangeRate={props.usdExchangeRate}
tokenUnit={props.tokenUnit}
showRechargePrice={showRechargePrice}
/>
</TabsContent>
<TabsContent value='performance' className='outline-none'>
@@ -873,10 +966,6 @@ export function ModelDetailsContent(props: ModelDetailsContentProps) {
endpointMap={props.endpointMap}
/>
</TabsContent>
<TabsContent value='apps' className='outline-none'>
<ModelDetailsApps model={props.model} />
</TabsContent>
</Tabs>
</div>
)