267 lines
8.4 KiB
TypeScript
Vendored
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>
|
|
)
|
|
}
|