286 lines
9.1 KiB
TypeScript
Vendored
286 lines
9.1 KiB
TypeScript
Vendored
/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
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-3 pt-16 pb-8 sm:px-6 sm:pt-20 sm:pb-10 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-3 pt-16 pb-8 sm:px-6 sm:pt-20 sm:pb-10 xl:px-8'>
|
|
<header className='mx-auto mb-5 max-w-3xl pt-5 text-center sm:mb-10 sm:pt-10'>
|
|
<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-3 text-sm sm:mt-4 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-4 max-w-2xl sm:mt-6'
|
|
/>
|
|
</header>
|
|
|
|
<div className='grid gap-4 xl: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='hover-scrollbar sticky top-4 hidden max-h-[calc(100dvh-2rem)] self-start 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>
|
|
)
|
|
}
|