import { useMemo } from 'react' import { Tag as TagIcon } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useSystemConfigStore } from '@/stores/system-config-store' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { BILLING_PRICING_VARS, MATCH_CONTAINS, MATCH_EQ, MATCH_EXISTS, MATCH_GTE, MATCH_LT, MATCH_RANGE, SOURCE_TIME, parseTiersFromExpr, splitBillingExprAndRequestRules, tryParseRequestRuleExpr, type ParsedTier, type RequestCondition, type RequestRuleGroup, type TierCondition, } from '../lib/billing-expr' type DynamicPricingBreakdownProps = { billingExpr: string | null | undefined /** * Label of the tier that fired for the current request. When provided, * the corresponding row is highlighted and tagged as "Matched". Used by * the usage-log details dialog to show which tier the engine selected. */ matchedTierLabel?: string | null /** * Hide cache-pricing columns regardless of the per-tier values. The log * details dialog passes this when the actual request did not consume any * cache tokens, so users only see pricing rows that were relevant to the * call they are inspecting. Defaults to false (show all configured prices). */ hideCacheColumns?: boolean } const VAR_LABELS: Record = { p: 'Input', c: 'Output', len: 'Length', } const OP_LABELS: Record = { '<': '<', '<=': '≤', '>': '>', '>=': '≥', } const TIME_FUNC_LABELS: Record = { hour: 'Hour', minute: 'Minute', weekday: 'Weekday', month: 'Month', day: 'Day', } function formatTokenHint(value: string | number): string { const n = Number(value) if (!Number.isFinite(n) || n === 0) return '' if (n >= 1_000_000) { return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M` } if (n >= 1000) { return `${(n / 1000).toFixed(n % 1000 === 0 ? 0 : 1)}K` } return String(n) } function formatConditionSummary( conditions: TierCondition[], t: (key: string) => string ): string { return conditions .map((c) => { const varLabel = t(VAR_LABELS[c.var] || c.var) const hint = formatTokenHint(c.value) return `${varLabel} ${OP_LABELS[c.op] || c.op} ${hint || c.value}` }) .filter(Boolean) .join(' && ') } function describeCondition( cond: RequestCondition, t: (key: string) => string ): string { if (cond.source === SOURCE_TIME) { const fn = t(TIME_FUNC_LABELS[cond.timeFunc] || cond.timeFunc) const tz = cond.timezone || 'UTC' if (cond.mode === MATCH_RANGE) { return `${fn} ${cond.rangeStart}:00~${cond.rangeEnd}:00 (${tz})` } const opMap: Record = { [MATCH_EQ]: '=', [MATCH_GTE]: '≥', [MATCH_LT]: '<', } return `${fn} ${opMap[cond.mode] || '='} ${cond.value} (${tz})` } const src = cond.source === 'header' ? t('Header') : t('Body param') const path = cond.path || '' if (cond.mode === MATCH_EXISTS) return `${src} ${path} ${t('Exists')}` if (cond.mode === MATCH_CONTAINS) { return `${src} ${path} ${t('Contains')} "${cond.value}"` } const opMap: Record = { eq: '=', gt: '>', gte: '≥', lt: '<', lte: '≤', } return `${src} ${path} ${opMap[cond.mode] || '='} ${cond.value}` } function describeGroup( group: RequestRuleGroup, t: (key: string) => string ): string { return (group.conditions || []) .map((c) => describeCondition(c, t)) .join(' && ') } export function DynamicPricingBreakdown({ billingExpr, matchedTierLabel, hideCacheColumns = false, }: DynamicPricingBreakdownProps) { const { t } = useTranslation() const expr = billingExpr || '' const currency = useSystemConfigStore((s) => s.config.currency) const { symbol, rate } = useMemo(() => { if (currency.quotaDisplayType === 'CNY') { return { symbol: '¥', rate: currency.usdExchangeRate || 7 } } if (currency.quotaDisplayType === 'CUSTOM') { return { symbol: currency.customCurrencySymbol || '¤', rate: currency.customCurrencyExchangeRate || 1, } } return { symbol: '$', rate: 1 } }, [currency]) const { tiers, ruleGroups } = useMemo(() => { const split = splitBillingExprAndRequestRules(expr) const parsedTiers = parseTiersFromExpr(split.billingExpr) const parsedRules = tryParseRequestRuleExpr(split.requestRuleExpr || '') return { tiers: parsedTiers, ruleGroups: parsedRules || [], } }, [expr]) const hasTiers = tiers.length > 0 const hasRules = ruleGroups.length > 0 if (!expr) return null if (!hasTiers) { return (
{t('Special billing expression')}
{t('Unable to parse structured pricing')}
{t('Raw expression')}
{expr}
) } const visiblePriceFields = BILLING_PRICING_VARS.filter((v) => { if (!hasTiers) return false if (hideCacheColumns && v.group === 'cache') return false return tiers.some( (tier) => Number(tier[v.field as string as keyof ParsedTier] || 0) > 0 ) }) return (
{t('Dynamic Pricing')}
{t('Prices vary by usage tier and request conditions')}
{hasTiers && (
{t('Tiered price table')}
{tiers.map((tier, i) => { const condSummary = formatConditionSummary(tier.conditions, t) const isMatched = matchedTierLabel != null && matchedTierLabel !== '' && tier.label === matchedTierLabel return (
{tier.label || t('Default')} {isMatched && ( {t('Matched')} )}
{condSummary && (
{condSummary}
)}
{visiblePriceFields.map((v) => { const value = Number( tier[v.field as string as keyof ParsedTier] || 0 ) return (
{t(v.shortLabel)}
{value > 0 ? `${symbol}${(value * rate).toFixed(4)}` : '-'}
) })}
) })}
{t('Tier')} {visiblePriceFields.map((v) => ( {t(v.shortLabel)} ))} {tiers.map((tier, i) => { const condSummary = formatConditionSummary(tier.conditions, t) const isMatched = matchedTierLabel != null && matchedTierLabel !== '' && tier.label === matchedTierLabel return (
{tier.label || t('Default')} {isMatched && ( {t('Matched')} )}
{condSummary && (
{condSummary}
)}
{visiblePriceFields.map((v) => { const value = Number( tier[v.field as string as keyof ParsedTier] || 0 ) return ( {value > 0 ? ( {`${symbol}${(value * rate).toFixed(4)}`} ) : ( '-' )} ) })}
) })}
)} {hasRules && (
{t('Conditional multipliers')}
    {ruleGroups.map((group, gi) => (
  • {describeGroup(group, t)} {group.multiplier}x
  • ))}
)}
) }