267 lines
8.4 KiB
TypeScript
Vendored

import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PublicLayout } from '@/components/layout'
import { PageTransition } from '@/components/page-transition'
import {
LoadingSkeleton,
EmptyState,
SearchBar,
PricingTable,
PricingSidebar,
PricingToolbar,
ModelCardGrid,
ModelDetailsDrawer,
} from './components'
import { EXCLUDED_GROUPS, VIEW_MODES } from './constants'
import { useFilters } from './hooks/use-filters'
import { usePricingData } from './hooks/use-pricing-data'
export function Pricing() {
const { t } = useTranslation()
const [selectedModelName, setSelectedModelName] = useState<string | null>(null)
const {
models,
vendors,
groupRatio,
usableGroup,
endpointMap,
autoGroups,
isLoading,
priceRate,
usdExchangeRate,
} = usePricingData()
const {
searchInput,
sortBy,
vendorFilter,
groupFilter,
quotaTypeFilter,
endpointTypeFilter,
tagFilter,
tokenUnit,
viewMode,
showRechargePrice,
setSearchInput,
setSortBy,
setVendorFilter,
setGroupFilter,
setQuotaTypeFilter,
setEndpointTypeFilter,
setTagFilter,
setTokenUnit,
setViewMode,
setShowRechargePrice,
filteredModels,
hasActiveFilters,
activeFilterCount,
availableTags,
clearFilters,
clearSearch,
} = useFilters(models || [])
const handleModelClick = useCallback(
(modelName: string) => {
setSelectedModelName(modelName)
},
[]
)
const selectedModel = useMemo(
() =>
selectedModelName
? (models || []).find((model) => model.model_name === selectedModelName) ||
null
: null,
[models, selectedModelName]
)
const availableGroups = useMemo(
() =>
Object.keys(usableGroup || {}).filter(
(g) => !EXCLUDED_GROUPS.includes(g)
),
[usableGroup]
)
const handleClearAll = useCallback(() => {
clearFilters()
clearSearch()
}, [clearFilters, clearSearch])
const renderPricingContent = () => {
if (filteredModels.length === 0) {
return (
<EmptyState
searchQuery={searchInput}
hasActiveFilters={hasActiveFilters}
onClearFilters={handleClearAll}
/>
)
}
if (viewMode === VIEW_MODES.CARD) {
return (
<ModelCardGrid
models={filteredModels}
onModelClick={handleModelClick}
priceRate={priceRate}
usdExchangeRate={usdExchangeRate}
tokenUnit={tokenUnit}
showRechargePrice={showRechargePrice}
/>
)
}
return (
<PricingTable
models={filteredModels}
priceRate={priceRate}
usdExchangeRate={usdExchangeRate}
tokenUnit={tokenUnit}
showRechargePrice={showRechargePrice}
onModelClick={handleModelClick}
/>
)
}
if (isLoading) {
return (
<PublicLayout showMainContainer={false}>
<div className='mx-auto w-full max-w-[1800px] px-4 pt-20 pb-10 sm:px-6 xl:px-8'>
<LoadingSkeleton viewMode={viewMode} />
</div>
</PublicLayout>
)
}
return (
<PublicLayout showMainContainer={false}>
<div className='relative'>
<div
aria-hidden
className='pointer-events-none absolute inset-x-0 top-0 h-[600px] opacity-20 dark:opacity-[0.10]'
style={{
background: [
'radial-gradient(ellipse 60% 50% at 20% 20%, oklch(0.72 0.18 250 / 80%) 0%, transparent 70%)',
'radial-gradient(ellipse 50% 40% at 80% 15%, oklch(0.65 0.15 200 / 60%) 0%, transparent 70%)',
'radial-gradient(ellipse 40% 35% at 50% 70%, oklch(0.70 0.12 280 / 40%) 0%, transparent 70%)',
].join(', '),
maskImage: 'linear-gradient(to bottom, black 40%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 40%, transparent 100%)',
}}
/>
<PageTransition className='relative mx-auto w-full max-w-[1800px] px-4 pt-20 pb-10 sm:px-6 xl:px-8'>
<header className='mx-auto mb-8 max-w-3xl pt-8 text-center sm:mb-10 sm:pt-10'>
<p className='text-muted-foreground mb-3 text-xs font-medium tracking-widest uppercase'>
{t('Models Directory')}
</p>
<h1 className='text-[clamp(2rem,5.5vw,3.5rem)] leading-[1.15] font-bold tracking-tight'>
{t('Model Square')}
</h1>
<p className='text-muted-foreground/80 mt-4 text-sm sm:text-base'>
{t('This site currently has {{count}} models enabled', {
count: models?.length || 0,
})}
</p>
<p className='text-muted-foreground/60 mx-auto mt-2 max-w-2xl text-xs leading-relaxed sm:text-sm'>
{t(
'Discover curated AI models, compare pricing and capabilities, and choose the right model for every scenario.'
)}
</p>
<SearchBar
value={searchInput}
onChange={setSearchInput}
onClear={clearSearch}
placeholder={t('Search model name, provider, endpoint, or tag...')}
className='mx-auto mt-6 max-w-2xl'
/>
</header>
<div className='grid gap-4 xl:grid-cols-[330px_minmax(0,1fr)] 2xl:grid-cols-[330px_minmax(0,1fr)]'>
<PricingSidebar
quotaTypeFilter={quotaTypeFilter}
endpointTypeFilter={endpointTypeFilter}
vendorFilter={vendorFilter}
groupFilter={groupFilter}
tagFilter={tagFilter}
onQuotaTypeChange={setQuotaTypeFilter}
onEndpointTypeChange={setEndpointTypeFilter}
onVendorChange={setVendorFilter}
onGroupChange={setGroupFilter}
onTagChange={setTagFilter}
vendors={vendors || []}
groups={availableGroups}
groupRatios={groupRatio}
tags={availableTags}
models={models || []}
hasActiveFilters={hasActiveFilters}
onClearFilters={clearFilters}
className='sticky top-20 hidden max-h-[calc(100vh-6rem)] overflow-y-auto xl:block'
/>
<main className='min-w-0 space-y-4'>
<PricingToolbar
filteredCount={filteredModels.length}
totalCount={models?.length}
sortBy={sortBy}
onSortChange={setSortBy}
tokenUnit={tokenUnit}
onTokenUnitChange={setTokenUnit}
showRechargePrice={showRechargePrice}
onRechargePriceChange={setShowRechargePrice}
viewMode={viewMode}
onViewModeChange={setViewMode}
quotaTypeFilter={quotaTypeFilter}
endpointTypeFilter={endpointTypeFilter}
vendorFilter={vendorFilter}
groupFilter={groupFilter}
tagFilter={tagFilter}
onQuotaTypeChange={setQuotaTypeFilter}
onEndpointTypeChange={setEndpointTypeFilter}
onVendorChange={setVendorFilter}
onGroupChange={setGroupFilter}
onTagChange={setTagFilter}
vendors={vendors || []}
groups={availableGroups}
groupRatios={groupRatio}
tags={availableTags}
models={models || []}
hasActiveFilters={hasActiveFilters}
activeFilterCount={activeFilterCount}
onClearFilters={clearFilters}
/>
{renderPricingContent()}
</main>
</div>
{selectedModel && (
<ModelDetailsDrawer
open={Boolean(selectedModel)}
onOpenChange={(open) => {
if (!open) setSelectedModelName(null)
}}
model={selectedModel}
groupRatio={groupRatio || {}}
usableGroup={usableGroup || {}}
endpointMap={
(endpointMap as Record<
string,
{ path?: string; method?: string }
>) || {}
}
autoGroups={autoGroups || []}
priceRate={priceRate ?? 1}
usdExchangeRate={usdExchangeRate ?? 1}
tokenUnit={tokenUnit}
showRechargePrice={showRechargePrice}
/>
)}
</PageTransition>
</div>
</PublicLayout>
)
}