diff --git a/web/default/src/features/system-settings/models/tiered-pricing-editor.tsx b/web/default/src/features/system-settings/models/tiered-pricing-editor.tsx index e51486fd..ca63afe2 100644 --- a/web/default/src/features/system-settings/models/tiered-pricing-editor.tsx +++ b/web/default/src/features/system-settings/models/tiered-pricing-editor.tsx @@ -1,4 +1,15 @@ -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ChangeEvent, + type FocusEvent, + type InputHTMLAttributes, + type MouseEvent as ReactMouseEvent, +} from 'react' import { Copy, Plus, Trash2 } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -285,6 +296,93 @@ function formatTokenHint(n: number | string | null | undefined): string { return `= ${v.toLocaleString()} tokens` } +function formatNumberDraft(value: number | string): string { + if (value === '') return '' + if (typeof value === 'number') + return Number.isFinite(value) ? String(value) : '0' + return value +} + +function parseNumberDraft(value: string): number { + if (value.trim() === '') return 0 + const next = Number(value) + return Number.isFinite(next) ? next : 0 +} + +function isZeroDraft(value: string): boolean { + return value.trim() !== '' && parseNumberDraft(value) === 0 +} + +type DraftNumberInputProps = Omit< + InputHTMLAttributes, + 'type' | 'value' | 'onChange' +> & { + value: number | string + onValueChange: (next: number) => void + selectZeroOnFocus?: boolean +} + +function DraftNumberInput({ + value, + onValueChange, + selectZeroOnFocus = true, + onBlur, + onFocus, + onMouseUp, + ...props +}: DraftNumberInputProps) { + const [draft, setDraft] = useState(() => formatNumberDraft(value)) + const [focused, setFocused] = useState(false) + + useEffect(() => { + if (!focused) { + setDraft(formatNumberDraft(value)) + } + }, [focused, value]) + + const handleChange = (event: ChangeEvent) => { + const nextDraft = event.target.value + setDraft(nextDraft) + onValueChange(parseNumberDraft(nextDraft)) + } + + const handleFocus = (event: FocusEvent) => { + setFocused(true) + onFocus?.(event) + if (selectZeroOnFocus && isZeroDraft(event.currentTarget.value)) { + event.currentTarget.select() + } + } + + const handleMouseUp = (event: ReactMouseEvent) => { + onMouseUp?.(event) + if (selectZeroOnFocus && isZeroDraft(event.currentTarget.value)) { + event.preventDefault() + event.currentTarget.select() + } + } + + const handleBlur = (event: FocusEvent) => { + const normalized = parseNumberDraft(event.currentTarget.value) + setFocused(false) + setDraft(String(normalized)) + onValueChange(normalized) + onBlur?.(event) + } + + return ( + + ) +} + // --------------------------------------------------------------------------- // Tier condition row // --------------------------------------------------------------------------- @@ -332,13 +430,10 @@ function ConditionRow({ condition, onChange, onRemove }: ConditionRowProps) { ))} - - onChange({ ...condition, value: event.target.value }) - } + value={condition.value} + onValueChange={(value) => onChange({ ...condition, value })} placeholder='tokens' className='w-32' /> @@ -381,12 +476,11 @@ function PriceField({
- onChange(Number(event.target.value) || 0)} + onValueChange={onChange} className='w-32' /> {showSuffix && ( @@ -802,32 +896,29 @@ function RuleConditionRow({ {timeCond.mode === MATCH_RANGE ? ( <> - - onChange({ ...timeCond, rangeStart: event.target.value }) + onValueChange={(value) => + onChange({ ...timeCond, rangeStart: String(value) }) } placeholder='start' className='w-20' /> ~ - - onChange({ ...timeCond, rangeEnd: event.target.value }) + onValueChange={(value) => + onChange({ ...timeCond, rangeEnd: String(value) }) } placeholder='end' className='w-20' /> ) : ( - - onChange({ ...timeCond, value: event.target.value }) + onValueChange={(value) => + onChange({ ...timeCond, value: String(value) }) } placeholder='value' className='w-24' @@ -991,13 +1082,12 @@ function RuleGroupCard({
- - onChange({ ...group, multiplier: event.target.value }) + onValueChange={(value) => + onChange({ ...group, multiplier: String(value) }) } className='w-32' placeholder='1.0' @@ -1114,24 +1204,18 @@ function CostEstimator({ effectiveExpr }: EstimatorProps) {
- - setPromptTokens(Number(event.target.value) || 0) - } + onValueChange={setPromptTokens} />
- - setCompletionTokens(Number(event.target.value) || 0) - } + onValueChange={setCompletionTokens} />
@@ -1151,14 +1235,13 @@ function CostEstimator({ effectiveExpr }: EstimatorProps) { - + onValueChange={(value) => setExtras((prev) => ({ ...prev, - [stateKey]: Number(event.target.value) || 0, + [stateKey]: value, })) } />