fix: improve tiered pricing number input editing (#4536)

* fix: follow required marker styling convention

* fix: improve tiered pricing number input editing
This commit is contained in:
yyhhyyyyyy 2026-04-30 14:41:15 +08:00 committed by GitHub
parent 8f3c41ae77
commit 5f86839c7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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<HTMLInputElement>,
'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<HTMLInputElement>) => {
const nextDraft = event.target.value
setDraft(nextDraft)
onValueChange(parseNumberDraft(nextDraft))
}
const handleFocus = (event: FocusEvent<HTMLInputElement>) => {
setFocused(true)
onFocus?.(event)
if (selectZeroOnFocus && isZeroDraft(event.currentTarget.value)) {
event.currentTarget.select()
}
}
const handleMouseUp = (event: ReactMouseEvent<HTMLInputElement>) => {
onMouseUp?.(event)
if (selectZeroOnFocus && isZeroDraft(event.currentTarget.value)) {
event.preventDefault()
event.currentTarget.select()
}
}
const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
const normalized = parseNumberDraft(event.currentTarget.value)
setFocused(false)
setDraft(String(normalized))
onValueChange(normalized)
onBlur?.(event)
}
return (
<Input
{...props}
type='number'
value={draft}
onChange={handleChange}
onFocus={handleFocus}
onMouseUp={handleMouseUp}
onBlur={handleBlur}
/>
)
}
// ---------------------------------------------------------------------------
// Tier condition row
// ---------------------------------------------------------------------------
@ -332,13 +430,10 @@ function ConditionRow({ condition, onChange, onRemove }: ConditionRowProps) {
))}
</SelectContent>
</Select>
<Input
type='number'
<DraftNumberInput
min={0}
value={condition.value === '' ? '' : Number(condition.value)}
onChange={(event) =>
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({
<div className='space-y-1'>
<Label className='text-xs'>{label}</Label>
<div className='flex items-center gap-2'>
<Input
type='number'
<DraftNumberInput
min={0}
step={0.01}
value={Number.isFinite(value) ? value : 0}
onChange={(event) => onChange(Number(event.target.value) || 0)}
onValueChange={onChange}
className='w-32'
/>
{showSuffix && (
@ -802,32 +896,29 @@ function RuleConditionRow({
</Select>
{timeCond.mode === MATCH_RANGE ? (
<>
<Input
type='number'
<DraftNumberInput
value={timeCond.rangeStart}
onChange={(event) =>
onChange({ ...timeCond, rangeStart: event.target.value })
onValueChange={(value) =>
onChange({ ...timeCond, rangeStart: String(value) })
}
placeholder='start'
className='w-20'
/>
<span className='text-muted-foreground text-xs'>~</span>
<Input
type='number'
<DraftNumberInput
value={timeCond.rangeEnd}
onChange={(event) =>
onChange({ ...timeCond, rangeEnd: event.target.value })
onValueChange={(value) =>
onChange({ ...timeCond, rangeEnd: String(value) })
}
placeholder='end'
className='w-20'
/>
</>
) : (
<Input
type='number'
<DraftNumberInput
value={timeCond.value}
onChange={(event) =>
onChange({ ...timeCond, value: event.target.value })
onValueChange={(value) =>
onChange({ ...timeCond, value: String(value) })
}
placeholder='value'
className='w-24'
@ -991,13 +1082,12 @@ function RuleGroupCard({
<div className='flex items-center gap-2'>
<Label className='text-xs'>{t('Multiplier')}</Label>
<Input
type='number'
<DraftNumberInput
min={0}
step={0.01}
value={group.multiplier}
onChange={(event) =>
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) {
<div className='grid grid-cols-2 gap-3'>
<div className='space-y-1'>
<Label className='text-xs'>{t('Input tokens')} (p)</Label>
<Input
type='number'
<DraftNumberInput
min={0}
value={promptTokens}
onChange={(event) =>
setPromptTokens(Number(event.target.value) || 0)
}
onValueChange={setPromptTokens}
/>
</div>
<div className='space-y-1'>
<Label className='text-xs'>{t('Output tokens')} (c)</Label>
<Input
type='number'
<DraftNumberInput
min={0}
value={completionTokens}
onChange={(event) =>
setCompletionTokens(Number(event.target.value) || 0)
}
onValueChange={setCompletionTokens}
/>
</div>
</div>
@ -1151,14 +1235,13 @@ function CostEstimator({ effectiveExpr }: EstimatorProps) {
<Label className='text-xs'>
{variable.shortLabel} ({variable.key})
</Label>
<Input
type='number'
<DraftNumberInput
min={0}
value={extras[stateKey]}
onChange={(event) =>
onValueChange={(value) =>
setExtras((prev) => ({
...prev,
[stateKey]: Number(event.target.value) || 0,
[stateKey]: value,
}))
}
/>