import type { ReactNode } from 'react' import { ChevronDown, RotateCcw } from 'lucide-react' import { useTranslation } from 'react-i18next' import { getLobeIcon } from '@/lib/lobe-icon' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' import { ENDPOINT_TYPES, FILTER_ALL, QUOTA_TYPES, getEndpointTypeLabels, getQuotaTypeLabels, } from '../constants' import { parseTags } from '../lib/filters' import type { PricingModel, PricingVendor } from '../types' type FilterOption = { value: string label: string count?: number suffix?: string icon?: ReactNode } type FilterSectionProps = { title: string value: string options: FilterOption[] onChange: (value: string) => void } export interface PricingSidebarProps { 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 tags: string[] models: PricingModel[] hasActiveFilters: boolean onClearFilters: () => void className?: string } function countBy( models: PricingModel[], predicate: (model: PricingModel) => boolean ): number { return models.reduce((count, model) => count + (predicate(model) ? 1 : 0), 0) } function formatGroupRatio(ratio: number | undefined): string | undefined { if (ratio == null) return undefined const formatted = Number.isInteger(ratio) ? ratio.toString() : ratio.toFixed(3).replace(/0+$/, '').replace(/\.$/, '') return `x${formatted}` } function FilterChip(props: { option: FilterOption active: boolean onClick: () => void }) { return ( ) } function FilterSection(props: FilterSectionProps) { return ( {props.title}
{props.options.map((option) => ( props.onChange(option.value)} /> ))}
) } export function PricingSidebar(props: PricingSidebarProps) { const { t } = useTranslation() const quotaTypeLabels = getQuotaTypeLabels(t) const endpointTypeLabels = getEndpointTypeLabels(t) const vendorOptions: FilterOption[] = [ { value: FILTER_ALL, label: t('All Vendors'), count: props.models.length, }, ...props.vendors .map((vendor) => ({ value: vendor.name, label: vendor.name, count: countBy( props.models, (model) => model.vendor_name === vendor.name ), icon: vendor.icon ? getLobeIcon(vendor.icon, 14) : undefined, })) .filter((vendor) => vendor.count > 0), ] const groupOptions: FilterOption[] = [ { value: FILTER_ALL, label: t('All Groups'), }, ...props.groups.map((group) => ({ value: group, label: group, suffix: formatGroupRatio(props.groupRatios?.[group]), })), ] const quotaOptions: FilterOption[] = [ { value: QUOTA_TYPES.ALL, label: quotaTypeLabels[QUOTA_TYPES.ALL], count: props.models.length, }, { value: QUOTA_TYPES.TOKEN, label: quotaTypeLabels[QUOTA_TYPES.TOKEN], count: countBy(props.models, (model) => model.quota_type === 0), }, { value: QUOTA_TYPES.REQUEST, label: quotaTypeLabels[QUOTA_TYPES.REQUEST], count: countBy(props.models, (model) => model.quota_type === 1), }, ] const tagOptions: FilterOption[] = [ { value: FILTER_ALL, label: t('All Tags'), count: props.models.length, }, ...props.tags.map((tag) => ({ value: tag, label: tag, count: countBy(props.models, (model) => parseTags(model.tags) .map((item) => item.toLowerCase()) .includes(tag.toLowerCase()) ), })), ] const endpointOptions: FilterOption[] = [ { value: ENDPOINT_TYPES.ALL, label: endpointTypeLabels[ENDPOINT_TYPES.ALL], count: props.models.length, }, ...Object.entries(endpointTypeLabels) .filter(([value]) => value !== ENDPOINT_TYPES.ALL) .map(([value, label]) => ({ value, label, count: countBy(props.models, (model) => model.supported_endpoint_types?.includes(value) ?? false ), })), ] return ( ) }