948780e3fa
Remove hard-coded and capped border radius overrides so shared controls and feature actions consistently follow the active theme radius. - Replace fixed radius utilities with semantic theme-aware radius tokens - Remove redundant `rounded-full` and pixel-based overrides from header, toolbar, Playground, and utility actions - Drop unused `StatusBadge` rounded prop usage - Keep existing component behavior intact while improving global theme consistency
289 lines
9.1 KiB
TypeScript
Vendored
289 lines
9.1 KiB
TypeScript
Vendored
import { useCallback, useState } from 'react'
|
|
import { ArrowUpDown, Check, Filter, Grid2X2, Table2 } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { cn } from '@/lib/utils'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from '@/components/ui/sheet'
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip'
|
|
import {
|
|
VIEW_MODES,
|
|
getSortLabels,
|
|
type SortOption,
|
|
type ViewMode,
|
|
} from '../constants'
|
|
import type { PricingModel, PricingVendor, TokenUnit } from '../types'
|
|
import { PricingSidebar } from './pricing-sidebar'
|
|
|
|
type SegmentOption = {
|
|
value: string
|
|
label?: string
|
|
icon?: React.ComponentType<{ className?: string }>
|
|
tooltip?: string
|
|
}
|
|
|
|
export interface PricingToolbarProps {
|
|
filteredCount: number
|
|
totalCount?: number
|
|
sortBy: string
|
|
onSortChange: (value: string) => void
|
|
tokenUnit: TokenUnit
|
|
onTokenUnitChange: (value: TokenUnit) => void
|
|
showRechargePrice: boolean
|
|
onRechargePriceChange: (value: boolean) => void
|
|
viewMode: ViewMode
|
|
onViewModeChange: (value: ViewMode) => void
|
|
quotaTypeFilter: string
|
|
endpointTypeFilter: string
|
|
vendorFilter: string
|
|
groupFilter: string
|
|
tagFilter: string
|
|
onQuotaTypeChange: (value: string) => void
|
|
onEndpointTypeChange: (value: string) => void
|
|
onVendorChange: (value: string) => void
|
|
onGroupChange: (value: string) => void
|
|
onTagChange: (value: string) => void
|
|
vendors: PricingVendor[]
|
|
groups: string[]
|
|
groupRatios?: Record<string, number>
|
|
tags: string[]
|
|
models: PricingModel[]
|
|
hasActiveFilters: boolean
|
|
activeFilterCount: number
|
|
onClearFilters: () => void
|
|
}
|
|
|
|
function SegmentedControl(props: {
|
|
options: SegmentOption[]
|
|
value: string
|
|
onChange: (value: string) => void
|
|
ariaLabel: string
|
|
}) {
|
|
return (
|
|
<div
|
|
role='group'
|
|
aria-label={props.ariaLabel}
|
|
className='bg-muted/60 inline-flex h-8 items-center rounded-lg border p-0.5'
|
|
>
|
|
{props.options.map((option) => {
|
|
const Icon = option.icon
|
|
const isActive = option.value === props.value
|
|
const button = (
|
|
<button
|
|
key={option.value}
|
|
type='button'
|
|
onClick={() => props.onChange(option.value)}
|
|
aria-pressed={isActive}
|
|
className={cn(
|
|
'inline-flex h-full items-center justify-center rounded-md text-xs font-medium transition-all',
|
|
Icon && !option.label ? 'w-7' : 'gap-1.5 px-3',
|
|
isActive
|
|
? 'bg-primary text-primary-foreground shadow-sm'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
)}
|
|
>
|
|
{Icon && <Icon className='size-3.5' />}
|
|
{option.label}
|
|
</button>
|
|
)
|
|
|
|
if (!option.tooltip) {
|
|
return button
|
|
}
|
|
|
|
return (
|
|
<Tooltip key={option.value}>
|
|
<TooltipTrigger render={button}></TooltipTrigger>
|
|
<TooltipContent side='bottom' className='text-xs'>
|
|
{option.tooltip}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function PricingToolbar(props: PricingToolbarProps) {
|
|
const { t } = useTranslation()
|
|
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false)
|
|
const sortLabels = getSortLabels(t)
|
|
|
|
const handleTokenUnitChange = useCallback(
|
|
(value: string) => props.onTokenUnitChange(value as TokenUnit),
|
|
[props]
|
|
)
|
|
|
|
const handleViewModeChange = useCallback(
|
|
(value: string) => props.onViewModeChange(value as ViewMode),
|
|
[props]
|
|
)
|
|
|
|
const handleRechargePriceChange = useCallback(
|
|
(value: string) => props.onRechargePriceChange(value === 'recharge'),
|
|
[props]
|
|
)
|
|
|
|
return (
|
|
<div className='rounded-xl border p-3'>
|
|
<div className='flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between'>
|
|
<div className='flex items-center gap-2'>
|
|
<Button
|
|
type='button'
|
|
variant='outline'
|
|
size='sm'
|
|
onClick={() => setMobileFiltersOpen(true)}
|
|
className='gap-1.5 xl:hidden'
|
|
>
|
|
<Filter className='size-4' />
|
|
{t('Filter')}
|
|
{props.activeFilterCount > 0 && (
|
|
<Badge className='ml-0.5 size-5 justify-center p-0 text-[10px]'>
|
|
{props.activeFilterCount}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
|
|
<div className='text-muted-foreground flex items-baseline gap-1 text-sm'>
|
|
<span className='text-foreground font-semibold tabular-nums'>
|
|
{props.filteredCount.toLocaleString()}
|
|
</span>
|
|
<span>{props.filteredCount === 1 ? t('model') : t('models')}</span>
|
|
{props.hasActiveFilters && props.totalCount && (
|
|
<span className='text-muted-foreground/60 text-xs'>
|
|
/ {props.totalCount.toLocaleString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className='flex flex-wrap items-center gap-2'>
|
|
<div className='hidden items-center gap-2 sm:flex'>
|
|
<SegmentedControl
|
|
options={[
|
|
{ value: 'standard', label: t('Standard') },
|
|
{ value: 'recharge', label: t('Recharge') },
|
|
]}
|
|
value={props.showRechargePrice ? 'recharge' : 'standard'}
|
|
onChange={handleRechargePriceChange}
|
|
ariaLabel={t('Price display mode')}
|
|
/>
|
|
<SegmentedControl
|
|
options={[
|
|
{ value: 'M', label: '/1M' },
|
|
{ value: 'K', label: '/1K' },
|
|
]}
|
|
value={props.tokenUnit}
|
|
onChange={handleTokenUnitChange}
|
|
ariaLabel={t('Token unit')}
|
|
/>
|
|
</div>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
render={
|
|
<Button
|
|
type='button'
|
|
variant='outline'
|
|
size='sm'
|
|
className='h-8 gap-1.5 px-3 text-xs'
|
|
/>
|
|
}
|
|
>
|
|
<ArrowUpDown className='size-3.5' />
|
|
<span>{sortLabels[props.sortBy as SortOption] || t('Sort')}</span>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align='end' className='w-44'>
|
|
{Object.entries(sortLabels).map(([value, label]) => (
|
|
<DropdownMenuItem
|
|
key={value}
|
|
onClick={() => props.onSortChange(value)}
|
|
className='gap-2'
|
|
>
|
|
<Check
|
|
className={cn(
|
|
'size-4 shrink-0',
|
|
props.sortBy === value ? 'opacity-100' : 'opacity-0'
|
|
)}
|
|
/>
|
|
{label}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<SegmentedControl
|
|
options={[
|
|
{
|
|
value: VIEW_MODES.CARD,
|
|
icon: Grid2X2,
|
|
tooltip: t('Card view'),
|
|
},
|
|
{
|
|
value: VIEW_MODES.TABLE,
|
|
icon: Table2,
|
|
tooltip: t('Table view'),
|
|
},
|
|
]}
|
|
value={props.viewMode}
|
|
onChange={handleViewModeChange}
|
|
ariaLabel={t('View mode')}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Sheet open={mobileFiltersOpen} onOpenChange={setMobileFiltersOpen}>
|
|
<SheetContent
|
|
side='right'
|
|
className='flex h-dvh w-full flex-col overflow-hidden p-0 sm:max-w-md'
|
|
>
|
|
<SheetHeader className='border-b px-4 py-3 sm:px-6 sm:py-4'>
|
|
<SheetTitle>{t('Filter')}</SheetTitle>
|
|
<SheetDescription>
|
|
{t('Filter models by provider, group, type, endpoint, and tags.')}
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
<div className='flex-1 overflow-y-auto p-3 sm:p-4'>
|
|
<PricingSidebar
|
|
quotaTypeFilter={props.quotaTypeFilter}
|
|
endpointTypeFilter={props.endpointTypeFilter}
|
|
vendorFilter={props.vendorFilter}
|
|
groupFilter={props.groupFilter}
|
|
tagFilter={props.tagFilter}
|
|
onQuotaTypeChange={props.onQuotaTypeChange}
|
|
onEndpointTypeChange={props.onEndpointTypeChange}
|
|
onVendorChange={props.onVendorChange}
|
|
onGroupChange={props.onGroupChange}
|
|
onTagChange={props.onTagChange}
|
|
vendors={props.vendors}
|
|
groups={props.groups}
|
|
groupRatios={props.groupRatios}
|
|
tags={props.tags}
|
|
models={props.models}
|
|
hasActiveFilters={props.hasActiveFilters}
|
|
onClearFilters={props.onClearFilters}
|
|
className='border-0 bg-transparent p-0 shadow-none'
|
|
/>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
</div>
|
|
)
|
|
}
|