Commit 0cb741d8 by 同語 Committed by GitHub

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

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