From abad0d3cc08908c3c84b035ba436aa37c10aec94 Mon Sep 17 00:00:00 2001 From: QuentinHsu Date: Wed, 3 Jun 2026 14:49:08 +0800 Subject: [PATCH] fix(model-pricing): detect visual pricing draft changes on save - expose a draft commit handle from the model pricing editor panel before saving. - commit the open visual editor into the parent form before page-level save runs. - support both desktop side editor and mobile sheet save paths. --- .../models/model-pricing-sheet.tsx | 111 +- .../models/model-ratio-form.tsx | 20 +- .../models/model-ratio-visual-editor.tsx | 1552 +++++++++-------- 3 files changed, 881 insertions(+), 802 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 b9e5c548..9b033d9c 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 @@ -16,7 +16,14 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { useEffect, useMemo, useState } from 'react' +import { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useState, +} from 'react' import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' @@ -130,6 +137,10 @@ type ModelPricingEditorPanelProps = Omit< className?: string } +export type ModelPricingEditorPanelHandle = { + commitDraft: () => Promise +} + type PreviewRow = { key: string label: string @@ -377,14 +388,13 @@ function buildPreviewRows( ] } -export function ModelPricingSheet({ - open, - onOpenChange, - onSave, - onCancel, - editData, - selectedTargetCount = 0, -}: ModelPricingSheetProps) { +export const ModelPricingSheet = forwardRef< + ModelPricingEditorPanelHandle, + ModelPricingSheetProps +>(function ModelPricingSheet( + { open, onOpenChange, onSave, onCancel, editData, selectedTargetCount = 0 }, + ref +) { const { t } = useTranslation() const title = editData ? t('Edit model pricing') : t('Add model pricing') const description = editData?.name || t('New model') @@ -400,6 +410,7 @@ export function ModelPricingSheet({ {description} ) -} +}) -export function ModelPricingEditorPanel({ - onSave, - editData, - selectedTargetCount = 0, - onCancel, - className, -}: ModelPricingEditorPanelProps) { +export const ModelPricingEditorPanel = forwardRef< + ModelPricingEditorPanelHandle, + ModelPricingEditorPanelProps +>(function ModelPricingEditorPanel( + { onSave, editData, selectedTargetCount = 0, onCancel, className }, + ref +) { const { t } = useTranslation() const [pricingMode, setPricingMode] = useState('per-token') const [promptPrice, setPromptPrice] = useState('') @@ -687,7 +698,7 @@ export function ModelPricingEditorPanel({ return nextWarnings }, [editData, laneEnabled, lanePrices, pricingMode, promptPrice, t]) - const handleSubmit = (values: ModelPricingFormValues) => { + const validatePricingValues = useCallback(() => { if ( pricingMode === 'per-token' && toNumberOrNull(promptPrice) === null && @@ -698,7 +709,7 @@ export function ModelPricingEditorPanel({ form.setError('ratio', { message: t('Input price is required before saving dependent prices.'), }) - return + return false } if ( @@ -709,27 +720,53 @@ export function ModelPricingEditorPanel({ form.setError('audioRatio', { message: t('Audio output price requires an audio input price.'), }) - return + return false } - 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 || '', - } + return true + }, [form, laneEnabled, lanePrices, pricingMode, promptPrice, t]) - if (pricingMode === 'tiered_expr') { - data.billingExpr = billingExpr - data.requestRuleExpr = requestRuleExpr - } + const buildSubmitData = useCallback( + (values: ModelPricingFormValues) => { + 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 + } + + return data + }, + [billingExpr, pricingMode, requestRuleExpr] + ) + + useImperativeHandle( + ref, + () => ({ + commitDraft: async () => { + const isValid = await form.trigger() + if (!isValid || !validatePricingValues()) return null + return buildSubmitData(form.getValues()) + }, + }), + [form, validatePricingValues, buildSubmitData] + ) + + const handleSubmit = (values: ModelPricingFormValues) => { + if (!validatePricingValues()) return + + const data = buildSubmitData(values) onSave(data) form.reset() onCancel?.() @@ -980,7 +1017,7 @@ export function ModelPricingEditorPanel({ ) -} +}) function PriceInput(props: { value: string 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 5b8d1f14..f098c025 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 @@ -16,7 +16,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { memo, useCallback, useState } from 'react' +import { memo, useCallback, useRef, useState } from 'react' import { type UseFormReturn } from 'react-hook-form' import { Code2, Eye } from 'lucide-react' import { useTranslation } from 'react-i18next' @@ -38,7 +38,10 @@ import { SettingsSwitchItem, } from '../components/settings-form-layout' import { SettingsPageActionsPortal } from '../components/settings-page-context' -import { ModelRatioVisualEditor } from './model-ratio-visual-editor' +import { + ModelRatioVisualEditor, + type ModelRatioVisualEditorHandle, +} from './model-ratio-visual-editor' type ModelFormValues = { ModelPrice: string @@ -71,6 +74,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({ }: ModelRatioFormProps) { const { t } = useTranslation() const [editMode, setEditMode] = useState<'visual' | 'json'>('visual') + const visualEditorRef = useRef(null) const handleFieldChange = useCallback( (field: keyof ModelFormValues, value: string) => { @@ -86,6 +90,15 @@ export const ModelRatioForm = memo(function ModelRatioForm({ setEditMode((prev) => (prev === 'visual' ? 'json' : 'visual')) }, []) + const handleSave = useCallback(async () => { + if (editMode === 'visual') { + const committed = await visualEditorRef.current?.commitOpenEditor() + if (committed === false) return + } + + await form.handleSubmit(onSave)() + }, [editMode, form, onSave]) + return (
@@ -118,7 +131,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({ + +
+ ), + enableHiding: false, + }, + ] + }, [handleEdit, handleDelete, t]) - const handleDelete = useCallback( - (name: string) => { - const priceMap = safeJsonParse>(modelPrice, { - fallback: {}, - silent: true, - }) - const ratioMap = safeJsonParse>(modelRatio, { - fallback: {}, - silent: true, - }) - const cacheMap = safeJsonParse>(cacheRatio, { - fallback: {}, - silent: true, - }) - const createCacheMap = safeJsonParse>( - createCacheRatio, - { fallback: {}, silent: true } - ) - const completionMap = safeJsonParse>( - completionRatio, - { fallback: {}, silent: true } - ) - const imageMap = safeJsonParse>(imageRatio, { - fallback: {}, - silent: true, - }) - const audioMap = safeJsonParse>(audioRatio, { - fallback: {}, - silent: true, - }) - const audioCompletionMap = safeJsonParse>( - audioCompletionRatio, - { fallback: {}, silent: true } - ) - const billingModeMap = safeJsonParse>( - billingMode, - { fallback: {}, silent: true } - ) - const billingExprMap = safeJsonParse>( - billingExpr, - { fallback: {}, silent: true } - ) + const table = useReactTable({ + data: models, + columns, + state: { + sorting, + columnFilters, + globalFilter, + columnVisibility, + pagination, + rowSelection, + }, + enableRowSelection: true, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onGlobalFilterChange: handleGlobalFilterChange, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + autoResetPageIndex: false, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + globalFilterFn: (row, _columnId, filterValue) => { + const searchValue = String(filterValue).toLowerCase() + return row.original.name.toLowerCase().includes(searchValue) + }, + }) + const persistPricingData = useCallback( + (data: ModelRatioData, targetNames: string[] = [data.name]) => { + const priceMap = safeJsonParse>(modelPrice, { + fallback: {}, + silent: true, + }) + const ratioMap = safeJsonParse>(modelRatio, { + fallback: {}, + silent: true, + }) + const cacheMap = safeJsonParse>(cacheRatio, { + fallback: {}, + silent: true, + }) + const createCacheMap = safeJsonParse>( + createCacheRatio, + { fallback: {}, silent: true } + ) + const completionMap = safeJsonParse>( + completionRatio, + { fallback: {}, silent: true } + ) + const imageMap = safeJsonParse>(imageRatio, { + fallback: {}, + silent: true, + }) + const audioMap = safeJsonParse>(audioRatio, { + fallback: {}, + silent: true, + }) + const audioCompletionMap = safeJsonParse>( + audioCompletionRatio, + { fallback: {}, silent: true } + ) + const billingModeMap = safeJsonParse>( + billingMode, + { fallback: {}, silent: true } + ) + const billingExprMap = safeJsonParse>( + billingExpr, + { fallback: {}, silent: true } + ) + + const setIfPresent = ( + target: Record, + name: string, + value: string | undefined + ) => { + if (!value || value === '') return + const parsed = parseFloat(value) + if (Number.isFinite(parsed)) target[name] = parsed + } + + targetNames.forEach((name) => { delete priceMap[name] delete ratioMap[name] delete cacheMap[name] @@ -521,514 +780,283 @@ export const ModelRatioVisualEditor = memo( delete billingModeMap[name] delete billingExprMap[name] - onChange('ModelPrice', JSON.stringify(priceMap, null, 2)) - onChange('ModelRatio', JSON.stringify(ratioMap, null, 2)) - onChange('CacheRatio', JSON.stringify(cacheMap, null, 2)) - onChange('CreateCacheRatio', JSON.stringify(createCacheMap, null, 2)) - onChange('CompletionRatio', JSON.stringify(completionMap, null, 2)) - onChange('ImageRatio', JSON.stringify(imageMap, null, 2)) - onChange('AudioRatio', JSON.stringify(audioMap, null, 2)) - onChange( - 'AudioCompletionRatio', - JSON.stringify(audioCompletionMap, null, 2) - ) - onChange( - 'billing_setting.billing_mode', - JSON.stringify(billingModeMap, null, 2) - ) - onChange( - 'billing_setting.billing_expr', - JSON.stringify(billingExprMap, null, 2) - ) - }, - [ - modelPrice, - modelRatio, - cacheRatio, - createCacheRatio, - completionRatio, - imageRatio, - audioRatio, - audioCompletionRatio, - billingMode, - billingExpr, - onChange, - ] - ) - - const columns = useMemo[]>(() => { - return [ - { - id: 'select', - header: ({ table }) => ( - - table.toggleAllPageRowsSelected(!!value) - } - aria-label={t('Select all')} - className='translate-y-[2px]' - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label={t('Select row')} - className='translate-y-[2px]' - /> - ), - enableSorting: false, - enableHiding: false, - meta: { label: t('Select') }, - }, - { - accessorKey: 'name', - header: ({ column }) => ( - - ), - cell: ({ row }) => ( -
- {row.getValue('name')} - {row.original.billingMode === 'tiered_expr' && ( - - )} - {row.original.hasConflict && ( - - )} -
- ), - enableHiding: false, - }, - { - accessorKey: 'billingMode', - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - - ), - filterFn: (row, id, value) => - filterBySelectedValues(row.getValue(id), value), - meta: { label: t('Mode') }, - }, - { - id: 'priceSummary', - header: ({ column }) => ( - - ), - cell: ({ row }) => ( -
- - {getPriceSummary(row.original, t)} - - - {getPriceDetail(row.original, t)} - -
- ), - sortingFn: (rowA, rowB) => - getPriceSummary(rowA.original, t).localeCompare( - getPriceSummary(rowB.original, t) - ), - meta: { label: t('Price summary') }, - }, - { - id: 'actions', - cell: ({ row }) => ( -
- - -
- ), - enableHiding: false, - }, - ] - }, [handleEdit, handleDelete, t]) - - const table = useReactTable({ - data: models, - columns, - state: { - sorting, - columnFilters, - globalFilter, - columnVisibility, - pagination, - rowSelection, - }, - enableRowSelection: true, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onGlobalFilterChange: handleGlobalFilterChange, - onColumnVisibilityChange: setColumnVisibility, - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - autoResetPageIndex: false, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - globalFilterFn: (row, _columnId, filterValue) => { - const searchValue = String(filterValue).toLowerCase() - return row.original.name.toLowerCase().includes(searchValue) - }, - }) - - const persistPricingData = useCallback( - (data: ModelRatioData, targetNames: string[] = [data.name]) => { - const priceMap = safeJsonParse>(modelPrice, { - fallback: {}, - silent: true, - }) - const ratioMap = safeJsonParse>(modelRatio, { - fallback: {}, - silent: true, - }) - const cacheMap = safeJsonParse>(cacheRatio, { - fallback: {}, - silent: true, - }) - const createCacheMap = safeJsonParse>( - createCacheRatio, - { fallback: {}, silent: true } - ) - const completionMap = safeJsonParse>( - completionRatio, - { fallback: {}, silent: true } - ) - const imageMap = safeJsonParse>(imageRatio, { - fallback: {}, - silent: true, - }) - const audioMap = safeJsonParse>(audioRatio, { - fallback: {}, - silent: true, - }) - const audioCompletionMap = safeJsonParse>( - audioCompletionRatio, - { fallback: {}, silent: true } - ) - const billingModeMap = safeJsonParse>( - billingMode, - { fallback: {}, silent: true } - ) - const billingExprMap = safeJsonParse>( - billingExpr, - { fallback: {}, silent: true } - ) - - const setIfPresent = ( - target: Record, - name: string, - value: string | undefined - ) => { - if (!value || value === '') return - const parsed = parseFloat(value) - if (Number.isFinite(parsed)) target[name] = parsed - } - - targetNames.forEach((name) => { - delete priceMap[name] - delete ratioMap[name] - delete cacheMap[name] - delete createCacheMap[name] - delete completionMap[name] - delete imageMap[name] - delete audioMap[name] - delete audioCompletionMap[name] - delete billingModeMap[name] - delete billingExprMap[name] - - if (data.billingMode === 'tiered_expr') { - const combined = combineBillingExpr( - data.billingExpr || '', - data.requestRuleExpr || '' - ) - if (combined) { - billingModeMap[name] = 'tiered_expr' - billingExprMap[name] = combined - } - // Always serialize ratio/price values for tiered_expr models so they - // serve as fallback during multi-instance sync delays. The backend's - // ModelPriceHelper checks billing_mode first, so these values are - // only consulted when billing_setting hasn't propagated yet. - setIfPresent(priceMap, name, data.price) - setIfPresent(ratioMap, name, data.ratio) - setIfPresent(cacheMap, name, data.cacheRatio) - setIfPresent(createCacheMap, name, data.createCacheRatio) - setIfPresent(completionMap, name, data.completionRatio) - setIfPresent(imageMap, name, data.imageRatio) - setIfPresent(audioMap, name, data.audioRatio) - setIfPresent(audioCompletionMap, name, data.audioCompletionRatio) - } else if (data.price && data.price !== '') { - setIfPresent(priceMap, name, data.price) - } else { - setIfPresent(ratioMap, name, data.ratio) - setIfPresent(cacheMap, name, data.cacheRatio) - setIfPresent(createCacheMap, name, data.createCacheRatio) - setIfPresent(completionMap, name, data.completionRatio) - setIfPresent(imageMap, name, data.imageRatio) - setIfPresent(audioMap, name, data.audioRatio) - setIfPresent(audioCompletionMap, name, data.audioCompletionRatio) + if (data.billingMode === 'tiered_expr') { + const combined = combineBillingExpr( + data.billingExpr || '', + data.requestRuleExpr || '' + ) + if (combined) { + billingModeMap[name] = 'tiered_expr' + billingExprMap[name] = combined } - }) + // Always serialize ratio/price values for tiered_expr models so they + // serve as fallback during multi-instance sync delays. The backend's + // ModelPriceHelper checks billing_mode first, so these values are + // only consulted when billing_setting hasn't propagated yet. + setIfPresent(priceMap, name, data.price) + setIfPresent(ratioMap, name, data.ratio) + setIfPresent(cacheMap, name, data.cacheRatio) + setIfPresent(createCacheMap, name, data.createCacheRatio) + setIfPresent(completionMap, name, data.completionRatio) + setIfPresent(imageMap, name, data.imageRatio) + setIfPresent(audioMap, name, data.audioRatio) + setIfPresent(audioCompletionMap, name, data.audioCompletionRatio) + } else if (data.price && data.price !== '') { + setIfPresent(priceMap, name, data.price) + } else { + setIfPresent(ratioMap, name, data.ratio) + setIfPresent(cacheMap, name, data.cacheRatio) + setIfPresent(createCacheMap, name, data.createCacheRatio) + setIfPresent(completionMap, name, data.completionRatio) + setIfPresent(imageMap, name, data.imageRatio) + setIfPresent(audioMap, name, data.audioRatio) + setIfPresent(audioCompletionMap, name, data.audioCompletionRatio) + } + }) - onChange('ModelPrice', JSON.stringify(priceMap, null, 2)) - onChange('ModelRatio', JSON.stringify(ratioMap, null, 2)) - onChange('CacheRatio', JSON.stringify(cacheMap, null, 2)) - onChange('CreateCacheRatio', JSON.stringify(createCacheMap, null, 2)) - onChange('CompletionRatio', JSON.stringify(completionMap, null, 2)) - onChange('ImageRatio', JSON.stringify(imageMap, null, 2)) - onChange('AudioRatio', JSON.stringify(audioMap, null, 2)) - onChange( - 'AudioCompletionRatio', - JSON.stringify(audioCompletionMap, null, 2) - ) - onChange( - 'billing_setting.billing_mode', - JSON.stringify(billingModeMap, null, 2) - ) - onChange( - 'billing_setting.billing_expr', - JSON.stringify(billingExprMap, null, 2) - ) - }, - [ - modelPrice, - modelRatio, - cacheRatio, - createCacheRatio, - completionRatio, - imageRatio, - audioRatio, - audioCompletionRatio, - billingMode, - billingExpr, - onChange, - ] + onChange('ModelPrice', JSON.stringify(priceMap, null, 2)) + onChange('ModelRatio', JSON.stringify(ratioMap, null, 2)) + onChange('CacheRatio', JSON.stringify(cacheMap, null, 2)) + onChange('CreateCacheRatio', JSON.stringify(createCacheMap, null, 2)) + onChange('CompletionRatio', JSON.stringify(completionMap, null, 2)) + onChange('ImageRatio', JSON.stringify(imageMap, null, 2)) + onChange('AudioRatio', JSON.stringify(audioMap, null, 2)) + onChange( + 'AudioCompletionRatio', + JSON.stringify(audioCompletionMap, null, 2) + ) + onChange( + 'billing_setting.billing_mode', + JSON.stringify(billingModeMap, null, 2) + ) + onChange( + 'billing_setting.billing_expr', + JSON.stringify(billingExprMap, null, 2) + ) + }, + [ + modelPrice, + modelRatio, + cacheRatio, + createCacheRatio, + completionRatio, + imageRatio, + audioRatio, + audioCompletionRatio, + billingMode, + billingExpr, + onChange, + ] + ) + + 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')) + return + } + + const targetNames = table + .getFilteredSelectedRowModel() + .rows.map((row) => row.original.name) + + if (targetNames.length === 0) { + toast.error(t('Select at least one target model')) + return + } + + persistPricingData(editData, targetNames) + table.resetRowSelection() + toast.success( + t('Applied {{name}} pricing to {{count}} models', { + name: editData.name, + count: targetNames.length, + }) ) + }, [editData, persistPricingData, t, table]) - const handleSave = useCallback( - (data: ModelRatioData) => { + useImperativeHandle( + ref, + () => ({ + commitOpenEditor: async () => { + if (!editorOpen || !editorPanelRef.current) return true + const data = await editorPanelRef.current.commitDraft() + if (!data) return false persistPricingData(data) setEditData(data) - setEditorOpen(true) - toast.success( - t( - 'Pricing changes saved to draft. Click "Save model prices" to apply.' - ) - ) + return true }, - [persistPricingData, t] - ) + }), + [editorOpen, persistPricingData] + ) - const handleBatchCopy = useCallback(() => { - if (!editData) { - toast.error(t('Open a source model first')) - return - } + const selectedTargetCount = table.getFilteredSelectedRowModel().rows.length - const targetNames = table - .getFilteredSelectedRowModel() - .rows.map((row) => row.original.name) + return ( +
+
+
+ + + {t('Add model')} + + } + /> - if (targetNames.length === 0) { - toast.error(t('Select at least one target model')) - return - } + {table.getRowModel().rows.length === 0 ? ( +
+ {table.getState().globalFilter + ? t('No models match your search') + : t('No models configured. Use Add model to get started.')} +
+ ) : ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + { + const target = event.target as HTMLElement + if (target.closest('button, [role="checkbox"]')) return + handleEdit(row.original) + }} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+
+ )} - persistPricingData(editData, targetNames) - table.resetRowSelection() - toast.success( - t('Applied {{name}} pricing to {{count}} models', { - name: editData.name, - count: targetNames.length, - }) - ) - }, [editData, persistPricingData, t, table]) - - const selectedTargetCount = table.getFilteredSelectedRowModel().rows.length - - return ( -
-
-
- - - {t('Add model')} - - } - /> - - {table.getRowModel().rows.length === 0 ? ( -
- {table.getState().globalFilter - ? t('No models match your search') - : t('No models configured. Use Add model to get started.')} -
- ) : ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - { - const target = event.target as HTMLElement - if (target.closest('button, [role="checkbox"]')) - return - handleEdit(row.original) - }} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
-
- )} - - {table.getRowModel().rows.length > 0 && ( - - )} -
- -
- {editorOpen ? ( - - ) : ( -
-
- {t('Select a model to edit pricing')} -
-

- {t( - 'Use the full-width table to scan prices, then select a row to edit it here.' - )} -

- -
- )} -
+ {table.getRowModel().rows.length > 0 && ( + + )}
- - - - - {isMobile && ( - - )} +
+ {editorOpen ? ( + + ) : ( +
+
+ {t('Select a model to edit pricing')} +
+

+ {t( + 'Use the full-width table to scan prices, then select a row to edit it here.' + )} +

+ +
+ )} +
- ) - }, + + + + + + {isMobile && ( + + )} +
+ ) +}) + +export const ModelRatioVisualEditor = memo( + ModelRatioVisualEditorComponent, // Custom equality check - only re-render if JSON props actually changed (prevProps, nextProps) => { return (