Files
new-api/web/default/src/features/system-settings/models/model-pricing-sheet.tsx
T
t0ng7u b08febaa3c refactor: system settings UI for consistent, compact layouts
Redesign the system settings interface to align with the rest of the console experience by using fixed header actions, removing redundant subtitles, respecting global content width, and standardizing responsive form layouts.

Introduce reusable settings layout primitives for forms, switch rows, grouped controls, nested control sections, title status indicators, and page action portals. Replace duplicated card-style switch markup with explicit compact components, improve nested switch readability, and reduce visual noise across authentication, billing, content, integrations, maintenance, models, and request-limit settings.

Also complete missing i18n translations, remove obsolete subtitle translation keys, refine i18n sync reporting, fix sidebar truncation for long labels, and verify the frontend with type checking and lint diagnostics.
2026-05-25 00:34:26 +08:00

1035 lines
30 KiB
TypeScript
Vendored

/*
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 { useEffect, useMemo, useState } from 'react'
import * as z from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { AlertTriangle, ChevronDown } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from '@/components/ui/field'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from '@/components/ui/input-group'
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { combineBillingExpr } from '@/features/pricing/lib/billing-expr'
import {
SettingsControlGroup,
SettingsSwitchField,
} from '../components/settings-form-layout'
import { formatPricingNumber } from './pricing-format'
import { TieredPricingEditor } from './tiered-pricing-editor'
const createModelPricingSchema = (t: (key: string) => string) =>
z.object({
name: z.string().min(1, t('Model name is required')),
price: z.string().optional(),
ratio: z.string().optional(),
cacheRatio: z.string().optional(),
createCacheRatio: z.string().optional(),
completionRatio: z.string().optional(),
imageRatio: z.string().optional(),
audioRatio: z.string().optional(),
audioCompletionRatio: z.string().optional(),
})
type ModelPricingFormValues = z.infer<
ReturnType<typeof createModelPricingSchema>
>
type PricingMode = 'per-token' | 'per-request' | 'tiered_expr'
type LaneKey =
| 'completion'
| 'cache'
| 'createCache'
| 'image'
| 'audioInput'
| 'audioOutput'
export type ModelRatioData = {
name: string
price?: string
ratio?: string
cacheRatio?: string
createCacheRatio?: string
completionRatio?: string
imageRatio?: string
audioRatio?: string
audioCompletionRatio?: string
billingMode?: PricingMode
billingExpr?: string
requestRuleExpr?: string
}
type ModelPricingSheetProps = {
open: boolean
onOpenChange: (open: boolean) => void
onSave: (data: ModelRatioData) => void
onCancel?: () => void
editData?: ModelRatioData | null
selectedTargetCount?: number
}
type ModelPricingEditorPanelProps = Omit<
ModelPricingSheetProps,
'open' | 'onOpenChange'
> & {
className?: string
}
type PreviewRow = {
key: string
label: string
value: string
multiline?: boolean
}
const numericDraftRegex = /^(\d+(\.\d*)?|\.\d*)?$/
const EMPTY_LANE_PRICES: Record<LaneKey, string> = {
completion: '',
cache: '',
createCache: '',
image: '',
audioInput: '',
audioOutput: '',
}
const EMPTY_LANE_ENABLED: Record<LaneKey, boolean> = {
completion: false,
cache: false,
createCache: false,
image: false,
audioInput: false,
audioOutput: false,
}
const ratioFieldByLane: Record<LaneKey, keyof ModelPricingFormValues> = {
completion: 'completionRatio',
cache: 'cacheRatio',
createCache: 'createCacheRatio',
image: 'imageRatio',
audioInput: 'audioRatio',
audioOutput: 'audioCompletionRatio',
}
const laneConfigs: Array<{
key: LaneKey
titleKey: string
descriptionKey: string
placeholder: string
}> = [
{
key: 'completion',
titleKey: 'Completion price',
descriptionKey: 'Output token price for generated tokens.',
placeholder: '15',
},
{
key: 'cache',
titleKey: 'Cache read price',
descriptionKey: 'Token price for cache reads.',
placeholder: '0.3',
},
{
key: 'createCache',
titleKey: 'Cache write price',
descriptionKey: 'Token price for creating cache entries.',
placeholder: '3.75',
},
{
key: 'image',
titleKey: 'Image input price',
descriptionKey: 'Token price for image input.',
placeholder: '2.5',
},
{
key: 'audioInput',
titleKey: 'Audio input price',
descriptionKey: 'Token price for audio input.',
placeholder: '3.81',
},
{
key: 'audioOutput',
titleKey: 'Audio output price',
descriptionKey: 'Token price for audio output.',
placeholder: '15.11',
},
]
function hasValue(value: unknown): boolean {
return (
value !== '' && value !== null && value !== undefined && value !== false
)
}
function toNumberOrNull(value: unknown): number | null {
if (!hasValue(value) && value !== 0) return null
const num = Number(value)
return Number.isFinite(num) ? num : null
}
function ratioToBasePrice(ratio: unknown): string {
const num = toNumberOrNull(ratio)
if (num === null) return ''
return formatPricingNumber(num * 2)
}
function deriveLanePrice(
ratio: unknown,
denominator: unknown,
fallback = ''
): string {
const ratioNumber = toNumberOrNull(ratio)
const denominatorNumber = toNumberOrNull(denominator)
if (ratioNumber === null || denominatorNumber === null) return fallback
return formatPricingNumber(ratioNumber * denominatorNumber)
}
function createInitialLaneState(data?: ModelRatioData | null) {
if (!data) {
return {
promptPrice: '',
prices: { ...EMPTY_LANE_PRICES },
enabled: { ...EMPTY_LANE_ENABLED },
}
}
const promptPrice = ratioToBasePrice(data.ratio)
const audioInputPrice = deriveLanePrice(data.audioRatio, promptPrice)
const prices: Record<LaneKey, string> = {
completion: deriveLanePrice(data.completionRatio, promptPrice),
cache: deriveLanePrice(data.cacheRatio, promptPrice),
createCache: deriveLanePrice(data.createCacheRatio, promptPrice),
image: deriveLanePrice(data.imageRatio, promptPrice),
audioInput: audioInputPrice,
audioOutput: deriveLanePrice(data.audioCompletionRatio, audioInputPrice),
}
return {
promptPrice,
prices,
enabled: {
completion: hasValue(data.completionRatio),
cache: hasValue(data.cacheRatio),
createCache: hasValue(data.createCacheRatio),
image: hasValue(data.imageRatio),
audioInput: hasValue(data.audioRatio),
audioOutput: hasValue(data.audioCompletionRatio),
},
}
}
function getModeLabel(mode: PricingMode) {
if (mode === 'per-request') return 'Per-request'
if (mode === 'tiered_expr') return 'Expression'
return 'Per-token'
}
function getModeBadgeVariant(
mode: PricingMode
): 'default' | 'secondary' | 'outline' {
if (mode === 'per-request') return 'secondary'
if (mode === 'tiered_expr') return 'default'
return 'outline'
}
function buildPreviewRows(
values: ModelPricingFormValues,
mode: PricingMode,
billingExpr: string,
requestRuleExpr: string,
promptPrice: string,
lanePrices: Record<LaneKey, string>,
laneEnabled: Record<LaneKey, boolean>,
t: (key: string) => string
): PreviewRow[] {
if (mode === 'tiered_expr') {
const effectiveExpr = combineBillingExpr(billingExpr, requestRuleExpr)
return [
{ key: 'mode', label: 'BillingMode', value: 'tiered_expr' },
{
key: 'expr',
label: t('Expression'),
value: effectiveExpr || t('Empty'),
multiline: true,
},
]
}
if (mode === 'per-request') {
return [
{
key: 'price',
label: 'ModelPrice',
value: values.price || t('Empty'),
},
]
}
return [
{
key: 'inputPrice',
label: t('Input price'),
value: promptPrice ? `$${promptPrice}` : t('Empty'),
},
{
key: 'completion',
label: t('Completion price'),
value:
laneEnabled.completion && lanePrices.completion
? `$${lanePrices.completion}`
: t('Empty'),
},
{
key: 'cache',
label: t('Cache read price'),
value:
laneEnabled.cache && lanePrices.cache
? `$${lanePrices.cache}`
: t('Empty'),
},
{
key: 'createCache',
label: t('Cache write price'),
value:
laneEnabled.createCache && lanePrices.createCache
? `$${lanePrices.createCache}`
: t('Empty'),
},
{
key: 'image',
label: t('Image input price'),
value:
laneEnabled.image && lanePrices.image
? `$${lanePrices.image}`
: t('Empty'),
},
{
key: 'audio',
label: t('Audio input price'),
value:
laneEnabled.audioInput && lanePrices.audioInput
? `$${lanePrices.audioInput}`
: t('Empty'),
},
{
key: 'audioCompletion',
label: t('Audio output price'),
value:
laneEnabled.audioOutput && lanePrices.audioOutput
? `$${lanePrices.audioOutput}`
: t('Empty'),
},
]
}
export function ModelPricingSheet({
open,
onOpenChange,
onSave,
onCancel,
editData,
selectedTargetCount = 0,
}: ModelPricingSheetProps) {
const { t } = useTranslation()
const title = editData ? t('Edit model pricing') : t('Add model pricing')
const description = editData?.name || t('New model')
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side='right' className='w-full gap-0 p-0 sm:max-w-2xl'>
<SheetHeader className='sr-only'>
<SheetTitle>{title}</SheetTitle>
<SheetDescription>{description}</SheetDescription>
</SheetHeader>
<ModelPricingEditorPanel
onSave={onSave}
editData={editData}
selectedTargetCount={selectedTargetCount}
onCancel={() => {
onCancel?.()
onOpenChange(false)
}}
className='h-full rounded-none border-0'
/>
</SheetContent>
</Sheet>
)
}
export function ModelPricingEditorPanel({
onSave,
editData,
selectedTargetCount = 0,
onCancel,
className,
}: ModelPricingEditorPanelProps) {
const { t } = useTranslation()
const [pricingMode, setPricingMode] = useState<PricingMode>('per-token')
const [promptPrice, setPromptPrice] = useState('')
const [lanePrices, setLanePrices] = useState<Record<LaneKey, string>>({
...EMPTY_LANE_PRICES,
})
const [laneEnabled, setLaneEnabled] = useState<Record<LaneKey, boolean>>({
...EMPTY_LANE_ENABLED,
})
const [billingExpr, setBillingExpr] = useState('')
const [requestRuleExpr, setRequestRuleExpr] = useState('')
const [previewOpen, setPreviewOpen] = useState(true)
const isEditMode = !!editData
const form = useForm<ModelPricingFormValues>({
resolver: zodResolver(createModelPricingSchema(t)),
defaultValues: {
name: '',
price: '',
ratio: '',
cacheRatio: '',
createCacheRatio: '',
completionRatio: '',
imageRatio: '',
audioRatio: '',
audioCompletionRatio: '',
},
})
useEffect(() => {
const nextLaneState = createInitialLaneState(editData)
if (editData) {
form.reset({
name: editData.name,
price: editData.price || '',
ratio: editData.ratio || '',
cacheRatio: editData.cacheRatio || '',
createCacheRatio: editData.createCacheRatio || '',
completionRatio: editData.completionRatio || '',
imageRatio: editData.imageRatio || '',
audioRatio: editData.audioRatio || '',
audioCompletionRatio: editData.audioCompletionRatio || '',
})
setPricingMode(
editData.billingMode === 'tiered_expr'
? 'tiered_expr'
: editData.price
? 'per-request'
: 'per-token'
)
setBillingExpr(editData.billingExpr || '')
setRequestRuleExpr(editData.requestRuleExpr || '')
} else {
form.reset({
name: '',
price: '',
ratio: '',
cacheRatio: '',
createCacheRatio: '',
completionRatio: '',
imageRatio: '',
audioRatio: '',
audioCompletionRatio: '',
})
setPricingMode('per-token')
setBillingExpr('')
setRequestRuleExpr('')
}
setPromptPrice(nextLaneState.promptPrice)
setLanePrices(nextLaneState.prices)
setLaneEnabled(nextLaneState.enabled)
setPreviewOpen(true)
}, [editData, form])
const setFormValue = (field: keyof ModelPricingFormValues, value: string) => {
form.setValue(field, value, {
shouldDirty: true,
shouldValidate: true,
})
}
const deriveLaneRatio = (
lane: LaneKey,
price: string,
nextPromptPrice = promptPrice,
nextLanePrices = lanePrices
) => {
const priceNumber = toNumberOrNull(price)
if (priceNumber === null) return ''
if (lane === 'audioOutput') {
const audioInputPrice = toNumberOrNull(nextLanePrices.audioInput)
if (audioInputPrice === null || audioInputPrice === 0) return ''
return formatPricingNumber(priceNumber / audioInputPrice)
}
const inputPrice = toNumberOrNull(nextPromptPrice)
if (inputPrice === null || inputPrice === 0) return ''
return formatPricingNumber(priceNumber / inputPrice)
}
const syncLaneRatios = (
nextPromptPrice = promptPrice,
nextLanePrices = lanePrices,
nextLaneEnabled = laneEnabled
) => {
const inputPrice = toNumberOrNull(nextPromptPrice)
setFormValue(
'ratio',
inputPrice !== null ? formatPricingNumber(inputPrice / 2) : ''
)
laneConfigs.forEach(({ key }) => {
const ratioField = ratioFieldByLane[key]
if (!nextLaneEnabled[key]) {
setFormValue(ratioField, '')
return
}
setFormValue(
ratioField,
deriveLaneRatio(
key,
nextLanePrices[key],
nextPromptPrice,
nextLanePrices
)
)
})
}
const handlePromptPriceChange = (value: string) => {
if (!numericDraftRegex.test(value)) return
setPromptPrice(value)
syncLaneRatios(value, lanePrices, laneEnabled)
}
const handleLanePriceChange = (lane: LaneKey, value: string) => {
if (!numericDraftRegex.test(value)) return
const nextLanePrices = { ...lanePrices, [lane]: value }
setLanePrices(nextLanePrices)
if (laneEnabled[lane]) {
setFormValue(
ratioFieldByLane[lane],
deriveLaneRatio(lane, value, promptPrice, nextLanePrices)
)
}
if (lane === 'audioInput' && laneEnabled.audioOutput) {
setFormValue(
'audioCompletionRatio',
deriveLaneRatio(
'audioOutput',
nextLanePrices.audioOutput,
promptPrice,
nextLanePrices
)
)
}
}
const handleLaneToggle = (lane: LaneKey, checked: boolean) => {
const nextEnabled = { ...laneEnabled, [lane]: checked }
let nextPrices = lanePrices
if (!checked) {
nextPrices = { ...nextPrices, [lane]: '' }
setFormValue(ratioFieldByLane[lane], '')
if (lane === 'audioInput') {
nextEnabled.audioOutput = false
nextPrices.audioOutput = ''
setFormValue('audioCompletionRatio', '')
}
}
setLaneEnabled(nextEnabled)
setLanePrices(nextPrices)
if (checked) {
setFormValue(
ratioFieldByLane[lane],
deriveLaneRatio(lane, nextPrices[lane], promptPrice, nextPrices)
)
}
}
const handleModeChange = (value: string) => {
const nextMode = value as PricingMode
setPricingMode(nextMode)
if (nextMode === 'tiered_expr' && !billingExpr) {
setBillingExpr('tier("base", p * 0 + c * 0)')
}
}
const watchedValues = form.watch()
const previewRows = useMemo(
() =>
buildPreviewRows(
watchedValues,
pricingMode,
billingExpr,
requestRuleExpr,
promptPrice,
lanePrices,
laneEnabled,
t
),
[
billingExpr,
laneEnabled,
lanePrices,
pricingMode,
promptPrice,
requestRuleExpr,
t,
watchedValues,
]
)
const warnings = useMemo(() => {
const nextWarnings: string[] = []
const hasConflict =
!!editData?.price &&
[
editData.ratio,
editData.completionRatio,
editData.cacheRatio,
editData.createCacheRatio,
editData.imageRatio,
editData.audioRatio,
editData.audioCompletionRatio,
].some(hasValue)
if (hasConflict) {
nextWarnings.push(
t(
'This model has both fixed-price and token-price settings. Saving the current mode will rewrite the conflicting fields.'
)
)
}
if (
pricingMode === 'per-token' &&
toNumberOrNull(promptPrice) === null &&
laneConfigs.some(
({ key }) => laneEnabled[key] && hasValue(lanePrices[key])
)
) {
nextWarnings.push(
t('Input price is required before saving dependent prices.')
)
}
if (
pricingMode === 'per-token' &&
laneEnabled.audioOutput &&
!hasValue(lanePrices.audioInput)
) {
nextWarnings.push(t('Audio output price requires an audio input price.'))
}
return nextWarnings
}, [editData, laneEnabled, lanePrices, pricingMode, promptPrice, t])
const handleSubmit = (values: ModelPricingFormValues) => {
if (
pricingMode === 'per-token' &&
toNumberOrNull(promptPrice) === null &&
laneConfigs.some(
({ key }) => laneEnabled[key] && hasValue(lanePrices[key])
)
) {
form.setError('ratio', {
message: t('Input price is required before saving dependent prices.'),
})
return
}
if (
pricingMode === 'per-token' &&
laneEnabled.audioOutput &&
!hasValue(lanePrices.audioInput)
) {
form.setError('audioRatio', {
message: t('Audio output price requires an audio input price.'),
})
return
}
const data: ModelRatioData = {
name: values.name.trim(),
billingMode: pricingMode,
price: values.price || '',
ratio: values.ratio || '',
cacheRatio: values.cacheRatio || '',
createCacheRatio: values.createCacheRatio || '',
completionRatio: values.completionRatio || '',
imageRatio: values.imageRatio || '',
audioRatio: values.audioRatio || '',
audioCompletionRatio: values.audioCompletionRatio || '',
}
if (pricingMode === 'tiered_expr') {
data.billingExpr = billingExpr
data.requestRuleExpr = requestRuleExpr
}
onSave(data)
form.reset()
onCancel?.()
}
const activeName = watchedValues.name || editData?.name || t('New model')
return (
<div
className={cn(
'bg-card flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border',
className
)}
>
<div className='border-b p-4'>
<div className='flex flex-wrap items-start justify-between gap-3'>
<div className='min-w-0'>
<h3 className='truncate text-base font-medium'>
{isEditMode ? t('Edit model pricing') : t('Add model pricing')}
</h3>
<p className='text-muted-foreground truncate text-sm'>
{activeName}
</p>
</div>
<Badge variant={getModeBadgeVariant(pricingMode)}>
{t(getModeLabel(pricingMode))}
</Badge>
</div>
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className='flex min-h-0 flex-1 flex-col'
autoComplete='off'
>
<div className='min-h-0 flex-1 overflow-y-auto p-4'>
<FieldGroup>
{warnings.length > 0 && (
<Alert variant='destructive'>
<AlertTriangle data-icon='inline-start' />
<AlertDescription>
<div className='flex flex-col gap-1'>
{warnings.map((warning) => (
<span key={warning}>{warning}</span>
))}
</div>
</AlertDescription>
</Alert>
)}
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Model name')}</FormLabel>
<FormControl>
<Input
placeholder={t('gpt-4')}
{...field}
disabled={isEditMode}
/>
</FormControl>
<FormDescription>
{t('The exact model identifier as used in API requests.')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Tabs value={pricingMode} onValueChange={handleModeChange}>
<TabsList className='grid w-full grid-cols-3'>
<TabsTrigger value='per-token'>{t('Per-token')}</TabsTrigger>
<TabsTrigger value='per-request'>
{t('Per-request')}
</TabsTrigger>
<TabsTrigger value='tiered_expr'>
{t('Expression')}
</TabsTrigger>
</TabsList>
<TabsContent value='per-token' className='flex flex-col gap-5'>
<FieldGroup>
<Field>
<FieldLabel>{t('Input price')}</FieldLabel>
<PriceInput
value={promptPrice}
placeholder='3'
onChange={handlePromptPriceChange}
/>
<FieldDescription>
{t('USD price per 1M input tokens.')}
</FieldDescription>
</Field>
<div className='grid gap-3 sm:grid-cols-2'>
{laneConfigs.map((lane) => {
const disabled =
lane.key === 'audioOutput' &&
(!laneEnabled.audioInput ||
!hasValue(lanePrices.audioInput))
return (
<PriceLane
key={lane.key}
title={t(lane.titleKey)}
description={t(lane.descriptionKey)}
placeholder={lane.placeholder}
value={lanePrices[lane.key]}
enabled={laneEnabled[lane.key]}
disabled={disabled}
onEnabledChange={(checked) =>
handleLaneToggle(lane.key, checked)
}
onChange={(value) =>
handleLanePriceChange(lane.key, value)
}
/>
)
})}
</div>
</FieldGroup>
</TabsContent>
<TabsContent
value='per-request'
className='flex flex-col gap-5'
>
<FormField
control={form.control}
name='price'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Fixed price')}</FormLabel>
<FormControl>
<InputGroup>
<InputGroupAddon>$</InputGroupAddon>
<InputGroupInput
inputMode='decimal'
placeholder='0.01'
{...field}
onChange={(event) => {
const value = event.target.value
if (numericDraftRegex.test(value)) {
field.onChange(value)
}
}}
/>
<InputGroupAddon align='inline-end'>
{t('per request')}
</InputGroupAddon>
</InputGroup>
</FormControl>
<FormDescription>
{t(
'Cost in USD per request, regardless of tokens used.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent
value='tiered_expr'
className='flex flex-col gap-5'
>
<TieredPricingEditor
modelName={watchedValues.name}
billingExpr={billingExpr}
requestRuleExpr={requestRuleExpr}
onBillingExprChange={setBillingExpr}
onRequestRuleExprChange={setRequestRuleExpr}
/>
</TabsContent>
</Tabs>
<Collapsible open={previewOpen} onOpenChange={setPreviewOpen}>
<CollapsibleTrigger
render={
<Button
type='button'
variant='outline'
className='flex w-full justify-between'
/>
}
>
<span>{t('Save preview')}</span>
<ChevronDown
className={cn(
'transition-transform',
previewOpen && 'rotate-180'
)}
/>
</CollapsibleTrigger>
<CollapsibleContent className='pt-3'>
<div className='rounded-lg border'>
{previewRows.map((row) => (
<div
key={row.key}
className='grid grid-cols-[140px_1fr] gap-3 border-b px-3 py-2 text-sm last:border-b-0'
>
<span className='text-muted-foreground text-xs'>
{row.label}
</span>
<span
className={cn(
'min-w-0',
row.multiline
? 'font-mono text-xs leading-5 break-words whitespace-pre-wrap'
: 'truncate'
)}
>
{row.value}
</span>
</div>
))}
</div>
</CollapsibleContent>
</Collapsible>
</FieldGroup>
</div>
<SheetFooter className='bg-background/95 border-t sm:flex-row sm:items-center sm:justify-between'>
<div className='text-muted-foreground text-xs'>
{selectedTargetCount > 0
? t('{{count}} selected targets available for bulk copy.', {
count: selectedTargetCount,
})
: t('Changes are written to the settings draft on save.')}
</div>
<div className='flex justify-end gap-2'>
<Button type='button' variant='outline' onClick={onCancel}>
{t('Cancel')}
</Button>
<Button type='submit'>
{isEditMode ? t('Update') : t('Add')}
</Button>
</div>
</SheetFooter>
</form>
</Form>
</div>
)
}
function PriceInput(props: {
value: string
placeholder?: string
disabled?: boolean
onChange: (value: string) => void
}) {
return (
<InputGroup>
<InputGroupAddon>$</InputGroupAddon>
<InputGroupInput
inputMode='decimal'
value={props.value}
placeholder={props.placeholder}
disabled={props.disabled}
onChange={(event) => props.onChange(event.target.value)}
/>
<InputGroupAddon align='inline-end'>$/1M</InputGroupAddon>
</InputGroup>
)
}
function PriceLane(props: {
title: string
description: string
placeholder: string
value: string
enabled: boolean
disabled?: boolean
onEnabledChange: (checked: boolean) => void
onChange: (value: string) => void
}) {
const { t } = useTranslation()
const effectiveDisabled = props.disabled || !props.enabled
return (
<SettingsControlGroup
className={cn('space-y-3', effectiveDisabled && 'opacity-75')}
data-disabled={effectiveDisabled || undefined}
>
<SettingsSwitchField
checked={props.enabled}
disabled={props.disabled}
onCheckedChange={props.onEnabledChange}
label={props.title}
description={props.description}
aria-label={props.title}
/>
<PriceInput
value={props.value}
placeholder={props.placeholder}
disabled={effectiveDisabled}
onChange={props.onChange}
/>
<p className='text-muted-foreground text-xs'>
{props.enabled
? t('USD price per 1M tokens.')
: t('Disabled lanes are omitted on save.')}
</p>
</SettingsControlGroup>
)
}