From a2dacd9046361ad5a7012414b1c3d47f85e81585 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Fri, 1 May 2026 00:09:46 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=F0=9F=8E=A8=20feat(web/default):=20add?= =?UTF-8?q?=20shadcn-style=20theme=20presets,=20radius=20prefs,=20and=20fi?= =?UTF-8?q?x=20selection=20badges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrate the qn-platform–style OKLCH color system into the default frontend while keeping the existing blue-tinted dark tokens for the default theme. Add [data-theme-preset] palettes for seven named presets plus the default zinc-like scale, define [data-theme-radius] overrides so user radius beats preset --radius, and align the Tailwind @custom-variant dark helper with .dark usage. Introduce ThemeCustomizationProvider to own preset and radius state, persist choices in cookies (theme-preset, theme-radius), and sync data-theme-preset / data-theme-radius on . Wrap the tree in main.tsx. Extend ConfigDrawer with theme preset swatches (scoped data-theme-preset) and radius previews wired to context; refactor swatch/card markup so selected CircleCheck badges sit outside clipped rows (remove outer overflow-hidden that hid the centered checkmark). Add i18n keys for preset names, radius, and accessibility labels across en, zh, fr, ja, ru, vi. --- web/default/src/components/config-drawer.tsx | 245 +++-- .../data-table/mobile-card-list.tsx | 2 +- .../components/layout/components/footer.tsx | 9 +- .../layout/components/public-layout.tsx | 2 +- .../layout/components/section-page-layout.tsx | 4 +- .../layout/components/workspace-switcher.tsx | 2 +- web/default/src/components/status-badge.tsx | 3 +- web/default/src/components/ui/titled-card.tsx | 14 +- .../context/theme-customization-provider.tsx | 144 +++ .../channels/components/channels-table.tsx | 4 +- .../models/consumption-distribution-chart.tsx | 16 +- .../components/models/log-stat-cards.tsx | 2 +- .../models/models-chart-preferences.tsx | 2 +- .../models/models-filter-dialog.tsx | 4 +- .../dashboard/components/ui/stat-card.tsx | 4 +- web/default/src/features/dashboard/index.tsx | 13 +- .../src/features/dashboard/lib/charts.ts | 20 +- .../src/features/dashboard/lib/filters.ts | 4 +- .../home/components/hero-terminal-demo.tsx | 40 +- .../components/api-key-group-combobox.tsx | 9 +- .../components/api-keys-mutate-drawer.tsx | 9 +- .../keys/components/api-keys-table.tsx | 23 +- .../dialogs/view-details-dialog.tsx | 8 +- web/default/src/features/models/index.tsx | 4 +- .../components/dynamic-pricing-breakdown.tsx | 3 +- .../pricing/components/filter-bar.tsx | 727 --------------- .../src/features/pricing/components/index.ts | 1 - .../pricing/components/model-card.tsx | 62 +- .../pricing/components/model-details.tsx | 50 +- .../pricing/components/pricing-columns.tsx | 10 +- .../pricing/components/pricing-sidebar.tsx | 21 +- .../pricing/components/pricing-toolbar.tsx | 12 +- .../src/features/pricing/hooks/use-filters.ts | 23 +- web/default/src/features/pricing/index.tsx | 32 +- .../src/features/pricing/lib/dynamic-price.ts | 6 +- web/default/src/features/profile/api.ts | 4 +- .../components/language-preferences-card.tsx | 58 +- .../profile/components/passkey-card.tsx | 64 +- .../components/profile-security-card.tsx | 60 +- .../components/profile-settings-card.tsx | 62 +- .../dialogs/subscription-purchase-dialog.tsx | 2 +- .../components/subscriptions-columns.tsx | 4 +- .../components/columns/column-helpers.tsx | 5 +- .../columns/common-logs-columns.tsx | 86 +- .../columns/drawing-logs-columns.tsx | 2 +- .../components/columns/task-logs-columns.tsx | 107 +-- .../components/common-logs-filter-bar.tsx | 22 +- .../components/common-logs-stats.tsx | 4 +- .../compact-date-time-range-picker.tsx | 5 +- .../components/dialogs/details-dialog.tsx | 4 +- .../usage-logs/components/model-badge.tsx | 2 +- .../components/task-logs-filter-bar.tsx | 10 +- .../components/usage-logs-table.tsx | 9 +- web/default/src/features/usage-logs/index.tsx | 9 +- .../src/features/usage-logs/lib/format.ts | 68 +- .../users/components/users-columns.tsx | 6 +- .../components/affiliate-rewards-card.tsx | 2 +- .../wallet/components/recharge-form-card.tsx | 512 ++++++----- .../components/subscription-plans-card.tsx | 636 +++++++------ web/default/src/i18n/locales/en.json | 23 +- web/default/src/i18n/locales/fr.json | 23 +- web/default/src/i18n/locales/ja.json | 23 +- web/default/src/i18n/locales/ru.json | 23 +- web/default/src/i18n/locales/vi.json | 23 +- web/default/src/i18n/locales/zh.json | 23 +- web/default/src/lib/colors.ts | 18 +- web/default/src/main.tsx | 13 +- web/default/src/styles/theme.css | 858 +++++++++++++++++- 68 files changed, 2344 insertions(+), 1960 deletions(-) create mode 100644 web/default/src/context/theme-customization-provider.tsx delete mode 100644 web/default/src/features/pricing/components/filter-bar.tsx diff --git a/web/default/src/components/config-drawer.tsx b/web/default/src/components/config-drawer.tsx index c516db39b14..e32ea66ee2d 100644 --- a/web/default/src/components/config-drawer.tsx +++ b/web/default/src/components/config-drawer.tsx @@ -1,6 +1,6 @@ import { type SVGProps } from 'react' import { Root as Radio, Item } from '@radix-ui/react-radio-group' -import { CircleCheck, RotateCcw, Palette } from 'lucide-react' +import { CircleCheck, Palette, RotateCcw } from 'lucide-react' import { useTranslation } from 'react-i18next' import { IconDir } from '@/assets/custom/icon-dir' import { IconLayoutCompact } from '@/assets/custom/icon-layout-compact' @@ -15,6 +15,11 @@ import { IconThemeSystem } from '@/assets/custom/icon-theme-system' import { cn } from '@/lib/utils' import { useDirection } from '@/context/direction-provider' import { type Collapsible, useLayout } from '@/context/layout-provider' +import { + type ThemePreset, + type ThemeRadius, + useThemeCustomization, +} from '@/context/theme-customization-provider' import { useTheme } from '@/context/theme-provider' import { Button } from '@/components/ui/button' import { @@ -33,12 +38,14 @@ export function ConfigDrawer() { const { setOpen } = useSidebar() const { resetDir } = useDirection() const { resetTheme } = useTheme() + const { resetThemeCustomization } = useThemeCustomization() const { resetLayout } = useLayout() const handleReset = () => { setOpen(true) resetDir() resetTheme() + resetThemeCustomization() resetLayout() } @@ -64,6 +71,8 @@ export function ConfigDrawer() {
+ + @@ -82,12 +91,7 @@ export function ConfigDrawer() { ) } -function SectionTitle({ - title, - showReset = false, - onReset, - className, -}: { +function SectionTitle(props: { title: string showReset?: boolean onReset?: () => void @@ -97,16 +101,16 @@ function SectionTitle({
- {title} - {showReset && onReset && ( + {props.title} + {props.showReset && props.onReset && ( diff --git a/web/default/src/features/models/index.tsx b/web/default/src/features/models/index.tsx index df8fb005e02..76b8291f45b 100644 --- a/web/default/src/features/models/index.tsx +++ b/web/default/src/features/models/index.tsx @@ -107,9 +107,7 @@ function ModelsContent() { return ( <> - - {t(meta.titleKey)} - + {t(meta.titleKey)} {t(meta.descriptionKey)} diff --git a/web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx b/web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx index d9f50f0f133..f0847e14995 100644 --- a/web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx +++ b/web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx @@ -242,8 +242,7 @@ export function DynamicPricingBreakdown({ key={`tier-mobile-${i}`} className={cn( 'rounded-md border p-2', - isMatched && - 'border-emerald-500/40 bg-emerald-500/10' + isMatched && 'border-emerald-500/40 bg-emerald-500/10' )} >
diff --git a/web/default/src/features/pricing/components/filter-bar.tsx b/web/default/src/features/pricing/components/filter-bar.tsx deleted file mode 100644 index dc68f856978..00000000000 --- a/web/default/src/features/pricing/components/filter-bar.tsx +++ /dev/null @@ -1,727 +0,0 @@ -import { useCallback, useMemo, useState } from 'react' -import { - ArrowUpDown, - Check, - ChevronDown, - Filter, - RotateCcw, - Table2, - X, -} 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 { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from '@/components/ui/command' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover' -import { - Sheet, - SheetContent, - SheetDescription, - SheetFooter, - SheetHeader, - SheetTitle, -} from '@/components/ui/sheet' -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@/components/ui/tooltip' -import { - FILTER_ALL, - QUOTA_TYPES, - ENDPOINT_TYPES, - VIEW_MODES, - getSortLabels, - getQuotaTypeLabels, - getEndpointTypeLabels, - type SortOption, - type ViewMode, -} from '../constants' -import type { PricingVendor, TokenUnit } from '../types' - -interface FilterOption { - value: string - label: string - icon?: React.ReactNode -} - -interface FilterChipProps { - label: string - value: string - options: FilterOption[] - onChange: (value: string) => void - defaultValue?: string - searchPlaceholder?: string - className?: string -} - -function FilterChip(props: FilterChipProps) { - const { t } = useTranslation() - const [open, setOpen] = useState(false) - const defaultVal = props.defaultValue ?? FILTER_ALL - const isActive = props.value !== defaultVal - const selectedOption = props.options.find((o) => o.value === props.value) - - return ( - - - - - - - - - {t('No results found')} - - {props.options.map((option) => ( - { - props.onChange(option.value) - setOpen(false) - }} - className='gap-2' - > - - {option.icon && ( - {option.icon} - )} - {option.label} - - ))} - - - - - - ) -} - -interface SegmentedControlProps { - options: Array<{ - value: string - label?: string - icon?: React.ComponentType<{ className?: string }> - tooltip?: string - }> - value: string - onChange: (value: string) => void - ariaLabel?: string -} - -function SegmentedControl(props: SegmentedControlProps) { - return ( -
- {props.options.map((option) => { - const isActive = option.value === props.value - const Icon = option.icon - const isIconOnly = Icon && !option.label - const button = ( - - ) - - if (option.tooltip) { - return ( - - {button} - - {option.tooltip} - - - ) - } - - return button - })} -
- ) -} - -interface ActiveFilterBadgeProps { - label: string - onRemove: () => void -} - -function ActiveFilterBadge(props: ActiveFilterBadgeProps) { - const { t } = useTranslation() - - return ( - - {props.label} - - - ) -} - -interface MobileFilterGroupProps { - title: string - value: string - options: FilterOption[] - onChange: (value: string) => void -} - -function MobileFilterGroup(props: MobileFilterGroupProps) { - return ( -
-

{props.title}

-
- {props.options.map((option) => { - const isActive = props.value === option.value - return ( - - ) - })} -
-
- ) -} - -export interface FilterBarProps { - quotaTypeFilter: string - endpointTypeFilter: string - vendorFilter: string - groupFilter: string - tagFilter: string - onQuotaTypeChange: (v: string) => void - onEndpointTypeChange: (v: string) => void - onVendorChange: (v: string) => void - onGroupChange: (v: string) => void - onTagChange: (v: string) => void - vendors: PricingVendor[] - groups: string[] - tags: string[] - sortBy: string - onSortChange: (v: string) => void - tokenUnit: TokenUnit - onTokenUnitChange: (v: TokenUnit) => void - showRechargePrice: boolean - onRechargePriceChange: (v: boolean) => void - viewMode: ViewMode - onViewModeChange: (v: ViewMode) => void - hasActiveFilters: boolean - activeFilterCount: number - onClearFilters: () => void - filteredCount: number - totalCount?: number -} - -export function FilterBar(props: FilterBarProps) { - const { t } = useTranslation() - const [mobileOpen, setMobileOpen] = useState(false) - - const { - quotaTypeFilter, - endpointTypeFilter, - vendorFilter, - groupFilter, - tagFilter, - onQuotaTypeChange, - onEndpointTypeChange, - onVendorChange, - onGroupChange, - onTagChange, - vendors, - groups, - tags, - onTokenUnitChange, - onViewModeChange, - onRechargePriceChange, - } = props - - const quotaTypeLabels = getQuotaTypeLabels(t) - const endpointTypeLabels = getEndpointTypeLabels(t) - const sortLabels = getSortLabels(t) - - const typeOptions = useMemo( - () => - Object.entries(quotaTypeLabels).map(([value, label]) => ({ - value, - label, - })), - [quotaTypeLabels] - ) - - const endpointOptions = useMemo( - () => - Object.entries(endpointTypeLabels).map(([value, label]) => ({ - value, - label, - })), - [endpointTypeLabels] - ) - - const vendorOptions = useMemo( - () => [ - { value: FILTER_ALL, label: t('All Vendors') }, - ...vendors.map((v) => ({ - value: v.name, - label: v.name, - icon: v.icon ? getLobeIcon(v.icon, 14) : undefined, - })), - ], - [vendors, t] - ) - - const groupOptions = useMemo( - () => [ - { value: FILTER_ALL, label: t('All Groups') }, - ...groups.map((g) => ({ value: g, label: g })), - ], - [groups, t] - ) - - const tagOptions = useMemo( - () => [ - { value: FILTER_ALL, label: t('All Tags') }, - ...tags.map((tag) => ({ value: tag, label: tag })), - ], - [tags, t] - ) - - const activeFilters = useMemo(() => { - const filters: Array<{ key: string; label: string; onRemove: () => void }> = - [] - - if (quotaTypeFilter !== QUOTA_TYPES.ALL) { - filters.push({ - key: 'quotaType', - label: - quotaTypeLabels[quotaTypeFilter as keyof typeof quotaTypeLabels] || - quotaTypeFilter, - onRemove: () => onQuotaTypeChange(QUOTA_TYPES.ALL), - }) - } - - if (endpointTypeFilter !== ENDPOINT_TYPES.ALL) { - filters.push({ - key: 'endpointType', - label: - endpointTypeLabels[ - endpointTypeFilter as keyof typeof endpointTypeLabels - ] || endpointTypeFilter, - onRemove: () => onEndpointTypeChange(ENDPOINT_TYPES.ALL), - }) - } - - if (vendorFilter !== FILTER_ALL) { - filters.push({ - key: 'vendor', - label: vendorFilter, - onRemove: () => onVendorChange(FILTER_ALL), - }) - } - - if (groupFilter !== FILTER_ALL) { - filters.push({ - key: 'group', - label: groupFilter, - onRemove: () => onGroupChange(FILTER_ALL), - }) - } - - if (tagFilter !== FILTER_ALL) { - filters.push({ - key: 'tag', - label: tagFilter, - onRemove: () => onTagChange(FILTER_ALL), - }) - } - - return filters - }, [ - quotaTypeFilter, - endpointTypeFilter, - vendorFilter, - groupFilter, - tagFilter, - onQuotaTypeChange, - onEndpointTypeChange, - onVendorChange, - onGroupChange, - onTagChange, - quotaTypeLabels, - endpointTypeLabels, - ]) - - const handleTokenUnitChange = useCallback( - (v: string) => onTokenUnitChange(v as TokenUnit), - [onTokenUnitChange] - ) - - const handleViewModeChange = useCallback( - (v: string) => onViewModeChange(v as ViewMode), - [onViewModeChange] - ) - - const handleRechargePriceChange = useCallback( - (v: string) => onRechargePriceChange(v === 'recharge'), - [onRechargePriceChange] - ) - - return ( -
-
-
- - - {props.vendors.length > 0 && ( - - )} - {props.groups.length > 0 && ( - - )} - {props.tags.length > 0 && ( - - )} -
- - - -
- -
-
- - -
-
- - - - - - - {Object.entries(sortLabels).map(([value, label]) => ( - props.onSortChange(value)} - className='gap-2' - > - - {label} - - ))} - - - - -
-
- - {activeFilters.length > 0 && ( -
- {activeFilters.map((filter) => ( - - ))} - -
- )} - -
- - {props.filteredCount.toLocaleString()} - - {props.filteredCount === 1 ? t('model') : t('models')} - {props.hasActiveFilters && props.totalCount && ( - - {t('of')} {props.totalCount.toLocaleString()} - - )} -
- - - - - {t('Filters')} - - {t('Filter models by type, endpoint, vendor, group and tags')} - - - -
- - - {props.vendors.length > 0 && ( - - )} - {props.groups.length > 0 && ( - - )} - {props.tags.length > 0 && ( - - )} - -
-

- {t('Display Options')} -

-
-
-

- {t('Price display')} -

- -
-
-

- {t('Token unit')} -

- -
-
-
-
- - -
- {props.hasActiveFilters && ( - - )} - -
-
-
-
-
- ) -} diff --git a/web/default/src/features/pricing/components/index.ts b/web/default/src/features/pricing/components/index.ts index e862c740909..8273f10fcfb 100644 --- a/web/default/src/features/pricing/components/index.ts +++ b/web/default/src/features/pricing/components/index.ts @@ -1,4 +1,3 @@ -export { FilterBar } from './filter-bar' export { PricingSidebar } from './pricing-sidebar' export { PricingToolbar } from './pricing-toolbar' export { ModelCard } from './model-card' diff --git a/web/default/src/features/pricing/components/model-card.tsx b/web/default/src/features/pricing/components/model-card.tsx index ca0e12d09be..36d459ebed6 100644 --- a/web/default/src/features/pricing/components/model-card.tsx +++ b/web/default/src/features/pricing/components/model-card.tsx @@ -3,16 +3,16 @@ import { ChevronRight, Copy } from 'lucide-react' import { useTranslation } from 'react-i18next' import { getLobeIcon } from '@/lib/lobe-icon' import { cn } from '@/lib/utils' -import { StatusBadge } from '@/components/status-badge' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' +import { StatusBadge } from '@/components/status-badge' import { DEFAULT_TOKEN_UNIT } from '../constants' -import { parseTags } from '../lib/filters' -import { isTokenBasedModel } from '../lib/model-helpers' -import { formatPrice, formatRequestPrice } from '../lib/price' import { getDynamicDisplayGroupRatio, getDynamicPricingSummary, } from '../lib/dynamic-price' +import { parseTags } from '../lib/filters' +import { isTokenBasedModel } from '../lib/model-helpers' +import { formatPrice, formatRequestPrice } from '../lib/price' import type { PricingModel, TokenUnit } from '../types' export interface ModelCardProps { @@ -41,7 +41,8 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { : null const initial = props.model.model_name?.charAt(0).toUpperCase() || '?' const isDynamicPricing = - props.model.billing_mode === 'tiered_expr' && Boolean(props.model.billing_expr) + props.model.billing_mode === 'tiered_expr' && + Boolean(props.model.billing_expr) const hasCachedPrice = isTokenBased && props.model.cache_ratio != null const dynamicSummary = isDynamicPricing ? getDynamicPricingSummary(props.model, { @@ -83,7 +84,7 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { )}
-

+

{props.model.model_name}

@@ -93,7 +94,7 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { {t('Special billing expression')} - + {dynamicSummary.rawExpression} @@ -122,14 +123,28 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { {t('Input')}{' '} - {formatPrice(props.model, 'input', tokenUnit, showRechargePrice, priceRate, usdExchangeRate)} + {formatPrice( + props.model, + 'input', + tokenUnit, + showRechargePrice, + priceRate, + usdExchangeRate + )} /{tokenUnitLabel} {t('Output')}{' '} - {formatPrice(props.model, 'output', tokenUnit, showRechargePrice, priceRate, usdExchangeRate)} + {formatPrice( + props.model, + 'output', + tokenUnit, + showRechargePrice, + priceRate, + usdExchangeRate + )} /{tokenUnitLabel} @@ -137,7 +152,14 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { {t('Cached')}{' '} - {formatPrice(props.model, 'cache', tokenUnit, showRechargePrice, priceRate, usdExchangeRate)} + {formatPrice( + props.model, + 'cache', + tokenUnit, + showRechargePrice, + priceRate, + usdExchangeRate + )} )} @@ -145,9 +167,14 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { ) : ( - {formatRequestPrice(props.model, showRechargePrice, priceRate, usdExchangeRate)} - - {' '}/ {t('request')} + {formatRequestPrice( + props.model, + showRechargePrice, + priceRate, + usdExchangeRate + )} + {' '} + / {t('request')} )}
@@ -202,14 +229,13 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { {/* Footer row 2: endpoint + tag chips */}
{bottomTags.map((item) => ( - + {item} ))} - {tokenUnitLabel} + + {tokenUnitLabel} + {hiddenCount > 0 && ( +{hiddenCount} diff --git a/web/default/src/features/pricing/components/model-details.tsx b/web/default/src/features/pricing/components/model-details.tsx index 059e3cebe7a..fca4ee43afa 100644 --- a/web/default/src/features/pricing/components/model-details.tsx +++ b/web/default/src/features/pricing/components/model-details.tsx @@ -4,6 +4,13 @@ import { ArrowLeft } from 'lucide-react' import { useTranslation } from 'react-i18next' import { getLobeIcon } from '@/lib/lobe-icon' import { Button } from '@/components/ui/button' +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet' import { Skeleton } from '@/components/ui/skeleton' import { Table, @@ -13,30 +20,23 @@ import { TableHeader, TableRow, } from '@/components/ui/table' -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from '@/components/ui/sheet' import { CopyButton } from '@/components/copy-button' import { GroupBadge } from '@/components/group-badge' import { PublicLayout } from '@/components/layout' import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants' import { usePricingData } from '../hooks/use-pricing-data' -import { parseTags } from '../lib/filters' -import { - getAvailableGroups, - replaceModelInPath, - isTokenBasedModel, -} from '../lib/model-helpers' import { getDynamicPriceEntries, getDynamicPricingSummary, getDynamicPricingTiers, isDynamicPricingModel, } from '../lib/dynamic-price' +import { parseTags } from '../lib/filters' +import { + getAvailableGroups, + replaceModelInPath, + isTokenBasedModel, +} from '../lib/model-helpers' import { formatGroupPrice, formatFixedPrice } from '../lib/price' import type { PricingModel, TokenUnit, PriceType } from '../types' import { DynamicPricingBreakdown } from './dynamic-pricing-breakdown' @@ -128,13 +128,8 @@ function PriceSection(props: { showRechargePrice: boolean }) { const { t } = useTranslation() - const { - model, - priceRate, - usdExchangeRate, - tokenUnit, - showRechargePrice, - } = props + const { model, priceRate, usdExchangeRate, tokenUnit, showRechargePrice } = + props const isTokenBased = isTokenBasedModel(model) const tokenUnitLabel = tokenUnit === 'K' ? '1K' : '1M' const baseGroupKey = '_base' @@ -190,7 +185,7 @@ function PriceSection(props: {
{t('Base Price')}
-
+
{t('Special billing expression')}

@@ -200,7 +195,7 @@ function PriceSection(props: {

{t('Raw expression')}
- + {dynamicSummary.rawExpression}
@@ -215,7 +210,10 @@ function PriceSection(props: { {dynamicSummary.primaryEntries.length > 0 ? (
{dynamicSummary.primaryEntries.map((entry) => ( -
+
{t(entry.shortLabel)}
@@ -306,7 +304,7 @@ function PriceSection(props: { {t('Base Price')}
{primaryPriceTypes.map((item) => ( -
+
{item.label}
{renderPrice(item.type)} @@ -482,7 +480,7 @@ function GroupPricingSection(props: { {t('Pricing by Group')}
-
+
{t('Special billing expression')}

@@ -494,7 +492,7 @@ function GroupPricingSection(props: {

{t('Raw expression')}
- + {model.billing_expr}
diff --git a/web/default/src/features/pricing/components/pricing-columns.tsx b/web/default/src/features/pricing/components/pricing-columns.tsx index 48e09475983..5b7dad7e57c 100644 --- a/web/default/src/features/pricing/components/pricing-columns.tsx +++ b/web/default/src/features/pricing/components/pricing-columns.tsx @@ -10,12 +10,12 @@ import { import { DataTableColumnHeader } from '@/components/data-table/column-header' import { GroupBadge } from '@/components/group-badge' import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants' -import { parseTags } from '../lib/filters' -import { isTokenBasedModel } from '../lib/model-helpers' import { getDynamicDisplayGroupRatio, getDynamicPricingSummary, } from '../lib/dynamic-price' +import { parseTags } from '../lib/filters' +import { isTokenBasedModel } from '../lib/model-helpers' import { formatPrice, formatRequestPrice, @@ -152,14 +152,14 @@ export function usePricingColumns( if (dynamicSummary) { if (dynamicSummary.isSpecialExpression) { return ( -
-
+
+
{t('Special billing expression')}
{t('Unable to parse structured pricing')}
- + {dynamicSummary.rawExpression}
diff --git a/web/default/src/features/pricing/components/pricing-sidebar.tsx b/web/default/src/features/pricing/components/pricing-sidebar.tsx index 35598a0bb13..5bcd117deb2 100644 --- a/web/default/src/features/pricing/components/pricing-sidebar.tsx +++ b/web/default/src/features/pricing/components/pricing-sidebar.tsx @@ -88,7 +88,9 @@ function FilterChip(props: { )} title={props.option.label} > - {props.option.icon && {props.option.icon}} + {props.option.icon && ( + {props.option.icon} + )} {props.option.label} {(props.option.suffix || props.option.count != null) && ( + {props.title} @@ -213,19 +218,15 @@ export function PricingSidebar(props: PricingSidebarProps) { .map(([value, label]) => ({ value, label, - count: countBy(props.models, (model) => - model.supported_endpoint_types?.includes(value) ?? false + count: countBy( + props.models, + (model) => model.supported_endpoint_types?.includes(value) ?? false ), })), ] return ( -