2026-05-06 18:20:02 +08:00

127 lines
3.6 KiB
TypeScript
Vendored

import {
ArrowDownRight,
ArrowUpRight,
TrendingDown,
TrendingUp,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { getLobeIcon } from '@/lib/lobe-icon'
import { cn } from '@/lib/utils'
import type { RankingMover } from '../types'
import { ModelLink, VendorLink } from './entity-links'
type PulseSectionProps = {
movers: RankingMover[]
droppers: RankingMover[]
}
/**
* Rank movement panel: gainers and losers calculated from the previous period.
*/
export function PulseSection(props: PulseSectionProps) {
const { t } = useTranslation()
return (
<section className='grid grid-cols-1 gap-4 lg:grid-cols-2'>
<PulseCard
title={t('Trending up')}
description={t('Models climbing the leaderboard')}
icon={<TrendingUp className='size-4 text-emerald-500' />}
>
{props.movers.length === 0 ? (
<PulseEmpty label={t('No notable climbers right now')} />
) : (
<ul>
{props.movers.map((row) => (
<MoverRow key={row.model_name} row={row} intent='up' />
))}
</ul>
)}
</PulseCard>
<PulseCard
title={t('Trending down')}
description={t('Models losing positions')}
icon={<TrendingDown className='size-4 text-rose-500' />}
>
{props.droppers.length === 0 ? (
<PulseEmpty label={t('No notable drops right now')} />
) : (
<ul>
{props.droppers.map((row) => (
<MoverRow key={row.model_name} row={row} intent='down' />
))}
</ul>
)}
</PulseCard>
</section>
)
}
function PulseCard(props: {
title: string
description: string
icon: React.ReactNode
children: React.ReactNode
}) {
return (
<div className='bg-card overflow-hidden rounded-lg border'>
<header className='border-b px-4 py-3'>
<h3 className='text-foreground inline-flex items-center gap-2 text-sm font-semibold'>
{props.icon}
{props.title}
</h3>
<p className='text-muted-foreground/80 mt-0.5 text-xs'>
{props.description}
</p>
</header>
<div className='py-1'>{props.children}</div>
</div>
)
}
function PulseEmpty(props: { label: string }) {
return (
<div className='text-muted-foreground/80 px-4 py-6 text-center text-xs'>
{props.label}
</div>
)
}
function MoverRow(props: { row: RankingMover; intent: 'up' | 'down' }) {
return (
<li className='flex items-center gap-3 px-4 py-2'>
<span className='shrink-0'>{getLobeIcon(props.row.vendor_icon, 20)}</span>
<div className='min-w-0 flex-1'>
<ModelLink
modelName={props.row.model_name}
className='text-foreground block truncate font-mono text-xs font-medium'
>
{props.row.model_name}
</ModelLink>
<p className='text-muted-foreground/80 truncate text-[11px]'>
#{props.row.current_rank} ·{' '}
<VendorLink vendor={props.row.vendor}>
{props.row.vendor.toLowerCase()}
</VendorLink>
</p>
</div>
<span
className={cn(
'inline-flex shrink-0 items-center gap-0.5 font-mono text-xs font-semibold tabular-nums',
props.intent === 'up'
? 'text-emerald-600 dark:text-emerald-400'
: 'text-rose-600 dark:text-rose-400'
)}
>
{props.intent === 'up' ? (
<ArrowUpRight className='size-3' />
) : (
<ArrowDownRight className='size-3' />
)}
{Math.abs(props.row.rank_delta)}
</span>
</li>
)
}