| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- 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>
- )
- }
|