perf(model-pricing): optimize upstream price sync tables (#6092)

Merge pull request #6092 from QuantumNous/perf/model-pricing-sync-table
This commit is contained in:
同語
2026-07-11 00:49:03 +08:00
committed by GitHub
27 changed files with 652 additions and 325 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ export function BadgeCell({ className, ...props }: BadgeCellProps) {
<div
data-slot='badge-cell'
className={cn(
'-ml-1.5 flex max-w-full min-w-0 items-center gap-1 overflow-hidden [&_[data-slot=status-badge]]:max-w-full [&_[data-slot=status-badge]]:min-w-0',
'flex max-w-full min-w-0 items-center gap-1 overflow-hidden [&_[data-slot=status-badge]]:max-w-full [&_[data-slot=status-badge]]:min-w-0',
className
)}
{...props}
@@ -35,7 +35,6 @@ interface BadgeListCellProps {
/**
* Table cell renderer for a list of badges with overflow tooltip.
* Displays up to `max` badges inline; remaining items appear in a tooltip.
* Applies -ml-1.5 to compensate for badge px-1.5 and align with column header.
*/
export function BadgeListCell({
items,
@@ -51,7 +50,7 @@ export function BadgeListCell({
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<div className='-ml-1.5 max-w-full' />}>
<TooltipTrigger render={<div className='max-w-full' />}>
<StatusBadgeList
items={items}
max={max}
@@ -114,7 +114,7 @@ function CompactContent<TData>({ row }: { row: Row<TData> }) {
{label}
</div>
)}
<div className='min-w-0 overflow-hidden text-xs [&_:is([data-slot=badge-cell],[data-slot=provider-badge],[data-slot=status-badge])]:ml-0'>
<div className='min-w-0 overflow-hidden text-xs'>
<StatusBadgeTypeContext.Provider value='text'>
{renderCellContent(cell) ?? '-'}
</StatusBadgeTypeContext.Provider>
@@ -164,10 +164,7 @@ function FallbackContent<TData>({ row }: { row: Row<TData> }) {
if (!label) {
return (
<div
key={cell.id}
className='flex justify-end overflow-hidden [&_:is([data-slot=badge-cell],[data-slot=provider-badge],[data-slot=status-badge])]:ml-0'
>
<div key={cell.id} className='flex justify-end overflow-hidden'>
<StatusBadgeTypeContext.Provider value='text'>
{renderCellContent(cell)}
</StatusBadgeTypeContext.Provider>
@@ -183,7 +180,7 @@ function FallbackContent<TData>({ row }: { row: Row<TData> }) {
<span className='text-muted-foreground shrink-0 text-[10px] font-medium select-none'>
{label}
</span>
<div className='flex min-w-0 flex-1 items-center justify-end overflow-hidden text-xs [&_:is([data-slot=badge-cell],[data-slot=provider-badge],[data-slot=status-badge])]:ml-0'>
<div className='flex min-w-0 flex-1 items-center justify-end overflow-hidden text-xs'>
<StatusBadgeTypeContext.Provider value='text'>
{renderCellContent(cell) ?? '-'}
</StatusBadgeTypeContext.Provider>
+1 -1
View File
@@ -50,7 +50,7 @@ export function ProviderBadge({
autoColor={colorText ? label : undefined}
variant={colorText ? undefined : 'neutral'}
size='sm'
className={cn('min-w-0 shrink overflow-hidden', !icon && 'pl-0')}
className='min-w-0 shrink overflow-hidden'
{...badgeProps}
/>
</div>
+3 -3
View File
@@ -87,9 +87,9 @@ export const StatusBadgeTypeContext =
React.createContext<StatusBadgeType>('badge')
const sizeMap = {
sm: 'h-5 gap-1 px-1.5 text-sm leading-none',
md: 'h-5 gap-1 px-1.5 text-sm leading-none',
lg: 'h-6 gap-1.5 px-2 text-sm leading-none',
sm: 'h-5 gap-1 text-sm leading-none',
md: 'h-5 gap-1 text-sm leading-none',
lg: 'h-6 gap-1.5 text-sm leading-none',
} as const
const textSizeMap = {
+1 -1
View File
@@ -27,7 +27,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
type={type}
data-slot='input'
className={cn(
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 focus-visible:ring-inset disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 aria-invalid:ring-inset md:text-sm',
className
)}
{...props}
@@ -159,7 +159,7 @@ function ChannelCardComponent({
{/* Last row: groups span the full width, showing every group (no label) */}
<div className='min-w-0'>
{groups.length > 0 ? (
<div className='-ml-1.5 flex flex-wrap gap-1'>
<div className='flex flex-wrap gap-1'>
{groups.map((g) => (
<GroupBadge
key={g}
@@ -46,12 +46,12 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { toIntlLocale } from '@/i18n/languages'
import {
formatCurrencyFromUSD,
formatQuotaWithCurrency,
getCurrencyLabel,
} from '@/lib/currency'
import { toIntlLocale } from '@/i18n/languages'
import { formatTimestampToDate } from '@/lib/format'
import { truncateText } from '@/lib/utils'
@@ -372,7 +372,7 @@ function BalanceCell({ channel }: { channel: Channel }) {
size='sm'
copyable={false}
showDot={false}
className='-ml-1.5 cursor-help'
className='cursor-help'
/>
}
/>
@@ -435,7 +435,7 @@ function BalanceCell({ channel }: { channel: Channel }) {
return (
<TooltipProvider>
<div className='-ml-1.5 flex items-center gap-1'>
<div className='flex items-center gap-1'>
<Tooltip>
<TooltipTrigger
render={
@@ -703,7 +703,6 @@ export function useChannelsColumns(
variant='blue'
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
}
@@ -846,7 +845,6 @@ export function useChannelsColumns(
variant='success'
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
} else {
@@ -856,7 +854,6 @@ export function useChannelsColumns(
variant='neutral'
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
}
@@ -1025,14 +1022,7 @@ export function useChannelsColumns(
return <span className='text-muted-foreground text-xs'>-</span>
}
return (
<StatusBadge
label={tag}
autoColor={tag}
size='sm'
className='-ml-1.5'
/>
)
return <StatusBadge label={tag} autoColor={tag} size='sm' />
},
size: 120,
enableSorting: false,
@@ -1080,7 +1070,6 @@ export function useChannelsColumns(
variant={config.variant}
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
},
@@ -1114,7 +1103,7 @@ export function useChannelsColumns(
variant='neutral'
size='sm'
copyable={false}
className='-ml-1.5 cursor-pointer'
className='cursor-pointer'
/>
}
/>
+17 -22
View File
@@ -36,7 +36,7 @@ import {
} from '@/components/ui/tooltip'
import { copyToClipboard } from '@/lib/copy-to-clipboard'
import { type ApiKey } from '../types'
import type { ApiKey } from '../types'
import { useApiKeys } from './api-keys-provider'
export function ApiKeyCell({ apiKey }: { apiKey: ApiKey }) {
@@ -54,6 +54,19 @@ export function ApiKeyCell({ apiKey }: { apiKey: ApiKey }) {
const resolvedFullKey = resolvedKeys[apiKey.id]
const isCopied = copiedKeyId === apiKey.id
const maskedKey = `sk-${apiKey.key}`
let copyIcon = <Copy className='size-3.5' />
if (isLoading) {
copyIcon = <Loader2 className='size-3.5 animate-spin' />
} else if (isCopied) {
copyIcon = <Check className='size-3.5 text-green-600' />
}
let copyTooltip = t('Copy API key')
if (isLoading) {
copyTooltip = t('Loading...')
} else if (isCopied) {
copyTooltip = t('Copied!')
}
const handlePopoverOpen = useCallback(
(open: boolean) => {
@@ -135,21 +148,9 @@ export function ApiKeyCell({ apiKey }: { apiKey: ApiKey }) {
/>
}
>
{isLoading ? (
<Loader2 className='size-3.5 animate-spin' />
) : isCopied ? (
<Check className='size-3.5 text-green-600' />
) : (
<Copy className='size-3.5' />
)}
{copyIcon}
</TooltipTrigger>
<TooltipContent>
{isLoading
? t('Loading...')
: isCopied
? t('Copied!')
: t('Copy API key')}
</TooltipContent>
<TooltipContent>{copyTooltip}</TooltipContent>
</Tooltip>
</div>
)
@@ -160,12 +161,7 @@ export function ModelLimitsCell({ apiKey }: { apiKey: ApiKey }) {
if (!apiKey.model_limits_enabled || !apiKey.model_limits) {
return (
<StatusBadge
label={t('Unlimited')}
variant='neutral'
copyable={false}
className='-ml-1.5'
/>
<StatusBadge label={t('Unlimited')} variant='neutral' copyable={false} />
)
}
@@ -203,7 +199,6 @@ export function IpRestrictionsCell({ apiKey }: { apiKey: ApiKey }) {
label={t('No restriction')}
variant='neutral'
copyable={false}
className='-ml-1.5'
/>
)
}
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useQuery } from '@tanstack/react-query'
import { type ColumnDef } from '@tanstack/react-table'
import type { ColumnDef } from '@tanstack/react-table'
import { useTranslation } from 'react-i18next'
import { BadgeCell, TruncatedCell } from '@/components/data-table'
@@ -35,7 +35,7 @@ import { formatQuota, formatTimestampToDate } from '@/lib/format'
import { cn } from '@/lib/utils'
import { API_KEY_STATUSES } from '../constants'
import { type ApiKey } from '../types'
import type { ApiKey } from '../types'
import {
ApiKeyCell,
ModelLimitsCell,
@@ -116,7 +116,6 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
label={t(statusConfig.label)}
variant={statusConfig.variant}
copyable={false}
className='-ml-1.5'
/>
)
},
@@ -144,7 +143,6 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
label={t('Unlimited')}
variant='neutral'
copyable={false}
className='-ml-1.5'
/>
)
}
@@ -224,7 +222,6 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
}
return (
<TruncatedCell
className='-ml-1.5'
tooltipContent={group || '-'}
tooltipClassName='break-all'
>
@@ -292,7 +289,6 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
label={t('Never')}
variant='neutral'
copyable={false}
className='-ml-1.5'
/>
)
}
@@ -74,7 +74,7 @@ export function useDeploymentsColumns(opts: {
variant='neutral'
copyText={name}
size='sm'
className='-ml-1.5 font-mono'
className='font-mono'
/>
)
},
@@ -98,7 +98,6 @@ export function useDeploymentsColumns(opts: {
variant={config.variant}
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
},
@@ -130,7 +129,6 @@ export function useDeploymentsColumns(opts: {
autoColor={String(provider)}
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
},
+13 -18
View File
@@ -16,7 +16,7 @@ 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 { useTranslation } from 'react-i18next'
import { BadgeCell, BadgeListCell } from '@/components/data-table'
@@ -139,7 +139,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
variant='neutral'
copyText={name}
size='sm'
className='-ml-1.5 font-mono'
className='font-mono'
/>
)
},
@@ -172,7 +172,6 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
| 'info'
}
size='sm'
className='-ml-1.5'
/>
)
@@ -182,16 +181,14 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
model.matched_models &&
model.matched_models.length > 0
) {
const matchedBadges = model.matched_models.map((m, idx) => (
<StatusBadge key={idx} label={m} autoColor={m} size='sm' />
const matchedBadges = model.matched_models.map((m) => (
<StatusBadge key={m} label={m} autoColor={m} size='sm' />
))
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<div className='-ml-1.5' />}>
{badge}
</TooltipTrigger>
<TooltipTrigger render={<div />}>{badge}</TooltipTrigger>
<TooltipContent
side='top'
className='border-border bg-popover max-h-48 max-w-[320px] overflow-y-auto p-2'
@@ -225,7 +222,6 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
variant={config.variant}
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
},
@@ -293,8 +289,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const tagArray = parseModelTags(tags)
return (
<BadgeListCell
items={tagArray.map((tag, idx) => (
<StatusBadge key={idx} label={tag} autoColor={tag} size='sm' />
items={tagArray.map((tag) => (
<StatusBadge key={tag} label={tag} autoColor={tag} size='sm' />
))}
/>
)
@@ -313,8 +309,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const endpointArray = formatEndpointsDisplay(endpoints)
return (
<BadgeListCell
items={endpointArray.map((ep, idx) => (
<StatusBadge key={idx} label={ep} autoColor={ep} size='sm' />
items={endpointArray.map((ep) => (
<StatusBadge key={ep} label={ep} autoColor={ep} size='sm' />
))}
/>
)
@@ -337,9 +333,9 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
}>
return (
<BadgeListCell
items={(channels ?? []).map((c, idx) => (
items={(channels ?? []).map((c) => (
<StatusBadge
key={idx}
key={`${c.id}-${c.name}-${c.type ?? ''}`}
label={`${c.name} (${c.type})`}
autoColor={c.name}
size='sm'
@@ -380,11 +376,11 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
const quotaTypes = row.getValue('quota_types') as number[]
return (
<BadgeListCell
items={(quotaTypes ?? []).map((qt, idx) => {
items={(quotaTypes ?? []).map((qt) => {
const config = QUOTA_TYPE_CONFIG[qt]
return (
<StatusBadge
key={idx}
key={qt}
label={config?.label || String(qt)}
variant={
(config?.color === 'error' ? 'danger' : config?.color) as
@@ -418,7 +414,6 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
variant={syncOfficial === 1 ? 'success' : 'warning'}
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
},
@@ -104,7 +104,6 @@ export function usePricingColumns(
label={isTokenBased ? t('Token') : t('Request')}
variant={isTokenBased ? 'info' : 'neutral'}
copyable={false}
className='-ml-1.5'
/>
)
},
@@ -16,7 +16,7 @@ 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 { useTranslation } from 'react-i18next'
import { MaskedValueDisplay } from '@/components/masked-value-display'
@@ -32,7 +32,7 @@ import { formatQuota, formatTimestampToDate } from '@/lib/format'
import { REDEMPTION_FILTER_EXPIRED, REDEMPTION_STATUSES } from '../constants'
import { isRedemptionExpired, isTimestampExpired } from '../lib'
import { type Redemption } from '../types'
import type { Redemption } from '../types'
import { DataTableRowActions } from './data-table-row-actions'
export function useRedemptionsColumns(): ColumnDef<Redemption>[] {
@@ -96,7 +96,6 @@ export function useRedemptionsColumns(): ColumnDef<Redemption>[] {
label={t('Expired')}
variant='warning'
copyable={false}
className='-ml-1.5'
/>
)
}
@@ -112,7 +111,6 @@ export function useRedemptionsColumns(): ColumnDef<Redemption>[] {
label={t(statusConfig.labelKey)}
variant={statusConfig.variant}
copyable={false}
className='-ml-1.5'
/>
)
},
@@ -164,7 +162,6 @@ export function useRedemptionsColumns(): ColumnDef<Redemption>[] {
label={formatQuota(quota)}
variant='neutral'
copyable={false}
className='-ml-1.5'
/>
)
},
@@ -195,7 +192,6 @@ export function useRedemptionsColumns(): ColumnDef<Redemption>[] {
label={t('Never')}
variant='neutral'
copyable={false}
className='-ml-1.5'
/>
)
}
@@ -233,7 +229,7 @@ export function useRedemptionsColumns(): ColumnDef<Redemption>[] {
className='cursor-help'
/>
}
></TooltipTrigger>
/>
<TooltipContent>
<div className='space-y-1 text-xs'>
<div>
@@ -16,7 +16,7 @@ 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 { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -118,14 +118,12 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] {
label={t('Enable')}
variant='success'
copyable={false}
className='-ml-1.5'
/>
) : (
<StatusBadge
label={t('Disable')}
variant='neutral'
copyable={false}
className='-ml-1.5'
/>
),
size: 80,
@@ -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>
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { type ColumnDef, type RowSelectionState } from '@tanstack/react-table'
import type { ColumnDef, RowSelectionState } from '@tanstack/react-table'
import { Search } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -119,6 +119,8 @@ export function ChannelSelectorDialog({
() => [
{
id: 'select',
size: 44,
minSize: 44,
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
@@ -142,6 +144,8 @@ export function ChannelSelectorDialog({
{
accessorKey: 'name',
header: t('Name'),
size: 300,
minSize: 220,
cell: ({ row }) => {
const name = row.getValue('name') as string
const channel = row.original
@@ -165,6 +169,8 @@ export function ChannelSelectorDialog({
{
accessorKey: 'base_url',
header: t('Base URL'),
size: 340,
minSize: 260,
cell: ({ row }) => {
const url = row.getValue('base_url') as string
return (
@@ -180,6 +186,8 @@ export function ChannelSelectorDialog({
{
accessorKey: 'status',
header: t('Status'),
size: 140,
minSize: 120,
cell: ({ row }) => {
const status = row.getValue('status') as number
const config =
@@ -198,7 +206,7 @@ export function ChannelSelectorDialog({
return (
<StatusBadge
label={config.label}
label={t(config.label)}
variant={config.variant}
size='sm'
copyable={false}
@@ -209,6 +217,8 @@ export function ChannelSelectorDialog({
{
id: 'endpoint',
header: t('Sync Endpoint'),
size: 460,
minSize: 360,
cell: ({ row }) => {
const channel = row.original
const currentEndpoint =
@@ -224,14 +234,12 @@ export function ChannelSelectorDialog({
}
return (
<div className='flex items-center gap-2'>
<div className='flex min-w-0 items-center gap-2'>
<Select
items={[
...ENDPOINT_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
})),
]}
items={ENDPOINT_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
}))}
value={endpointType}
onValueChange={(v) => v !== null && handleTypeChange(v)}
>
@@ -253,7 +261,7 @@ export function ChannelSelectorDialog({
value={currentEndpoint}
onChange={(e) => updateEndpoint(channel.id, e.target.value)}
placeholder={t('/your/endpoint')}
className='h-8 w-40 font-mono text-xs'
className='h-8 min-w-0 flex-1 font-mono text-xs'
/>
)}
</div>
@@ -315,7 +323,7 @@ export function ChannelSelectorDialog({
)}
contentClassName='flex max-h-[90vh] max-w-[calc(100%-2rem)] flex-col sm:max-w-[90vw] xl:max-w-[1400px]'
contentHeight='min(72vh, 720px)'
bodyClassName='space-y-4'
bodyClassName='flex h-full min-h-0 flex-col overflow-hidden'
footer={
<>
<Button variant='outline' onClick={() => onOpenChange(false)}>
@@ -325,27 +333,41 @@ export function ChannelSelectorDialog({
</>
}
>
<div className='flex flex-1 flex-col gap-4 overflow-hidden'>
<div className='flex items-center gap-2'>
<div className='flex h-full min-h-0 flex-col gap-4 overflow-hidden'>
<div className='flex shrink-0 items-center gap-2'>
<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 pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2' />
<Input
placeholder={t('Search by name or URL...')}
value={search}
onChange={(e) => setSearch(e.target.value)}
className='ps-8'
className='ps-9'
/>
</div>
</div>
<DataTableView
table={table}
containerClassName='flex-1 overflow-auto rounded-md'
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={(columnId, part) => {
if (columnId === 'select') return 'w-11 text-center align-middle'
if (columnId === 'status') {
return part === 'header' ? 'h-11 align-middle' : 'align-middle'
}
return part === 'header' ? 'h-11 align-middle' : 'align-middle'
}}
emptyContent={t('No channels found')}
emptyCellClassName='h-24 text-center'
/>
<DataTablePagination table={table} />
<div className='shrink-0'>
<DataTablePagination table={table} />
</div>
</div>
</Dialog>
)
@@ -73,5 +73,5 @@ export const RATIO_TYPE_OPTIONS = [
export const CHANNEL_STATUS_CONFIG = {
1: { label: 'Enabled', variant: 'success' as const },
2: { label: 'Disabled', variant: 'danger' as const },
3: { label: 'Auto-Disabled', variant: 'warning' as const },
3: { label: 'Auto Disabled', variant: 'warning' as const },
} as const
@@ -113,7 +113,6 @@ export function buildModelRatioColumns({
variant={getModeVariant(row.original.billingMode)}
copyable={false}
showDot={false}
className='-ml-1.5 px-0'
/>
),
filterFn: (row, id, value) =>
@@ -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)}
</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>
))}
@@ -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) =>
getPreferredSyncField(
row.original.ratioTypes,
ratioType,
upstreamName
) === ratioType
)
return (
<div className='flex max-w-full min-w-0 flex-col gap-2'>
<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
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>
@@ -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
}
@@ -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<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,
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 (
<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'
/>
<DataTablePagination table={table} />
<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>
<UpstreamRatioSyncTable
differences={differences}
resolutions={resolutions}
isDisabled={isLoading}
isSyncing={fetchMutation.isPending}
onSelectValue={handleSelectValue}
onUnselectValue={handleUnselectValue}
/>
<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}
@@ -129,7 +129,6 @@ export function useDrawingLogsColumns(
icon={getDrawingTypeIcon(action)}
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
},
@@ -181,7 +180,6 @@ export function useDrawingLogsColumns(
variant={mjSubmitResultMapper.getVariant(String(code))}
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
},
@@ -207,7 +207,6 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
variant={taskStatusMapper.getVariant(status)}
size='sm'
copyable={false}
className='-ml-1.5'
/>
)
},
@@ -145,7 +145,7 @@ export function useUsersColumns(): ColumnDef<User>[] {
return (
<Tooltip>
<TooltipTrigger render={<div className='-ml-1.5 cursor-help' />}>
<TooltipTrigger render={<div className='cursor-help' />}>
<StatusBadge
label={t(statusConfig.labelKey)}
variant={statusConfig.variant}
@@ -184,7 +184,6 @@ export function useUsersColumns(): ColumnDef<User>[] {
label={t('No Quota')}
variant='neutral'
copyable={false}
className='-ml-1.5'
/>
)
}