feat(default): add real rankings data
This commit is contained in:
+223
-134
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user