From 77d31575920eefc90ed4df09d7e2ff646c3b289f Mon Sep 17 00:00:00 2001 From: QuentinHsu Date: Thu, 4 Jun 2026 17:22:50 +0800 Subject: [PATCH] fix(model-pricing): commit visual pricing drafts on save - Commit the open visual editor draft before saving model pricing settings - Show unsaved draft differences against persisted model pricing values - Move model pricing actions into the editor toolbar and refine the visual editor layout --- .../models/model-pricing-sheet.tsx | 452 +++++++--------- .../models/model-ratio-form.tsx | 51 +- .../models/model-ratio-visual-editor.tsx | 511 +++++++++++------- .../models/ratio-settings-card.tsx | 8 + web/default/src/i18n/locales/en.json | 2 + web/default/src/i18n/locales/fr.json | 2 + web/default/src/i18n/locales/ja.json | 2 + web/default/src/i18n/locales/ru.json | 2 + web/default/src/i18n/locales/vi.json | 2 + web/default/src/i18n/locales/zh.json | 2 + 10 files changed, 561 insertions(+), 473 deletions(-) diff --git a/web/default/src/features/system-settings/models/model-pricing-sheet.tsx b/web/default/src/features/system-settings/models/model-pricing-sheet.tsx index f35afb80..ab0ac72e 100644 --- a/web/default/src/features/system-settings/models/model-pricing-sheet.tsx +++ b/web/default/src/features/system-settings/models/model-pricing-sheet.tsx @@ -27,17 +27,10 @@ import { import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' -import { AlertTriangle, ChevronDown } from 'lucide-react' +import { AlertTriangle } 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, @@ -63,15 +56,11 @@ import { Sheet, SheetContent, SheetDescription, - SheetFooter, SheetHeader, SheetTitle, } from '@/components/ui/sheet' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { - sideDrawerContentClassName, - sideDrawerFooterClassName, -} from '@/components/drawer-layout' +import { sideDrawerContentClassName } from '@/components/drawer-layout' import { combineBillingExpr } from '@/features/pricing/lib/billing-expr' import { SettingsControlGroup, @@ -124,10 +113,7 @@ export type ModelRatioData = { type ModelPricingSheetProps = { open: boolean onOpenChange: (open: boolean) => void - onSave: (data: ModelRatioData) => void - onCancel?: () => void editData?: ModelRatioData | null - selectedTargetCount?: number } type ModelPricingEditorPanelProps = Omit< @@ -284,20 +270,6 @@ function createInitialLaneState(data?: ModelRatioData | null) { } } -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, @@ -391,10 +363,7 @@ function buildPreviewRows( export const ModelPricingSheet = forwardRef< ModelPricingEditorPanelHandle, ModelPricingSheetProps ->(function ModelPricingSheet( - { open, onOpenChange, onSave, onCancel, editData, selectedTargetCount = 0 }, - ref -) { +>(function ModelPricingSheet({ open, onOpenChange, editData }, ref) { const { t } = useTranslation() const title = editData ? t('Edit model pricing') : t('Add model pricing') const description = editData?.name || t('New model') @@ -411,13 +380,7 @@ export const ModelPricingSheet = forwardRef< { - onCancel?.() - onOpenChange(false) - }} className='h-full rounded-none border-0' /> @@ -428,10 +391,7 @@ export const ModelPricingSheet = forwardRef< export const ModelPricingEditorPanel = forwardRef< ModelPricingEditorPanelHandle, ModelPricingEditorPanelProps ->(function ModelPricingEditorPanel( - { onSave, editData, selectedTargetCount = 0, onCancel, className }, - ref -) { +>(function ModelPricingEditorPanel({ editData, className }, ref) { const { t } = useTranslation() const [pricingMode, setPricingMode] = useState('per-token') const [promptPrice, setPromptPrice] = useState('') @@ -443,7 +403,6 @@ export const ModelPricingEditorPanel = forwardRef< }) const [billingExpr, setBillingExpr] = useState('') const [requestRuleExpr, setRequestRuleExpr] = useState('') - const [previewOpen, setPreviewOpen] = useState(true) const isEditMode = !!editData const form = useForm({ @@ -505,7 +464,6 @@ export const ModelPricingEditorPanel = forwardRef< setPromptPrice(nextLaneState.promptPrice) setLanePrices(nextLaneState.prices) setLaneEnabled(nextLaneState.enabled) - setPreviewOpen(true) }, [editData, form]) const setFormValue = (field: keyof ModelPricingFormValues, value: string) => { @@ -763,15 +721,6 @@ export const ModelPricingEditorPanel = forwardRef< [form, validatePricingValues, buildSubmitData] ) - const handleSubmit = (values: ModelPricingFormValues) => { - if (!validatePricingValues()) return - - const data = buildSubmitData(values) - onSave(data) - form.reset() - onCancel?.() - } - const activeName = watchedValues.name || editData?.name || t('New model') return ( @@ -791,232 +740,197 @@ export const ModelPricingEditorPanel = forwardRef< {activeName}

- - {t(getModeLabel(pricingMode))} -
event.preventDefault()} className='flex min-h-0 flex-1 flex-col' autoComplete='off' >
- - {warnings.length > 0 && ( - - - -
- {warnings.map((warning) => ( - {warning} - ))} -
-
-
- )} - - ( - - {t('Model name')} - - - - - {t('The exact model identifier as used in API requests.')} - - - - )} - /> - - - - {t('Per-token')} - - {t('Per-request')} - - - {t('Expression')} - - - - - - - {t('Input price')} - - - {t('USD price per 1M input tokens.')} - - - -
- {laneConfigs.map((lane) => { - const disabled = - lane.key === 'audioOutput' && - (!laneEnabled.audioInput || - !hasValue(lanePrices.audioInput)) - return ( - - handleLaneToggle(lane.key, checked) - } - onChange={(value) => - handleLanePriceChange(lane.key, value) - } - /> - ) - })} -
-
-
- - - - ( - - - {t('Fixed price')} - - - $ - { - const value = event.target.value - if (numericDraftRegex.test(value)) { - field.onChange(value) - } - }} - /> - - {t('per request')} - - - - - {t( - 'Cost in USD per request, regardless of tokens used.' - )} - - - - - )} - /> - - - - - - - - -
- - - - } - > - {t('Save preview')} - - - -
- {previewRows.map((row) => ( -
- - {row.label} - - - {row.value} - +
+ + {warnings.length > 0 && ( + + + +
+ {warnings.map((warning) => ( + {warning} + ))}
- ))} -
- - - -
+ + + )} - -
- {selectedTargetCount > 0 - ? t('{{count}} selected targets available for bulk copy.', { - count: selectedTargetCount, - }) - : t('Changes are written to the settings draft on save.')} + ( + + {t('Model name')} + + + + + {t( + 'The exact model identifier as used in API requests.' + )} + + + + )} + /> + + + + + {t('Per-token')} + + + {t('Per-request')} + + + {t('Expression')} + + + + + + + {t('Input price')} + + + {t('USD price per 1M input tokens.')} + + + +
+ {laneConfigs.map((lane) => { + const disabled = + lane.key === 'audioOutput' && + (!laneEnabled.audioInput || + !hasValue(lanePrices.audioInput)) + return ( + + handleLaneToggle(lane.key, checked) + } + onChange={(value) => + handleLanePriceChange(lane.key, value) + } + /> + ) + })} +
+
+
+ + + + ( + + + {t('Fixed price')} + + + $ + { + const value = event.target.value + if (numericDraftRegex.test(value)) { + field.onChange(value) + } + }} + /> + + {t('per request')} + + + + + {t( + 'Cost in USD per request, regardless of tokens used.' + )} + + + + + )} + /> + + + + + + + + +
+ + +
-
- - -
-
+
diff --git a/web/default/src/features/system-settings/models/model-ratio-form.tsx b/web/default/src/features/system-settings/models/model-ratio-form.tsx index f098c025..a1110903 100644 --- a/web/default/src/features/system-settings/models/model-ratio-form.tsx +++ b/web/default/src/features/system-settings/models/model-ratio-form.tsx @@ -37,7 +37,6 @@ import { SettingsSwitchContent, SettingsSwitchItem, } from '../components/settings-form-layout' -import { SettingsPageActionsPortal } from '../components/settings-page-context' import { ModelRatioVisualEditor, type ModelRatioVisualEditorHandle, @@ -59,6 +58,7 @@ type ModelFormValues = { type ModelRatioFormProps = { form: UseFormReturn + savedValues: ModelFormValues onSave: (values: ModelFormValues) => Promise onReset: () => void isSaving: boolean @@ -67,6 +67,7 @@ type ModelRatioFormProps = { export const ModelRatioForm = memo(function ModelRatioForm({ form, + savedValues, onSave, onReset, isSaving, @@ -101,7 +102,24 @@ export const ModelRatioForm = memo(function ModelRatioForm({ return (
-
+
+ +
- - - - {editMode === 'visual' ? (
void } -type ModelRow = { +type ModelPricingSnapshot = { name: string price?: string ratio?: string @@ -107,6 +109,14 @@ type ModelRow = { hasConflict: boolean } +type ModelRow = ModelPricingSnapshot & { + saved?: ModelPricingSnapshot + draft?: ModelPricingSnapshot + isDraftChanged: boolean + isDraftDeleted: boolean + isDraftNew: boolean +} + export type ModelRatioVisualEditorHandle = { commitOpenEditor: () => Promise } @@ -148,7 +158,10 @@ const getModeVariant = (mode?: string): 'warning' | 'info' | 'success' => { return 'success' } -const getExpressionSummary = (row: ModelRow, t: (key: string) => string) => { +const getExpressionSummary = ( + row: ModelPricingSnapshot, + t: (key: string) => string +) => { const tierCount = (row.billingExpr?.match(/tier\(/g) || []).length if (tierCount > 0) { return `${t('Tiered pricing')} · ${tierCount} ${t('tiers')}` @@ -156,7 +169,10 @@ const getExpressionSummary = (row: ModelRow, t: (key: string) => string) => { return t('Expression pricing') } -const getPriceSummary = (row: ModelRow, t: (key: string) => string) => { +const getPriceSummary = ( + row: ModelPricingSnapshot, + t: (key: string) => string +) => { if (row.billingMode === 'tiered_expr') { return getExpressionSummary(row, t) } @@ -181,7 +197,10 @@ const getPriceSummary = (row: ModelRow, t: (key: string) => string) => { : `${t('Input')} $${inputPrice}` } -const getPriceDetail = (row: ModelRow, t: (key: string) => string) => { +const getPriceDetail = ( + row: ModelPricingSnapshot, + t: (key: string) => string +) => { if (row.billingMode === 'tiered_expr') { return row.requestRuleExpr ? t('Includes request rules') @@ -206,11 +225,172 @@ const getPriceDetail = (row: ModelRow, t: (key: string) => string) => { return details.length > 0 ? details.join(' · ') : t('Base input price only') } +const buildModelSnapshots = ({ + modelPrice, + modelRatio, + cacheRatio, + createCacheRatio, + completionRatio, + imageRatio, + audioRatio, + audioCompletionRatio, + billingMode, + billingExpr, +}: Pick< + ModelRatioVisualEditorProps, + | 'modelPrice' + | 'modelRatio' + | 'cacheRatio' + | 'createCacheRatio' + | 'completionRatio' + | 'imageRatio' + | 'audioRatio' + | 'audioCompletionRatio' + | 'billingMode' + | 'billingExpr' +>): ModelPricingSnapshot[] => { + const priceMap = safeJsonParse>(modelPrice, { + fallback: {}, + context: 'model prices', + }) + const ratioMap = safeJsonParse>(modelRatio, { + fallback: {}, + context: 'model ratios', + }) + const cacheMap = safeJsonParse>(cacheRatio, { + fallback: {}, + context: 'cache ratios', + }) + const createCacheMap = safeJsonParse>( + createCacheRatio, + { fallback: {}, context: 'create cache ratios' } + ) + const completionMap = safeJsonParse>(completionRatio, { + fallback: {}, + context: 'completion ratios', + }) + const imageMap = safeJsonParse>(imageRatio, { + fallback: {}, + context: 'image ratios', + }) + const audioMap = safeJsonParse>(audioRatio, { + fallback: {}, + context: 'audio ratios', + }) + const audioCompletionMap = safeJsonParse>( + audioCompletionRatio, + { fallback: {}, context: 'audio completion ratios' } + ) + const billingModeMap = safeJsonParse>(billingMode, { + fallback: {}, + context: 'billing mode', + }) + const billingExprMap = safeJsonParse>(billingExpr, { + fallback: {}, + context: 'billing expression', + }) + + const modelNames = new Set([ + ...Object.keys(priceMap), + ...Object.keys(ratioMap), + ...Object.keys(cacheMap), + ...Object.keys(createCacheMap), + ...Object.keys(completionMap), + ...Object.keys(imageMap), + ...Object.keys(audioMap), + ...Object.keys(audioCompletionMap), + ...Object.keys(billingModeMap), + ...Object.keys(billingExprMap), + ]) + + return Array.from(modelNames).map((name) => { + const price = priceMap[name]?.toString() || '' + const ratio = ratioMap[name]?.toString() || '' + const cache = cacheMap[name]?.toString() || '' + const createCache = createCacheMap[name]?.toString() || '' + const completion = completionMap[name]?.toString() || '' + const image = imageMap[name]?.toString() || '' + const audio = audioMap[name]?.toString() || '' + const audioCompletion = audioCompletionMap[name]?.toString() || '' + + const modeForModel = billingModeMap[name] + if (modeForModel === 'tiered_expr') { + const fullExpr = billingExprMap[name] || '' + const { billingExpr: pureExpr, requestRuleExpr } = + splitBillingExprAndRequestRules(fullExpr) + return { + name, + billingMode: 'tiered_expr', + billingExpr: pureExpr, + requestRuleExpr, + price, + ratio, + cacheRatio: cache, + createCacheRatio: createCache, + completionRatio: completion, + imageRatio: image, + audioRatio: audio, + audioCompletionRatio: audioCompletion, + hasConflict: false, + } + } + + return { + name, + price, + ratio, + cacheRatio: cache, + createCacheRatio: createCache, + completionRatio: completion, + imageRatio: image, + audioRatio: audio, + audioCompletionRatio: audioCompletion, + billingMode: price !== '' ? 'per-request' : 'per-token', + hasConflict: + price !== '' && + (ratio !== '' || + completion !== '' || + cache !== '' || + createCache !== '' || + image !== '' || + audio !== '' || + audioCompletion !== ''), + } + }) +} + +const getSnapshotSignature = (snapshot?: ModelPricingSnapshot) => { + if (!snapshot) return '' + return JSON.stringify({ + price: snapshot.price || '', + ratio: snapshot.ratio || '', + cacheRatio: snapshot.cacheRatio || '', + createCacheRatio: snapshot.createCacheRatio || '', + completionRatio: snapshot.completionRatio || '', + imageRatio: snapshot.imageRatio || '', + audioRatio: snapshot.audioRatio || '', + audioCompletionRatio: snapshot.audioCompletionRatio || '', + billingMode: snapshot.billingMode || 'per-token', + billingExpr: snapshot.billingExpr || '', + requestRuleExpr: snapshot.requestRuleExpr || '', + }) +} + const ModelRatioVisualEditorComponent = forwardRef< ModelRatioVisualEditorHandle, ModelRatioVisualEditorProps >(function ModelRatioVisualEditor( { + savedModelPrice, + savedModelRatio, + savedCacheRatio, + savedCreateCacheRatio, + savedCompletionRatio, + savedImageRatio, + savedAudioRatio, + savedAudioCompletionRatio, + savedBillingMode, + savedBillingExpr, modelPrice, modelRatio, cacheRatio, @@ -279,120 +459,64 @@ const ModelRatioVisualEditorComponent = forwardRef< }, [columnVisibility]) const models = useMemo(() => { - const priceMap = safeJsonParse>(modelPrice, { - fallback: {}, - context: 'model prices', + const savedRows = buildModelSnapshots({ + modelPrice: savedModelPrice, + modelRatio: savedModelRatio, + cacheRatio: savedCacheRatio, + createCacheRatio: savedCreateCacheRatio, + completionRatio: savedCompletionRatio, + imageRatio: savedImageRatio, + audioRatio: savedAudioRatio, + audioCompletionRatio: savedAudioCompletionRatio, + billingMode: savedBillingMode, + billingExpr: savedBillingExpr, }) - const ratioMap = safeJsonParse>(modelRatio, { - fallback: {}, - context: 'model ratios', - }) - const cacheMap = safeJsonParse>(cacheRatio, { - fallback: {}, - context: 'cache ratios', - }) - const createCacheMap = safeJsonParse>( + const draftRows = buildModelSnapshots({ + modelPrice, + modelRatio, + cacheRatio, createCacheRatio, - { fallback: {}, context: 'create cache ratios' } - ) - const completionMap = safeJsonParse>( completionRatio, - { fallback: {}, context: 'completion ratios' } - ) - const imageMap = safeJsonParse>(imageRatio, { - fallback: {}, - context: 'image ratios', - }) - const audioMap = safeJsonParse>(audioRatio, { - fallback: {}, - context: 'audio ratios', - }) - const audioCompletionMap = safeJsonParse>( + imageRatio, + audioRatio, audioCompletionRatio, - { fallback: {}, context: 'audio completion ratios' } - ) - const billingModeMap = safeJsonParse>(billingMode, { - fallback: {}, - context: 'billing mode', - }) - const billingExprMap = safeJsonParse>(billingExpr, { - fallback: {}, - context: 'billing expression', + billingMode, + billingExpr, }) - const modelNames = new Set([ - ...Object.keys(priceMap), - ...Object.keys(ratioMap), - ...Object.keys(cacheMap), - ...Object.keys(createCacheMap), - ...Object.keys(completionMap), - ...Object.keys(imageMap), - ...Object.keys(audioMap), - ...Object.keys(audioCompletionMap), - ...Object.keys(billingModeMap), - ...Object.keys(billingExprMap), - ]) + const savedByName = new Map(savedRows.map((row) => [row.name, row])) + const draftByName = new Map(draftRows.map((row) => [row.name, row])) + const modelNames = new Set([...savedByName.keys(), ...draftByName.keys()]) - const modelData: ModelRow[] = Array.from(modelNames).map((name) => { - const price = priceMap[name]?.toString() || '' - const ratio = ratioMap[name]?.toString() || '' - const cache = cacheMap[name]?.toString() || '' - const createCache = createCacheMap[name]?.toString() || '' - const completion = completionMap[name]?.toString() || '' - const image = imageMap[name]?.toString() || '' - const audio = audioMap[name]?.toString() || '' - const audioCompletion = audioCompletionMap[name]?.toString() || '' + return Array.from(modelNames) + .map((name) => { + const saved = savedByName.get(name) + const draft = draftByName.get(name) + const displayed = saved ?? draft + const savedSignature = getSnapshotSignature(saved) + const draftSignature = getSnapshotSignature(draft) - const modeForModel = billingModeMap[name] - if (modeForModel === 'tiered_expr') { - // Tiered_expr models may also retain ratio/price values as fallback - // during multi-instance sync delays. We preserve them in the row so - // the edit dialog round-trip and the next save don't drop them. - const fullExpr = billingExprMap[name] || '' - const { billingExpr: pureExpr, requestRuleExpr } = - splitBillingExprAndRequestRules(fullExpr) return { - name, - billingMode: 'tiered_expr', - billingExpr: pureExpr, - requestRuleExpr, - price, - ratio, - cacheRatio: cache, - createCacheRatio: createCache, - completionRatio: completion, - imageRatio: image, - audioRatio: audio, - audioCompletionRatio: audioCompletion, - hasConflict: false, + ...displayed!, + saved, + draft, + isDraftChanged: savedSignature !== draftSignature, + isDraftDeleted: Boolean(saved && !draft), + isDraftNew: Boolean(!saved && draft), } - } - - return { - name, - price, - ratio, - cacheRatio: cache, - createCacheRatio: createCache, - completionRatio: completion, - imageRatio: image, - audioRatio: audio, - audioCompletionRatio: audioCompletion, - billingMode: price !== '' ? 'per-request' : 'per-token', - hasConflict: - price !== '' && - (ratio !== '' || - completion !== '' || - cache !== '' || - createCache !== '' || - image !== '' || - audio !== '' || - audioCompletion !== ''), - } - }) - - return modelData.sort((a, b) => a.name.localeCompare(b.name)) + }) + .sort((a, b) => a.name.localeCompare(b.name)) }, [ + savedModelPrice, + savedModelRatio, + savedCacheRatio, + savedCreateCacheRatio, + savedCompletionRatio, + savedImageRatio, + savedAudioRatio, + savedAudioCompletionRatio, + savedBillingMode, + savedBillingExpr, modelPrice, modelRatio, cacheRatio, @@ -428,24 +552,25 @@ const ModelRatioVisualEditorComponent = forwardRef< const handleEdit = useCallback( (model: ModelRow) => { + const editableModel = model.draft ?? model.saved ?? model setEditData({ - name: model.name, - price: model.price, - ratio: model.ratio, - cacheRatio: model.cacheRatio, - createCacheRatio: model.createCacheRatio, - completionRatio: model.completionRatio, - imageRatio: model.imageRatio, - audioRatio: model.audioRatio, - audioCompletionRatio: model.audioCompletionRatio, + name: editableModel.name, + price: editableModel.price, + ratio: editableModel.ratio, + cacheRatio: editableModel.cacheRatio, + createCacheRatio: editableModel.createCacheRatio, + completionRatio: editableModel.completionRatio, + imageRatio: editableModel.imageRatio, + audioRatio: editableModel.audioRatio, + audioCompletionRatio: editableModel.audioCompletionRatio, billingMode: - model.billingMode === 'tiered_expr' + editableModel.billingMode === 'tiered_expr' ? 'tiered_expr' - : model.price && model.price !== '' + : editableModel.price && editableModel.price !== '' ? 'per-request' : 'per-token', - billingExpr: model.billingExpr, - requestRuleExpr: model.requestRuleExpr, + billingExpr: editableModel.billingExpr, + requestRuleExpr: editableModel.requestRuleExpr, }) setEditorOpen(true) if (isMobile) setSheetOpen(true) @@ -459,12 +584,6 @@ const ModelRatioVisualEditorComponent = forwardRef< if (isMobile) setSheetOpen(true) }, [isMobile]) - const handleCancel = useCallback(() => { - setEditData(null) - setEditorOpen(false) - setSheetOpen(false) - }, []) - const handleGlobalFilterChange = useCallback>( (updater) => { setGlobalFilter((previous) => { @@ -604,6 +723,13 @@ const ModelRatioVisualEditorComponent = forwardRef< cell: ({ row }) => (
{row.getValue('name')} + {row.original.isDraftChanged && ( + + )} {row.original.billingMode === 'tiered_expr' && ( ), cell: ({ row }) => ( -
- - {getPriceSummary(row.original, t)} - - - {getPriceDetail(row.original, t)} - +
+
+ + {getPriceSummary(row.original, t)} + + + {getPriceDetail(row.original, t)} + +
+ {row.original.isDraftChanged && ( +
+
+ + {!row.original.isDraftDeleted && row.original.draft && ( + + )} + + {row.original.isDraftDeleted + ? t('Will be removed') + : getPriceSummary(row.original.draft ?? row.original, t)} + +
+ {!row.original.isDraftDeleted && row.original.draft && ( + + {getPriceDetail(row.original.draft, t)} + + )} +
+ )}
), sortingFn: (rowA, rowB) => @@ -849,18 +1007,6 @@ const ModelRatioVisualEditorComponent = forwardRef< ] ) - const handleSave = useCallback( - (data: ModelRatioData) => { - persistPricingData(data) - setEditData(data) - setEditorOpen(true) - toast.success( - t('Pricing changes saved to draft. Click "Save model prices" to apply.') - ) - }, - [persistPricingData, t] - ) - const handleBatchCopy = useCallback(() => { if (!editData) { toast.error(t('Open a source model first')) @@ -901,12 +1047,10 @@ const ModelRatioVisualEditorComponent = forwardRef< [editorOpen, persistPricingData] ) - const selectedTargetCount = table.getFilteredSelectedRowModel().rows.length - return (
-
-
+
+
) : ( -
- - +
+
+ {table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => ( - + ))} - + ))} - - + + {table.getRowModel().rows.map((row) => ( - { const target = event.target as HTMLElement @@ -983,17 +1131,20 @@ const ModelRatioVisualEditorComponent = forwardRef< }} > {row.getVisibleCells().map((cell) => ( - + ))} - + ))} - -
{header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} - +
{flexRender( cell.column.columnDef.cell, cell.getContext() )} - +
+ +
)} @@ -1002,18 +1153,15 @@ const ModelRatioVisualEditorComponent = forwardRef< )}
-
+
{editorOpen ? ( ) : ( -
+
{t('Select a model to edit pricing')}
@@ -1045,10 +1193,7 @@ const ModelRatioVisualEditorComponent = forwardRef< ref={editorPanelRef} open={sheetOpen} onOpenChange={setSheetOpen} - onSave={handleSave} - onCancel={handleCancel} editData={editData} - selectedTargetCount={selectedTargetCount} /> )}
diff --git a/web/default/src/features/system-settings/models/ratio-settings-card.tsx b/web/default/src/features/system-settings/models/ratio-settings-card.tsx index 7d72e404..6f9d9f58 100644 --- a/web/default/src/features/system-settings/models/ratio-settings-card.tsx +++ b/web/default/src/features/system-settings/models/ratio-settings-card.tsx @@ -250,6 +250,9 @@ export function RatioSettingsCard({ BillingMode: normalizeJsonString(modelDefaults.BillingMode), BillingExpr: normalizeJsonString(modelDefaults.BillingExpr), }) + const [savedModelValues, setSavedModelValues] = useState( + modelNormalizedDefaults.current + ) const groupNormalizedDefaults = useRef({ GroupRatio: normalizeJsonString(groupDefaults.GroupRatio), @@ -315,6 +318,7 @@ export function RatioSettingsCard({ BillingMode: normalizeJsonString(modelDefaults.BillingMode), BillingExpr: normalizeJsonString(modelDefaults.BillingExpr), } + setSavedModelValues(modelNormalizedDefaults.current) modelForm.reset({ ...modelDefaults, @@ -395,6 +399,9 @@ export function RatioSettingsCard({ const apiKey = apiKeyMap[key as string] || (key as string) await updateOption.mutateAsync({ key: apiKey, value: normalized[key] }) } + + modelNormalizedDefaults.current = normalized + setSavedModelValues(normalized) }, [t, updateOption] ) @@ -462,6 +469,7 @@ export function RatioSettingsCard({ return (