dify
/
web
/app
/components
/header
/account-setting
/model-provider-page
/provider-added-card
/model-load-balancing-configs.tsx
| import type { Dispatch, SetStateAction } from 'react' | |
| import { useCallback } from 'react' | |
| import { useTranslation } from 'react-i18next' | |
| import { | |
| RiDeleteBinLine, | |
| } from '@remixicon/react' | |
| import type { ConfigurationMethodEnum, CustomConfigurationModelFixedFields, ModelLoadBalancingConfig, ModelLoadBalancingConfigEntry, ModelProvider } 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 { Edit02, Plus02 } from '@/app/components/base/icons/src/vender/line/general' | |
| import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' | |
| import { useModalContextSelector } from '@/context/modal-context' | |
| 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' | |
| export type ModelLoadBalancingConfigsProps = { | |
| draftConfig?: ModelLoadBalancingConfig | |
| setDraftConfig: Dispatch<SetStateAction<ModelLoadBalancingConfig | undefined>> | |
| provider: ModelProvider | |
| configurationMethod: ConfigurationMethodEnum | |
| currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields | |
| withSwitch?: boolean | |
| className?: string | |
| } | |
| const ModelLoadBalancingConfigs = ({ | |
| draftConfig, | |
| setDraftConfig, | |
| provider, | |
| configurationMethod, | |
| currentCustomConfigurationModelFixedFields, | |
| withSwitch = false, | |
| className, | |
| }: ModelLoadBalancingConfigsProps) => { | |
| const { t } = useTranslation() | |
| 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 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 setShowModelLoadBalancingEntryModal = useModalContextSelector(state => state.setShowModelLoadBalancingEntryModal) | |
| const toggleEntryModal = useCallback((index?: number, entry?: ModelLoadBalancingConfigEntry) => { | |
| setShowModelLoadBalancingEntryModal({ | |
| payload: { | |
| currentProvider: provider, | |
| currentConfigurationMethod: configurationMethod, | |
| currentCustomConfigurationModelFixedFields, | |
| entry, | |
| index, | |
| }, | |
| onSaveCallback: ({ entry: result }) => { | |
| if (entry) { | |
| // edit | |
| setDraftConfig(prev => ({ | |
| ...prev, | |
| enabled: !!prev?.enabled, | |
| configs: prev?.configs.map((config, i) => i === index ? result! : config) || [], | |
| })) | |
| } | |
| else { | |
| // add | |
| setDraftConfig(prev => ({ | |
| ...prev, | |
| enabled: !!prev?.enabled, | |
| configs: (prev?.configs || []).concat([{ ...result!, enabled: true }]), | |
| })) | |
| } | |
| }, | |
| onRemoveCallback: ({ index }) => { | |
| if (index !== undefined && (draftConfig?.configs?.length ?? 0) > index) { | |
| setDraftConfig(prev => ({ | |
| ...prev, | |
| enabled: !!prev?.enabled, | |
| configs: prev?.configs.filter((_, i) => i !== index) || [], | |
| })) | |
| } | |
| }, | |
| }) | |
| }, [ | |
| configurationMethod, | |
| currentCustomConfigurationModelFixedFields, | |
| draftConfig?.configs?.length, | |
| provider, | |
| setDraftConfig, | |
| setShowModelLoadBalancingEntryModal, | |
| ]) | |
| const clearCountdown = useCallback((index: number) => { | |
| updateConfigEntry(index, ({ ttl: _, ...entry }) => { | |
| return { | |
| ...entry, | |
| in_cooldown: false, | |
| } | |
| }) | |
| }, [updateConfigEntry]) | |
| if (!draftConfig) | |
| return null | |
| return ( | |
| <> | |
| <div | |
| className={classNames( | |
| 'min-h-16 bg-gray-50 border rounded-xl transition-colors', | |
| (withSwitch || !draftConfig.enabled) ? 'border-gray-200' : 'border-primary-400', | |
| (withSwitch || draftConfig.enabled) ? 'cursor-default' : 'cursor-pointer', | |
| className, | |
| )} | |
| onClick={(!withSwitch && !draftConfig.enabled) ? () => toggleModalBalancing(true) : undefined} | |
| > | |
| <div className='flex items-center px-[15px] py-3 gap-2 select-none'> | |
| <div className='grow-0 shrink-0 flex items-center justify-center w-8 h-8 text-primary-600 bg-indigo-50 border border-indigo-100 rounded-lg'> | |
| <Balance className='w-4 h-4' /> | |
| </div> | |
| <div className='grow'> | |
| <div className='flex items-center gap-1 text-sm'> | |
| {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-gray-500'>{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'> | |
| {draftConfig.configs.map((config, index) => { | |
| const isProviderManaged = config.name === '__inherit__' | |
| return ( | |
| <div key={config.id || index} className='group flex items-center px-3 h-10 bg-white border border-gray-200 rounded-lg shadow-xs'> | |
| <div className='grow flex items-center'> | |
| <div className='flex items-center justify-center mr-2 w-3 h-3'> | |
| {(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='text-[13px] mr-1'> | |
| {isProviderManaged ? t('common.modelProvider.defaultConfig') : config.name} | |
| </div> | |
| {isProviderManaged && ( | |
| <span className='px-1 text-2xs uppercase text-gray-500 border border-black/8 rounded-[5px]'>{t('common.modelProvider.providerManaged')}</span> | |
| )} | |
| </div> | |
| <div className='flex items-center gap-1'> | |
| {!isProviderManaged && ( | |
| <> | |
| <div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'> | |
| <span | |
| className='flex items-center justify-center w-8 h-8 text-gray-500 bg-white rounded-lg transition-colors cursor-pointer hover:bg-black/5' | |
| onClick={() => toggleEntryModal(index, config)} | |
| > | |
| <Edit02 className='w-4 h-4' /> | |
| </span> | |
| <span | |
| className='flex items-center justify-center w-8 h-8 text-gray-500 bg-white rounded-lg transition-colors cursor-pointer hover:bg-black/5' | |
| onClick={() => updateConfigEntry(index, () => undefined)} | |
| > | |
| <RiDeleteBinLine className='w-4 h-4' /> | |
| </span> | |
| <span className='mr-2 h-3 border-r border-r-gray-100' /> | |
| </div> | |
| </> | |
| )} | |
| <Switch | |
| defaultValue={Boolean(config.enabled)} | |
| size='md' | |
| className='justify-self-end' | |
| onChange={value => toggleConfigEntryEnabled(index, value)} | |
| /> | |
| </div> | |
| </div> | |
| ) | |
| })} | |
| <div | |
| className='flex items-center px-3 mt-1 h-8 text-[13px] font-medium text-primary-600' | |
| onClick={() => toggleEntryModal()} | |
| > | |
| <div className='flex items-center cursor-pointer'> | |
| <Plus02 className='mr-2 w-3 h-3' />{t('common.modelProvider.addConfig')} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| { | |
| draftConfig.enabled && draftConfig.configs.length < 2 && ( | |
| <div className='flex items-center px-6 h-[34px] text-xs text-gray-700 bg-black/2 border-t border-t-black/5'> | |
| <AlertTriangle className='mr-1 w-3 h-3 text-[#f79009]' /> | |
| {t('common.modelProvider.loadBalancingLeastKeyWarning')} | |
| </div> | |
| ) | |
| } | |
| </div> | |
| {!modelLoadBalancingEnabled && !IS_CE_EDITION && ( | |
| <GridMask canvasClassName='!rounded-xl'> | |
| <div className='flex items-center justify-between mt-2 px-4 h-14 border-[0.5px] border-gray-200 rounded-xl shadow-md'> | |
| <div | |
| className={classNames('text-sm font-semibold leading-tight text-gradient', s.textGradient)} | |
| > | |
| {t('common.modelProvider.upgradeForLoadBalancing')} | |
| </div> | |
| <UpgradeBtn /> | |
| </div> | |
| </GridMask> | |
| )} | |
| </> | |
| ) | |
| } | |
| export default ModelLoadBalancingConfigs | |