feat: update theme colors

This commit is contained in:
CaIon
2026-07-11 19:25:04 +08:00
parent 6bbddb1046
commit 162f87925c
75 changed files with 1553 additions and 655 deletions
@@ -0,0 +1,54 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useTranslation } from 'react-i18next'
import { StatusBadge, type StatusVariant } from '@/components/status-badge'
import { isDynamicPricingModel } from '../lib/dynamic-price'
import { isTokenBasedModel } from '../lib/model-helpers'
import type { PricingModel } from '../types'
interface ModelBillingModeBadgeProps {
model: PricingModel
className?: string
}
export function ModelBillingModeBadge(props: ModelBillingModeBadgeProps) {
const { t } = useTranslation()
let label = t('Per Request')
let variant: StatusVariant = 'purple'
if (isDynamicPricingModel(props.model)) {
label = t('Dynamic Pricing')
variant = 'warning'
} else if (isTokenBasedModel(props.model)) {
label = t('Token-based')
variant = 'info'
}
return (
<StatusBadge
label={label}
variant={variant}
copyable={false}
size='sm'
className={props.className}
/>
)
}
+6 -16
View File
@@ -20,7 +20,6 @@ import { ChevronRight, Copy } from 'lucide-react'
import { memo, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { StatusBadge } from '@/components/status-badge'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { getLobeIcon } from '@/lib/lobe-icon'
import { cn } from '@/lib/utils'
@@ -34,6 +33,7 @@ import { parseTags } from '../lib/filters'
import { isTokenBasedModel } from '../lib/model-helpers'
import { formatPrice, formatRequestPrice } from '../lib/price'
import type { PricingModel, TokenUnit } from '../types'
import { ModelBillingModeBadge } from './model-billing-mode-badge'
import { ModelPerfBadge, type ModelPerfBadgeData } from './model-perf-badge'
export interface ModelCardProps {
@@ -159,9 +159,9 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
</span>
</span>
{hasCachedPrice && (
<span className='text-muted-foreground/60 whitespace-nowrap'>
<span className='text-muted-foreground whitespace-nowrap'>
{t('Cached')}{' '}
<span className='font-mono'>
<span className='text-foreground font-mono font-semibold'>
{formatPrice(
props.model,
'cache',
@@ -249,21 +249,11 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
<div className='mt-2 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-x-2 gap-y-1 sm:mt-4'>
<div className='flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1'>
{primaryGroup && (
<span className='text-muted-foreground text-xs font-medium'>
{primaryGroup} {t('Groups')}
<span className='text-muted-foreground text-sm font-medium'>
{primaryGroup}
</span>
)}
<span className='text-muted-foreground text-xs font-medium'>
{isTokenBased ? t('Token-based') : t('Per Request')}
</span>
{isDynamicPricing && (
<StatusBadge
label={t('Dynamic Pricing')}
variant='warning'
copyable={false}
size='sm'
/>
)}
<ModelBillingModeBadge model={props.model} />
</div>
<ModelPerfBadge perf={props.perf} className='row-span-2 self-start' />
+23 -38
View File
@@ -58,7 +58,7 @@ import {
import { getLobeIcon } from '@/lib/lobe-icon'
import { cn } from '@/lib/utils'
import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants'
import { DEFAULT_TOKEN_UNIT } from '../constants'
import { usePricingData } from '../hooks/use-pricing-data'
import {
getDynamicPriceEntries,
@@ -76,6 +76,7 @@ import type {
TokenUnit,
} from '../types'
import { DynamicPricingBreakdown } from './dynamic-pricing-breakdown'
import { ModelBillingModeBadge } from './model-billing-mode-badge'
import { ModelDetailsApi } from './model-details-api'
import { ModelDetailsPerformance } from './model-details-performance'
@@ -117,6 +118,7 @@ const MODALITY_LABEL_KEYS: Record<string, string> = {
const TOKEN_FORMAT = new Intl.NumberFormat(undefined, {
maximumFractionDigits: 1,
})
const MODEL_DETAILS_SKELETON_KEYS = ['first', 'second', 'third', 'fourth']
function formatCatalogTokenCount(tokens: number): string {
if (!Number.isFinite(tokens) || tokens <= 0) return ''
@@ -459,11 +461,7 @@ function ModelBackendProviderSection(props: { model: PricingModel }) {
cells.push(
<CatalogInfoCell key='type' label={t('Type')}>
<CatalogTextValue>
{model.quota_type === QUOTA_TYPE_VALUES.TOKEN
? t('Token-based')
: t('Per Request')}
</CatalogTextValue>
<ModelBillingModeBadge model={model} />
</CatalogInfoCell>
)
@@ -531,10 +529,6 @@ function ModelHeader(props: { model: PricingModel }) {
const modelIconKey = model.icon || model.vendor_icon
const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 20) : null
const description = model.description || model.vendor_description || null
const isSpecialExpression =
model.billing_mode === 'tiered_expr' &&
Boolean(model.billing_expr) &&
getDynamicPricingTiers(model).length === 0
return (
<header className='pb-4'>
@@ -557,21 +551,7 @@ function ModelHeader(props: { model: PricingModel }) {
<span className='text-muted-foreground'>{model.vendor_name}</span>
)}
<span className='text-muted-foreground/30'>·</span>
<span className='text-muted-foreground/70'>
{model.quota_type === QUOTA_TYPE_VALUES.TOKEN
? t('Token-based')
: t('Per Request')}
</span>
{model.billing_mode === 'tiered_expr' && model.billing_expr && (
<>
<span className='text-muted-foreground/30'>·</span>
<span className='rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:bg-amber-500/20 dark:text-amber-300'>
{isSpecialExpression
? t('Special billing expression')
: t('Dynamic Pricing')}
</span>
</>
)}
<ModelBillingModeBadge model={model} />
</div>
{description && (
<p className='text-muted-foreground mt-2 text-sm leading-relaxed'>
@@ -839,13 +819,13 @@ function getDynamicPriceFields(
tiers: DynamicPricingTier[],
options: DynamicPriceOptions
) {
return Array.from(
new Map(
return [
...new Map(
tiers
.flatMap((tier) => getDynamicPriceEntries(tier, options))
.map((entry) => [entry.field, entry])
).values()
)
).values(),
]
}
function getDynamicFormattedPricesByTier(
@@ -892,19 +872,24 @@ function GroupPricingSection(props: {
const extraPriceTypes = useMemo(() => {
const types: { label: string; type: PriceType }[] = []
if (props.model.cache_ratio != null)
if (props.model.cache_ratio != null) {
types.push({ label: t('Cache'), type: 'cache' })
if (props.model.create_cache_ratio != null)
}
if (props.model.create_cache_ratio != null) {
types.push({ label: t('Cache Write'), type: 'create_cache' })
if (props.model.image_ratio != null)
}
if (props.model.image_ratio != null) {
types.push({ label: t('Image'), type: 'image' })
if (props.model.audio_ratio != null)
}
if (props.model.audio_ratio != null) {
types.push({ label: t('Audio In'), type: 'audio_input' })
}
if (
props.model.audio_ratio != null &&
props.model.audio_completion_ratio != null
)
) {
types.push({ label: t('Audio Out'), type: 'audio_output' })
}
return types
}, [props.model, t])
@@ -1299,13 +1284,13 @@ export function ModelDetails() {
<Skeleton className='h-4 w-full max-w-md' />
</div>
<div className='mt-6 grid grid-cols-2 gap-2 sm:grid-cols-4'>
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className='h-16 w-full' />
{MODEL_DETAILS_SKELETON_KEYS.map((key) => (
<Skeleton key={`metric-${key}`} className='h-16 w-full' />
))}
</div>
<div className='mt-6 space-y-3'>
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className='h-24 w-full' />
{MODEL_DETAILS_SKELETON_KEYS.map((key) => (
<Skeleton key={`section-${key}`} className='h-24 w-full' />
))}
</div>
</div>
@@ -28,7 +28,7 @@ import { GroupBadge } from '@/components/group-badge'
import { StatusBadge } from '@/components/status-badge'
import { getLobeIcon } from '@/lib/lobe-icon'
import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants'
import { DEFAULT_TOKEN_UNIT } from '../constants'
import {
getDynamicDisplayGroupRatio,
getDynamicPricingSummary,
@@ -41,6 +41,7 @@ import {
stripTrailingZeros,
} from '../lib/price'
import type { PricingModel, TokenUnit } from '../types'
import { ModelBillingModeBadge } from './model-billing-mode-badge'
// ----------------------------------------------------------------------------
// Pricing Table Columns
@@ -97,18 +98,10 @@ export function usePricingColumns(
{
accessorKey: 'quota_type',
header: t('Type'),
cell: ({ row }) => {
const isTokenBased = row.original.quota_type === QUOTA_TYPE_VALUES.TOKEN
return (
<StatusBadge
label={isTokenBased ? t('Token') : t('Request')}
variant={isTokenBased ? 'info' : 'neutral'}
copyable={false}
className='-ml-1.5'
/>
)
},
size: 80,
cell: ({ row }) => (
<ModelBillingModeBadge model={row.original} className='-ml-1.5' />
),
size: 110,
enableSorting: false,
},
@@ -115,7 +115,7 @@ function FilterChip(props: {
{(props.option.suffix || props.option.count != null) && (
<span
className={cn(
'rounded-md px-1.5 py-0.5 text-[10px]',
'rounded-md px-1.5 py-0.5 text-[12px]',
props.active
? 'bg-background text-foreground'
: 'bg-muted text-muted-foreground'