♻️ refactor(web): refine data-table cards and pricing page layout

Replace collapsible card details with always-visible label/value rows, add badge-list full display for cards, and polish pricing/channel toolbars and mobile cards.
This commit is contained in:
t0ng7u
2026-07-11 06:02:16 +08:00
parent 0918bdb49a
commit 9d1ca545e2
68 changed files with 1971 additions and 1840 deletions
+254 -296
View File
@@ -28,7 +28,6 @@ import {
Layers,
Maximize2,
Sparkles,
Timer,
} from 'lucide-react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -42,16 +41,10 @@ import {
TabsList,
TabsTrigger,
} from '@/components/design-system/tabs'
import { sideDrawerContentClassName } from '@/components/drawer-layout'
import { GroupBadge } from '@/components/group-badge'
import { PublicLayout } from '@/components/layout'
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import { PageTransition } from '@/components/page-transition'
import { StatusBadge } from '@/components/status-badge'
import { Skeleton } from '@/components/ui/skeleton'
import { getPerfMetrics } from '@/features/performance-metrics/api'
import {
@@ -88,11 +81,19 @@ import { ModelDetailsPerformance } from './model-details-performance'
// Local UI helpers
// ----------------------------------------------------------------------------
function SectionTitle(props: { children: React.ReactNode }) {
function SectionTitle(props: {
children: React.ReactNode
description?: string
}) {
return (
<h2 className='text-muted-foreground mb-3 text-xs font-semibold tracking-wider uppercase'>
{props.children}
</h2>
<div className='mb-4'>
<h2 className='text-base font-semibold'>{props.children}</h2>
{props.description && (
<p className='text-muted-foreground mt-1 text-sm'>
{props.description}
</p>
)}
</div>
)
}
@@ -150,28 +151,22 @@ function normalizeCatalogItems(items?: readonly string[]): string[] {
}
function OverviewMetric(props: {
icon: React.ComponentType<{ className?: string }>
label: string
value: React.ReactNode
valueClassName?: string
}) {
const Icon = props.icon
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-xs font-medium tracking-wider uppercase'>
{props.label}
</div>
<div
className={cn(
'text-foreground truncate text-sm font-semibold tabular-nums',
props.valueClassName
)}
>
{props.value}
</div>
<div className='min-w-0 px-4 py-3'>
<div className='text-muted-foreground text-xs font-medium'>
{props.label}
</div>
<div
className={cn(
'text-foreground mt-1 truncate text-sm font-semibold tabular-nums',
props.valueClassName
)}
>
{props.value}
</div>
</div>
)
@@ -212,19 +207,13 @@ function OverviewSummaryGrid(props: { model: PricingModel }) {
: 0
return (
<div className='bg-muted/20 grid overflow-hidden rounded-lg border sm:grid-cols-3 sm:divide-x'>
<div className='divide-border grid divide-y overflow-hidden rounded-xl border sm:grid-cols-3 sm:divide-x sm:divide-y-0'>
<OverviewMetric label='TPS' value={formatThroughput(avgTps)} />
<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)}
valueClassName={getSuccessRateTextClass(successRate)}
@@ -237,12 +226,9 @@ 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'
>
<StatusBadge key={item} variant='neutral' size='md'>
{item}
</span>
</StatusBadge>
))}
</div>
)
@@ -250,7 +236,7 @@ function CatalogPillList(props: { items: string[] }) {
function CatalogTextValue(props: { children: React.ReactNode }) {
return (
<span className='text-foreground min-w-0 truncate text-sm font-semibold'>
<span className='text-foreground min-w-0 truncate text-sm font-medium'>
{props.children}
</span>
)
@@ -258,8 +244,8 @@ function CatalogTextValue(props: { children: React.ReactNode }) {
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-xs font-medium tracking-wider uppercase'>
<div className='flex min-w-0 flex-col gap-1.5 px-4 py-3'>
<span className='text-muted-foreground text-xs font-medium'>
{props.label}
</span>
{props.children}
@@ -358,23 +344,20 @@ function ModelBackendQuickStats(props: { model: PricingModel }) {
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'>
<div className='divide-border grid grid-cols-2 divide-x divide-y overflow-hidden rounded-xl 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-xs font-medium tracking-wider uppercase'>
<Icon className='size-3 shrink-0' />
<div key={stat.key} className='flex min-w-0 flex-col gap-1 px-4 py-3'>
<span className='text-muted-foreground inline-flex min-w-0 items-center gap-1.5 text-xs font-medium'>
<Icon className='size-3.5 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-xs'>
<span className='text-muted-foreground truncate text-xs'>
{stat.hint}
</span>
)}
@@ -401,11 +384,9 @@ function ModelBackendSignalsSection(props: { model: PricingModel }) {
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 ? (
<SectionTitle>{t('Capabilities')}</SectionTitle>
<div className='space-y-4 rounded-xl border p-4'>
{capabilities.length > 0 && (
<CatalogPillList
items={capabilities.map((capability) =>
t(
@@ -414,13 +395,11 @@ function ModelBackendSignalsSection(props: { model: PricingModel }) {
)
)}
/>
) : (
<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'>
<div className='bg-muted/20 flex items-center justify-between gap-3 rounded-lg px-3 py-2.5'>
<span className='text-muted-foreground text-xs font-medium'>
{t('Input')}
</span>
@@ -430,7 +409,7 @@ function ModelBackendSignalsSection(props: { model: PricingModel }) {
</div>
)}
{outputModalities.length > 0 && (
<div className='flex items-center justify-between gap-3 rounded-lg border px-3 py-2'>
<div className='bg-muted/20 flex items-center justify-between gap-3 rounded-lg px-3 py-2.5'>
<span className='text-muted-foreground text-xs font-medium'>
{t('Output')}
</span>
@@ -475,7 +454,11 @@ function ModelBackendProviderSection(props: { model: PricingModel }) {
if (groups.length > 0) {
cells.push(
<CatalogInfoCell key='groups' label={t('Groups')}>
<CatalogPillList items={groups} />
<div className='flex min-w-0 flex-wrap gap-1.5'>
{groups.map((group) => (
<GroupBadge key={group} group={group} size='md' />
))}
</div>
</CatalogInfoCell>
)
}
@@ -509,7 +492,7 @@ function ModelBackendProviderSection(props: { model: PricingModel }) {
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'>
<div className='divide-border grid grid-cols-1 overflow-hidden rounded-xl border sm:grid-cols-2 [&>*]:border-b [&>*:nth-child(odd)]:sm:border-r [&>*:nth-last-child(-n+2)]:sm:border-b-0'>
{cells}
</div>
</section>
@@ -519,7 +502,6 @@ function ModelBackendProviderSection(props: { model: PricingModel }) {
function ModelBackendDetailsSection(props: { model: PricingModel }) {
return (
<>
<ModelBackendQuickStats model={props.model} />
<ModelBackendSignalsSection model={props.model} />
<ModelBackendProviderSection model={props.model} />
</>
@@ -534,55 +516,84 @@ function ModelHeader(props: { model: PricingModel }) {
const { t } = useTranslation()
const model = props.model
const modelIconKey = model.icon || model.vendor_icon
const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 20) : null
const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 28) : null
const description = model.description || model.vendor_description || null
const tags = parseTags(model.tags)
const endpoints = normalizeCatalogItems(model.supported_endpoint_types)
const isSpecialExpression =
model.billing_mode === 'tiered_expr' &&
Boolean(model.billing_expr) &&
getDynamicPricingTiers(model).length === 0
return (
<header className='pb-4'>
<div className='flex items-center gap-2.5'>
{modelIcon}
<h1 className='font-mono text-xl font-bold tracking-tight sm:text-2xl'>
{model.model_name}
</h1>
<CopyButton
value={model.model_name || ''}
size='icon-xs'
iconClassName='size-3'
tooltip={t('Copy model name')}
successTooltip={t('Copied!')}
aria-label={t('Copy model name')}
/>
</div>
<div className='mt-1 flex flex-wrap items-center gap-1.5 text-xs'>
{model.vendor_name && (
<span className='text-muted-foreground'>{model.vendor_name}</span>
)}
<span className='text-muted-foreground/30'>·</span>
<span className='text-muted-foreground/70'>
{model.quota_type === QUOTA_TYPE_VALUES.TOKEN
? t('Token-based')
: t('Per Request')}
</span>
{model.billing_mode === 'tiered_expr' && model.billing_expr && (
<>
<span className='text-muted-foreground/30'>·</span>
<span className='bg-warning/15 text-warning rounded px-1.5 py-0.5 text-xs font-medium'>
{isSpecialExpression
? t('Special billing expression')
: t('Dynamic Pricing')}
<header className='space-y-5'>
<div className='flex items-start gap-4'>
<div className='bg-muted flex size-12 shrink-0 items-center justify-center rounded-xl'>
{modelIcon || (
<span className='text-muted-foreground text-base font-medium'>
{model.model_name?.charAt(0).toUpperCase() || '?'}
</span>
</>
)}
)}
</div>
<div className='min-w-0 flex-1'>
<div className='flex min-w-0 flex-wrap items-center gap-2'>
<h1 className='truncate font-mono text-lg font-semibold tracking-tight'>
{model.model_name}
</h1>
<CopyButton
value={model.model_name || ''}
size='icon-xs'
iconClassName='size-3'
tooltip={t('Copy model name')}
successTooltip={t('Copied!')}
aria-label={t('Copy model name')}
/>
{model.billing_mode === 'tiered_expr' && model.billing_expr && (
<StatusBadge variant='warning' size='md'>
{isSpecialExpression
? t('Special billing expression')
: t('Dynamic Pricing')}
</StatusBadge>
)}
</div>
<div className='text-muted-foreground mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm'>
{model.vendor_name && <span>{model.vendor_name}</span>}
{model.vendor_name && (
<span className='text-muted-foreground/40'>·</span>
)}
<span>
{model.quota_type === QUOTA_TYPE_VALUES.TOKEN
? t('Token-based')
: t('Per Request')}
</span>
</div>
</div>
</div>
{description && (
<p className='text-muted-foreground mt-2 text-sm leading-relaxed'>
<p className='text-muted-foreground max-w-3xl text-sm leading-relaxed'>
{description}
</p>
)}
{(tags.length > 0 || endpoints.length > 0) && (
<div className='flex flex-wrap gap-1.5'>
{tags.map((tag) => (
<StatusBadge key={`tag-${tag}`} variant='neutral' size='md'>
{tag}
</StatusBadge>
))}
{endpoints.map((endpoint) => (
<StatusBadge
key={`endpoint-${endpoint}`}
variant='neutral'
size='md'
>
{endpoint}
</StatusBadge>
))}
</div>
)}
</header>
)
}
@@ -653,16 +664,16 @@ function PriceSection(props: {
if (dynamicSummary.isSpecialExpression) {
return (
<section>
<SectionTitle>{t('Base Price')}</SectionTitle>
<div className='border-warning/30 bg-warning/10 rounded-lg border p-3'>
<SectionTitle>{t('Pricing')}</SectionTitle>
<div className='border-warning/30 bg-warning/10 rounded-xl border p-4'>
<div className='text-warning text-sm font-medium'>
{t('Special billing expression')}
</div>
<p className='text-muted-foreground mt-1 text-xs'>
<p className='text-muted-foreground mt-1 text-sm'>
{t('Unable to parse structured pricing')}
</p>
<div className='mt-3'>
<div className='text-muted-foreground mb-1 text-xs font-medium tracking-wider uppercase'>
<div className='text-muted-foreground mb-1 text-xs font-medium'>
{t('Raw expression')}
</div>
<code className='text-muted-foreground bg-background/80 block max-h-28 overflow-auto rounded-md border px-2 py-1.5 font-mono text-xs break-all'>
@@ -674,55 +685,37 @@ function PriceSection(props: {
)
}
const priceRows = [
...dynamicSummary.primaryEntries,
...dynamicSummary.secondaryEntries,
]
return (
<section>
<SectionTitle>{t('Base Price')}</SectionTitle>
{dynamicSummary.primaryEntries.length > 0 ? (
<div className='grid grid-cols-2 gap-2'>
{dynamicSummary.primaryEntries.map((entry) => (
<SectionTitle>{t('Pricing')}</SectionTitle>
<div className='overflow-hidden rounded-xl border'>
<div className='flex items-baseline justify-between gap-3 border-b px-4 py-3'>
<span className='text-sm font-medium'>{t('Text tokens')}</span>
<span className='text-muted-foreground text-xs'>
{t('Prices shown per')} {tokenUnitLabel} {t('tokens')}
</span>
</div>
<div className='divide-y'>
{priceRows.map((entry) => (
<div
key={entry.key}
className='bg-muted/20 rounded-lg border p-3'
className='flex items-baseline justify-between gap-4 px-4 py-3'
>
<div className='text-muted-foreground text-xs'>
<span className='text-muted-foreground text-sm'>
{t(entry.shortLabel)}
</div>
<div className='text-foreground mt-1 text-base font-semibold tabular-nums'>
</span>
<span className='text-sm font-medium tabular-nums'>
{entry.formatted}
<span className='text-muted-foreground/40 ml-1 text-xs font-normal'>
/ {tokenUnitLabel}
</span>
</div>
</span>
</div>
))}
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('Dynamic Pricing')}
</p>
)}
{dynamicSummary.secondaryEntries.length > 0 && (
<div className='bg-muted/20 mt-3 rounded-lg border px-3 py-2.5'>
<div className='space-y-1.5'>
{dynamicSummary.secondaryEntries.map((entry) => (
<div
key={entry.key}
className='flex items-baseline justify-between gap-4'
>
<span className='text-muted-foreground/70 text-sm'>
{t(entry.shortLabel)}
</span>
<span className='text-muted-foreground text-sm tabular-nums'>
{entry.formatted}
<span className='text-muted-foreground/40 ml-1 text-xs font-normal'>
/ {tokenUnitLabel}
</span>
</span>
</div>
))}
</div>
</div>
)}
</div>
</section>
)
}
@@ -730,77 +723,72 @@ function PriceSection(props: {
if (!isTokenBased) {
return (
<section>
<SectionTitle>{t('Base Price')}</SectionTitle>
<div className='flex items-baseline justify-between'>
<span className='text-muted-foreground text-sm'>
{t('Per request')}
</span>
<span className='text-foreground text-sm font-semibold tabular-nums'>
{formatFixedPrice(
props.model,
baseGroupKey,
props.showRechargePrice,
props.priceRate,
props.usdExchangeRate,
baseGroupRatioMap
)}
</span>
<SectionTitle>{t('Pricing')}</SectionTitle>
<div className='overflow-hidden rounded-xl border'>
<div className='flex items-baseline justify-between gap-4 px-4 py-3'>
<span className='text-muted-foreground text-sm'>
{t('Per request')}
</span>
<span className='text-sm font-medium tabular-nums'>
{formatFixedPrice(
props.model,
baseGroupKey,
props.showRechargePrice,
props.priceRate,
props.usdExchangeRate,
baseGroupRatioMap
)}
</span>
</div>
</div>
</section>
)
}
const secondaryItems = secondaryPriceTypes.filter((p) => p.available)
const renderPrice = (type: PriceType) => (
<>
{formatGroupPrice(
props.model,
baseGroupKey,
type,
props.tokenUnit,
props.showRechargePrice,
props.priceRate,
props.usdExchangeRate,
baseGroupRatioMap
)}
<span className='text-muted-foreground/40 ml-1 text-xs font-normal'>
/ {tokenUnitLabel}
</span>
</>
)
const priceRows = [
...primaryPriceTypes,
...secondaryItems.map((item) => ({
label: item.label,
type: item.type,
})),
]
return (
<section>
<SectionTitle>{t('Base Price')}</SectionTitle>
<div className='grid grid-cols-2 gap-2'>
{primaryPriceTypes.map((item) => (
<div key={item.type} className='bg-muted/20 rounded-lg border p-3'>
<div className='text-muted-foreground text-xs'>{item.label}</div>
<div className='text-foreground mt-1 text-base font-semibold tabular-nums'>
{renderPrice(item.type)}
</div>
</div>
))}
</div>
{secondaryItems.length > 0 && (
<div className='bg-muted/20 mt-3 rounded-lg border px-3 py-2.5'>
<div className='space-y-1.5'>
{secondaryItems.map((item) => (
<div
key={item.type}
className='flex items-baseline justify-between gap-4'
>
<span className='text-muted-foreground/70 text-sm'>
{item.label}
</span>
<span className='text-muted-foreground text-sm tabular-nums'>
{renderPrice(item.type)}
</span>
</div>
))}
</div>
<SectionTitle>{t('Pricing')}</SectionTitle>
<div className='overflow-hidden rounded-xl border'>
<div className='flex items-baseline justify-between gap-3 border-b px-4 py-3'>
<span className='text-sm font-medium'>{t('Text tokens')}</span>
<span className='text-muted-foreground text-xs'>
{t('Prices shown per')} {tokenUnitLabel} {t('tokens')}
</span>
</div>
)}
<div className='divide-y'>
{priceRows.map((item) => (
<div
key={item.type}
className='flex items-baseline justify-between gap-4 px-4 py-3'
>
<span className='text-muted-foreground text-sm'>
{item.label}
</span>
<span className='text-sm font-medium tabular-nums'>
{formatGroupPrice(
props.model,
baseGroupKey,
item.type,
props.tokenUnit,
props.showRechargePrice,
props.priceRate,
props.usdExchangeRate,
baseGroupRatioMap
)}
</span>
</div>
))}
</div>
</div>
</section>
)
}
@@ -844,13 +832,13 @@ function getDynamicPriceFields(
tiers: DynamicPricingTier[],
options: DynamicPriceOptions
) {
return Array.from(
new Map(
return [
...new Map(
tiers
.flatMap((tier) => getDynamicPriceEntries(tier, options))
.map((entry) => [entry.field, entry])
).values()
)
).values(),
]
}
function getDynamicFormattedPricesByTier(
@@ -897,19 +885,24 @@ function GroupPricingSection(props: {
const extraPriceTypes = useMemo(() => {
const types: { label: string; type: PriceType }[] = []
if (props.model.cache_ratio != null)
if (props.model.cache_ratio != null) {
types.push({ label: t('Cache'), type: 'cache' })
if (props.model.create_cache_ratio != null)
}
if (props.model.create_cache_ratio != null) {
types.push({ label: t('Cache Write'), type: 'create_cache' })
if (props.model.image_ratio != null)
}
if (props.model.image_ratio != null) {
types.push({ label: t('Image'), type: 'image' })
if (props.model.audio_ratio != null)
}
if (props.model.audio_ratio != null) {
types.push({ label: t('Audio In'), type: 'audio_input' })
}
if (
props.model.audio_ratio != null &&
props.model.audio_completion_ratio != null
)
) {
types.push({ label: t('Audio Out'), type: 'audio_output' })
}
return types
}, [props.model, t])
@@ -927,8 +920,7 @@ function GroupPricingSection(props: {
)
}
const thClass =
'text-muted-foreground py-2 text-xs font-medium tracking-wider uppercase'
const thClass = 'text-muted-foreground py-2 text-xs font-medium'
if (isDynamicPricingModel(props.model)) {
const dynamicTiers = getDynamicPricingTiers(props.model)
@@ -948,7 +940,7 @@ function GroupPricingSection(props: {
)}
</p>
<div className='mt-3'>
<div className='text-muted-foreground mb-1 text-xs font-medium tracking-wider uppercase'>
<div className='text-muted-foreground mb-1 text-xs font-medium'>
{t('Raw expression')}
</div>
<code className='text-muted-foreground bg-background/80 block max-h-28 overflow-auto rounded-md border px-2 py-1.5 font-mono text-xs break-all'>
@@ -1166,53 +1158,52 @@ export function ModelDetailsContent(props: ModelDetailsContentProps) {
Boolean(props.model.billing_expr)
return (
<div className='@container/details space-y-4'>
<div className='@container/details space-y-8'>
<ModelHeader model={props.model} />
<ModelBackendQuickStats model={props.model} />
<OverviewSummaryGrid model={props.model} />
<Tabs defaultValue='overview' className='gap-4'>
<TabsList className='bg-muted/60 grid w-full grid-cols-3 gap-1 rounded-lg p-1 group-data-horizontal/tabs:h-auto sm:group-data-horizontal/tabs:h-auto'>
<Tabs defaultValue='overview' className='gap-6'>
<TabsList
variant='line'
className='w-full justify-start gap-6 overflow-x-auto overflow-y-hidden border-b p-0'
>
{TAB_VALUES.map((value) => {
const Icon = TAB_META[value].icon
return (
<TabsTrigger
key={value}
value={value}
className='h-8 min-w-0 gap-1.5 rounded-md px-3 text-xs sm:text-sm'
className='flex-none gap-1.5 px-0.5 pb-3'
>
<Icon className='size-3.5' />
<Icon aria-hidden='true' className='size-3.5' />
<span className='truncate'>{t(TAB_META[value].labelKey)}</span>
</TabsTrigger>
)
})}
</TabsList>
<TabsContent value='overview' className='space-y-6 outline-none'>
<OverviewSummaryGrid model={props.model} />
<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>
<TabsContent value='overview' className='space-y-8 outline-none'>
<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}
/>
<ModelBackendDetailsSection model={props.model} />
</TabsContent>
@@ -1231,39 +1222,6 @@ export function ModelDetailsContent(props: ModelDetailsContentProps) {
)
}
// ----------------------------------------------------------------------------
// Drawer & page wrappers
// ----------------------------------------------------------------------------
export interface ModelDetailsDrawerProps extends ModelDetailsContentProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function ModelDetailsDrawer(props: ModelDetailsDrawerProps) {
const { t } = useTranslation()
const { open, onOpenChange, ...contentProps } = props
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
side='right'
className={sideDrawerContentClassName(
'sm:max-w-2xl lg:max-w-3xl xl:max-w-4xl 2xl:max-w-5xl'
)}
>
<SheetHeader className='sr-only'>
<SheetTitle>{props.model.model_name}</SheetTitle>
<SheetDescription>{t('Model details')}</SheetDescription>
</SheetHeader>
<div className='flex-1 overflow-y-auto px-4 pt-11 pb-5 sm:px-6 sm:pt-12 sm:pb-6'>
<ModelDetailsContent {...contentProps} />
</div>
</SheetContent>
</Sheet>
)
}
export function ModelDetails() {
const { t } = useTranslation()
const { modelId } = useParams({ from: '/pricing/$modelId/' })
@@ -1295,8 +1253,8 @@ export function ModelDetails() {
if (isLoading) {
return (
<PublicLayout>
<div className='mx-auto max-w-5xl px-4 sm:px-6'>
<PublicLayout showMainContainer={false}>
<div className='mx-auto max-w-6xl px-4 pt-20 pb-10 sm:px-6 lg:px-8'>
<Skeleton className='mb-4 h-5 w-16' />
<div className='space-y-2'>
<Skeleton className='h-7 w-64' />
@@ -1304,13 +1262,13 @@ export function ModelDetails() {
<Skeleton className='h-4 w-full max-w-md' />
</div>
<div className='mt-6 grid grid-cols-2 gap-2 sm:grid-cols-4'>
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className='h-16 w-full' />
{['stats-a', 'stats-b', 'stats-c', 'stats-d'].map((key) => (
<Skeleton key={key} className='h-16 w-full' />
))}
</div>
<div className='mt-6 space-y-3'>
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className='h-24 w-full' />
{['block-a', 'block-b', 'block-c', 'block-d'].map((key) => (
<Skeleton key={key} className='h-24 w-full' />
))}
</div>
</div>
@@ -1320,8 +1278,8 @@ export function ModelDetails() {
if (!model) {
return (
<PublicLayout>
<div className='mx-auto max-w-2xl px-4 text-center sm:px-6'>
<PublicLayout showMainContainer={false}>
<div className='mx-auto max-w-2xl px-4 pt-24 pb-10 text-center sm:px-6'>
<h2 className='mb-1 text-base font-semibold'>
{t('Model not found')}
</h2>
@@ -1337,14 +1295,14 @@ export function ModelDetails() {
}
return (
<PublicLayout>
<div className='mx-auto max-w-5xl px-4 sm:px-6'>
<PublicLayout showMainContainer={false}>
<PageTransition className='mx-auto max-w-5xl px-4 pt-20 pb-16 sm:px-6 lg:px-8'>
<Button
variant='ghost'
onClick={handleBack}
className='text-muted-foreground hover:text-foreground mb-4 h-auto gap-1 px-0 py-1 text-xs sm:h-auto'
className='text-muted-foreground hover:text-foreground mb-8 -ml-2'
>
<ArrowLeft className='size-3.5' />
<ArrowLeft aria-hidden='true' />
{t('Back')}
</Button>
@@ -1364,7 +1322,7 @@ export function ModelDetails() {
>) || {}
}
/>
</div>
</PageTransition>
</PublicLayout>
)
}