import type { TFunction } from 'i18next' import { Plus, Trash2 } from 'lucide-react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { Button, InputNumber, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tooltip } from '@cherrystudio/ui' import type { Model } from '@shared/data/types/model' import ProviderField from '../../primitives/ProviderField' import { drawerClasses } from '../../primitives/ProviderSettingsPrimitives' import { appendModelPricingTier, buildModelPricingFromDraft, clearModelPricingDraftError, createModelPricingDraft, getModelPricingCurrencySymbol, isModelPricingCurrencySymbol, MODEL_PRICING_CURRENCY_SYMBOLS, type ModelPricingCurrencySymbol, type ModelPricingDraft, type ModelPricingDraftError, type ModelPricingDraftErrors, type ModelPricingDraftField, type ModelPricingTierDraft, removeModelPricingTier, updateModelPricingTier } from './modelPricing' interface ModelPricingFieldsProps { pricing: Model['pricing'] onCommit: (pricing: NonNullable) => void } interface TierPriceFieldProps { tierIndex: number field: Exclude label: string ariaLabel: string value: string currencySymbol: ModelPricingCurrencySymbol error?: string optional?: boolean onChange: (field: ModelPricingDraftField, value: string) => void onCommit: (field: ModelPricingDraftField, value: string) => void } function TierPriceField({ tierIndex, field, label, ariaLabel, value, currencySymbol, error, optional = false, onChange, onCommit }: TierPriceFieldProps) { const { t } = useTranslation() const errorId = `model-pricing-tier-${tierIndex}-${field}-error` return ( {error} ) : null }>
onChange(field, next === null ? '' : String(next))} onBlur={(next) => onCommit(field, next === null ? '' : String(next))} /> {currencySymbol} / {t('models.price.million_tokens')}
) } interface ModelPricingTierFieldsProps { tier: ModelPricingTierDraft tierIndex: number currencySymbol: ModelPricingCurrencySymbol errors: Partial> onChange: (tierIndex: number, field: ModelPricingDraftField, value: string) => void onCommit: (tierIndex: number, field: ModelPricingDraftField, value: string) => void onRemove: (tierIndex: number) => void } function ModelPricingTierFields({ tier, tierIndex, currencySymbol, errors, onChange, onCommit, onRemove }: ModelPricingTierFieldsProps) { const { t } = useTranslation() const displayIndex = tierIndex + 1 const removeLabel = t('models.price.remove_tier', { index: displayIndex }) const minInputTokensLabel = t('models.price.min_input_tokens') const minInputTokensAriaLabel = t('models.price.field_for_tier', { field: minInputTokensLabel, index: displayIndex }) const minInputTokensHelpId = `model-pricing-tier-${tierIndex}-min-input-tokens-help` const fieldLabel = (label: string) => tierIndex === 0 ? label : t('models.price.field_for_tier', { field: label, index: displayIndex }) return (
{tierIndex > 0 ?
: null}
{t('models.price.tier', { index: displayIndex })}
{tier.minInputTokens.trim() ? (
{t('models.price.tier_from', { boundary: tier.minInputTokens })}
) : null}
{tierIndex > 0 ? ( ) : null}
{tierIndex > 0 ? ( {errors.minInputTokens ?? t('models.price.min_input_tokens_help')}
}> {/* No `min`: the real floor is the previous tier's boundary, which the field cannot know, so `buildModelPricingDraft` stays the only judge of what is valid here. */} onChange(tierIndex, 'minInputTokens', value === null ? '' : String(value))} onBlur={(value) => onCommit(tierIndex, 'minInputTokens', value === null ? '' : String(value))} /> ) : null} onChange(tierIndex, field, value)} onCommit={(field, value) => onCommit(tierIndex, field, value)} /> onChange(tierIndex, field, value)} onCommit={(field, value) => onCommit(tierIndex, field, value)} /> onChange(tierIndex, field, value)} onCommit={(field, value) => onCommit(tierIndex, field, value)} /> onChange(tierIndex, field, value)} onCommit={(field, value) => onCommit(tierIndex, field, value)} />
) } function getPricingErrorMessage(error: ModelPricingDraftError | undefined, t: TFunction) { switch (error) { case 'invalidPrice': return t('models.price.validation_price') case 'invalidMinInputTokens': return t('models.price.validation_min_input_tokens') case 'minInputTokensNotIncreasing': return t('models.price.validation_min_input_tokens_order') default: return undefined } } function translateErrors(errors: ModelPricingDraftErrors, t: TFunction) { return errors.map( (tierErrors) => Object.fromEntries( Object.entries(tierErrors).map(([field, error]) => [field, getPricingErrorMessage(error, t)]) ) as Partial> ) } export function ModelPricingFields({ pricing, onCommit }: ModelPricingFieldsProps) { const { t } = useTranslation() const [currencySymbol, setCurrencySymbol] = useState(() => getModelPricingCurrencySymbol(pricing) ) const [draft, setDraft] = useState(() => createModelPricingDraft(pricing)) const [errors, setErrors] = useState([]) const commitDraft = useCallback( (nextDraft: ModelPricingDraft, nextCurrencySymbol: ModelPricingCurrencySymbol = currencySymbol) => { const result = buildModelPricingFromDraft(pricing, nextDraft, nextCurrencySymbol) setErrors(result.errors) if (result.pricing) { onCommit(result.pricing) } }, [currencySymbol, onCommit, pricing] ) const handleTierChange = useCallback((tierIndex: number, field: ModelPricingDraftField, value: string) => { setDraft((current) => updateModelPricingTier(current, tierIndex, field, value)) setErrors((current) => clearModelPricingDraftError(current, tierIndex, field)) }, []) const handleTierCommit = useCallback( (tierIndex: number, field: ModelPricingDraftField, value: string) => { // Commit the next draft rather than the rendered one: `setDraft` above has // not taken effect yet in this tick. const nextDraft = updateModelPricingTier(draft, tierIndex, field, value) setDraft(nextDraft) setErrors((current) => clearModelPricingDraftError(current, tierIndex, field)) commitDraft(nextDraft) }, [commitDraft, draft] ) const handleAddTier = useCallback(() => { setDraft((current) => appendModelPricingTier(current)) setErrors((current) => [...current, {}]) }, []) const handleRemoveTier = useCallback( (tierIndex: number) => { const nextDraft = removeModelPricingTier(draft, tierIndex) setDraft(() => nextDraft) setErrors((current) => current.filter((_, index) => index !== tierIndex)) commitDraft(nextDraft) }, [commitDraft, draft] ) const translatedErrors = translateErrors(errors, t) return ( <>
{t('models.price.cache_fallback_help')}
{draft.tiers.map((tier, tierIndex) => ( ))}
) }