perf(model-pricing): optimize upstream price sync table
- batch select and unselect operations into single state updates to reduce lag with large model lists. - align current price and upstream preset rows by rendering the same ordered field set. - switch the table to a fixed header, internal body scrolling, and fixed pagination layout with compact upstream header counts. - hide synthesized preset internal IDs and count only effective selectable resolutions.
This commit is contained in:
@@ -83,7 +83,9 @@ function SettingsPageFrame(props: SettingsPageFrameProps) {
|
||||
/>
|
||||
</SectionPageLayout.Actions>
|
||||
<SectionPageLayout.Content>
|
||||
<div className='flex w-full flex-col gap-4'>{props.children}</div>
|
||||
<div className='flex h-full min-h-0 w-full flex-col gap-4'>
|
||||
{props.children}
|
||||
</div>
|
||||
</SectionPageLayout.Content>
|
||||
</SectionPageLayout>
|
||||
</SettingsPageProvider>
|
||||
|
||||
@@ -58,18 +58,24 @@ function formatJsonValidationError(
|
||||
)
|
||||
}
|
||||
|
||||
const parts = [
|
||||
error.line && error.column
|
||||
? t('JSON is invalid at line {{line}}, column {{column}}.', {
|
||||
let locationMessage: string
|
||||
if (error.line && error.column) {
|
||||
locationMessage = 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}}.', {
|
||||
}
|
||||
)
|
||||
} else if (error.position !== undefined) {
|
||||
locationMessage = t('JSON is invalid at position {{position}}.', {
|
||||
position: error.position,
|
||||
})
|
||||
: t('JSON is invalid. Please check the syntax.'),
|
||||
]
|
||||
} 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)}
|
||||
</SettingsSection>
|
||||
) : (
|
||||
<Tabs defaultValue={defaultTab} className='space-y-6'>
|
||||
<Tabs defaultValue={defaultTab} className='h-full min-h-0 gap-6'>
|
||||
<SettingsPageTitleStatusPortal>
|
||||
{renderTabSwitcher()}
|
||||
</SettingsPageTitleStatusPortal>
|
||||
|
||||
<SettingsSection title={t(titleKey)}>
|
||||
<SettingsSection title={t(titleKey)} className='min-h-0 flex-1'>
|
||||
{visibleTabs.map((tab) => (
|
||||
<TabsContent key={tab} value={tab}>
|
||||
<TabsContent key={tab} value={tab} className='min-h-0'>
|
||||
{renderTabContent(tab)}
|
||||
</TabsContent>
|
||||
))}
|
||||
|
||||
+85
-68
@@ -16,12 +16,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<string, UpstreamBulkSelectState>,
|
||||
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<ModelRow>[] {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -62,7 +69,11 @@ export function useUpstreamRatioSyncColumns(
|
||||
const baseColumns: ColumnDef<ModelRow>[] = [
|
||||
{
|
||||
accessorKey: 'model',
|
||||
header: t('Model'),
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Model')} />
|
||||
),
|
||||
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 }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Current Price')} />
|
||||
),
|
||||
size: 260,
|
||||
minSize: 220,
|
||||
cell: ({ row }) => {
|
||||
const fields = getOrderedRatioTypes(
|
||||
const fields = getAlignedRatioTypes(
|
||||
row.original.ratioTypes,
|
||||
upstreamNames,
|
||||
ratioTypeFilter
|
||||
)
|
||||
return (
|
||||
<div className='flex max-w-full min-w-0 flex-col gap-2'>
|
||||
<div className={syncFieldListClassName}>
|
||||
{fields.map((ratioType) => {
|
||||
const current = row.original.ratioTypes[ratioType]?.current
|
||||
return (
|
||||
<BadgeCell key={ratioType} className='ml-0 flex-wrap gap-2'>
|
||||
<div key={ratioType} className={syncFieldRowClassName}>
|
||||
<StatusBadge
|
||||
label={getSyncFieldLabel(ratioType, t)}
|
||||
autoColor={ratioType}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className={syncFieldLabelClassName}
|
||||
/>
|
||||
{current === null || current === undefined ? (
|
||||
<StatusBadge
|
||||
@@ -124,10 +141,10 @@ export function useUpstreamRatioSyncColumns(
|
||||
label={String(current)}
|
||||
variant='info'
|
||||
size='sm'
|
||||
className='max-w-[200px] truncate'
|
||||
className='max-w-[160px] truncate font-mono'
|
||||
/>
|
||||
}
|
||||
></TooltipTrigger>
|
||||
/>
|
||||
<TooltipContent>
|
||||
<p className='max-w-xs text-xs break-all'>
|
||||
{String(current)}
|
||||
@@ -136,7 +153,7 @@ export function useUpstreamRatioSyncColumns(
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</BadgeCell>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@@ -148,41 +165,19 @@ export function useUpstreamRatioSyncColumns(
|
||||
const upstreamColumns: ColumnDef<ModelRow>[] = 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 (
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='flex h-9 min-w-0 items-center gap-1.5'>
|
||||
{selectableCount > 0 && (
|
||||
<Checkbox
|
||||
checked={allSelected}
|
||||
@@ -190,56 +185,68 @@ export function useUpstreamRatioSyncColumns(
|
||||
disabled={isDisabled}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
onBulkSelect(upstreamName, rows)
|
||||
onBulkSelect(upstreamName)
|
||||
} else {
|
||||
onBulkUnselect(upstreamName, rows)
|
||||
onBulkUnselect(upstreamName)
|
||||
}
|
||||
}}
|
||||
aria-label={t('Select all (filtered)')}
|
||||
className='shrink-0'
|
||||
/>
|
||||
)}
|
||||
<span className='font-medium'>{upstreamName}</span>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-1.5'>
|
||||
<span className='min-w-0 truncate font-medium'>
|
||||
{displayName}
|
||||
</span>
|
||||
{selectableCount > 0 && (
|
||||
<span className='bg-muted text-muted-foreground shrink-0 rounded px-1.5 py-0.5 text-[11px] leading-none font-normal tabular-nums'>
|
||||
{selectedCount}/{selectableCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const fields = getOrderedRatioTypes(
|
||||
const fields = getAlignedRatioTypes(
|
||||
row.original.ratioTypes,
|
||||
upstreamNames,
|
||||
ratioTypeFilter
|
||||
).filter(
|
||||
(ratioType) =>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={syncFieldListClassName}>
|
||||
{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 (
|
||||
<div className='flex max-w-full min-w-0 flex-col gap-2'>
|
||||
{fields.map((ratioType) => {
|
||||
const diff = row.original.ratioTypes[ratioType]
|
||||
const upstreamVal = diff?.upstreams?.[upstreamName]
|
||||
const isConfident = diff?.confidence?.[upstreamName] !== false
|
||||
|
||||
return (
|
||||
<div
|
||||
key={ratioType}
|
||||
className='flex min-w-0 items-start gap-2'
|
||||
>
|
||||
<div key={ratioType} className={syncFieldRowClassName}>
|
||||
<StatusBadge
|
||||
label={getSyncFieldLabel(ratioType, t)}
|
||||
autoColor={ratioType}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='shrink-0'
|
||||
className={syncFieldLabelClassName}
|
||||
/>
|
||||
<div className='min-w-0 flex-1'>
|
||||
{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 (
|
||||
<StatusBadge label='—' variant='neutral' size='sm' copyable={false} />
|
||||
)
|
||||
}
|
||||
|
||||
if (upstreamVal === null || upstreamVal === undefined) {
|
||||
return (
|
||||
@@ -314,7 +330,7 @@ function renderUpstreamValue(args: RenderUpstreamValueArgs) {
|
||||
const text = String(upstreamVal)
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 items-center gap-2'>
|
||||
<div className='flex h-full min-w-0 items-center gap-2'>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={isDisabled}
|
||||
@@ -325,6 +341,7 @@ function renderUpstreamValue(args: RenderUpstreamValueArgs) {
|
||||
args.onUnselect()
|
||||
}
|
||||
}}
|
||||
className='size-4'
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
|
||||
+308
-1
@@ -17,7 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
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<string, Record<string, number | string>>
|
||||
|
||||
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<string, Set<RatioType>>
|
||||
|
||||
export const RATIO_SYNC_FIELDS: RatioType[] = [
|
||||
'model_ratio',
|
||||
'completion_ratio',
|
||||
@@ -95,8 +119,291 @@ export function getPreferredSyncField(
|
||||
return ratioType
|
||||
}
|
||||
|
||||
export function getVisibleRatioTypesForSource(
|
||||
ratioTypes: Partial<Record<RatioType, RatioDifferenceEntry>>,
|
||||
sourceName: string,
|
||||
filter?: string
|
||||
): RatioType[] {
|
||||
return getOrderedRatioTypes(ratioTypes, filter).filter(
|
||||
(ratioType) =>
|
||||
getPreferredSyncField(ratioTypes, ratioType, sourceName) === ratioType
|
||||
)
|
||||
}
|
||||
|
||||
export function getAlignedRatioTypes(
|
||||
ratioTypes: Partial<Record<RatioType, RatioDifferenceEntry>>,
|
||||
sourceNames: string[],
|
||||
filter?: string
|
||||
): RatioType[] {
|
||||
const ordered = getOrderedRatioTypes(ratioTypes, filter)
|
||||
if (sourceNames.length === 0) return ordered
|
||||
|
||||
const visible = new Set<RatioType>()
|
||||
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<string, Record<string, number | string>>,
|
||||
resolutions: ResolutionsMap,
|
||||
model: string
|
||||
): Record<string, number | string> {
|
||||
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<string, Record<string, number | string>>,
|
||||
resolutions: ResolutionsMap,
|
||||
differences: Record<string, Partial<Record<RatioType, RatioDifferenceEntry>>>,
|
||||
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<string, Partial<Record<RatioType, RatioDifferenceEntry>>>,
|
||||
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<string, Partial<Record<RatioType, RatioDifferenceEntry>>>,
|
||||
selections: ResolutionSelection[]
|
||||
): ResolvedResolutionSelection[] {
|
||||
const effectiveByKey = new Map<string, ResolvedResolutionSelection>()
|
||||
|
||||
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<string, Partial<Record<RatioType, RatioDifferenceEntry>>>,
|
||||
selections: ResolutionSelection[]
|
||||
): ResolutionsMap {
|
||||
if (selections.length === 0) return resolutions
|
||||
|
||||
const next = { ...resolutions }
|
||||
const drafts = new Map<string, Record<string, number | string>>()
|
||||
|
||||
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<string, Partial<Record<RatioType, RatioDifferenceEntry>>>,
|
||||
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
|
||||
}
|
||||
|
||||
+99
-43
@@ -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(
|
||||
const bulkSelectStateByUpstream = useMemo<
|
||||
Record<string, UpstreamBulkSelectState>
|
||||
>(() => {
|
||||
return upstreamNames.reduce<Record<string, UpstreamBulkSelectState>>(
|
||||
(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,
|
||||
upstream
|
||||
)
|
||||
if (
|
||||
preferredField === ratioType &&
|
||||
isSelectableUpstreamValue(upstreamVal)
|
||||
) {
|
||||
onSelectValue(
|
||||
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
|
||||
},
|
||||
[ratioTypeFilter, onSelectValue]
|
||||
{}
|
||||
)
|
||||
}, [differences, filteredData, ratioTypeFilter, resolutions, upstreamNames])
|
||||
|
||||
const handleBulkSelect = useCallback(
|
||||
(upstream: string) => {
|
||||
const selections = bulkSelectStateByUpstream[upstream]?.selections ?? []
|
||||
onSelectValues(selections)
|
||||
},
|
||||
[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 (
|
||||
<div className='space-y-4'>
|
||||
<div className='flex flex-col gap-2 sm:flex-row sm:items-center'>
|
||||
<div className='flex h-full min-h-[520px] flex-col gap-4'>
|
||||
<div className='flex shrink-0 flex-col gap-2 sm:flex-row sm:items-center'>
|
||||
<div className='relative flex-1'>
|
||||
<Search className='text-muted-foreground absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2' />
|
||||
<Input
|
||||
@@ -251,15 +299,23 @@ export function UpstreamRatioSyncTable({
|
||||
|
||||
<DataTableView
|
||||
table={table}
|
||||
containerClassName='rounded-md'
|
||||
tableContainerClassName='overflow-x-auto'
|
||||
getColumnClassName={() => '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'
|
||||
/>
|
||||
|
||||
<div className='shrink-0'>
|
||||
<DataTablePagination table={table} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
billing_mode: 'billing_setting.billing_mode',
|
||||
@@ -117,25 +115,6 @@ function parseJsonRecord<T>(raw: string | undefined | null): Record<string, T> {
|
||||
}
|
||||
}
|
||||
|
||||
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<number>(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 (
|
||||
<div className='space-y-4'>
|
||||
<div className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div className='flex h-full min-h-0 flex-col gap-4'>
|
||||
<div className='flex shrink-0 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div className='flex flex-col gap-2 sm:flex-row'>
|
||||
<Button onClick={handleOpenChannelDialog} disabled={isLoading}>
|
||||
<RefreshCcw className='mr-2 h-4 w-4' />
|
||||
@@ -540,14 +496,18 @@ export function UpstreamRatioSync({ modelRatios }: UpstreamRatioSyncProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='min-h-0 flex-1'>
|
||||
<UpstreamRatioSyncTable
|
||||
differences={differences}
|
||||
resolutions={resolutions}
|
||||
isDisabled={isLoading}
|
||||
isSyncing={fetchMutation.isPending}
|
||||
onSelectValue={handleSelectValue}
|
||||
onSelectValues={handleSelectValues}
|
||||
onUnselectValue={handleUnselectValue}
|
||||
onUnselectValues={handleUnselectValues}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ChannelSelectorDialog
|
||||
open={channelDialogOpen}
|
||||
|
||||
Reference in New Issue
Block a user