c19d5aa663
Add a shared `performance-metrics` feature module for perf metric APIs, DTOs, and formatting, then surface global 24h model performance on the dashboard with cards and a top-model table. Reuse the shared metrics module from pricing model details, remove duplicated perf API/formatting code from pricing, and add localized labels for the new dashboard performance UI.
111 lines
3.6 KiB
TypeScript
Vendored
111 lines
3.6 KiB
TypeScript
Vendored
import { useMemo, useState } from 'react'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { Button } from '@/components/ui/button'
|
|
import { getPerfMetricsSummary } from '@/features/performance-metrics/api'
|
|
import { DEFAULT_PRICING_PAGE_SIZE, DEFAULT_TOKEN_UNIT } from '../constants'
|
|
import type { PricingModel, TokenUnit } from '../types'
|
|
import { ModelCard } from './model-card'
|
|
import type { ModelPerfBadgeData } from './model-perf-badge'
|
|
|
|
export interface ModelCardGridProps {
|
|
models: PricingModel[]
|
|
onModelClick: (modelName: string) => void
|
|
priceRate?: number
|
|
usdExchangeRate?: number
|
|
tokenUnit?: TokenUnit
|
|
showRechargePrice?: boolean
|
|
}
|
|
|
|
export function ModelCardGrid(props: ModelCardGridProps) {
|
|
const { t } = useTranslation()
|
|
const [page, setPage] = useState(1)
|
|
const pageSize = DEFAULT_PRICING_PAGE_SIZE
|
|
const tokenUnit = props.tokenUnit ?? DEFAULT_TOKEN_UNIT
|
|
const totalPages = Math.max(1, Math.ceil(props.models.length / pageSize))
|
|
const currentPage = Math.min(page, totalPages)
|
|
|
|
const perfQuery = useQuery({
|
|
queryKey: ['perf-metrics-summary', 24],
|
|
queryFn: () => getPerfMetricsSummary(24),
|
|
staleTime: 60 * 1000,
|
|
retry: false,
|
|
})
|
|
|
|
const pagedModels = useMemo(() => {
|
|
const start = (currentPage - 1) * pageSize
|
|
return props.models.slice(start, start + pageSize)
|
|
}, [currentPage, pageSize, props.models])
|
|
|
|
const perfMap = useMemo(() => {
|
|
const map = new Map<string, ModelPerfBadgeData>()
|
|
for (const model of perfQuery.data?.data?.models ?? []) {
|
|
if (model.request_count > 0) {
|
|
map.set(model.model_name, model)
|
|
}
|
|
}
|
|
return map
|
|
}, [perfQuery.data])
|
|
|
|
if (props.models.length === 0) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className='space-y-4 sm:space-y-5'>
|
|
<div className='grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-2 lg:grid-cols-3'>
|
|
{pagedModels.map((model) => (
|
|
<ModelCard
|
|
key={model.id ?? model.model_name}
|
|
model={model}
|
|
tokenUnit={tokenUnit}
|
|
priceRate={props.priceRate}
|
|
usdExchangeRate={props.usdExchangeRate}
|
|
showRechargePrice={props.showRechargePrice}
|
|
perf={perfMap.get(model.model_name || '')}
|
|
onClick={() => props.onModelClick(model.model_name || '')}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{totalPages > 1 && (
|
|
<div className='text-muted-foreground flex flex-col items-center justify-between gap-3 border-t px-4 py-3 text-sm sm:flex-row'>
|
|
<p className='text-muted-foreground'>
|
|
{t('Page {{current}} of {{total}}', {
|
|
current: currentPage,
|
|
total: totalPages,
|
|
})}
|
|
</p>
|
|
<div className='flex items-center gap-2'>
|
|
<Button
|
|
type='button'
|
|
variant='outline'
|
|
size='sm'
|
|
onClick={() => setPage((current) => Math.max(1, current - 1))}
|
|
disabled={currentPage <= 1}
|
|
className='gap-1.5'
|
|
>
|
|
<ChevronLeft className='size-4' />
|
|
{t('Previous')}
|
|
</Button>
|
|
<Button
|
|
type='button'
|
|
variant='outline'
|
|
size='sm'
|
|
onClick={() =>
|
|
setPage((current) => Math.min(totalPages, current + 1))
|
|
}
|
|
disabled={currentPage >= totalPages}
|
|
className='gap-1.5'
|
|
>
|
|
{t('Next')}
|
|
<ChevronRight className='size-4' />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|