feat(ui): redesign model square pricing page
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import { formatBillingCurrencyFromUSD } from '@/lib/currency'
|
||||
import { TOKEN_UNIT_DIVISORS } from '../constants'
|
||||
import {
|
||||
BILLING_PRICING_VARS,
|
||||
parseTiersFromExpr,
|
||||
splitBillingExprAndRequestRules,
|
||||
tryParseRequestRuleExpr,
|
||||
type BillingVar,
|
||||
type ParsedTier,
|
||||
} from './billing-expr'
|
||||
import type { PricingModel, TokenUnit } from '../types'
|
||||
|
||||
type DynamicPriceOptions = {
|
||||
tokenUnit: TokenUnit
|
||||
showRechargePrice?: boolean
|
||||
priceRate?: number
|
||||
usdExchangeRate?: number
|
||||
groupRatioMultiplier?: number
|
||||
}
|
||||
|
||||
export type DynamicPriceEntry = {
|
||||
key: string
|
||||
field: string
|
||||
label: string
|
||||
shortLabel: string
|
||||
value: number
|
||||
formatted: string
|
||||
variable: BillingVar
|
||||
}
|
||||
|
||||
export type DynamicPricingSummary = {
|
||||
tiers: ParsedTier[]
|
||||
tier: ParsedTier | null
|
||||
tierCount: number
|
||||
hasRequestRules: boolean
|
||||
isSpecialExpression: boolean
|
||||
rawExpression: string
|
||||
entries: DynamicPriceEntry[]
|
||||
primaryEntries: DynamicPriceEntry[]
|
||||
secondaryEntries: DynamicPriceEntry[]
|
||||
}
|
||||
|
||||
const PRIMARY_DYNAMIC_FIELDS = new Set(['inputPrice', 'outputPrice'])
|
||||
|
||||
export function isDynamicPricingModel(model: PricingModel): boolean {
|
||||
return model.billing_mode === 'tiered_expr' && Boolean(model.billing_expr)
|
||||
}
|
||||
|
||||
export function getDynamicDisplayGroupRatio(model: PricingModel): number {
|
||||
const groups = Array.isArray(model.enable_groups) ? model.enable_groups : []
|
||||
const ratios = model.group_ratio || {}
|
||||
if (groups.length === 0) return 1
|
||||
|
||||
let minRatio = Number.POSITIVE_INFINITY
|
||||
for (const group of groups) {
|
||||
const ratio = ratios[group]
|
||||
if (ratio !== undefined && ratio < minRatio) {
|
||||
minRatio = ratio
|
||||
}
|
||||
}
|
||||
|
||||
return minRatio === Number.POSITIVE_INFINITY ? 1 : minRatio
|
||||
}
|
||||
|
||||
function applyRechargeRate(
|
||||
price: number,
|
||||
showWithRecharge: boolean,
|
||||
priceRate: number,
|
||||
usdExchangeRate: number
|
||||
): number {
|
||||
if (!showWithRecharge) return price
|
||||
return (price * priceRate) / usdExchangeRate
|
||||
}
|
||||
|
||||
export function formatDynamicUnitPrice(
|
||||
valuePerMillionTokens: number,
|
||||
options: DynamicPriceOptions
|
||||
): string {
|
||||
const groupRatio = options.groupRatioMultiplier ?? 1
|
||||
const priceRate = options.priceRate ?? 1
|
||||
const usdExchangeRate = options.usdExchangeRate ?? 1
|
||||
const priceUSD =
|
||||
(valuePerMillionTokens * groupRatio) /
|
||||
TOKEN_UNIT_DIVISORS[options.tokenUnit]
|
||||
const displayPrice = applyRechargeRate(
|
||||
priceUSD,
|
||||
options.showRechargePrice ?? false,
|
||||
priceRate,
|
||||
usdExchangeRate
|
||||
)
|
||||
|
||||
return formatBillingCurrencyFromUSD(displayPrice, {
|
||||
digitsLarge: 4,
|
||||
digitsSmall: 6,
|
||||
abbreviate: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function getDynamicPricingTiers(model: PricingModel): ParsedTier[] {
|
||||
if (!isDynamicPricingModel(model)) return []
|
||||
const { billingExpr } = splitBillingExprAndRequestRules(model.billing_expr || '')
|
||||
return parseTiersFromExpr(billingExpr)
|
||||
}
|
||||
|
||||
export function hasDynamicRequestRules(model: PricingModel): boolean {
|
||||
if (!isDynamicPricingModel(model)) return false
|
||||
const { requestRuleExpr } = splitBillingExprAndRequestRules(
|
||||
model.billing_expr || ''
|
||||
)
|
||||
return Boolean(tryParseRequestRuleExpr(requestRuleExpr || '')?.length)
|
||||
}
|
||||
|
||||
export function getDynamicPriceEntries(
|
||||
tier: ParsedTier | null,
|
||||
options: DynamicPriceOptions
|
||||
): DynamicPriceEntry[] {
|
||||
if (!tier) return []
|
||||
|
||||
return BILLING_PRICING_VARS.flatMap((variable) => {
|
||||
if (!variable.field) return []
|
||||
const value = Number(tier[variable.field])
|
||||
if (!Number.isFinite(value) || value <= 0) return []
|
||||
|
||||
return [
|
||||
{
|
||||
key: variable.key,
|
||||
field: variable.field,
|
||||
label: variable.label,
|
||||
shortLabel: variable.shortLabel,
|
||||
value,
|
||||
formatted: formatDynamicUnitPrice(value, options),
|
||||
variable,
|
||||
},
|
||||
]
|
||||
}).sort((a, b) => {
|
||||
const aPrimary = PRIMARY_DYNAMIC_FIELDS.has(a.field)
|
||||
const bPrimary = PRIMARY_DYNAMIC_FIELDS.has(b.field)
|
||||
if (aPrimary !== bPrimary) return aPrimary ? -1 : 1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
export function getDynamicPricingSummary(
|
||||
model: PricingModel,
|
||||
options: DynamicPriceOptions
|
||||
): DynamicPricingSummary | null {
|
||||
if (!isDynamicPricingModel(model)) return null
|
||||
|
||||
const tiers = getDynamicPricingTiers(model)
|
||||
const tier = tiers[0] || null
|
||||
const entries = getDynamicPriceEntries(tier, options)
|
||||
const rawExpression = model.billing_expr || ''
|
||||
|
||||
return {
|
||||
tiers,
|
||||
tier,
|
||||
tierCount: tiers.length,
|
||||
hasRequestRules: hasDynamicRequestRules(model),
|
||||
isSpecialExpression: rawExpression.trim().length > 0 && tiers.length === 0,
|
||||
rawExpression,
|
||||
entries,
|
||||
primaryEntries: entries.filter((entry) =>
|
||||
PRIMARY_DYNAMIC_FIELDS.has(entry.field)
|
||||
),
|
||||
secondaryEntries: entries.filter(
|
||||
(entry) => !PRIMARY_DYNAMIC_FIELDS.has(entry.field)
|
||||
),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user