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.Actions>
|
||||||
<SectionPageLayout.Content>
|
<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.Content>
|
||||||
</SectionPageLayout>
|
</SectionPageLayout>
|
||||||
</SettingsPageProvider>
|
</SettingsPageProvider>
|
||||||
|
|||||||
@@ -58,18 +58,24 @@ function formatJsonValidationError(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = [
|
let locationMessage: string
|
||||||
error.line && error.column
|
if (error.line && error.column) {
|
||||||
? t('JSON is invalid at line {{line}}, column {{column}}.', {
|
locationMessage = t(
|
||||||
line: error.line,
|
'JSON is invalid at line {{line}}, column {{column}}.',
|
||||||
column: error.column,
|
{
|
||||||
})
|
line: error.line,
|
||||||
: error.position !== undefined
|
column: error.column,
|
||||||
? t('JSON is invalid at position {{position}}.', {
|
}
|
||||||
position: error.position,
|
)
|
||||||
})
|
} else if (error.position !== undefined) {
|
||||||
: t('JSON is invalid. Please check the syntax.'),
|
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) {
|
if (error.missingCommaLine) {
|
||||||
parts.push(
|
parts.push(
|
||||||
@@ -459,14 +465,14 @@ export function RatioSettingsCard({
|
|||||||
{renderTabContent(defaultTab)}
|
{renderTabContent(defaultTab)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
) : (
|
) : (
|
||||||
<Tabs defaultValue={defaultTab} className='space-y-6'>
|
<Tabs defaultValue={defaultTab} className='h-full min-h-0 gap-6'>
|
||||||
<SettingsPageTitleStatusPortal>
|
<SettingsPageTitleStatusPortal>
|
||||||
{renderTabSwitcher()}
|
{renderTabSwitcher()}
|
||||||
</SettingsPageTitleStatusPortal>
|
</SettingsPageTitleStatusPortal>
|
||||||
|
|
||||||
<SettingsSection title={t(titleKey)}>
|
<SettingsSection title={t(titleKey)} className='min-h-0 flex-1'>
|
||||||
{visibleTabs.map((tab) => (
|
{visibleTabs.map((tab) => (
|
||||||
<TabsContent key={tab} value={tab}>
|
<TabsContent key={tab} value={tab} className='min-h-0'>
|
||||||
{renderTabContent(tab)}
|
{renderTabContent(tab)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+83
-66
@@ -16,12 +16,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
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 { AlertTriangle } from 'lucide-react'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { BadgeCell } from '@/components/data-table'
|
import { DataTableColumnHeader } from '@/components/data-table'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import {
|
import {
|
||||||
@@ -33,16 +33,23 @@ import {
|
|||||||
|
|
||||||
import type { RatioType } from '../types'
|
import type { RatioType } from '../types'
|
||||||
import {
|
import {
|
||||||
getOrderedRatioTypes,
|
getAlignedRatioTypes,
|
||||||
getPreferredSyncField,
|
getPreferredSyncField,
|
||||||
getSyncFieldLabel,
|
getSyncFieldLabel,
|
||||||
isSelectableUpstreamValue,
|
isSelectedResolutionValue,
|
||||||
type ModelRow,
|
type ModelRow,
|
||||||
type ResolutionsMap,
|
type ResolutionsMap,
|
||||||
} from './upstream-ratio-sync-helpers'
|
} 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(
|
export function useUpstreamRatioSyncColumns(
|
||||||
upstreamNames: string[],
|
upstreamNames: string[],
|
||||||
|
bulkSelectStateByUpstream: Record<string, UpstreamBulkSelectState>,
|
||||||
resolutions: ResolutionsMap,
|
resolutions: ResolutionsMap,
|
||||||
ratioTypeFilter: string,
|
ratioTypeFilter: string,
|
||||||
isDisabled: boolean,
|
isDisabled: boolean,
|
||||||
@@ -53,8 +60,8 @@ export function useUpstreamRatioSyncColumns(
|
|||||||
sourceName: string
|
sourceName: string
|
||||||
) => void,
|
) => void,
|
||||||
onUnselectValue: (model: string, ratioType: RatioType) => void,
|
onUnselectValue: (model: string, ratioType: RatioType) => void,
|
||||||
onBulkSelect: (upstreamName: string, rows: ModelRow[]) => void,
|
onBulkSelect: (upstreamName: string) => void,
|
||||||
onBulkUnselect: (upstreamName: string, rows: ModelRow[]) => void
|
onBulkUnselect: (upstreamName: string) => void
|
||||||
): ColumnDef<ModelRow>[] {
|
): ColumnDef<ModelRow>[] {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -62,7 +69,11 @@ export function useUpstreamRatioSyncColumns(
|
|||||||
const baseColumns: ColumnDef<ModelRow>[] = [
|
const baseColumns: ColumnDef<ModelRow>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'model',
|
accessorKey: 'model',
|
||||||
header: t('Model'),
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t('Model')} />
|
||||||
|
),
|
||||||
|
size: 220,
|
||||||
|
minSize: 180,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const model = row.original.model
|
const model = row.original.model
|
||||||
return (
|
return (
|
||||||
@@ -90,23 +101,29 @@ export function useUpstreamRatioSyncColumns(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'current',
|
id: 'current',
|
||||||
header: t('Current Price'),
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t('Current Price')} />
|
||||||
|
),
|
||||||
|
size: 260,
|
||||||
|
minSize: 220,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const fields = getOrderedRatioTypes(
|
const fields = getAlignedRatioTypes(
|
||||||
row.original.ratioTypes,
|
row.original.ratioTypes,
|
||||||
|
upstreamNames,
|
||||||
ratioTypeFilter
|
ratioTypeFilter
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<div className='flex max-w-full min-w-0 flex-col gap-2'>
|
<div className={syncFieldListClassName}>
|
||||||
{fields.map((ratioType) => {
|
{fields.map((ratioType) => {
|
||||||
const current = row.original.ratioTypes[ratioType]?.current
|
const current = row.original.ratioTypes[ratioType]?.current
|
||||||
return (
|
return (
|
||||||
<BadgeCell key={ratioType} className='ml-0 flex-wrap gap-2'>
|
<div key={ratioType} className={syncFieldRowClassName}>
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
label={getSyncFieldLabel(ratioType, t)}
|
label={getSyncFieldLabel(ratioType, t)}
|
||||||
autoColor={ratioType}
|
autoColor={ratioType}
|
||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
|
className={syncFieldLabelClassName}
|
||||||
/>
|
/>
|
||||||
{current === null || current === undefined ? (
|
{current === null || current === undefined ? (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
@@ -124,10 +141,10 @@ export function useUpstreamRatioSyncColumns(
|
|||||||
label={String(current)}
|
label={String(current)}
|
||||||
variant='info'
|
variant='info'
|
||||||
size='sm'
|
size='sm'
|
||||||
className='max-w-[200px] truncate'
|
className='max-w-[160px] truncate font-mono'
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
></TooltipTrigger>
|
/>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p className='max-w-xs text-xs break-all'>
|
<p className='max-w-xs text-xs break-all'>
|
||||||
{String(current)}
|
{String(current)}
|
||||||
@@ -136,7 +153,7 @@ export function useUpstreamRatioSyncColumns(
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</BadgeCell>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -148,41 +165,19 @@ export function useUpstreamRatioSyncColumns(
|
|||||||
const upstreamColumns: ColumnDef<ModelRow>[] = upstreamNames.map(
|
const upstreamColumns: ColumnDef<ModelRow>[] = upstreamNames.map(
|
||||||
(upstreamName) => ({
|
(upstreamName) => ({
|
||||||
id: `upstream_${upstreamName}`,
|
id: `upstream_${upstreamName}`,
|
||||||
header: ({ table }) => {
|
size: 280,
|
||||||
const rows = table.getFilteredRowModel().rows.map((r) => r.original)
|
minSize: 240,
|
||||||
|
header: () => {
|
||||||
let selectableCount = 0
|
const bulkSelectState = bulkSelectStateByUpstream[upstreamName]
|
||||||
let selectedCount = 0
|
const displayName = bulkSelectState?.displayName ?? upstreamName
|
||||||
|
const selectableCount = bulkSelectState?.selectableCount ?? 0
|
||||||
rows.forEach((row) => {
|
const selectedCount = bulkSelectState?.selectedCount ?? 0
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const allSelected =
|
const allSelected =
|
||||||
selectableCount > 0 && selectedCount === selectableCount
|
selectableCount > 0 && selectedCount === selectableCount
|
||||||
const someSelected =
|
const someSelected =
|
||||||
selectedCount > 0 && selectedCount < selectableCount
|
selectedCount > 0 && selectedCount < selectableCount
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex h-9 min-w-0 items-center gap-1.5'>
|
||||||
{selectableCount > 0 && (
|
{selectableCount > 0 && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={allSelected}
|
checked={allSelected}
|
||||||
@@ -190,56 +185,68 @@ export function useUpstreamRatioSyncColumns(
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
onBulkSelect(upstreamName, rows)
|
onBulkSelect(upstreamName)
|
||||||
} else {
|
} 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>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const fields = getOrderedRatioTypes(
|
const fields = getAlignedRatioTypes(
|
||||||
row.original.ratioTypes,
|
row.original.ratioTypes,
|
||||||
|
upstreamNames,
|
||||||
ratioTypeFilter
|
ratioTypeFilter
|
||||||
).filter(
|
|
||||||
(ratioType) =>
|
|
||||||
getPreferredSyncField(
|
|
||||||
row.original.ratioTypes,
|
|
||||||
ratioType,
|
|
||||||
upstreamName
|
|
||||||
) === ratioType
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex max-w-full min-w-0 flex-col gap-2'>
|
<div className={syncFieldListClassName}>
|
||||||
{fields.map((ratioType) => {
|
{fields.map((ratioType) => {
|
||||||
const diff = row.original.ratioTypes[ratioType]
|
const diff = row.original.ratioTypes[ratioType]
|
||||||
const upstreamVal = diff?.upstreams?.[upstreamName]
|
const upstreamVal = diff?.upstreams?.[upstreamName]
|
||||||
const isConfident = diff?.confidence?.[upstreamName] !== false
|
const isConfident = diff?.confidence?.[upstreamName] !== false
|
||||||
|
const isVisibleForSource =
|
||||||
|
getPreferredSyncField(
|
||||||
|
row.original.ratioTypes,
|
||||||
|
ratioType,
|
||||||
|
upstreamName
|
||||||
|
) === ratioType
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={ratioType} className={syncFieldRowClassName}>
|
||||||
key={ratioType}
|
|
||||||
className='flex min-w-0 items-start gap-2'
|
|
||||||
>
|
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
label={getSyncFieldLabel(ratioType, t)}
|
label={getSyncFieldLabel(ratioType, t)}
|
||||||
autoColor={ratioType}
|
autoColor={ratioType}
|
||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
className='shrink-0'
|
className={syncFieldLabelClassName}
|
||||||
/>
|
/>
|
||||||
<div className='min-w-0 flex-1'>
|
<div className='min-w-0 flex-1'>
|
||||||
{renderUpstreamValue({
|
{renderUpstreamValue({
|
||||||
upstreamVal,
|
upstreamVal,
|
||||||
|
isAvailable: isVisibleForSource,
|
||||||
isConfident,
|
isConfident,
|
||||||
isSelected:
|
isSelected: isSelectedResolutionValue(
|
||||||
resolutions[row.original.model]?.[ratioType] ===
|
resolutions,
|
||||||
upstreamVal,
|
row.original.model,
|
||||||
|
ratioType,
|
||||||
|
upstreamVal
|
||||||
|
),
|
||||||
isDisabled,
|
isDisabled,
|
||||||
t,
|
t,
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
@@ -265,6 +272,7 @@ export function useUpstreamRatioSyncColumns(
|
|||||||
return [...baseColumns, ...upstreamColumns]
|
return [...baseColumns, ...upstreamColumns]
|
||||||
}, [
|
}, [
|
||||||
upstreamNames,
|
upstreamNames,
|
||||||
|
bulkSelectStateByUpstream,
|
||||||
resolutions,
|
resolutions,
|
||||||
ratioTypeFilter,
|
ratioTypeFilter,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
@@ -278,6 +286,7 @@ export function useUpstreamRatioSyncColumns(
|
|||||||
|
|
||||||
type RenderUpstreamValueArgs = {
|
type RenderUpstreamValueArgs = {
|
||||||
upstreamVal: number | string | 'same' | null | undefined
|
upstreamVal: number | string | 'same' | null | undefined
|
||||||
|
isAvailable: boolean
|
||||||
isConfident: boolean
|
isConfident: boolean
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
isDisabled: boolean
|
isDisabled: boolean
|
||||||
@@ -287,7 +296,14 @@ type RenderUpstreamValueArgs = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderUpstreamValue(args: 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) {
|
if (upstreamVal === null || upstreamVal === undefined) {
|
||||||
return (
|
return (
|
||||||
@@ -314,7 +330,7 @@ function renderUpstreamValue(args: RenderUpstreamValueArgs) {
|
|||||||
const text = String(upstreamVal)
|
const text = String(upstreamVal)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-w-0 items-center gap-2'>
|
<div className='flex h-full min-w-0 items-center gap-2'>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
@@ -325,6 +341,7 @@ function renderUpstreamValue(args: RenderUpstreamValueArgs) {
|
|||||||
args.onUnselect()
|
args.onUnselect()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
className='size-4'
|
||||||
/>
|
/>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<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
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import type { RatioType } from '../types'
|
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 = {
|
export type RatioDifferenceEntry = {
|
||||||
current: number | string | null
|
current: number | string | null
|
||||||
@@ -34,6 +40,24 @@ export type ModelRow = {
|
|||||||
|
|
||||||
export type ResolutionsMap = Record<string, Record<string, number | string>>
|
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[] = [
|
export const RATIO_SYNC_FIELDS: RatioType[] = [
|
||||||
'model_ratio',
|
'model_ratio',
|
||||||
'completion_ratio',
|
'completion_ratio',
|
||||||
@@ -95,8 +119,291 @@ export function getPreferredSyncField(
|
|||||||
return ratioType
|
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(
|
export function isSelectableUpstreamValue(
|
||||||
value: number | string | 'same' | null | undefined
|
value: number | string | 'same' | null | undefined
|
||||||
): boolean {
|
): boolean {
|
||||||
return value !== null && value !== undefined && value !== 'same'
|
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
|
||||||
|
}
|
||||||
|
|||||||
+100
-44
@@ -39,11 +39,16 @@ import type { DifferencesMap, RatioType } from '../types'
|
|||||||
import { RATIO_TYPE_OPTIONS } from './constants'
|
import { RATIO_TYPE_OPTIONS } from './constants'
|
||||||
import { useUpstreamRatioSyncColumns } from './upstream-ratio-sync-columns'
|
import { useUpstreamRatioSyncColumns } from './upstream-ratio-sync-columns'
|
||||||
import {
|
import {
|
||||||
|
getAlignedRatioTypes,
|
||||||
|
getEffectiveResolutionSelections,
|
||||||
getOrderedRatioTypes,
|
getOrderedRatioTypes,
|
||||||
getPreferredSyncField,
|
getUpstreamDisplayName,
|
||||||
|
isSelectedResolutionValue,
|
||||||
isSelectableUpstreamValue,
|
isSelectableUpstreamValue,
|
||||||
RATIO_SYNC_FIELDS,
|
RATIO_SYNC_FIELDS,
|
||||||
type ModelRow,
|
type ModelRow,
|
||||||
|
type ResolutionRemovalPlan,
|
||||||
|
type ResolutionSelection,
|
||||||
type ResolutionsMap,
|
type ResolutionsMap,
|
||||||
} from './upstream-ratio-sync-helpers'
|
} from './upstream-ratio-sync-helpers'
|
||||||
|
|
||||||
@@ -58,7 +63,17 @@ type UpstreamRatioSyncTableProps = {
|
|||||||
value: number | string,
|
value: number | string,
|
||||||
sourceName: string
|
sourceName: string
|
||||||
) => void
|
) => void
|
||||||
|
onSelectValues: (selections: ResolutionSelection[]) => void
|
||||||
onUnselectValue: (model: string, ratioType: RatioType) => 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({
|
export function UpstreamRatioSyncTable({
|
||||||
@@ -67,7 +82,9 @@ export function UpstreamRatioSyncTable({
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
isSyncing,
|
isSyncing,
|
||||||
onSelectValue,
|
onSelectValue,
|
||||||
|
onSelectValues,
|
||||||
onUnselectValue,
|
onUnselectValue,
|
||||||
|
onUnselectValues,
|
||||||
}: UpstreamRatioSyncTableProps) {
|
}: UpstreamRatioSyncTableProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
@@ -112,57 +129,88 @@ export function UpstreamRatioSyncTable({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
return Array.from(set)
|
return [...set]
|
||||||
}, [filteredData, ratioTypeFilter])
|
}, [filteredData, ratioTypeFilter])
|
||||||
|
|
||||||
const handleBulkSelect = useCallback(
|
const bulkSelectStateByUpstream = useMemo<
|
||||||
(upstream: string, rows: ModelRow[]) => {
|
Record<string, UpstreamBulkSelectState>
|
||||||
rows.forEach((row) => {
|
>(() => {
|
||||||
getOrderedRatioTypes(row.ratioTypes, ratioTypeFilter).forEach(
|
return upstreamNames.reduce<Record<string, UpstreamBulkSelectState>>(
|
||||||
(ratioType) => {
|
(states, upstreamName) => {
|
||||||
const upstreamVal = row.ratioTypes[ratioType]?.upstreams?.[upstream]
|
const selections: ResolutionSelection[] = []
|
||||||
const preferredField = getPreferredSyncField(
|
const removalPlan: ResolutionRemovalPlan = new Map()
|
||||||
row.ratioTypes,
|
|
||||||
ratioType,
|
filteredData.forEach((row) => {
|
||||||
upstream
|
getAlignedRatioTypes(
|
||||||
)
|
row.ratioTypes,
|
||||||
if (
|
[upstreamName],
|
||||||
preferredField === ratioType &&
|
ratioTypeFilter
|
||||||
isSelectableUpstreamValue(upstreamVal)
|
).forEach((ratioType) => {
|
||||||
) {
|
const upstreamVal =
|
||||||
onSelectValue(
|
row.ratioTypes[ratioType]?.upstreams?.[upstreamName]
|
||||||
row.model,
|
if (isSelectableUpstreamValue(upstreamVal)) {
|
||||||
|
selections.push({
|
||||||
|
model: row.model,
|
||||||
ratioType,
|
ratioType,
|
||||||
upstreamVal as number | string,
|
value: upstreamVal as number | string,
|
||||||
upstream
|
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(
|
const handleBulkUnselect = useCallback(
|
||||||
(upstream: string, rows: ModelRow[]) => {
|
(upstream: string) => {
|
||||||
rows.forEach((row) => {
|
const removalPlan =
|
||||||
getOrderedRatioTypes(row.ratioTypes, ratioTypeFilter).forEach(
|
bulkSelectStateByUpstream[upstream]?.removalPlan ?? new Map()
|
||||||
(ratioType) => {
|
onUnselectValues(removalPlan)
|
||||||
if (
|
|
||||||
row.ratioTypes[ratioType]?.upstreams?.[upstream] !== undefined
|
|
||||||
) {
|
|
||||||
onUnselectValue(row.model, ratioType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
[ratioTypeFilter, onUnselectValue]
|
[bulkSelectStateByUpstream, onUnselectValues]
|
||||||
)
|
)
|
||||||
|
|
||||||
const columns = useUpstreamRatioSyncColumns(
|
const columns = useUpstreamRatioSyncColumns(
|
||||||
upstreamNames,
|
upstreamNames,
|
||||||
|
bulkSelectStateByUpstream,
|
||||||
resolutions,
|
resolutions,
|
||||||
ratioTypeFilter,
|
ratioTypeFilter,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
@@ -209,8 +257,8 @@ export function UpstreamRatioSyncTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-4'>
|
<div className='flex h-full min-h-[520px] flex-col gap-4'>
|
||||||
<div className='flex flex-col gap-2 sm:flex-row sm:items-center'>
|
<div className='flex shrink-0 flex-col gap-2 sm:flex-row sm:items-center'>
|
||||||
<div className='relative flex-1'>
|
<div className='relative flex-1'>
|
||||||
<Search className='text-muted-foreground absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2' />
|
<Search className='text-muted-foreground absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2' />
|
||||||
<Input
|
<Input
|
||||||
@@ -251,15 +299,23 @@ export function UpstreamRatioSyncTable({
|
|||||||
|
|
||||||
<DataTableView
|
<DataTableView
|
||||||
table={table}
|
table={table}
|
||||||
containerClassName='rounded-md'
|
containerClassName='min-h-0 flex-1 rounded-md'
|
||||||
tableContainerClassName='overflow-x-auto'
|
tableContainerClassName='h-full min-h-0'
|
||||||
getColumnClassName={() => 'align-top'}
|
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'}
|
getRowClassName={() => 'align-top'}
|
||||||
emptyContent={t('No results found')}
|
emptyContent={t('No results found')}
|
||||||
emptyCellClassName='h-24 text-center'
|
emptyCellClassName='h-24 text-center'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataTablePagination table={table} />
|
<div className='shrink-0'>
|
||||||
|
<DataTablePagination table={table} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,12 @@ import {
|
|||||||
import {
|
import {
|
||||||
NUMERIC_SYNC_FIELDS,
|
NUMERIC_SYNC_FIELDS,
|
||||||
RATIO_SYNC_FIELDS,
|
RATIO_SYNC_FIELDS,
|
||||||
getPreferredSyncField,
|
applyResolutionRemovalPlan,
|
||||||
|
applyResolutionSelection,
|
||||||
|
applyResolutionSelections,
|
||||||
|
deleteResolutionField,
|
||||||
|
type ResolutionRemovalPlan,
|
||||||
|
type ResolutionSelection,
|
||||||
type ResolutionsMap,
|
type ResolutionsMap,
|
||||||
} from './upstream-ratio-sync-helpers'
|
} from './upstream-ratio-sync-helpers'
|
||||||
import { UpstreamRatioSyncTable } from './upstream-ratio-sync-table'
|
import { UpstreamRatioSyncTable } from './upstream-ratio-sync-table'
|
||||||
@@ -90,13 +95,6 @@ function getDefaultEndpointForChannel(channel: UpstreamChannel): string {
|
|||||||
return DEFAULT_ENDPOINT
|
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 {
|
function optionKeyBySyncField(ratioType: string): string {
|
||||||
const explicit: Record<string, string> = {
|
const explicit: Record<string, string> = {
|
||||||
billing_mode: 'billing_setting.billing_mode',
|
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
|
// Component
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -276,53 +255,24 @@ export function UpstreamRatioSync({ modelRatios }: UpstreamRatioSyncProps) {
|
|||||||
value: number | string,
|
value: number | string,
|
||||||
sourceName: string
|
sourceName: string
|
||||||
) => {
|
) => {
|
||||||
const modelDiffs = differences[model]
|
setResolutions((prev) =>
|
||||||
|
applyResolutionSelection(prev, differences, {
|
||||||
// Prefer billing_expr over individual ratio fields when available
|
model,
|
||||||
const preferredType = sourceName
|
ratioType,
|
||||||
? getPreferredSyncField(modelDiffs || {}, ratioType, sourceName)
|
value,
|
||||||
: ratioType
|
sourceName,
|
||||||
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]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[differences]
|
||||||
|
)
|
||||||
|
|
||||||
newModelRes[finalType] = finalValue
|
const handleSelectValues = useCallback(
|
||||||
|
(selections: ResolutionSelection[]) => {
|
||||||
// When selecting a tiered field, auto-populate paired fields from the same source
|
if (selections.length === 0) return
|
||||||
if (category === 'tiered' && sourceName && modelDiffs) {
|
setResolutions((prev) =>
|
||||||
const modeVal = modelDiffs.billing_mode?.upstreams?.[sourceName]
|
applyResolutionSelections(prev, differences, selections)
|
||||||
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 }
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
[differences]
|
[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(() => {
|
const parsedRatios = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
ModelRatio: parseJsonRecord<number>(modelRatios.ModelRatio),
|
ModelRatio: parseJsonRecord<number>(modelRatios.ModelRatio),
|
||||||
@@ -370,8 +325,9 @@ export function UpstreamRatioSync({ modelRatios }: UpstreamRatioSyncProps) {
|
|||||||
currentRatios.ImageRatio[model] !== undefined ||
|
currentRatios.ImageRatio[model] !== undefined ||
|
||||||
currentRatios.AudioRatio[model] !== undefined ||
|
currentRatios.AudioRatio[model] !== undefined ||
|
||||||
currentRatios.AudioCompletionRatio[model] !== undefined
|
currentRatios.AudioCompletionRatio[model] !== undefined
|
||||||
)
|
) {
|
||||||
return 'ratio'
|
return 'ratio'
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,8 +475,8 @@ export function UpstreamRatioSync({ modelRatios }: UpstreamRatioSyncProps) {
|
|||||||
const isLoading = fetchMutation.isPending || isSyncPending || confirmLoading
|
const isLoading = fetchMutation.isPending || isSyncPending || confirmLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-4'>
|
<div className='flex h-full min-h-0 flex-col gap-4'>
|
||||||
<div className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
|
<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'>
|
<div className='flex flex-col gap-2 sm:flex-row'>
|
||||||
<Button onClick={handleOpenChannelDialog} disabled={isLoading}>
|
<Button onClick={handleOpenChannelDialog} disabled={isLoading}>
|
||||||
<RefreshCcw className='mr-2 h-4 w-4' />
|
<RefreshCcw className='mr-2 h-4 w-4' />
|
||||||
@@ -540,14 +496,18 @@ export function UpstreamRatioSync({ modelRatios }: UpstreamRatioSyncProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UpstreamRatioSyncTable
|
<div className='min-h-0 flex-1'>
|
||||||
differences={differences}
|
<UpstreamRatioSyncTable
|
||||||
resolutions={resolutions}
|
differences={differences}
|
||||||
isDisabled={isLoading}
|
resolutions={resolutions}
|
||||||
isSyncing={fetchMutation.isPending}
|
isDisabled={isLoading}
|
||||||
onSelectValue={handleSelectValue}
|
isSyncing={fetchMutation.isPending}
|
||||||
onUnselectValue={handleUnselectValue}
|
onSelectValue={handleSelectValue}
|
||||||
/>
|
onSelectValues={handleSelectValues}
|
||||||
|
onUnselectValue={handleUnselectValue}
|
||||||
|
onUnselectValues={handleUnselectValues}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ChannelSelectorDialog
|
<ChannelSelectorDialog
|
||||||
open={channelDialogOpen}
|
open={channelDialogOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user