diff --git a/web/default/src/features/system-settings/components/settings-page.tsx b/web/default/src/features/system-settings/components/settings-page.tsx index 5c4c8c6b..678b387e 100644 --- a/web/default/src/features/system-settings/components/settings-page.tsx +++ b/web/default/src/features/system-settings/components/settings-page.tsx @@ -83,7 +83,9 @@ function SettingsPageFrame(props: SettingsPageFrameProps) { /> -
{props.children}
+
+ {props.children} +
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 25e4b519..767b8dfe 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 @@ -58,18 +58,24 @@ function formatJsonValidationError( ) } - const parts = [ - error.line && error.column - ? t('JSON is invalid at line {{line}}, column {{column}}.', { - line: error.line, - column: error.column, - }) - : error.position !== undefined - ? t('JSON is invalid at position {{position}}.', { - position: error.position, - }) - : t('JSON is invalid. Please check the syntax.'), - ] + let locationMessage: string + if (error.line && error.column) { + locationMessage = t( + 'JSON is invalid at line {{line}}, column {{column}}.', + { + line: error.line, + column: error.column, + } + ) + } else if (error.position !== undefined) { + locationMessage = t('JSON is invalid at position {{position}}.', { + position: error.position, + }) + } else { + locationMessage = t('JSON is invalid. Please check the syntax.') + } + + const parts = [locationMessage] if (error.missingCommaLine) { parts.push( @@ -459,14 +465,14 @@ export function RatioSettingsCard({ {renderTabContent(defaultTab)} ) : ( - + {renderTabSwitcher()} - + {visibleTabs.map((tab) => ( - + {renderTabContent(tab)} ))} diff --git a/web/default/src/features/system-settings/models/upstream-ratio-sync-columns.tsx b/web/default/src/features/system-settings/models/upstream-ratio-sync-columns.tsx index 13374b78..fad27659 100644 --- a/web/default/src/features/system-settings/models/upstream-ratio-sync-columns.tsx +++ b/web/default/src/features/system-settings/models/upstream-ratio-sync-columns.tsx @@ -16,12 +16,12 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { type ColumnDef } from '@tanstack/react-table' +import type { ColumnDef } from '@tanstack/react-table' import { AlertTriangle } from 'lucide-react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { BadgeCell } from '@/components/data-table' +import { DataTableColumnHeader } from '@/components/data-table' import { StatusBadge } from '@/components/status-badge' import { Checkbox } from '@/components/ui/checkbox' import { @@ -33,16 +33,23 @@ import { import type { RatioType } from '../types' import { - getOrderedRatioTypes, + getAlignedRatioTypes, getPreferredSyncField, getSyncFieldLabel, - isSelectableUpstreamValue, + isSelectedResolutionValue, type ModelRow, type ResolutionsMap, } from './upstream-ratio-sync-helpers' +import type { UpstreamBulkSelectState } from './upstream-ratio-sync-table' + +const syncFieldListClassName = 'flex max-w-full min-w-0 flex-col gap-1.5' +const syncFieldRowClassName = + 'bg-muted/30 flex h-8 w-fit max-w-full min-w-0 items-center gap-2 rounded-md px-2' +const syncFieldLabelClassName = 'min-w-[4.5rem] shrink-0' export function useUpstreamRatioSyncColumns( upstreamNames: string[], + bulkSelectStateByUpstream: Record, resolutions: ResolutionsMap, ratioTypeFilter: string, isDisabled: boolean, @@ -53,8 +60,8 @@ export function useUpstreamRatioSyncColumns( sourceName: string ) => void, onUnselectValue: (model: string, ratioType: RatioType) => void, - onBulkSelect: (upstreamName: string, rows: ModelRow[]) => void, - onBulkUnselect: (upstreamName: string, rows: ModelRow[]) => void + onBulkSelect: (upstreamName: string) => void, + onBulkUnselect: (upstreamName: string) => void ): ColumnDef[] { const { t } = useTranslation() @@ -62,7 +69,11 @@ export function useUpstreamRatioSyncColumns( const baseColumns: ColumnDef[] = [ { accessorKey: 'model', - header: t('Model'), + header: ({ column }) => ( + + ), + size: 220, + minSize: 180, cell: ({ row }) => { const model = row.original.model return ( @@ -90,23 +101,29 @@ export function useUpstreamRatioSyncColumns( }, { id: 'current', - header: t('Current Price'), + header: ({ column }) => ( + + ), + size: 260, + minSize: 220, cell: ({ row }) => { - const fields = getOrderedRatioTypes( + const fields = getAlignedRatioTypes( row.original.ratioTypes, + upstreamNames, ratioTypeFilter ) return ( -
+
{fields.map((ratioType) => { const current = row.original.ratioTypes[ratioType]?.current return ( - +
{current === null || current === undefined ? ( } - > + />

{String(current)} @@ -136,7 +153,7 @@ export function useUpstreamRatioSyncColumns( )} - +

) })}
@@ -148,41 +165,19 @@ export function useUpstreamRatioSyncColumns( const upstreamColumns: ColumnDef[] = upstreamNames.map( (upstreamName) => ({ id: `upstream_${upstreamName}`, - header: ({ table }) => { - const rows = table.getFilteredRowModel().rows.map((r) => r.original) - - let selectableCount = 0 - let selectedCount = 0 - - rows.forEach((row) => { - getOrderedRatioTypes(row.ratioTypes, ratioTypeFilter).forEach( - (ratioType) => { - const upstreamVal = - row.ratioTypes[ratioType]?.upstreams?.[upstreamName] - const preferredField = getPreferredSyncField( - row.ratioTypes, - ratioType, - upstreamName - ) - if ( - preferredField === ratioType && - isSelectableUpstreamValue(upstreamVal) - ) { - selectableCount++ - if (resolutions[row.model]?.[ratioType] === upstreamVal) { - selectedCount++ - } - } - } - ) - }) - + size: 280, + minSize: 240, + header: () => { + const bulkSelectState = bulkSelectStateByUpstream[upstreamName] + const displayName = bulkSelectState?.displayName ?? upstreamName + const selectableCount = bulkSelectState?.selectableCount ?? 0 + const selectedCount = bulkSelectState?.selectedCount ?? 0 const allSelected = selectableCount > 0 && selectedCount === selectableCount const someSelected = selectedCount > 0 && selectedCount < selectableCount return ( -
+
{selectableCount > 0 && ( { if (checked) { - onBulkSelect(upstreamName, rows) + onBulkSelect(upstreamName) } else { - onBulkUnselect(upstreamName, rows) + onBulkUnselect(upstreamName) } }} + aria-label={t('Select all (filtered)')} + className='shrink-0' /> )} - {upstreamName} +
+ + {displayName} + + {selectableCount > 0 && ( + + {selectedCount}/{selectableCount} + + )} +
) }, cell: ({ row }) => { - const fields = getOrderedRatioTypes( + const fields = getAlignedRatioTypes( row.original.ratioTypes, + upstreamNames, ratioTypeFilter - ).filter( - (ratioType) => - getPreferredSyncField( - row.original.ratioTypes, - ratioType, - upstreamName - ) === ratioType ) return ( -
+
{fields.map((ratioType) => { const diff = row.original.ratioTypes[ratioType] const upstreamVal = diff?.upstreams?.[upstreamName] const isConfident = diff?.confidence?.[upstreamName] !== false + const isVisibleForSource = + getPreferredSyncField( + row.original.ratioTypes, + ratioType, + upstreamName + ) === ratioType return ( -
+
{renderUpstreamValue({ upstreamVal, + isAvailable: isVisibleForSource, isConfident, - isSelected: - resolutions[row.original.model]?.[ratioType] === - upstreamVal, + isSelected: isSelectedResolutionValue( + resolutions, + row.original.model, + ratioType, + upstreamVal + ), isDisabled, t, onSelect: () => @@ -265,6 +272,7 @@ export function useUpstreamRatioSyncColumns( return [...baseColumns, ...upstreamColumns] }, [ upstreamNames, + bulkSelectStateByUpstream, resolutions, ratioTypeFilter, isDisabled, @@ -278,6 +286,7 @@ export function useUpstreamRatioSyncColumns( type RenderUpstreamValueArgs = { upstreamVal: number | string | 'same' | null | undefined + isAvailable: boolean isConfident: boolean isSelected: boolean isDisabled: boolean @@ -287,7 +296,14 @@ type RenderUpstreamValueArgs = { } function renderUpstreamValue(args: RenderUpstreamValueArgs) { - const { upstreamVal, isConfident, isSelected, isDisabled, t } = args + const { upstreamVal, isAvailable, isConfident, isSelected, isDisabled, t } = + args + + if (!isAvailable) { + return ( + + ) + } if (upstreamVal === null || upstreamVal === undefined) { return ( @@ -314,7 +330,7 @@ function renderUpstreamValue(args: RenderUpstreamValueArgs) { const text = String(upstreamVal) return ( -
+
diff --git a/web/default/src/features/system-settings/models/upstream-ratio-sync-helpers.ts b/web/default/src/features/system-settings/models/upstream-ratio-sync-helpers.ts index 61cfd0e0..5dbd086e 100644 --- a/web/default/src/features/system-settings/models/upstream-ratio-sync-helpers.ts +++ b/web/default/src/features/system-settings/models/upstream-ratio-sync-helpers.ts @@ -17,7 +17,13 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import type { RatioType } from '../types' -import { RATIO_TYPE_OPTIONS } from './constants' +import { + MODELS_DEV_PRESET_ID, + MODELS_DEV_PRESET_NAME, + OFFICIAL_CHANNEL_ID, + OFFICIAL_CHANNEL_NAME, + RATIO_TYPE_OPTIONS, +} from './constants' export type RatioDifferenceEntry = { current: number | string | null @@ -34,6 +40,24 @@ export type ModelRow = { export type ResolutionsMap = Record> +export type ResolutionSelection = { + model: string + ratioType: RatioType + value: number | string + sourceName: string +} + +export type ResolvedResolutionSelection = ResolutionSelection & { + ratioType: RatioType +} + +export type ResolutionRemoval = { + model: string + ratioType: RatioType +} + +export type ResolutionRemovalPlan = Map> + export const RATIO_SYNC_FIELDS: RatioType[] = [ 'model_ratio', 'completion_ratio', @@ -95,8 +119,291 @@ export function getPreferredSyncField( return ratioType } +export function getVisibleRatioTypesForSource( + ratioTypes: Partial>, + sourceName: string, + filter?: string +): RatioType[] { + return getOrderedRatioTypes(ratioTypes, filter).filter( + (ratioType) => + getPreferredSyncField(ratioTypes, ratioType, sourceName) === ratioType + ) +} + +export function getAlignedRatioTypes( + ratioTypes: Partial>, + sourceNames: string[], + filter?: string +): RatioType[] { + const ordered = getOrderedRatioTypes(ratioTypes, filter) + if (sourceNames.length === 0) return ordered + + const visible = new Set() + sourceNames.forEach((sourceName) => { + getVisibleRatioTypesForSource(ratioTypes, sourceName, filter).forEach( + (ratioType) => visible.add(ratioType) + ) + }) + + return ordered.filter((ratioType) => visible.has(ratioType)) +} + +export function getBillingCategory( + ratioType: string +): 'price' | 'ratio' | 'tiered' { + if (ratioType === 'model_price') return 'price' + if (ratioType === 'billing_mode' || ratioType === 'billing_expr') { + return 'tiered' + } + return 'ratio' +} + export function isSelectableUpstreamValue( value: number | string | 'same' | null | undefined ): boolean { return value !== null && value !== undefined && value !== 'same' } + +export function getUpstreamDisplayName(sourceName: string): string { + const synthesizedPresets = [ + { name: OFFICIAL_CHANNEL_NAME, id: OFFICIAL_CHANNEL_ID }, + { name: MODELS_DEV_PRESET_NAME, id: MODELS_DEV_PRESET_ID }, + ] + + for (const preset of synthesizedPresets) { + if (sourceName === `${preset.name}(${preset.id})`) { + return preset.name + } + } + + return sourceName +} + +export function isSelectedResolutionValue( + resolutions: ResolutionsMap, + model: string, + ratioType: RatioType, + upstreamValue: number | string | 'same' | null | undefined +): boolean { + if (!isSelectableUpstreamValue(upstreamValue)) return false + + const selectedValue = resolutions[model]?.[ratioType] + if (selectedValue === undefined) return false + + if (NUMERIC_SYNC_FIELDS.has(ratioType)) { + const selectedNumber = Number(selectedValue) + const upstreamNumber = Number(upstreamValue) + return ( + Number.isFinite(selectedNumber) && + Number.isFinite(upstreamNumber) && + selectedNumber === upstreamNumber + ) + } + + return selectedValue === upstreamValue +} + +export function deleteResolutionField( + resolutions: ResolutionsMap, + model: string, + ratioType: RatioType +): ResolutionsMap { + return applyResolutionRemovals(resolutions, [{ model, ratioType }]) +} + +function getDraftModelResolution( + drafts: Map>, + resolutions: ResolutionsMap, + model: string +): Record { + const existingDraft = drafts.get(model) + if (existingDraft) return existingDraft + + const draft = resolutions[model] ? { ...resolutions[model] } : {} + drafts.set(model, draft) + return draft +} + +function applyResolutionSelectionToDraft( + drafts: Map>, + resolutions: ResolutionsMap, + differences: Record>>, + selection: ResolutionSelection +) { + const modelDiffs = differences[selection.model] + const preferredType = getPreferredSyncField( + modelDiffs || {}, + selection.ratioType, + selection.sourceName + ) + const preferredValue = + preferredType === selection.ratioType + ? selection.value + : (modelDiffs?.[preferredType]?.upstreams?.[selection.sourceName] ?? + selection.value) + + const finalType = preferredType + const finalValue = preferredValue as number | string + const category = getBillingCategory(finalType) + const newModelRes = getDraftModelResolution( + drafts, + resolutions, + selection.model + ) + + Object.keys(newModelRes).forEach((rt) => { + if ( + category !== 'tiered' && + getBillingCategory(rt) !== 'tiered' && + getBillingCategory(rt) !== category + ) { + delete newModelRes[rt] + } + }) + + newModelRes[finalType] = finalValue + + if (category === 'tiered' && modelDiffs) { + const modeVal = modelDiffs.billing_mode?.upstreams?.[selection.sourceName] + const exprVal = modelDiffs.billing_expr?.upstreams?.[selection.sourceName] + if (modeVal !== undefined && modeVal !== null && modeVal !== 'same') { + newModelRes['billing_mode'] = modeVal + } else if (finalType === 'billing_expr') { + newModelRes['billing_mode'] = 'tiered_expr' + } + if (exprVal !== undefined && exprVal !== null && exprVal !== 'same') { + newModelRes['billing_expr'] = exprVal + } + } +} + +export function resolveResolutionSelection( + differences: Record>>, + selection: ResolutionSelection +): ResolvedResolutionSelection { + const modelDiffs = differences[selection.model] + const preferredType = getPreferredSyncField( + modelDiffs || {}, + selection.ratioType, + selection.sourceName + ) + const preferredValue = + preferredType === selection.ratioType + ? selection.value + : (modelDiffs?.[preferredType]?.upstreams?.[selection.sourceName] ?? + selection.value) + + return { + ...selection, + ratioType: preferredType, + value: preferredValue as number | string, + } +} + +export function getEffectiveResolutionSelections( + differences: Record>>, + selections: ResolutionSelection[] +): ResolvedResolutionSelection[] { + const effectiveByKey = new Map() + + selections.forEach((selection) => { + const resolved = resolveResolutionSelection(differences, selection) + const category = getBillingCategory(resolved.ratioType) + + if (category !== 'tiered') { + for (const [key, existing] of effectiveByKey) { + if ( + existing.model === resolved.model && + getBillingCategory(existing.ratioType) !== 'tiered' && + getBillingCategory(existing.ratioType) !== category + ) { + effectiveByKey.delete(key) + } + } + } + + effectiveByKey.set(`${resolved.model}\u0000${resolved.ratioType}`, resolved) + }) + + return [...effectiveByKey.values()] +} + +export function applyResolutionSelections( + resolutions: ResolutionsMap, + differences: Record>>, + selections: ResolutionSelection[] +): ResolutionsMap { + if (selections.length === 0) return resolutions + + const next = { ...resolutions } + const drafts = new Map>() + + selections.forEach((selection) => { + applyResolutionSelectionToDraft(drafts, resolutions, differences, selection) + }) + + drafts.forEach((draft, model) => { + if (Object.keys(draft).length === 0) { + delete next[model] + } else { + next[model] = draft + } + }) + + return next +} + +export function applyResolutionSelection( + resolutions: ResolutionsMap, + differences: Record>>, + selection: ResolutionSelection +): ResolutionsMap { + return applyResolutionSelections(resolutions, differences, [selection]) +} + +export function applyResolutionRemovals( + resolutions: ResolutionsMap, + removals: ResolutionRemoval[] +): ResolutionsMap { + if (removals.length === 0) return resolutions + + const plan: ResolutionRemovalPlan = new Map() + removals.forEach((removal) => { + const ratioTypes = plan.get(removal.model) + if (ratioTypes) { + ratioTypes.add(removal.ratioType) + } else { + plan.set(removal.model, new Set([removal.ratioType])) + } + }) + + return applyResolutionRemovalPlan(resolutions, plan) +} + +export function applyResolutionRemovalPlan( + resolutions: ResolutionsMap, + plan: ResolutionRemovalPlan +): ResolutionsMap { + if (plan.size === 0) return resolutions + + const next = { ...resolutions } + + plan.forEach((ratioTypes, model) => { + const current = resolutions[model] + if (!current) return + + const draft = { ...current } + ratioTypes.forEach((ratioType) => { + delete draft[ratioType] + if (ratioType === 'billing_expr') delete draft['billing_mode'] + if (ratioType === 'billing_mode') delete draft['billing_expr'] + }) + if (Object.keys(draft).length === 0) { + delete next[model] + } else { + next[model] = draft + } + }) + + return next +} diff --git a/web/default/src/features/system-settings/models/upstream-ratio-sync-table.tsx b/web/default/src/features/system-settings/models/upstream-ratio-sync-table.tsx index 317c7ce5..ccc1b483 100644 --- a/web/default/src/features/system-settings/models/upstream-ratio-sync-table.tsx +++ b/web/default/src/features/system-settings/models/upstream-ratio-sync-table.tsx @@ -39,11 +39,16 @@ import type { DifferencesMap, RatioType } from '../types' import { RATIO_TYPE_OPTIONS } from './constants' import { useUpstreamRatioSyncColumns } from './upstream-ratio-sync-columns' import { + getAlignedRatioTypes, + getEffectiveResolutionSelections, getOrderedRatioTypes, - getPreferredSyncField, + getUpstreamDisplayName, + isSelectedResolutionValue, isSelectableUpstreamValue, RATIO_SYNC_FIELDS, type ModelRow, + type ResolutionRemovalPlan, + type ResolutionSelection, type ResolutionsMap, } from './upstream-ratio-sync-helpers' @@ -58,7 +63,17 @@ type UpstreamRatioSyncTableProps = { value: number | string, sourceName: string ) => void + onSelectValues: (selections: ResolutionSelection[]) => void onUnselectValue: (model: string, ratioType: RatioType) => void + onUnselectValues: (plan: ResolutionRemovalPlan) => void +} + +export type UpstreamBulkSelectState = { + displayName: string + selections: ResolutionSelection[] + removalPlan: ResolutionRemovalPlan + selectableCount: number + selectedCount: number } export function UpstreamRatioSyncTable({ @@ -67,7 +82,9 @@ export function UpstreamRatioSyncTable({ isDisabled, isSyncing, onSelectValue, + onSelectValues, onUnselectValue, + onUnselectValues, }: UpstreamRatioSyncTableProps) { const { t } = useTranslation() const [search, setSearch] = useState('') @@ -112,57 +129,88 @@ export function UpstreamRatioSyncTable({ } ) }) - return Array.from(set) + return [...set] }, [filteredData, ratioTypeFilter]) - const handleBulkSelect = useCallback( - (upstream: string, rows: ModelRow[]) => { - rows.forEach((row) => { - getOrderedRatioTypes(row.ratioTypes, ratioTypeFilter).forEach( - (ratioType) => { - const upstreamVal = row.ratioTypes[ratioType]?.upstreams?.[upstream] - const preferredField = getPreferredSyncField( - row.ratioTypes, - ratioType, - upstream - ) - if ( - preferredField === ratioType && - isSelectableUpstreamValue(upstreamVal) - ) { - onSelectValue( - row.model, + const bulkSelectStateByUpstream = useMemo< + Record + >(() => { + return upstreamNames.reduce>( + (states, upstreamName) => { + const selections: ResolutionSelection[] = [] + const removalPlan: ResolutionRemovalPlan = new Map() + + filteredData.forEach((row) => { + getAlignedRatioTypes( + row.ratioTypes, + [upstreamName], + ratioTypeFilter + ).forEach((ratioType) => { + const upstreamVal = + row.ratioTypes[ratioType]?.upstreams?.[upstreamName] + if (isSelectableUpstreamValue(upstreamVal)) { + selections.push({ + model: row.model, ratioType, - upstreamVal as number | string, - upstream - ) + value: upstreamVal as number | string, + sourceName: upstreamName, + }) + const removalRatioTypes = removalPlan.get(row.model) + if (removalRatioTypes) { + removalRatioTypes.add(ratioType) + } else { + removalPlan.set(row.model, new Set([ratioType])) + } } - } + }) + }) + + const effectiveSelections = getEffectiveResolutionSelections( + differences, + selections ) - }) + const selectedCount = effectiveSelections.filter((selection) => + isSelectedResolutionValue( + resolutions, + selection.model, + selection.ratioType, + selection.value + ) + ).length + + states[upstreamName] = { + displayName: getUpstreamDisplayName(upstreamName), + selections: effectiveSelections, + removalPlan, + selectableCount: effectiveSelections.length, + selectedCount, + } + return states + }, + {} + ) + }, [differences, filteredData, ratioTypeFilter, resolutions, upstreamNames]) + + const handleBulkSelect = useCallback( + (upstream: string) => { + const selections = bulkSelectStateByUpstream[upstream]?.selections ?? [] + onSelectValues(selections) }, - [ratioTypeFilter, onSelectValue] + [bulkSelectStateByUpstream, onSelectValues] ) const handleBulkUnselect = useCallback( - (upstream: string, rows: ModelRow[]) => { - rows.forEach((row) => { - getOrderedRatioTypes(row.ratioTypes, ratioTypeFilter).forEach( - (ratioType) => { - if ( - row.ratioTypes[ratioType]?.upstreams?.[upstream] !== undefined - ) { - onUnselectValue(row.model, ratioType) - } - } - ) - }) + (upstream: string) => { + const removalPlan = + bulkSelectStateByUpstream[upstream]?.removalPlan ?? new Map() + onUnselectValues(removalPlan) }, - [ratioTypeFilter, onUnselectValue] + [bulkSelectStateByUpstream, onUnselectValues] ) const columns = useUpstreamRatioSyncColumns( upstreamNames, + bulkSelectStateByUpstream, resolutions, ratioTypeFilter, isDisabled, @@ -209,8 +257,8 @@ export function UpstreamRatioSyncTable({ } return ( -
-
+
+
'align-top'} + containerClassName='min-h-0 flex-1 rounded-md' + tableContainerClassName='h-full min-h-0' + tableHeaderClassName='[background-color:var(--table-header)]' + splitHeaderScrollClassName='h-full' + bodyContainerClassName='[scrollbar-gutter:stable]' + splitHeader + getColumnClassName={(_, part) => + part === 'header' ? 'h-11 align-middle' : 'align-top' + } getRowClassName={() => 'align-top'} emptyContent={t('No results found')} emptyCellClassName='h-24 text-center' /> - +
+ +
) } diff --git a/web/default/src/features/system-settings/models/upstream-ratio-sync.tsx b/web/default/src/features/system-settings/models/upstream-ratio-sync.tsx index 4d80f136..ad0770c7 100644 --- a/web/default/src/features/system-settings/models/upstream-ratio-sync.tsx +++ b/web/default/src/features/system-settings/models/upstream-ratio-sync.tsx @@ -52,7 +52,12 @@ import { import { NUMERIC_SYNC_FIELDS, RATIO_SYNC_FIELDS, - getPreferredSyncField, + applyResolutionRemovalPlan, + applyResolutionSelection, + applyResolutionSelections, + deleteResolutionField, + type ResolutionRemovalPlan, + type ResolutionSelection, type ResolutionsMap, } from './upstream-ratio-sync-helpers' import { UpstreamRatioSyncTable } from './upstream-ratio-sync-table' @@ -90,13 +95,6 @@ function getDefaultEndpointForChannel(channel: UpstreamChannel): string { return DEFAULT_ENDPOINT } -function getBillingCategory(ratioType: string): 'price' | 'ratio' | 'tiered' { - if (ratioType === 'model_price') return 'price' - if (ratioType === 'billing_mode' || ratioType === 'billing_expr') - return 'tiered' - return 'ratio' -} - function optionKeyBySyncField(ratioType: string): string { const explicit: Record = { billing_mode: 'billing_setting.billing_mode', @@ -117,25 +115,6 @@ function parseJsonRecord(raw: string | undefined | null): Record { } } -function deleteResolutionField( - res: ResolutionsMap, - model: string, - ratioType: string -): ResolutionsMap { - if (!res[model]) return res - const newModelRes = { ...res[model] } - delete newModelRes[ratioType] - if (ratioType === 'billing_expr') delete newModelRes['billing_mode'] - if (ratioType === 'billing_mode') delete newModelRes['billing_expr'] - const next = { ...res } - if (Object.keys(newModelRes).length === 0) { - delete next[model] - } else { - next[model] = newModelRes - } - return next -} - // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- @@ -276,53 +255,24 @@ export function UpstreamRatioSync({ modelRatios }: UpstreamRatioSyncProps) { value: number | string, sourceName: string ) => { - const modelDiffs = differences[model] - - // Prefer billing_expr over individual ratio fields when available - const preferredType = sourceName - ? getPreferredSyncField(modelDiffs || {}, ratioType, sourceName) - : ratioType - const preferredValue = - preferredType === ratioType - ? value - : (modelDiffs?.[preferredType]?.upstreams?.[sourceName] ?? value) - - const finalType = preferredType - const finalValue = preferredValue as number | string - const category = getBillingCategory(finalType) - - setResolutions((prev) => { - const newModelRes = { ...(prev[model] || {}) } - - // Clear conflicting categories - Object.keys(newModelRes).forEach((rt) => { - if ( - category !== 'tiered' && - getBillingCategory(rt) !== 'tiered' && - getBillingCategory(rt) !== category - ) { - delete newModelRes[rt] - } + setResolutions((prev) => + applyResolutionSelection(prev, differences, { + model, + ratioType, + value, + sourceName, }) + ) + }, + [differences] + ) - newModelRes[finalType] = finalValue - - // When selecting a tiered field, auto-populate paired fields from the same source - if (category === 'tiered' && sourceName && modelDiffs) { - const modeVal = modelDiffs.billing_mode?.upstreams?.[sourceName] - const exprVal = modelDiffs.billing_expr?.upstreams?.[sourceName] - if (modeVal !== undefined && modeVal !== null && modeVal !== 'same') { - newModelRes['billing_mode'] = modeVal - } else if (finalType === 'billing_expr') { - newModelRes['billing_mode'] = 'tiered_expr' - } - if (exprVal !== undefined && exprVal !== null && exprVal !== 'same') { - newModelRes['billing_expr'] = exprVal - } - } - - return { ...prev, [model]: newModelRes } - }) + const handleSelectValues = useCallback( + (selections: ResolutionSelection[]) => { + if (selections.length === 0) return + setResolutions((prev) => + applyResolutionSelections(prev, differences, selections) + ) }, [differences] ) @@ -334,6 +284,11 @@ export function UpstreamRatioSync({ modelRatios }: UpstreamRatioSyncProps) { [] ) + const handleUnselectValues = useCallback((plan: ResolutionRemovalPlan) => { + if (plan.size === 0) return + setResolutions((prev) => applyResolutionRemovalPlan(prev, plan)) + }, []) + const parsedRatios = useMemo(() => { return { ModelRatio: parseJsonRecord(modelRatios.ModelRatio), @@ -370,8 +325,9 @@ export function UpstreamRatioSync({ modelRatios }: UpstreamRatioSyncProps) { currentRatios.ImageRatio[model] !== undefined || currentRatios.AudioRatio[model] !== undefined || currentRatios.AudioCompletionRatio[model] !== undefined - ) + ) { return 'ratio' + } return null } @@ -519,8 +475,8 @@ export function UpstreamRatioSync({ modelRatios }: UpstreamRatioSyncProps) { const isLoading = fetchMutation.isPending || isSyncPending || confirmLoading return ( -
-
+
+
- +
+ +