Files
dify/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx
2025-09-03 13:36:59 +08:00

286 lines
11 KiB
TypeScript

import type { Dispatch, SetStateAction } from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiIndeterminateCircleLine,
} from '@remixicon/react'
import type {
Credential,
CustomConfigurationModelFixedFields,
CustomModelCredential,
ModelCredential,
ModelLoadBalancingConfig,
ModelLoadBalancingConfigEntry,
ModelProvider,
} from '../declarations'
import { ConfigurationMethodEnum } from '../declarations'
import Indicator from '../../../indicator'
import CooldownTimer from './cooldown-timer'
import classNames from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import s from '@/app/components/custom/style.module.css'
import GridMask from '@/app/components/base/grid-mask'
import { useProviderContextSelector } from '@/context/provider-context'
import { IS_CE_EDITION } from '@/config'
import { AddCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import Badge from '@/app/components/base/badge/index'
export type ModelLoadBalancingConfigsProps = {
draftConfig?: ModelLoadBalancingConfig
setDraftConfig: Dispatch<SetStateAction<ModelLoadBalancingConfig | undefined>>
provider: ModelProvider
configurationMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
withSwitch?: boolean
className?: string
modelCredential: ModelCredential
onUpdate?: (payload?: any, formValues?: Record<string, any>) => void
onRemove?: (credentialId: string) => void
model: CustomModelCredential
}
const ModelLoadBalancingConfigs = ({
draftConfig,
setDraftConfig,
provider,
model,
configurationMethod,
currentCustomConfigurationModelFixedFields,
withSwitch = false,
className,
modelCredential,
onUpdate,
onRemove,
}: ModelLoadBalancingConfigsProps) => {
const { t } = useTranslation()
const providerFormSchemaPredefined = configurationMethod === ConfigurationMethodEnum.predefinedModel
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
const updateConfigEntry = useCallback(
(
index: number,
modifier: (entry: ModelLoadBalancingConfigEntry) => ModelLoadBalancingConfigEntry | undefined,
) => {
setDraftConfig((prev) => {
if (!prev)
return prev
const newConfigs = [...prev.configs]
const modifiedConfig = modifier(newConfigs[index])
if (modifiedConfig)
newConfigs[index] = modifiedConfig
else
newConfigs.splice(index, 1)
return {
...prev,
configs: newConfigs,
}
})
},
[setDraftConfig],
)
const addConfigEntry = useCallback((credential: Credential) => {
setDraftConfig((prev: any) => {
if (!prev)
return prev
return {
...prev,
configs: [...prev.configs, {
credential_id: credential.credential_id,
enabled: true,
name: credential.credential_name,
}],
}
})
}, [setDraftConfig])
const toggleModalBalancing = useCallback((enabled: boolean) => {
if ((modelLoadBalancingEnabled || !enabled) && draftConfig) {
setDraftConfig({
...draftConfig,
enabled,
})
}
}, [draftConfig, modelLoadBalancingEnabled, setDraftConfig])
const toggleConfigEntryEnabled = useCallback((index: number, state?: boolean) => {
updateConfigEntry(index, entry => ({
...entry,
enabled: typeof state === 'boolean' ? state : !entry.enabled,
}))
}, [updateConfigEntry])
const clearCountdown = useCallback((index: number) => {
updateConfigEntry(index, ({ ttl: _, ...entry }) => {
return {
...entry,
in_cooldown: false,
}
})
}, [updateConfigEntry])
const validDraftConfigList = useMemo(() => {
if (!draftConfig)
return []
return draftConfig.configs
}, [draftConfig])
const handleUpdate = useCallback((payload?: any, formValues?: Record<string, any>) => {
onUpdate?.(payload, formValues)
}, [onUpdate])
const handleRemove = useCallback((credentialId: string) => {
const index = draftConfig?.configs.findIndex(item => item.credential_id === credentialId && item.name !== '__inherit__')
if (index && index > -1)
updateConfigEntry(index, () => undefined)
onRemove?.(credentialId)
}, [draftConfig?.configs, updateConfigEntry, onRemove])
if (!draftConfig)
return null
return (
<>
<div
className={classNames(
'min-h-16 rounded-xl border bg-components-panel-bg transition-colors',
(withSwitch || !draftConfig.enabled) ? 'border-components-panel-border' : 'border-util-colors-blue-blue-600',
(withSwitch || draftConfig.enabled) ? 'cursor-default' : 'cursor-pointer',
className,
)}
onClick={(!withSwitch && !draftConfig.enabled) ? () => toggleModalBalancing(true) : undefined}
>
<div className='flex select-none items-center gap-2 px-[15px] py-3'>
<div className='flex h-8 w-8 shrink-0 grow-0 items-center justify-center rounded-lg border border-util-colors-indigo-indigo-100 bg-util-colors-indigo-indigo-50 text-util-colors-blue-blue-600'>
<Balance className='h-4 w-4' />
</div>
<div className='grow'>
<div className='flex items-center gap-1 text-sm text-text-primary'>
{t('common.modelProvider.loadBalancing')}
<Tooltip
popupContent={t('common.modelProvider.loadBalancingInfo')}
popupClassName='max-w-[300px]'
triggerClassName='w-3 h-3'
/>
</div>
<div className='text-xs text-text-tertiary'>{t('common.modelProvider.loadBalancingDescription')}</div>
</div>
{
withSwitch && (
<Switch
defaultValue={Boolean(draftConfig.enabled)}
size='l'
className='ml-3 justify-self-end'
disabled={!modelLoadBalancingEnabled && !draftConfig.enabled}
onChange={value => toggleModalBalancing(value)}
/>
)
}
</div>
{draftConfig.enabled && (
<div className='flex flex-col gap-1 px-3 pb-3'>
{validDraftConfigList.map((config, index) => {
const isProviderManaged = config.name === '__inherit__'
const credential = modelCredential.available_credentials.find(c => c.credential_id === config.credential_id)
return (
<div key={config.id || index} className='group flex h-10 items-center rounded-lg border border-components-panel-border bg-components-panel-on-panel-item-bg px-3 shadow-xs'>
<div className='flex grow items-center'>
<div className='mr-2 flex h-3 w-3 items-center justify-center'>
{(config.in_cooldown && Boolean(config.ttl))
? (
<CooldownTimer secondsRemaining={config.ttl} onFinish={() => clearCountdown(index)} />
)
: (
<Tooltip popupContent={t('common.modelProvider.apiKeyStatusNormal')}>
<Indicator color='green' />
</Tooltip>
)}
</div>
<div className='mr-1 text-[13px] text-text-secondary'>
{isProviderManaged ? t('common.modelProvider.defaultConfig') : config.name}
</div>
{isProviderManaged && providerFormSchemaPredefined && (
<Badge className='ml-2'>{t('common.modelProvider.providerManaged')}</Badge>
)}
{
credential?.from_enterprise && (
<Badge className='ml-2'>Enterprise</Badge>
)
}
</div>
<div className='flex items-center gap-1'>
{!isProviderManaged && (
<>
<div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
<Tooltip popupContent={t('common.operation.remove')}>
<span
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg text-text-tertiary transition-colors hover:bg-components-button-secondary-bg-hover'
onClick={() => updateConfigEntry(index, () => undefined)}
>
<RiIndeterminateCircleLine className='h-4 w-4' />
</span>
</Tooltip>
</div>
</>
)}
{
(config.credential_id || config.name === '__inherit__') && (
<>
<span className='mr-2 h-3 border-r border-r-divider-subtle' />
<Switch
defaultValue={Boolean(config.enabled)}
size='md'
className='justify-self-end'
onChange={value => toggleConfigEntryEnabled(index, value)}
disabled={credential?.not_allowed_to_use}
/>
</>
)
}
</div>
</div>
)
})}
<AddCredentialInLoadBalancing
provider={provider}
model={model}
configurationMethod={configurationMethod}
modelCredential={modelCredential}
onSelectCredential={addConfigEntry}
onUpdate={handleUpdate}
onRemove={handleRemove}
/>
</div>
)}
{
draftConfig.enabled && validDraftConfigList.length < 2 && (
<div className='flex h-[34px] items-center rounded-b-xl border-t border-t-divider-subtle bg-components-panel-bg px-6 text-xs text-text-secondary'>
<AlertTriangle className='mr-1 h-3 w-3 text-[#f79009]' />
{t('common.modelProvider.loadBalancingLeastKeyWarning')}
</div>
)
}
</div>
{!modelLoadBalancingEnabled && !IS_CE_EDITION && (
<GridMask canvasClassName='!rounded-xl'>
<div className='mt-2 flex h-14 items-center justify-between rounded-xl border-[0.5px] border-components-panel-border px-4 shadow-md'>
<div
className={classNames('text-gradient text-sm font-semibold leading-tight', s.textGradient)}
>
{t('common.modelProvider.upgradeForLoadBalancing')}
</div>
<UpgradeBtn />
</div>
</GridMask>
)}
</>
)
}
export default ModelLoadBalancingConfigs