feat: update theme colors
This commit is contained in:
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user