Files
urbanLifeline/dify/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx
2025-12-01 17:21:38 +08:00

377 lines
13 KiB
TypeScript

import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type {
Credential,
CustomConfigurationModelFixedFields,
ModelItem,
ModelLoadBalancingConfig,
ModelLoadBalancingConfigEntry,
ModelProvider,
} from '../declarations'
import {
ConfigurationMethodEnum,
FormTypeEnum,
} from '../declarations'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import ModelLoadBalancingConfigs from './model-load-balancing-configs'
import classNames from '@/utils/classnames'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import { useToastContext } from '@/app/components/base/toast'
import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import {
useGetModelCredential,
useUpdateModelLoadBalancingConfig,
} from '@/service/use-models'
import { useAuth } from '../model-auth/hooks/use-auth'
import Confirm from '@/app/components/base/confirm'
import { useRefreshModel } from '../hooks'
export type ModelLoadBalancingModalProps = {
provider: ModelProvider
configurateMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
model: ModelItem
credential?: Credential
open?: boolean
onClose?: () => void
onSave?: (provider: string) => void
}
// model balancing config modal
const ModelLoadBalancingModal = ({
provider,
configurateMethod,
currentCustomConfigurationModelFixedFields,
model,
credential,
open = false,
onClose,
onSave,
}: ModelLoadBalancingModalProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const {
doingAction,
deleteModel,
openConfirmDelete,
closeConfirmDelete,
handleConfirmDelete,
} = useAuth(
provider,
configurateMethod,
currentCustomConfigurationModelFixedFields,
{
isModelCredential: true,
},
)
const [loading, setLoading] = useState(false)
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
const configFrom = providerFormSchemaPredefined ? 'predefined-model' : 'custom-model'
const {
isLoading,
data,
refetch,
} = useGetModelCredential(true, provider.provider, credential?.credential_id, model.model, model.model_type, configFrom)
const modelCredential = data
const {
load_balancing,
current_credential_id,
available_credentials,
current_credential_name,
} = modelCredential ?? {}
const originalConfig = load_balancing
const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>()
const originalConfigMap = useMemo(() => {
if (!originalConfig)
return {}
return originalConfig?.configs.reduce((prev, config) => {
if (config.id)
prev[config.id] = config
return prev
}, {} as Record<string, ModelLoadBalancingConfigEntry>)
}, [originalConfig])
useEffect(() => {
if (originalConfig)
setDraftConfig(originalConfig)
}, [originalConfig])
const toggleModalBalancing = useCallback((enabled: boolean) => {
if (draftConfig) {
setDraftConfig({
...draftConfig,
enabled,
})
}
}, [draftConfig])
const extendedSecretFormSchemas = useMemo(
() => {
if (providerFormSchemaPredefined) {
return provider?.provider_credential_schema?.credential_form_schemas?.filter(
({ type }) => type === FormTypeEnum.secretInput,
) ?? []
}
return provider?.model_credential_schema?.credential_form_schemas?.filter(
({ type }) => type === FormTypeEnum.secretInput,
) ?? []
},
[provider?.model_credential_schema?.credential_form_schemas, provider?.provider_credential_schema?.credential_form_schemas, providerFormSchemaPredefined],
)
const encodeConfigEntrySecretValues = useCallback((entry: ModelLoadBalancingConfigEntry) => {
const result = { ...entry }
extendedSecretFormSchemas.forEach(({ variable }) => {
if (entry.id && result.credentials[variable] === originalConfigMap[entry.id]?.credentials?.[variable])
result.credentials[variable] = '[__HIDDEN__]'
})
return result
}, [extendedSecretFormSchemas, originalConfigMap])
const { mutateAsync: updateModelLoadBalancingConfig } = useUpdateModelLoadBalancingConfig(provider.provider)
const initialCustomModelCredential = useMemo(() => {
if (!current_credential_id)
return undefined
return {
credential_id: current_credential_id,
credential_name: current_credential_name,
}
}, [current_credential_id, current_credential_name])
const [customModelCredential, setCustomModelCredential] = useState<Credential | undefined>(initialCustomModelCredential)
const { handleRefreshModel } = useRefreshModel()
const handleSave = async () => {
try {
setLoading(true)
const res = await updateModelLoadBalancingConfig(
{
credential_id: customModelCredential?.credential_id || current_credential_id,
config_from: configFrom,
model: model.model,
model_type: model.model_type,
load_balancing: {
...draftConfig,
configs: draftConfig!.configs.map(encodeConfigEntrySecretValues),
enabled: Boolean(draftConfig?.enabled),
},
},
)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
handleRefreshModel(provider, currentCustomConfigurationModelFixedFields, false)
onSave?.(provider.provider)
onClose?.()
}
}
finally {
setLoading(false)
}
}
const handleDeleteModel = useCallback(async () => {
await handleConfirmDelete()
onClose?.()
}, [handleConfirmDelete, onClose])
const handleUpdate = useCallback(async (payload?: any, formValues?: Record<string, any>) => {
const result = await refetch()
const available_credentials = result.data?.available_credentials || []
const credentialName = formValues?.__authorization_name__
const modelCredential = payload?.credential
if (!available_credentials.length) {
onClose?.()
return
}
if (!modelCredential) {
const currentCredential = available_credentials.find(c => c.credential_name === credentialName)
if (currentCredential) {
setDraftConfig((prev: any) => {
if (!prev)
return prev
return {
...prev,
configs: [...prev.configs, {
credential_id: currentCredential.credential_id,
enabled: true,
name: currentCredential.credential_name,
}],
}
})
}
}
else {
setDraftConfig((prev) => {
if (!prev)
return prev
const newConfigs = [...prev.configs]
const prevIndex = newConfigs.findIndex(item => item.credential_id === modelCredential.credential_id && item.name !== '__inherit__')
const newIndex = available_credentials.findIndex(c => c.credential_id === modelCredential.credential_id)
if (newIndex > -1 && prevIndex > -1)
newConfigs[prevIndex].name = available_credentials[newIndex].credential_name || ''
return {
...prev,
configs: newConfigs,
}
})
}
}, [refetch, credential])
const handleUpdateWhenSwitchCredential = useCallback(async () => {
const result = await refetch()
const available_credentials = result.data?.available_credentials || []
if (!available_credentials.length)
onClose?.()
}, [refetch, onClose])
return (
<>
<Modal
isShow={Boolean(model) && open}
onClose={onClose}
className='w-[640px] max-w-none px-8 pt-8'
title={
<div className='pb-3 font-semibold'>
<div className='h-[30px]'>{
draftConfig?.enabled
? t('common.modelProvider.auth.configLoadBalancing')
: t('common.modelProvider.auth.configModel')
}</div>
{Boolean(model) && (
<div className='flex h-5 items-center'>
<ModelIcon
className='mr-2 shrink-0'
provider={provider}
modelName={model!.model}
/>
<ModelName
className='system-md-regular grow text-text-secondary'
modelItem={model!}
showModelType
showMode
showContextSize
/>
</div>
)}
</div>
}
>
{!draftConfig
? <Loading type='area' />
: (
<>
<div className='py-2'>
<div
className={classNames(
'min-h-16 rounded-xl border bg-components-panel-bg transition-colors',
draftConfig.enabled ? 'cursor-pointer border-components-panel-border' : 'cursor-default border-util-colors-blue-blue-600',
)}
onClick={draftConfig.enabled ? () => toggleModalBalancing(false) : 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-components-card-border bg-components-card-bg'>
{Boolean(model) && (
<ModelIcon className='shrink-0' provider={provider} modelName={model!.model} />
)}
</div>
<div className='grow'>
<div className='text-sm text-text-secondary'>{
providerFormSchemaPredefined
? t('common.modelProvider.auth.providerManaged')
: t('common.modelProvider.auth.specifyModelCredential')
}</div>
<div className='text-xs text-text-tertiary'>{
providerFormSchemaPredefined
? t('common.modelProvider.auth.providerManagedTip')
: t('common.modelProvider.auth.specifyModelCredentialTip')
}</div>
</div>
{
!providerFormSchemaPredefined && (
<SwitchCredentialInLoadBalancing
provider={provider}
customModelCredential={customModelCredential ?? initialCustomModelCredential}
setCustomModelCredential={setCustomModelCredential}
model={model}
credentials={available_credentials}
onUpdate={handleUpdateWhenSwitchCredential}
onRemove={handleUpdateWhenSwitchCredential}
/>
)
}
</div>
</div>
{
modelCredential && (
<ModelLoadBalancingConfigs {...{
draftConfig,
setDraftConfig,
provider,
currentCustomConfigurationModelFixedFields: {
__model_name: model.model,
__model_type: model.model_type,
},
configurationMethod: model.fetch_from,
className: 'mt-2',
modelCredential,
onUpdate: handleUpdate,
onRemove: handleUpdateWhenSwitchCredential,
model: {
model: model.model,
model_type: model.model_type,
},
}} />
)
}
</div>
<div className='mt-6 flex items-center justify-between gap-2'>
<div>
{
!providerFormSchemaPredefined && (
<Button
onClick={() => openConfirmDelete(undefined, { model: model.model, model_type: model.model_type })}
className='text-components-button-destructive-secondary-text'
>
{t('common.modelProvider.auth.removeModel')}
</Button>
)
}
</div>
<div className='space-x-2'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button
variant='primary'
onClick={handleSave}
disabled={
loading
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
|| isLoading
}
>{t('common.operation.save')}</Button>
</div>
</div>
</>
)
}
</Modal >
{
deleteModel && (
<Confirm
isShow
title={t('common.modelProvider.confirmDelete')}
onCancel={closeConfirmDelete}
onConfirm={handleDeleteModel}
isDisabled={doingAction}
/>
)
}
</>
)
}
export default memo(ModelLoadBalancingModal)