model-load-balancing-configs.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import classNames from 'classnames'
  2. import type { Dispatch, SetStateAction } from 'react'
  3. import { useCallback } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import type { ConfigurationMethodEnum, CustomConfigurationModelFixedFields, ModelLoadBalancingConfig, ModelLoadBalancingConfigEntry, ModelProvider } from '../declarations'
  6. import Indicator from '../../../indicator'
  7. import CooldownTimer from './cooldown-timer'
  8. import TooltipPlus from '@/app/components/base/tooltip-plus'
  9. import Switch from '@/app/components/base/switch'
  10. import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
  11. import { Edit02, HelpCircle, Plus02, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
  12. import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
  13. import { useModalContextSelector } from '@/context/modal-context'
  14. import UpgradeBtn from '@/app/components/billing/upgrade-btn'
  15. import s from '@/app/components/custom/style.module.css'
  16. import GridMask from '@/app/components/base/grid-mask'
  17. import { useProviderContextSelector } from '@/context/provider-context'
  18. import { IS_CE_EDITION } from '@/config'
  19. export type ModelLoadBalancingConfigsProps = {
  20. draftConfig?: ModelLoadBalancingConfig
  21. setDraftConfig: Dispatch<SetStateAction<ModelLoadBalancingConfig | undefined>>
  22. provider: ModelProvider
  23. configurationMethod: ConfigurationMethodEnum
  24. currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
  25. withSwitch?: boolean
  26. className?: string
  27. }
  28. const ModelLoadBalancingConfigs = ({
  29. draftConfig,
  30. setDraftConfig,
  31. provider,
  32. configurationMethod,
  33. currentCustomConfigurationModelFixedFields,
  34. withSwitch = false,
  35. className,
  36. }: ModelLoadBalancingConfigsProps) => {
  37. const { t } = useTranslation()
  38. const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
  39. const updateConfigEntry = useCallback(
  40. (
  41. index: number,
  42. modifier: (entry: ModelLoadBalancingConfigEntry) => ModelLoadBalancingConfigEntry | undefined,
  43. ) => {
  44. setDraftConfig((prev) => {
  45. if (!prev)
  46. return prev
  47. const newConfigs = [...prev.configs]
  48. const modifiedConfig = modifier(newConfigs[index])
  49. if (modifiedConfig)
  50. newConfigs[index] = modifiedConfig
  51. else
  52. newConfigs.splice(index, 1)
  53. return {
  54. ...prev,
  55. configs: newConfigs,
  56. }
  57. })
  58. },
  59. [setDraftConfig],
  60. )
  61. const toggleModalBalancing = useCallback((enabled: boolean) => {
  62. if ((modelLoadBalancingEnabled || !enabled) && draftConfig) {
  63. setDraftConfig({
  64. ...draftConfig,
  65. enabled,
  66. })
  67. }
  68. }, [draftConfig, modelLoadBalancingEnabled, setDraftConfig])
  69. const toggleConfigEntryEnabled = useCallback((index: number, state?: boolean) => {
  70. updateConfigEntry(index, entry => ({
  71. ...entry,
  72. enabled: typeof state === 'boolean' ? state : !entry.enabled,
  73. }))
  74. }, [updateConfigEntry])
  75. const setShowModelLoadBalancingEntryModal = useModalContextSelector(state => state.setShowModelLoadBalancingEntryModal)
  76. const toggleEntryModal = useCallback((index?: number, entry?: ModelLoadBalancingConfigEntry) => {
  77. setShowModelLoadBalancingEntryModal({
  78. payload: {
  79. currentProvider: provider,
  80. currentConfigurationMethod: configurationMethod,
  81. currentCustomConfigurationModelFixedFields,
  82. entry,
  83. index,
  84. },
  85. onSaveCallback: ({ entry: result }) => {
  86. if (entry) {
  87. // edit
  88. setDraftConfig(prev => ({
  89. ...prev,
  90. enabled: !!prev?.enabled,
  91. configs: prev?.configs.map((config, i) => i === index ? result! : config) || [],
  92. }))
  93. }
  94. else {
  95. // add
  96. setDraftConfig(prev => ({
  97. ...prev,
  98. enabled: !!prev?.enabled,
  99. configs: (prev?.configs || []).concat([{ ...result!, enabled: true }]),
  100. }))
  101. }
  102. },
  103. onRemoveCallback: ({ index }) => {
  104. if (index !== undefined && (draftConfig?.configs?.length ?? 0) > index) {
  105. setDraftConfig(prev => ({
  106. ...prev,
  107. enabled: !!prev?.enabled,
  108. configs: prev?.configs.filter((_, i) => i !== index) || [],
  109. }))
  110. }
  111. },
  112. })
  113. }, [
  114. configurationMethod,
  115. currentCustomConfigurationModelFixedFields,
  116. draftConfig?.configs?.length,
  117. provider,
  118. setDraftConfig,
  119. setShowModelLoadBalancingEntryModal,
  120. ])
  121. const clearCountdown = useCallback((index: number) => {
  122. updateConfigEntry(index, ({ ttl: _, ...entry }) => {
  123. return {
  124. ...entry,
  125. in_cooldown: false,
  126. }
  127. })
  128. }, [updateConfigEntry])
  129. if (!draftConfig)
  130. return null
  131. return (
  132. <>
  133. <div
  134. className={classNames(
  135. 'min-h-16 bg-gray-50 border rounded-xl transition-colors',
  136. (withSwitch || !draftConfig.enabled) ? 'border-gray-200' : 'border-primary-400',
  137. (withSwitch || draftConfig.enabled) ? 'cursor-default' : 'cursor-pointer',
  138. className,
  139. )}
  140. onClick={(!withSwitch && !draftConfig.enabled) ? () => toggleModalBalancing(true) : undefined}
  141. >
  142. <div className='flex items-center px-[15px] py-3 gap-2 select-none'>
  143. <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'>
  144. <Balance className='w-4 h-4' />
  145. </div>
  146. <div className='grow'>
  147. <div className='flex items-center gap-1 text-sm'>
  148. {t('common.modelProvider.loadBalancing')}
  149. <TooltipPlus popupContent={t('common.modelProvider.loadBalancingInfo')} popupClassName='max-w-[300px]'>
  150. <HelpCircle className='w-3 h-3 text-gray-400' />
  151. </TooltipPlus>
  152. </div>
  153. <div className='text-xs text-gray-500'>{t('common.modelProvider.loadBalancingDescription')}</div>
  154. </div>
  155. {
  156. withSwitch && (
  157. <Switch
  158. defaultValue={Boolean(draftConfig.enabled)}
  159. size='l'
  160. className='ml-3 justify-self-end'
  161. disabled={!modelLoadBalancingEnabled && !draftConfig.enabled}
  162. onChange={value => toggleModalBalancing(value)}
  163. />
  164. )
  165. }
  166. </div>
  167. {draftConfig.enabled && (
  168. <div className='flex flex-col gap-1 px-3 pb-3'>
  169. {draftConfig.configs.map((config, index) => {
  170. const isProviderManaged = config.name === '__inherit__'
  171. return (
  172. <div key={config.id || index} className='group flex items-center px-3 h-10 bg-white border border-gray-200 rounded-lg shadow-xs'>
  173. <div className='grow flex items-center'>
  174. <div className='flex items-center justify-center mr-2 w-3 h-3'>
  175. {(config.in_cooldown && Boolean(config.ttl))
  176. ? (
  177. <CooldownTimer secondsRemaining={config.ttl} onFinish={() => clearCountdown(index)} />
  178. )
  179. : (
  180. <TooltipPlus popupContent={t('common.modelProvider.apiKeyStatusNormal')}>
  181. <Indicator color='green' />
  182. </TooltipPlus>
  183. )}
  184. </div>
  185. <div className='text-[13px] mr-1'>
  186. {isProviderManaged ? t('common.modelProvider.defaultConfig') : config.name}
  187. </div>
  188. {isProviderManaged && (
  189. <span className='px-1 text-2xs uppercase text-gray-500 border border-black/8 rounded-[5px]'>{t('common.modelProvider.providerManaged')}</span>
  190. )}
  191. </div>
  192. <div className='flex items-center gap-1'>
  193. {!isProviderManaged && (
  194. <>
  195. <div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
  196. <span
  197. 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'
  198. onClick={() => toggleEntryModal(index, config)}
  199. >
  200. <Edit02 className='w-4 h-4' />
  201. </span>
  202. <span
  203. 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'
  204. onClick={() => updateConfigEntry(index, () => undefined)}
  205. >
  206. <Trash03 className='w-4 h-4' />
  207. </span>
  208. <span className='mr-2 h-3 border-r border-r-gray-100' />
  209. </div>
  210. </>
  211. )}
  212. <Switch
  213. defaultValue={Boolean(config.enabled)}
  214. size='md'
  215. className='justify-self-end'
  216. onChange={value => toggleConfigEntryEnabled(index, value)}
  217. />
  218. </div>
  219. </div>
  220. )
  221. })}
  222. <div
  223. className='flex items-center px-3 mt-1 h-8 text-[13px] font-medium text-primary-600'
  224. onClick={() => toggleEntryModal()}
  225. >
  226. <div className='flex items-center cursor-pointer'>
  227. <Plus02 className='mr-2 w-3 h-3' />{t('common.modelProvider.addConfig')}
  228. </div>
  229. </div>
  230. </div>
  231. )}
  232. {
  233. draftConfig.enabled && draftConfig.configs.length < 2 && (
  234. <div className='flex items-center px-6 h-[34px] text-xs text-gray-700 bg-black/2 border-t border-t-black/5'>
  235. <AlertTriangle className='mr-1 w-3 h-3 text-[#f79009]' />
  236. {t('common.modelProvider.loadBalancingLeastKeyWarning')}
  237. </div>
  238. )
  239. }
  240. </div>
  241. {!modelLoadBalancingEnabled && !IS_CE_EDITION && (
  242. <GridMask canvasClassName='!rounded-xl'>
  243. <div className='flex items-center justify-between mt-2 px-4 h-14 border-[0.5px] border-gray-200 rounded-xl shadow-md'>
  244. <div
  245. className={classNames('text-sm font-semibold leading-tight text-gradient', s.textGradient)}
  246. >
  247. {t('common.modelProvider.upgradeForLoadBalancing')}
  248. </div>
  249. <UpgradeBtn />
  250. </div>
  251. </GridMask>
  252. )}
  253. </>
  254. )
  255. }
  256. export default ModelLoadBalancingConfigs