|
import type { FC } from 'react' |
|
import { |
|
memo, |
|
useCallback, |
|
useEffect, |
|
useMemo, |
|
useState, |
|
} from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
import { |
|
RiErrorWarningFill, |
|
} from '@remixicon/react' |
|
import type { |
|
CredentialFormSchema, |
|
CredentialFormSchemaRadio, |
|
CredentialFormSchemaSelect, |
|
CustomConfigurationModelFixedFields, |
|
FormValue, |
|
ModelLoadBalancingConfig, |
|
ModelLoadBalancingConfigEntry, |
|
ModelProvider, |
|
} from '../declarations' |
|
import { |
|
ConfigurationMethodEnum, |
|
CustomConfigurationStatusEnum, |
|
FormTypeEnum, |
|
} from '../declarations' |
|
import { |
|
genModelNameFormSchema, |
|
genModelTypeFormSchema, |
|
removeCredentials, |
|
saveCredentials, |
|
} from '../utils' |
|
import { |
|
useLanguage, |
|
useProviderCredentialsAndLoadBalancing, |
|
} from '../hooks' |
|
import ProviderIcon from '../provider-icon' |
|
import { useValidate } from '../../key-validator/hooks' |
|
import { ValidatedStatus } from '../../key-validator/declarations' |
|
import ModelLoadBalancingConfigs from '../provider-added-card/model-load-balancing-configs' |
|
import Form from './Form' |
|
import Button from '@/app/components/base/button' |
|
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' |
|
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general' |
|
import { |
|
PortalToFollowElem, |
|
PortalToFollowElemContent, |
|
} from '@/app/components/base/portal-to-follow-elem' |
|
import { useToastContext } from '@/app/components/base/toast' |
|
import Confirm from '@/app/components/base/confirm' |
|
import { useAppContext } from '@/context/app-context' |
|
|
|
type ModelModalProps = { |
|
provider: ModelProvider |
|
configurateMethod: ConfigurationMethodEnum |
|
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields |
|
onCancel: () => void |
|
onSave: () => void |
|
} |
|
|
|
const ModelModal: FC<ModelModalProps> = ({ |
|
provider, |
|
configurateMethod, |
|
currentCustomConfigurationModelFixedFields, |
|
onCancel, |
|
onSave, |
|
}) => { |
|
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel |
|
const { |
|
credentials: formSchemasValue, |
|
loadBalancing: originalConfig, |
|
mutate, |
|
} = useProviderCredentialsAndLoadBalancing( |
|
provider.provider, |
|
configurateMethod, |
|
providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active, |
|
currentCustomConfigurationModelFixedFields, |
|
) |
|
const { isCurrentWorkspaceManager } = useAppContext() |
|
const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager |
|
const { t } = useTranslation() |
|
const { notify } = useToastContext() |
|
const language = useLanguage() |
|
const [loading, setLoading] = useState(false) |
|
const [showConfirm, setShowConfirm] = useState(false) |
|
|
|
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 && !draftConfig) |
|
setDraftConfig(originalConfig) |
|
}, [draftConfig, originalConfig]) |
|
|
|
const formSchemas = useMemo(() => { |
|
return providerFormSchemaPredefined |
|
? provider.provider_credential_schema.credential_form_schemas |
|
: [ |
|
genModelTypeFormSchema(provider.supported_model_types), |
|
genModelNameFormSchema(provider.model_credential_schema?.model), |
|
...(draftConfig?.enabled ? [] : provider.model_credential_schema.credential_form_schemas), |
|
] |
|
}, [ |
|
providerFormSchemaPredefined, |
|
provider.provider_credential_schema?.credential_form_schemas, |
|
provider.supported_model_types, |
|
provider.model_credential_schema?.credential_form_schemas, |
|
provider.model_credential_schema?.model, |
|
draftConfig?.enabled, |
|
]) |
|
const [ |
|
requiredFormSchemas, |
|
defaultFormSchemaValue, |
|
showOnVariableMap, |
|
] = useMemo(() => { |
|
const requiredFormSchemas: CredentialFormSchema[] = [] |
|
const defaultFormSchemaValue: Record<string, string | number> = {} |
|
const showOnVariableMap: Record<string, string[]> = {} |
|
|
|
formSchemas.forEach((formSchema) => { |
|
if (formSchema.required) |
|
requiredFormSchemas.push(formSchema) |
|
|
|
if (formSchema.default) |
|
defaultFormSchemaValue[formSchema.variable] = formSchema.default |
|
|
|
if (formSchema.show_on.length) { |
|
formSchema.show_on.forEach((showOnItem) => { |
|
if (!showOnVariableMap[showOnItem.variable]) |
|
showOnVariableMap[showOnItem.variable] = [] |
|
|
|
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable)) |
|
showOnVariableMap[showOnItem.variable].push(formSchema.variable) |
|
}) |
|
} |
|
|
|
if (formSchema.type === FormTypeEnum.select || formSchema.type === FormTypeEnum.radio) { |
|
(formSchema as (CredentialFormSchemaRadio | CredentialFormSchemaSelect)).options.forEach((option) => { |
|
if (option.show_on.length) { |
|
option.show_on.forEach((showOnItem) => { |
|
if (!showOnVariableMap[showOnItem.variable]) |
|
showOnVariableMap[showOnItem.variable] = [] |
|
|
|
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable)) |
|
showOnVariableMap[showOnItem.variable].push(formSchema.variable) |
|
}) |
|
} |
|
}) |
|
} |
|
}) |
|
|
|
return [ |
|
requiredFormSchemas, |
|
defaultFormSchemaValue, |
|
showOnVariableMap, |
|
] |
|
}, [formSchemas]) |
|
const initialFormSchemasValue: Record<string, string | number> = useMemo(() => { |
|
return { |
|
...defaultFormSchemaValue, |
|
...formSchemasValue, |
|
} as unknown as Record<string, string | number> |
|
}, [formSchemasValue, defaultFormSchemaValue]) |
|
const [value, setValue] = useState(initialFormSchemasValue) |
|
useEffect(() => { |
|
setValue(initialFormSchemasValue) |
|
}, [initialFormSchemasValue]) |
|
const [_, validating, validatedStatusState] = useValidate(value) |
|
const filteredRequiredFormSchemas = requiredFormSchemas.filter((requiredFormSchema) => { |
|
if (requiredFormSchema.show_on.length && requiredFormSchema.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)) |
|
return true |
|
|
|
if (!requiredFormSchema.show_on.length) |
|
return true |
|
|
|
return false |
|
}) |
|
|
|
const handleValueChange = (v: FormValue) => { |
|
setValue(v) |
|
} |
|
|
|
const extendedSecretFormSchemas = useMemo( |
|
() => |
|
(providerFormSchemaPredefined |
|
? provider.provider_credential_schema.credential_form_schemas |
|
: [ |
|
genModelTypeFormSchema(provider.supported_model_types), |
|
genModelNameFormSchema(provider.model_credential_schema?.model), |
|
...provider.model_credential_schema.credential_form_schemas, |
|
]).filter(({ type }) => type === FormTypeEnum.secretInput), |
|
[ |
|
provider.model_credential_schema?.credential_form_schemas, |
|
provider.model_credential_schema?.model, |
|
provider.provider_credential_schema?.credential_form_schemas, |
|
provider.supported_model_types, |
|
providerFormSchemaPredefined, |
|
], |
|
) |
|
|
|
const encodeSecretValues = useCallback((v: FormValue) => { |
|
const result = { ...v } |
|
extendedSecretFormSchemas.forEach(({ variable }) => { |
|
if (result[variable] === formSchemasValue?.[variable] && result[variable] !== undefined) |
|
result[variable] = '[__HIDDEN__]' |
|
}) |
|
return result |
|
}, [extendedSecretFormSchemas, formSchemasValue]) |
|
|
|
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 handleSave = async () => { |
|
try { |
|
setLoading(true) |
|
const res = await saveCredentials( |
|
providerFormSchemaPredefined, |
|
provider.provider, |
|
encodeSecretValues(value), |
|
{ |
|
...draftConfig, |
|
enabled: Boolean(draftConfig?.enabled), |
|
configs: draftConfig?.configs.map(encodeConfigEntrySecretValues) || [], |
|
}, |
|
) |
|
if (res.result === 'success') { |
|
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) |
|
mutate() |
|
onSave() |
|
onCancel() |
|
} |
|
} |
|
finally { |
|
setLoading(false) |
|
} |
|
} |
|
|
|
const handleRemove = async () => { |
|
try { |
|
setLoading(true) |
|
|
|
const res = await removeCredentials( |
|
providerFormSchemaPredefined, |
|
provider.provider, |
|
value, |
|
) |
|
if (res.result === 'success') { |
|
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) |
|
mutate() |
|
onSave() |
|
onCancel() |
|
} |
|
} |
|
finally { |
|
setLoading(false) |
|
} |
|
} |
|
|
|
const renderTitlePrefix = () => { |
|
const prefix = configurateMethod === ConfigurationMethodEnum.customizableModel ? t('common.operation.add') : t('common.operation.setup') |
|
|
|
return `${prefix} ${provider.label[language] || provider.label.en_US}` |
|
} |
|
|
|
return ( |
|
<PortalToFollowElem open> |
|
<PortalToFollowElemContent className='w-full h-full z-[60]'> |
|
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'> |
|
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'> |
|
<div className='px-8 pt-8'> |
|
<div className='flex justify-between items-center mb-2'> |
|
<div className='text-xl font-semibold text-gray-900'>{renderTitlePrefix()}</div> |
|
<ProviderIcon provider={provider} /> |
|
</div> |
|
|
|
<Form |
|
value={value} |
|
onChange={handleValueChange} |
|
formSchemas={formSchemas} |
|
validating={validating} |
|
validatedSuccess={validatedStatusState.status === ValidatedStatus.Success} |
|
showOnVariableMap={showOnVariableMap} |
|
isEditMode={isEditMode} |
|
/> |
|
|
|
<div className='mt-1 mb-4 border-t-[0.5px] border-t-gray-100' /> |
|
<ModelLoadBalancingConfigs withSwitch {...{ |
|
draftConfig, |
|
setDraftConfig, |
|
provider, |
|
currentCustomConfigurationModelFixedFields, |
|
configurationMethod: configurateMethod, |
|
}} /> |
|
|
|
<div className='sticky bottom-0 flex justify-between items-center mt-2 -mx-2 pt-4 px-2 pb-6 flex-wrap gap-y-2 bg-white'> |
|
{ |
|
(provider.help && (provider.help.title || provider.help.url)) |
|
? ( |
|
<a |
|
href={provider.help?.url[language] || provider.help?.url.en_US} |
|
target='_blank' rel='noopener noreferrer' |
|
className='inline-flex items-center text-xs text-primary-600' |
|
onClick={e => !provider.help.url && e.preventDefault()} |
|
> |
|
{provider.help.title?.[language] || provider.help.url[language] || provider.help.title?.en_US || provider.help.url.en_US} |
|
<LinkExternal02 className='ml-1 w-3 h-3' /> |
|
</a> |
|
) |
|
: <div /> |
|
} |
|
<div> |
|
{ |
|
isEditMode && ( |
|
<Button |
|
size='large' |
|
className='mr-2 text-[#D92D20]' |
|
onClick={() => setShowConfirm(true)} |
|
> |
|
{t('common.operation.remove')} |
|
</Button> |
|
) |
|
} |
|
<Button |
|
size='large' |
|
className='mr-2' |
|
onClick={onCancel} |
|
> |
|
{t('common.operation.cancel')} |
|
</Button> |
|
<Button |
|
size='large' |
|
variant='primary' |
|
onClick={handleSave} |
|
disabled={ |
|
loading |
|
|| filteredRequiredFormSchemas.some(item => value[item.variable] === undefined) |
|
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2) |
|
} |
|
|
|
> |
|
{t('common.operation.save')} |
|
</Button> |
|
</div> |
|
</div> |
|
</div> |
|
<div className='border-t-[0.5px] border-t-black/5'> |
|
{ |
|
(validatedStatusState.status === ValidatedStatus.Error && validatedStatusState.message) |
|
? ( |
|
<div className='flex px-[10px] py-3 bg-[#FEF3F2] text-xs text-[#D92D20]'> |
|
<RiErrorWarningFill className='mt-[1px] mr-2 w-[14px] h-[14px]' /> |
|
{validatedStatusState.message} |
|
</div> |
|
) |
|
: ( |
|
<div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'> |
|
<Lock01 className='mr-1 w-3 h-3 text-gray-500' /> |
|
{t('common.modelProvider.encrypted.front')} |
|
<a |
|
className='text-primary-600 mx-1' |
|
target='_blank' rel='noopener noreferrer' |
|
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html' |
|
> |
|
PKCS1_OAEP |
|
</a> |
|
{t('common.modelProvider.encrypted.back')} |
|
</div> |
|
) |
|
} |
|
</div> |
|
</div> |
|
{ |
|
showConfirm && ( |
|
<Confirm |
|
title={t('common.modelProvider.confirmDelete')} |
|
isShow={showConfirm} |
|
onCancel={() => setShowConfirm(false)} |
|
onConfirm={handleRemove} |
|
/> |
|
) |
|
} |
|
</div> |
|
</PortalToFollowElemContent> |
|
</PortalToFollowElem> |
|
) |
|
} |
|
|
|
export default memo(ModelModal) |
|
|