mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-07 01:56:53 +00:00
feat(ui): redesign model square pricing page
This commit is contained in:
+178
-86
@@ -1,6 +1,4 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useMediaQuery } from '@/hooks'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PublicLayout } from '@/components/layout'
|
||||
import { PageTransition } from '@/components/page-transition'
|
||||
@@ -8,9 +6,11 @@ import {
|
||||
LoadingSkeleton,
|
||||
EmptyState,
|
||||
SearchBar,
|
||||
FilterBar,
|
||||
VirtualModelList,
|
||||
PricingTable,
|
||||
PricingSidebar,
|
||||
PricingToolbar,
|
||||
ModelCardGrid,
|
||||
ModelDetailsDrawer,
|
||||
} from './components'
|
||||
import { EXCLUDED_GROUPS, VIEW_MODES } from './constants'
|
||||
import { useFilters } from './hooks/use-filters'
|
||||
@@ -18,13 +18,15 @@ import { usePricingData } from './hooks/use-pricing-data'
|
||||
|
||||
export function Pricing() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate({ from: '/pricing/' })
|
||||
const isMobile = useMediaQuery('(max-width: 640px)')
|
||||
const [selectedModelName, setSelectedModelName] = useState<string | null>(null)
|
||||
|
||||
const {
|
||||
models,
|
||||
vendors,
|
||||
groupRatio,
|
||||
usableGroup,
|
||||
endpointMap,
|
||||
autoGroups,
|
||||
isLoading,
|
||||
priceRate,
|
||||
usdExchangeRate,
|
||||
@@ -61,13 +63,18 @@ export function Pricing() {
|
||||
|
||||
const handleModelClick = useCallback(
|
||||
(modelName: string) => {
|
||||
navigate({
|
||||
to: '/pricing/$modelId',
|
||||
params: { modelId: modelName },
|
||||
search: (prev) => prev,
|
||||
})
|
||||
setSelectedModelName(modelName)
|
||||
},
|
||||
[navigate]
|
||||
[]
|
||||
)
|
||||
|
||||
const selectedModel = useMemo(
|
||||
() =>
|
||||
selectedModelName
|
||||
? (models || []).find((model) => model.model_name === selectedModelName) ||
|
||||
null
|
||||
: null,
|
||||
[models, selectedModelName]
|
||||
)
|
||||
|
||||
const availableGroups = useMemo(
|
||||
@@ -83,10 +90,46 @@ export function Pricing() {
|
||||
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>
|
||||
<div className='mx-auto max-w-6xl px-4 sm:px-6'>
|
||||
<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>
|
||||
@@ -94,81 +137,130 @@ export function Pricing() {
|
||||
}
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<PageTransition className='mx-auto max-w-6xl px-4 sm:px-6'>
|
||||
<header className='mb-6 sm:mb-8'>
|
||||
<h1 className='text-2xl font-bold tracking-tight sm:text-3xl'>
|
||||
{t('Model Pricing')}
|
||||
</h1>
|
||||
<p className='text-muted-foreground mt-1 text-sm'>
|
||||
{t('Browse and compare')} {models?.length || 0} {t('models')}
|
||||
</p>
|
||||
</header>
|
||||
<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='space-y-4'>
|
||||
<SearchBar
|
||||
value={searchInput}
|
||||
onChange={setSearchInput}
|
||||
onClear={clearSearch}
|
||||
/>
|
||||
|
||||
<FilterBar
|
||||
quotaTypeFilter={quotaTypeFilter}
|
||||
endpointTypeFilter={endpointTypeFilter}
|
||||
vendorFilter={vendorFilter}
|
||||
groupFilter={groupFilter}
|
||||
tagFilter={tagFilter}
|
||||
onQuotaTypeChange={setQuotaTypeFilter}
|
||||
onEndpointTypeChange={setEndpointTypeFilter}
|
||||
onVendorChange={setVendorFilter}
|
||||
onGroupChange={setGroupFilter}
|
||||
onTagChange={setTagFilter}
|
||||
vendors={vendors || []}
|
||||
groups={availableGroups}
|
||||
tags={availableTags}
|
||||
sortBy={sortBy}
|
||||
onSortChange={setSortBy}
|
||||
tokenUnit={tokenUnit}
|
||||
onTokenUnitChange={setTokenUnit}
|
||||
showRechargePrice={showRechargePrice}
|
||||
onRechargePriceChange={setShowRechargePrice}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
hasActiveFilters={hasActiveFilters}
|
||||
activeFilterCount={activeFilterCount}
|
||||
onClearFilters={clearFilters}
|
||||
filteredCount={filteredModels.length}
|
||||
totalCount={models?.length}
|
||||
/>
|
||||
|
||||
{filteredModels.length > 0 ? (
|
||||
isMobile || viewMode === VIEW_MODES.LIST ? (
|
||||
<VirtualModelList
|
||||
models={filteredModels}
|
||||
onModelClick={handleModelClick}
|
||||
priceRate={priceRate}
|
||||
usdExchangeRate={usdExchangeRate}
|
||||
tokenUnit={tokenUnit}
|
||||
showRechargePrice={showRechargePrice}
|
||||
/>
|
||||
) : (
|
||||
<PricingTable
|
||||
models={filteredModels}
|
||||
priceRate={priceRate}
|
||||
usdExchangeRate={usdExchangeRate}
|
||||
tokenUnit={tokenUnit}
|
||||
showRechargePrice={showRechargePrice}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<EmptyState
|
||||
searchQuery={searchInput}
|
||||
<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={handleClearAll}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PageTransition>
|
||||
</PageTransition>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user