Переглянути джерело

Refactor/model credential (#24994)

tags/1.8.1
zxhlyh 2 місяці тому
джерело
коміт
9e125e2029
Аккаунт користувача з таким Email не знайдено
30 змінених файлів з 1221 додано та 591 видалено
  1. 17
    20
      web/app/components/base/form/components/base/base-field.tsx
  2. 31
    3
      web/app/components/base/form/components/base/base-form.tsx
  3. 12
    0
      web/app/components/header/account-setting/model-provider-page/declarations.ts
  4. 14
    11
      web/app/components/header/account-setting/model-provider-page/hooks.ts
  5. 24
    47
      web/app/components/header/account-setting/model-provider-page/model-auth/add-credential-in-load-balancing.tsx
  6. 113
    57
      web/app/components/header/account-setting/model-provider-page/model-auth/add-custom-model.tsx
  7. 34
    34
      web/app/components/header/account-setting/model-provider-page/model-auth/authorized/authorized-item.tsx
  8. 15
    3
      web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.tsx
  9. 86
    51
      web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx
  10. 1
    1
      web/app/components/header/account-setting/model-provider-page/model-auth/config-model.tsx
  11. 16
    25
      web/app/components/header/account-setting/model-provider-page/model-auth/config-provider.tsx
  12. 115
    0
      web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx
  13. 1
    1
      web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth-service.ts
  14. 70
    35
      web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts
  15. 6
    0
      web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-custom-models.ts
  16. 27
    15
      web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-model-form-schemas.ts
  17. 2
    0
      web/app/components/header/account-setting/model-provider-page/model-auth/index.tsx
  18. 82
    0
      web/app/components/header/account-setting/model-provider-page/model-auth/manage-custom-model-credentials.tsx
  19. 20
    12
      web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx
  20. 239
    119
      web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx
  21. 0
    1
      web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx
  22. 15
    5
      web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx
  23. 8
    1
      web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx
  24. 26
    35
      web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx
  25. 213
    104
      web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx
  26. 1
    1
      web/app/components/header/account-setting/model-provider-page/utils.ts
  27. 14
    9
      web/context/modal-context.tsx
  28. 9
    0
      web/i18n/en-US/common.ts
  29. 9
    0
      web/i18n/zh-Hans/common.ts
  30. 1
    1
      web/service/use-models.ts

+ 17
- 20
web/app/components/base/form/components/base/base-field.tsx Переглянути файл

import { import {
isValidElement, isValidElement,
memo, memo,
useCallback,
useMemo, useMemo,
} from 'react' } from 'react'
import { RiExternalLinkLine } from '@remixicon/react' import { RiExternalLinkLine } from '@remixicon/react'
formSchema: FormSchema formSchema: FormSchema
field: AnyFieldApi field: AnyFieldApi
disabled?: boolean disabled?: boolean
onChange?: (field: string, value: any) => void
} }
const BaseField = ({ const BaseField = ({
fieldClassName, fieldClassName,
formSchema, formSchema,
field, field,
disabled: propsDisabled, disabled: propsDisabled,
onChange,
}: BaseFieldProps) => { }: BaseFieldProps) => {
const renderI18nObject = useRenderI18nObject() const renderI18nObject = useRenderI18nObject()
const { const {
placeholder, placeholder,
options, options,
labelClassName: formLabelClassName, labelClassName: formLabelClassName,
show_on = [],
disabled: formSchemaDisabled, disabled: formSchemaDisabled,
} = formSchema } = formSchema
const disabled = propsDisabled || formSchemaDisabled const disabled = propsDisabled || formSchemaDisabled
}) || [] }) || []
}, [options, renderI18nObject, optionValues]) }, [options, renderI18nObject, optionValues])
const value = useStore(field.form.store, s => s.values[field.name]) const value = useStore(field.form.store, s => s.values[field.name])
const values = useStore(field.form.store, (s) => {
return show_on.reduce((acc, condition) => {
acc[condition.variable] = s.values[condition.variable]
return acc
}, {} as Record<string, any>)
})
const show = useMemo(() => {
return show_on.every((condition) => {
const conditionValue = values[condition.variable]
return conditionValue === condition.value
})
}, [values, show_on])


if (!show)
return null
const handleChange = useCallback((value: any) => {
field.handleChange(value)
onChange?.(field.name, value)
}, [field, onChange])


return ( return (
<div className={cn(fieldClassName)}> <div className={cn(fieldClassName)}>
name={field.name} name={field.name}
className={cn(inputClassName)} className={cn(inputClassName)}
value={value || ''} value={value || ''}
onChange={e => field.handleChange(e.target.value)}
onChange={(e) => {
handleChange(e.target.value)
}}
onBlur={field.handleBlur} onBlur={field.handleBlur}
disabled={disabled} disabled={disabled}
placeholder={memorizedPlaceholder} placeholder={memorizedPlaceholder}
type='password' type='password'
className={cn(inputClassName)} className={cn(inputClassName)}
value={value || ''} value={value || ''}
onChange={e => field.handleChange(e.target.value)}
onChange={e => handleChange(e.target.value)}
onBlur={field.handleBlur} onBlur={field.handleBlur}
disabled={disabled} disabled={disabled}
placeholder={memorizedPlaceholder} placeholder={memorizedPlaceholder}
type='number' type='number'
className={cn(inputClassName)} className={cn(inputClassName)}
value={value || ''} value={value || ''}
onChange={e => field.handleChange(e.target.value)}
onChange={e => handleChange(e.target.value)}
onBlur={field.handleBlur} onBlur={field.handleBlur}
disabled={disabled} disabled={disabled}
placeholder={memorizedPlaceholder} placeholder={memorizedPlaceholder}
formSchema.type === FormTypeEnum.select && ( formSchema.type === FormTypeEnum.select && (
<PureSelect <PureSelect
value={value} value={value}
onChange={v => field.handleChange(v)}
onChange={v => handleChange(v)}
disabled={disabled} disabled={disabled}
placeholder={memorizedPlaceholder} placeholder={memorizedPlaceholder}
options={memorizedOptions} options={memorizedOptions}
triggerPopupSameWidth triggerPopupSameWidth
popupProps={{
className: 'max-h-[320px] overflow-y-auto',
}}
/> />
) )
} }
disabled && 'cursor-not-allowed opacity-50', disabled && 'cursor-not-allowed opacity-50',
inputClassName, inputClassName,
)} )}
onClick={() => !disabled && field.handleChange(option.value)}
onClick={() => !disabled && handleChange(option.value)}
> >
{ {
formSchema.showRadioUI && ( formSchema.showRadioUI && (

+ 31
- 3
web/app/components/base/form/components/base/base-form.tsx Переглянути файл

AnyFieldApi, AnyFieldApi,
AnyFormApi, AnyFormApi,
} from '@tanstack/react-form' } from '@tanstack/react-form'
import { useForm } from '@tanstack/react-form'
import {
useForm,
useStore,
} from '@tanstack/react-form'
import type { import type {
FormRef, FormRef,
FormSchema, FormSchema,
ref?: FormRef ref?: FormRef
disabled?: boolean disabled?: boolean
formFromProps?: AnyFormApi formFromProps?: AnyFormApi
onChange?: (field: string, value: any) => void
} & Pick<BaseFieldProps, 'fieldClassName' | 'labelClassName' | 'inputContainerClassName' | 'inputClassName'> } & Pick<BaseFieldProps, 'fieldClassName' | 'labelClassName' | 'inputContainerClassName' | 'inputClassName'>


const BaseForm = ({ const BaseForm = ({
ref, ref,
disabled, disabled,
formFromProps, formFromProps,
onChange,
}: BaseFormProps) => { }: BaseFormProps) => {
const initialDefaultValues = useMemo(() => { const initialDefaultValues = useMemo(() => {
if (defaultValues) if (defaultValues)
const { getFormValues } = useGetFormValues(form, formSchemas) const { getFormValues } = useGetFormValues(form, formSchemas)
const { getValidators } = useGetValidators() const { getValidators } = useGetValidators()


const showOnValues = useStore(form.store, (s: any) => {
const result: Record<string, any> = {}
formSchemas.forEach((schema) => {
const { show_on } = schema
if (show_on?.length) {
show_on.forEach((condition) => {
result[condition.variable] = s.values[condition.variable]
})
}
})
return result
})

useImperativeHandle(ref, () => { useImperativeHandle(ref, () => {
return { return {
getForm() { getForm() {
inputContainerClassName={inputContainerClassName} inputContainerClassName={inputContainerClassName}
inputClassName={inputClassName} inputClassName={inputClassName}
disabled={disabled} disabled={disabled}
onChange={onChange}
/> />
) )
} }


return null return null
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled])
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled, onChange])


const renderFieldWrapper = useCallback((formSchema: FormSchema) => { const renderFieldWrapper = useCallback((formSchema: FormSchema) => {
const validators = getValidators(formSchema) const validators = getValidators(formSchema)
const { const {
name, name,
show_on = [],
} = formSchema } = formSchema


const show = show_on?.every((condition) => {
const conditionValue = showOnValues[condition.variable]
return conditionValue === condition.value
})

if (!show)
return null

return ( return (
<form.Field <form.Field
key={name} key={name}
{renderField} {renderField}
</form.Field> </form.Field>
) )
}, [renderField, form, getValidators])
}, [renderField, form, getValidators, showOnValues])


if (!formSchemas?.length) if (!formSchemas?.length)
return null return null

+ 12
- 0
web/app/components/header/account-setting/model-provider-page/declarations.ts Переглянути файл

credentials?: Record<string, any> credentials?: Record<string, any>
available_model_credentials?: Credential[] available_model_credentials?: Credential[]
current_credential_id?: string current_credential_id?: string
current_credential_name?: string
} }


export type CredentialWithModel = Credential & { export type CredentialWithModel = Credential & {
current_credential_name?: string current_credential_name?: string
available_credentials?: Credential[] available_credentials?: Credential[]
custom_models?: CustomModelCredential[] custom_models?: CustomModelCredential[]
can_added_models?: {
model: string
model_type: ModelTypeEnum
}[]
} }
system_configuration: { system_configuration: {
enabled: boolean enabled: boolean
current_credential_id?: string current_credential_id?: string
current_credential_name?: string current_credential_name?: string
} }

export enum ModelModalModeEnum {
configProviderCredential = 'config-provider-credential',
configCustomModel = 'config-custom-model',
addCustomModelToModelList = 'add-custom-model-to-model-list',
configModelCredential = 'config-model-credential',
}

+ 14
- 11
web/app/components/header/account-setting/model-provider-page/hooks.ts Переглянути файл

DefaultModel, DefaultModel,
DefaultModelResponse, DefaultModelResponse,
Model, Model,
ModelModalModeEnum,
ModelProvider, ModelProvider,
ModelTypeEnum, ModelTypeEnum,
} from './declarations' } from './declarations'


export const useModelModalHandler = () => { export const useModelModalHandler = () => {
const setShowModelModal = useModalContextSelector(state => state.setShowModelModal) const setShowModelModal = useModalContextSelector(state => state.setShowModelModal)
const { handleRefreshModel } = useRefreshModel()


return ( return (
provider: ModelProvider, provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum, configurationMethod: ConfigurationMethodEnum,
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
isModelCredential?: boolean,
credential?: Credential,
model?: CustomModel,
onUpdate?: () => void,
extra: {
isModelCredential?: boolean,
credential?: Credential,
model?: CustomModel,
onUpdate?: (newPayload: any, formValues?: Record<string, any>) => void,
mode?: ModelModalModeEnum,
} = {},
) => { ) => {
setShowModelModal({ setShowModelModal({
payload: { payload: {
currentProvider: provider, currentProvider: provider,
currentConfigurationMethod: configurationMethod, currentConfigurationMethod: configurationMethod,
currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
isModelCredential,
credential,
model,
isModelCredential: extra.isModelCredential,
credential: extra.credential,
model: extra.model,
mode: extra.mode,
}, },
onSaveCallback: () => {
handleRefreshModel(provider, configurationMethod, CustomConfigurationModelFixedFields)
onUpdate?.()
onSaveCallback: (newPayload, formValues) => {
extra.onUpdate?.(newPayload, formValues)
}, },
}) })
} }

+ 24
- 47
web/app/components/header/account-setting/model-provider-page/model-auth/add-credential-in-load-balancing.tsx Переглянути файл

import { import {
memo, memo,
useCallback, useCallback,
useMemo,
} from 'react' } from 'react'
import { RiAddLine } from '@remixicon/react' import { RiAddLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { import type {
Credential, Credential,
CustomConfigurationModelFixedFields,
CustomModelCredential, CustomModelCredential,
ModelCredential, ModelCredential,
ModelProvider, ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import Tooltip from '@/app/components/base/tooltip'
import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'


type AddCredentialInLoadBalancingProps = { type AddCredentialInLoadBalancingProps = {
provider: ModelProvider provider: ModelProvider
model: CustomModelCredential model: CustomModelCredential
configurationMethod: ConfigurationMethodEnum configurationMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
modelCredential: ModelCredential modelCredential: ModelCredential
onSelectCredential: (credential: Credential) => void onSelectCredential: (credential: Credential) => void
onUpdate?: () => void
onUpdate?: (payload?: any, formValues?: Record<string, any>) => void
onRemove?: (credentialId: string) => void
} }
const AddCredentialInLoadBalancing = ({ const AddCredentialInLoadBalancing = ({
provider, provider,
modelCredential, modelCredential,
onSelectCredential, onSelectCredential,
onUpdate, onUpdate,
onRemove,
}: AddCredentialInLoadBalancingProps) => { }: AddCredentialInLoadBalancingProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
available_credentials, available_credentials,
} = modelCredential } = modelCredential
const customModel = configurationMethod === ConfigurationMethodEnum.customizableModel
const isCustomModel = configurationMethod === ConfigurationMethodEnum.customizableModel
const notAllowCustomCredential = provider.allow_custom_token === false const notAllowCustomCredential = provider.allow_custom_token === false

const ButtonComponent = useMemo(() => {
const Item = (
<div className={cn(
'system-sm-medium flex h-8 items-center rounded-lg px-3 text-text-accent hover:bg-state-base-hover',
notAllowCustomCredential && 'cursor-not-allowed opacity-50',
)}>
<RiAddLine className='mr-2 h-4 w-4' />
{
customModel
? t('common.modelProvider.auth.addCredential')
: t('common.modelProvider.auth.addApiKey')
}
</div>
)

if (notAllowCustomCredential) {
return (
<Tooltip
asChild
popupContent={t('plugin.auth.credentialUnavailable')}
>
{Item}
</Tooltip>
)
}
return Item
}, [notAllowCustomCredential, t, customModel])
const handleUpdate = useCallback((payload?: any, formValues?: Record<string, any>) => {
onUpdate?.(payload, formValues)
}, [onUpdate])


const renderTrigger = useCallback((open?: boolean) => { const renderTrigger = useCallback((open?: boolean) => {
const Item = ( const Item = (
open && 'bg-state-base-hover', open && 'bg-state-base-hover',
)}> )}>
<RiAddLine className='mr-2 h-4 w-4' /> <RiAddLine className='mr-2 h-4 w-4' />
{
customModel
? t('common.modelProvider.auth.addCredential')
: t('common.modelProvider.auth.addApiKey')
}
{t('common.modelProvider.auth.addCredential')}
</div> </div>
) )


return Item return Item
}, [t, customModel])

if (!available_credentials?.length)
return ButtonComponent
}, [t, isCustomModel])


return ( return (
<Authorized <Authorized
provider={provider} provider={provider}
renderTrigger={renderTrigger} renderTrigger={renderTrigger}
authParams={{
isModelCredential: isCustomModel,
mode: ModelModalModeEnum.configModelCredential,
onUpdate: handleUpdate,
onRemove,
}}
triggerOnlyOpenModal={!available_credentials?.length && !notAllowCustomCredential}
items={[ items={[
{ {
title: customModel ? t('common.modelProvider.auth.modelCredentials') : t('common.modelProvider.auth.apiKeys'),
model: customModel ? model : undefined,
title: isCustomModel ? '' : t('common.modelProvider.auth.apiKeys'),
model: isCustomModel ? model : undefined,
credentials: available_credentials ?? [], credentials: available_credentials ?? [],
}, },
]} ]}
showModelTitle={!isCustomModel}
configurationMethod={configurationMethod} configurationMethod={configurationMethod}
currentCustomConfigurationModelFixedFields={customModel ? {
currentCustomConfigurationModelFixedFields={isCustomModel ? {
__model_name: model.model, __model_name: model.model,
__model_type: model.model_type, __model_type: model.model_type,
} : undefined} } : undefined}
onItemClick={onSelectCredential} onItemClick={onSelectCredential}
placement='bottom-start' placement='bottom-start'
onUpdate={onUpdate}
isModelCredential={customModel}
popupTitle={isCustomModel ? t('common.modelProvider.auth.modelCredentials') : ''}
/> />
) )
} }

+ 113
- 57
web/app/components/header/account-setting/model-provider-page/model-auth/add-custom-model.tsx Переглянути файл

import { import {
memo, memo,
useCallback, useCallback,
useMemo,
useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiAddCircleFill, RiAddCircleFill,
RiAddLine,
} from '@remixicon/react' } from '@remixicon/react'
import { import {
Button, Button,
} from '@/app/components/base/button' } from '@/app/components/base/button'
import type { import type {
ConfigurationMethodEnum,
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
ModelProvider, ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import Authorized from './authorized'
import {
useAuth,
useCustomModels,
} from './hooks'
import { ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import ModelIcon from '../model-icon'
import { useCanAddedModels } from './hooks/use-custom-models'
import { useAuth } from './hooks/use-auth'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'


type AddCustomModelProps = { type AddCustomModelProps = {
provider: ModelProvider, provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum, configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
open?: boolean
onOpenChange?: (open: boolean) => void
} }
const AddCustomModel = ({ const AddCustomModel = ({
provider, provider,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
}: AddCustomModelProps) => { }: AddCustomModelProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const customModels = useCustomModels(provider)
const noModels = !customModels.length
const [open, setOpen] = useState(false)
const canAddedModels = useCanAddedModels(provider)
const noModels = !canAddedModels.length
const {
handleOpenModal: handleOpenModalForAddNewCustomModel,
} = useAuth(
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
{
isModelCredential: true,
mode: ModelModalModeEnum.configCustomModel,
},
)
const { const {
handleOpenModal,
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields, true)
handleOpenModal: handleOpenModalForAddCustomModelToModelList,
} = useAuth(
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
{
isModelCredential: true,
mode: ModelModalModeEnum.addCustomModelToModelList,
},
)
const notAllowCustomCredential = provider.allow_custom_token === false const notAllowCustomCredential = provider.allow_custom_token === false
const handleClick = useCallback(() => {
if (notAllowCustomCredential)
return


handleOpenModal()
}, [handleOpenModal, notAllowCustomCredential])
const ButtonComponent = useMemo(() => {
const renderTrigger = useCallback((open?: boolean) => {
const Item = ( const Item = (
<Button <Button
variant='ghost-accent'
variant='ghost'
size='small' size='small'
onClick={handleClick}
className={cn( className={cn(
notAllowCustomCredential && 'cursor-not-allowed opacity-50',
'text-text-tertiary',
open && 'bg-components-button-ghost-bg-hover',
notAllowCustomCredential && !!noModels && 'cursor-not-allowed opacity-50',
)} )}
> >
<RiAddCircleFill className='mr-1 h-3.5 w-3.5' /> <RiAddCircleFill className='mr-1 h-3.5 w-3.5' />
{t('common.modelProvider.addModel')} {t('common.modelProvider.addModel')}
</Button> </Button>
) )
if (notAllowCustomCredential) {
if (notAllowCustomCredential && !!noModels) {
return ( return (
<Tooltip
asChild
popupContent={t('plugin.auth.credentialUnavailable')}
>
<Tooltip asChild popupContent={t('plugin.auth.credentialUnavailable')}>
{Item} {Item}
</Tooltip> </Tooltip>
) )
} }
return Item return Item
}, [handleClick, notAllowCustomCredential, t])

const renderTrigger = useCallback((open?: boolean) => {
const Item = (
<Button
variant='ghost'
size='small'
className={cn(
open && 'bg-components-button-ghost-bg-hover',
)}
>
<RiAddCircleFill className='mr-1 h-3.5 w-3.5' />
{t('common.modelProvider.addModel')}
</Button>
)
return Item
}, [t])

if (noModels)
return ButtonComponent
}, [t, notAllowCustomCredential, noModels])


return ( return (
<Authorized
provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel}
items={customModels.map(model => ({
model,
credentials: model.available_model_credentials ?? [],
}))}
renderTrigger={renderTrigger}
isModelCredential
enableAddModelCredential
bottomAddModelCredentialText={t('common.modelProvider.auth.addNewModel')}
/>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: 0,
}}
>
<PortalToFollowElemTrigger onClick={() => {
if (noModels) {
if (notAllowCustomCredential)
return
handleOpenModalForAddNewCustomModel()
return
}

setOpen(prev => !prev)
}}>
{renderTrigger(open)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[100]'>
<div className='w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
<div className='max-h-[304px] overflow-y-auto p-1'>
{
canAddedModels.map(model => (
<div
key={model.model}
className='flex h-8 cursor-pointer items-center rounded-lg px-2 hover:bg-state-base-hover'
onClick={() => {
handleOpenModalForAddCustomModelToModelList(undefined, model)
setOpen(false)
}}
>
<ModelIcon
className='mr-1 h-5 w-5 shrink-0'
iconClassName='h-5 w-5'
provider={provider}
modelName={model.model}
/>
<div
className='system-md-regular grow truncate text-text-primary'
title={model.model}
>
{model.model}
</div>
</div>
))
}
</div>
{
!notAllowCustomCredential && (
<div
className='system-xs-medium flex cursor-pointer items-center border-t border-t-divider-subtle p-3 text-text-accent-light-mode-only'
onClick={() => {
handleOpenModalForAddNewCustomModel()
setOpen(false)
}}
>
<RiAddLine className='mr-1 h-4 w-4' />
{t('common.modelProvider.auth.addNewModel')}
</div>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
) )
} }



+ 34
- 34
web/app/components/header/account-setting/model-provider-page/model-auth/authorized/authorized-item.tsx Переглянути файл

memo, memo,
useCallback, useCallback,
} from 'react' } from 'react'
import { RiAddLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import CredentialItem from './credential-item' import CredentialItem from './credential-item'
import type { import type {
Credential, Credential,
CustomModel, CustomModel,
CustomModelCredential, CustomModelCredential,
ModelProvider,
} from '../../declarations' } from '../../declarations'
import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
import ModelIcon from '../../model-icon'


type AuthorizedItemProps = { type AuthorizedItemProps = {
provider: ModelProvider
model?: CustomModelCredential model?: CustomModelCredential
title?: string title?: string
disabled?: boolean disabled?: boolean
onItemClick?: (credential: Credential, model?: CustomModel) => void onItemClick?: (credential: Credential, model?: CustomModel) => void
enableAddModelCredential?: boolean enableAddModelCredential?: boolean
notAllowCustomCredential?: boolean notAllowCustomCredential?: boolean
showModelTitle?: boolean
disableDeleteButShowAction?: boolean
disableDeleteTip?: string
} }
export const AuthorizedItem = ({ export const AuthorizedItem = ({
provider,
model, model,
title, title,
credentials, credentials,
showItemSelectedIcon, showItemSelectedIcon,
selectedCredentialId, selectedCredentialId,
onItemClick, onItemClick,
enableAddModelCredential,
notAllowCustomCredential,
showModelTitle,
disableDeleteButShowAction,
disableDeleteTip,
}: AuthorizedItemProps) => { }: AuthorizedItemProps) => {
const { t } = useTranslation()
const handleEdit = useCallback((credential?: Credential) => { const handleEdit = useCallback((credential?: Credential) => {
onEdit?.(credential, model) onEdit?.(credential, model)
}, [onEdit, model]) }, [onEdit, model])


return ( return (
<div className='p-1'> <div className='p-1'>
<div
className='flex h-9 items-center'
>
<div className='h-5 w-5 shrink-0'></div>
<div
className='system-md-medium mx-1 grow truncate text-text-primary'
title={title ?? model?.model}
>
{title ?? model?.model}
</div>
{
enableAddModelCredential && !notAllowCustomCredential && (
<Tooltip
asChild
popupContent={t('common.modelProvider.auth.addModelCredential')}
{
showModelTitle && (
<div
className='flex h-9 items-center px-2'
>
{
model?.model && (
<ModelIcon
className='mr-1 h-5 w-5 shrink-0'
provider={provider}
modelName={model.model}
/>
)
}
<div
className='system-md-medium mx-1 grow truncate text-text-primary'
title={title ?? model?.model}
> >
<Button
className='h-6 w-6 shrink-0 rounded-full p-0'
size='small'
variant='secondary-accent'
onClick={() => handleEdit?.()}
>
<RiAddLine className='h-4 w-4' />
</Button>
</Tooltip>
)
}
</div>
{title ?? model?.model}
</div>
</div>
)
}
{ {
credentials.map(credential => ( credentials.map(credential => (
<CredentialItem <CredentialItem
showSelectedIcon={showItemSelectedIcon} showSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredentialId} selectedCredentialId={selectedCredentialId}
onItemClick={handleItemClick} onItemClick={handleItemClick}
disableDeleteButShowAction={disableDeleteButShowAction}
disableDeleteTip={disableDeleteTip}
/> />
)) ))
} }

+ 15
- 3
web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.tsx Переглянути файл

disableRename?: boolean disableRename?: boolean
disableEdit?: boolean disableEdit?: boolean
disableDelete?: boolean disableDelete?: boolean
disableDeleteButShowAction?: boolean
disableDeleteTip?: string
showSelectedIcon?: boolean showSelectedIcon?: boolean
selectedCredentialId?: string selectedCredentialId?: string
} }
disableRename, disableRename,
disableEdit, disableEdit,
disableDelete, disableDelete,
disableDeleteButShowAction,
disableDeleteTip,
showSelectedIcon, showSelectedIcon,
selectedCredentialId, selectedCredentialId,
}: CredentialItemProps) => { }: CredentialItemProps) => {
const showAction = useMemo(() => { const showAction = useMemo(() => {
return !(disableRename && disableEdit && disableDelete) return !(disableRename && disableEdit && disableDelete)
}, [disableRename, disableEdit, disableDelete]) }, [disableRename, disableEdit, disableDelete])
const disableDeleteWhenSelected = useMemo(() => {
return disableDeleteButShowAction && selectedCredentialId === credential.credential_id
}, [disableDeleteButShowAction, selectedCredentialId, credential.credential_id])


const Item = ( const Item = (
<div <div
} }
{ {
!disableDelete && !credential.from_enterprise && ( !disableDelete && !credential.from_enterprise && (
<Tooltip popupContent={t('common.operation.delete')}>
<Tooltip popupContent={disableDeleteWhenSelected ? disableDeleteTip : t('common.operation.delete')}>
<ActionButton <ActionButton
className='hover:bg-transparent' className='hover:bg-transparent'
disabled={disabled}
onClick={(e) => { onClick={(e) => {
if (disabled || disableDeleteWhenSelected)
return
e.stopPropagation() e.stopPropagation()
onDelete?.(credential) onDelete?.(credential)
}} }}
> >
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary hover:text-text-destructive' />
<RiDeleteBinLine className={cn(
'h-4 w-4 text-text-tertiary',
!disableDeleteWhenSelected && 'hover:text-text-destructive',
disableDeleteWhenSelected && 'opacity-50',
)} />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
) )

+ 86
- 51
web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx Переглянути файл

import { import {
Fragment,
memo, memo,
useCallback, useCallback,
useMemo,
useState, useState,
} from 'react' } from 'react'
import { import {
RiAddLine, RiAddLine,
RiEqualizer2Line,
} from '@remixicon/react' } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
Credential, Credential,
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
CustomModel, CustomModel,
ModelModalModeEnum,
ModelProvider, ModelProvider,
} from '../../declarations' } from '../../declarations'
import { useAuth } from '../hooks' import { useAuth } from '../hooks'
provider: ModelProvider, provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum, configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
isModelCredential?: boolean
authParams?: {
isModelCredential?: boolean
onUpdate?: (newPayload?: any, formValues?: Record<string, any>) => void
onRemove?: (credentialId: string) => void
mode?: ModelModalModeEnum
}
items: { items: {
title?: string title?: string
model?: CustomModel model?: CustomModel
selectedCredential?: Credential
credentials: Credential[] credentials: Credential[]
}[] }[]
selectedCredential?: Credential
disabled?: boolean disabled?: boolean
renderTrigger?: (open?: boolean) => React.ReactNode
renderTrigger: (open?: boolean) => React.ReactNode
isOpen?: boolean isOpen?: boolean
onOpenChange?: (open: boolean) => void onOpenChange?: (open: boolean) => void
offset?: PortalToFollowElemOptions['offset'] offset?: PortalToFollowElemOptions['offset']
triggerPopupSameWidth?: boolean triggerPopupSameWidth?: boolean
popupClassName?: string popupClassName?: string
showItemSelectedIcon?: boolean showItemSelectedIcon?: boolean
onUpdate?: () => void
onItemClick?: (credential: Credential, model?: CustomModel) => void onItemClick?: (credential: Credential, model?: CustomModel) => void
enableAddModelCredential?: boolean enableAddModelCredential?: boolean
bottomAddModelCredentialText?: string
triggerOnlyOpenModal?: boolean
hideAddAction?: boolean
disableItemClick?: boolean
popupTitle?: string
showModelTitle?: boolean
disableDeleteButShowAction?: boolean
disableDeleteTip?: string
} }
const Authorized = ({ const Authorized = ({
provider, provider,
configurationMethod, configurationMethod,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
items, items,
isModelCredential,
selectedCredential,
authParams,
disabled, disabled,
renderTrigger, renderTrigger,
isOpen, isOpen,
triggerPopupSameWidth = false, triggerPopupSameWidth = false,
popupClassName, popupClassName,
showItemSelectedIcon, showItemSelectedIcon,
onUpdate,
onItemClick, onItemClick,
enableAddModelCredential,
bottomAddModelCredentialText,
triggerOnlyOpenModal,
hideAddAction,
disableItemClick,
popupTitle,
showModelTitle,
disableDeleteButShowAction,
disableDeleteTip,
}: AuthorizedProps) => { }: AuthorizedProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isLocalOpen, setIsLocalOpen] = useState(false) const [isLocalOpen, setIsLocalOpen] = useState(false)


setIsLocalOpen(open) setIsLocalOpen(open)
}, [onOpenChange]) }, [onOpenChange])
const {
isModelCredential,
onUpdate,
onRemove,
mode,
} = authParams || {}
const { const {
openConfirmDelete, openConfirmDelete,
closeConfirmDelete, closeConfirmDelete,
handleConfirmDelete, handleConfirmDelete,
deleteCredentialId, deleteCredentialId,
handleOpenModal, handleOpenModal,
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields, isModelCredential, onUpdate)
} = useAuth(
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
{
isModelCredential,
onUpdate,
onRemove,
mode,
},
)


const handleEdit = useCallback((credential?: Credential, model?: CustomModel) => { const handleEdit = useCallback((credential?: Credential, model?: CustomModel) => {
handleOpenModal(credential, model) handleOpenModal(credential, model)
}, [handleOpenModal, setMergedIsOpen]) }, [handleOpenModal, setMergedIsOpen])


const handleItemClick = useCallback((credential: Credential, model?: CustomModel) => { const handleItemClick = useCallback((credential: Credential, model?: CustomModel) => {
if (disableItemClick)
return

if (onItemClick) if (onItemClick)
onItemClick(credential, model) onItemClick(credential, model)
else else
handleActiveCredential(credential, model) handleActiveCredential(credential, model)


setMergedIsOpen(false) setMergedIsOpen(false)
}, [handleActiveCredential, onItemClick, setMergedIsOpen])
}, [handleActiveCredential, onItemClick, setMergedIsOpen, disableItemClick])
const notAllowCustomCredential = provider.allow_custom_token === false const notAllowCustomCredential = provider.allow_custom_token === false


const Trigger = useMemo(() => {
const Item = (
<Button
className='grow'
size='small'
>
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
{t('common.operation.config')}
</Button>
)
return Item
}, [t])

return ( return (
<> <>
<PortalToFollowElem <PortalToFollowElem
> >
<PortalToFollowElemTrigger <PortalToFollowElemTrigger
onClick={() => { onClick={() => {
if (triggerOnlyOpenModal) {
handleOpenModal()
return
}

setMergedIsOpen(!mergedIsOpen) setMergedIsOpen(!mergedIsOpen)
}} }}
asChild asChild
> >
{
renderTrigger
? renderTrigger(mergedIsOpen)
: Trigger
}
{renderTrigger(mergedIsOpen)}
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[100]'> <PortalToFollowElemContent className='z-[100]'>
<div className={cn( <div className={cn(
'w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg',
'w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]',
popupClassName, popupClassName,
)}> )}>
{
popupTitle && (
<div className='system-xs-medium px-3 pb-0.5 pt-[10px] text-text-tertiary'>
{popupTitle}
</div>
)
}
<div className='max-h-[304px] overflow-y-auto'> <div className='max-h-[304px] overflow-y-auto'>
{ {
items.map((item, index) => ( items.map((item, index) => (
<AuthorizedItem
key={index}
title={item.title}
model={item.model}
credentials={item.credentials}
disabled={disabled}
onDelete={openConfirmDelete}
onEdit={handleEdit}
showItemSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredential?.credential_id}
onItemClick={handleItemClick}
enableAddModelCredential={enableAddModelCredential}
notAllowCustomCredential={notAllowCustomCredential}
/>
<Fragment key={index}>
<AuthorizedItem
provider={provider}
title={item.title}
model={item.model}
credentials={item.credentials}
disabled={disabled}
onDelete={openConfirmDelete}
disableDeleteButShowAction={disableDeleteButShowAction}
disableDeleteTip={disableDeleteTip}
onEdit={handleEdit}
showItemSelectedIcon={showItemSelectedIcon}
selectedCredentialId={item.selectedCredential?.credential_id}
onItemClick={handleItemClick}
showModelTitle={showModelTitle}
/>
{
index !== items.length - 1 && (
<div className='h-[1px] bg-divider-subtle'></div>
)
}
</Fragment>
)) ))
} }
</div> </div>
<div className='h-[1px] bg-divider-subtle'></div> <div className='h-[1px] bg-divider-subtle'></div>
{ {
isModelCredential && !notAllowCustomCredential && (
isModelCredential && !notAllowCustomCredential && !hideAddAction && (
<div <div
onClick={() => handleEdit( onClick={() => handleEdit(
undefined, undefined,
} }
: undefined, : undefined,
)} )}
className='system-xs-medium flex h-[30px] cursor-pointer items-center px-3 text-text-accent-light-mode-only'
className='system-xs-medium flex h-[40px] cursor-pointer items-center px-3 text-text-accent-light-mode-only'
> >
<RiAddLine className='mr-1 h-4 w-4' /> <RiAddLine className='mr-1 h-4 w-4' />
{bottomAddModelCredentialText ?? t('common.modelProvider.auth.addModelCredential')}
{t('common.modelProvider.auth.addModelCredential')}
</div> </div>
) )
} }
{ {
!isModelCredential && !notAllowCustomCredential && (
!isModelCredential && !notAllowCustomCredential && !hideAddAction && (
<div className='p-2'> <div className='p-2'>
<Button <Button
onClick={() => handleEdit()} onClick={() => handleEdit()}

+ 1
- 1
web/app/components/header/account-setting/model-provider-page/model-auth/config-model.tsx Переглянути файл

if (loadBalancingInvalid) { if (loadBalancingInvalid) {
return ( return (
<div <div
className='system-2xs-medium-uppercase relative flex h-[18px] items-center rounded-[5px] border border-text-warning bg-components-badge-bg-dimm px-1.5 text-text-warning'
className='system-2xs-medium-uppercase relative flex h-[18px] cursor-pointer items-center rounded-[5px] border border-text-warning bg-components-badge-bg-dimm px-1.5 text-text-warning'
onClick={onClick} onClick={onClick}
> >
<RiScales3Line className='mr-0.5 h-3 w-3' /> <RiScales3Line className='mr-0.5 h-3 w-3' />

+ 16
- 25
web/app/components/header/account-setting/model-provider-page/model-auth/config-provider.tsx Переглянути файл

import { import {
memo, memo,
useCallback, useCallback,
useMemo,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import Authorized from './authorized' import Authorized from './authorized'
import { useAuth, useCredentialStatus } from './hooks'
import { useCredentialStatus } from './hooks'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'


type ConfigProviderProps = { type ConfigProviderProps = {
provider: ModelProvider, provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
} }
const ConfigProvider = ({ const ConfigProvider = ({
provider, provider,
configurationMethod,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
}: ConfigProviderProps) => { }: ConfigProviderProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const {
handleOpenModal,
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields)
const { const {
hasCredential, hasCredential,
authorized, authorized,
available_credentials, available_credentials,
} = useCredentialStatus(provider) } = useCredentialStatus(provider)
const notAllowCustomCredential = provider.allow_custom_token === false const notAllowCustomCredential = provider.allow_custom_token === false
const handleClick = useCallback(() => {
if (!hasCredential && !notAllowCustomCredential)
handleOpenModal()
}, [handleOpenModal, hasCredential, notAllowCustomCredential])
const ButtonComponent = useMemo(() => {

const renderTrigger = useCallback(() => {
const Item = ( const Item = (
<Button <Button
className={cn('grow', notAllowCustomCredential && 'cursor-not-allowed opacity-50')}
className='grow'
size='small' size='small'
onClick={handleClick}
variant={!authorized ? 'secondary-accent' : 'secondary'} variant={!authorized ? 'secondary-accent' : 'secondary'}
> >
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' /> <RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
{t('common.operation.setup')}
{hasCredential && t('common.operation.config')}
{!hasCredential && t('common.operation.setup')}
</Button> </Button>
) )
if (notAllowCustomCredential) {
if (notAllowCustomCredential && !hasCredential) {
return ( return (
<Tooltip <Tooltip
asChild asChild
) )
} }
return Item return Item
}, [handleClick, authorized, notAllowCustomCredential, t])

if (!hasCredential)
return ButtonComponent
}, [authorized, hasCredential, notAllowCustomCredential, t])


return ( return (
<Authorized <Authorized
provider={provider} provider={provider}
configurationMethod={ConfigurationMethodEnum.predefinedModel} configurationMethod={ConfigurationMethodEnum.predefinedModel}
currentCustomConfigurationModelFixedFields={currentCustomConfigurationModelFixedFields}
items={[ items={[
{ {
title: t('common.modelProvider.auth.apiKeys'), title: t('common.modelProvider.auth.apiKeys'),
credentials: available_credentials ?? [], credentials: available_credentials ?? [],
selectedCredential: {
credential_id: current_credential_id ?? '',
credential_name: current_credential_name ?? '',
},
}, },
]} ]}
selectedCredential={{
credential_id: current_credential_id ?? '',
credential_name: current_credential_name ?? '',
}}
showItemSelectedIcon showItemSelectedIcon
showModelTitle
renderTrigger={renderTrigger}
triggerOnlyOpenModal={!hasCredential && !notAllowCustomCredential}
/> />
) )
} }

+ 115
- 0
web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx Переглянути файл

import {
memo,
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiAddLine,
RiArrowDownSLine,
} from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type { Credential } from '@/app/components/header/account-setting/model-provider-page/declarations'
import CredentialItem from './authorized/credential-item'
import Badge from '@/app/components/base/badge'
import Indicator from '@/app/components/header/indicator'

type CredentialSelectorProps = {
selectedCredential?: Credential & { addNewCredential?: boolean }
credentials: Credential[]
onSelect: (credential: Credential & { addNewCredential?: boolean }) => void
disabled?: boolean
notAllowAddNewCredential?: boolean
}
const CredentialSelector = ({
selectedCredential,
credentials,
onSelect,
disabled,
notAllowAddNewCredential,
}: CredentialSelectorProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const handleSelect = useCallback((credential: Credential & { addNewCredential?: boolean }) => {
setOpen(false)
onSelect(credential)
}, [onSelect])
const handleAddNewCredential = useCallback(() => {
handleSelect({
credential_id: '__add_new_credential',
addNewCredential: true,
credential_name: t('common.modelProvider.auth.addNewModelCredential'),
})
}, [handleSelect, t])

return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
triggerPopupSameWidth
>
<PortalToFollowElemTrigger asChild onClick={() => !disabled && setOpen(v => !v)}>
<div className='system-sm-regular flex h-8 w-full items-center justify-between rounded-lg bg-components-input-bg-normal px-2'>
{
selectedCredential && (
<div className='flex items-center'>
{
!selectedCredential.addNewCredential && <Indicator className='ml-1 mr-2 shrink-0' />
}
<div className='system-sm-regular truncate text-components-input-text-filled' title={selectedCredential.credential_name}>{selectedCredential.credential_name}</div>
{
selectedCredential.from_enterprise && (
<Badge className='shrink-0'>Enterprise</Badge>
)
}
</div>
)
}
{
!selectedCredential && (
<div className='system-sm-regular grow truncate text-components-input-text-placeholder'>{t('common.modelProvider.auth.selectModelCredential')}</div>
)
}
<RiArrowDownSLine className='h-4 w-4 text-text-quaternary' />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[100]'>
<div className='border-ccomponents-panel-border rounded-xl border-[0.5px] bg-components-panel-bg-blur shadow-lg'>
<div className='max-h-[320px] overflow-y-auto p-1'>
{
credentials.map(credential => (
<CredentialItem
key={credential.credential_id}
credential={credential}
disableDelete
disableEdit
disableRename
onItemClick={handleSelect}
showSelectedIcon
selectedCredentialId={selectedCredential?.credential_id}
/>
))
}
</div>
{
!notAllowAddNewCredential && (
<div
className='system-xs-medium flex h-10 cursor-pointer items-center border-t border-t-divider-subtle px-7 text-text-accent-light-mode-only'
onClick={handleAddNewCredential}
>
<RiAddLine className='mr-1 h-4 w-4' />
{t('common.modelProvider.auth.addNewModelCredential')}
</div>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}

export default memo(CredentialSelector)

+ 1
- 1
web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth-service.ts Переглянути файл



export const useGetCredential = (provider: string, isModelCredential?: boolean, credentialId?: string, model?: CustomModel, configFrom?: string) => { export const useGetCredential = (provider: string, isModelCredential?: boolean, credentialId?: string, model?: CustomModel, configFrom?: string) => {
const providerData = useGetProviderCredential(!isModelCredential && !!credentialId, provider, credentialId) const providerData = useGetProviderCredential(!isModelCredential && !!credentialId, provider, credentialId)
const modelData = useGetModelCredential(!!isModelCredential && !!credentialId, provider, credentialId, model?.model, model?.model_type, configFrom)
const modelData = useGetModelCredential(!!isModelCredential && (!!credentialId || !!model), provider, credentialId, model?.model, model?.model_type, configFrom)
return isModelCredential ? modelData : providerData return isModelCredential ? modelData : providerData
} }



+ 70
- 35
web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts Переглянути файл

Credential, Credential,
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
CustomModel, CustomModel,
ModelModalModeEnum,
ModelProvider, ModelProvider,
} from '../../declarations' } from '../../declarations'
import { import {
useModelModalHandler, useModelModalHandler,
useRefreshModel, useRefreshModel,
} from '@/app/components/header/account-setting/model-provider-page/hooks' } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useDeleteModel } from '@/service/use-models'


export const useAuth = ( export const useAuth = (
provider: ModelProvider, provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum, configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
isModelCredential?: boolean,
onUpdate?: () => void,
extra: {
isModelCredential?: boolean,
onUpdate?: (newPayload?: any, formValues?: Record<string, any>) => void,
onRemove?: (credentialId: string) => void,
mode?: ModelModalModeEnum,
} = {},
) => { ) => {
const {
isModelCredential,
onUpdate,
onRemove,
mode,
} = extra
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()
const { const {
getEditCredentialService, getEditCredentialService,
getAddCredentialService, getAddCredentialService,
} = useAuthService(provider.provider) } = useAuthService(provider.provider)
const { mutateAsync: deleteModelService } = useDeleteModel(provider.provider)
const handleOpenModelModal = useModelModalHandler() const handleOpenModelModal = useModelModalHandler()
const { handleRefreshModel } = useRefreshModel() const { handleRefreshModel } = useRefreshModel()
const pendingOperationCredentialId = useRef<string | null>(null) const pendingOperationCredentialId = useRef<string | null>(null)
const pendingOperationModel = useRef<CustomModel | null>(null)
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null) const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
const handleSetDeleteCredentialId = useCallback((credentialId: string | null) => {
setDeleteCredentialId(credentialId)
pendingOperationCredentialId.current = credentialId
}, [])
const pendingOperationModel = useRef<CustomModel | null>(null)
const [deleteModel, setDeleteModel] = useState<CustomModel | null>(null)
const handleSetDeleteModel = useCallback((model: CustomModel | null) => {
setDeleteModel(model)
pendingOperationModel.current = model
}, [])
const openConfirmDelete = useCallback((credential?: Credential, model?: CustomModel) => { const openConfirmDelete = useCallback((credential?: Credential, model?: CustomModel) => {
if (credential) if (credential)
pendingOperationCredentialId.current = credential.credential_id
handleSetDeleteCredentialId(credential.credential_id)
if (model) if (model)
pendingOperationModel.current = model

setDeleteCredentialId(pendingOperationCredentialId.current)
handleSetDeleteModel(model)
}, []) }, [])
const closeConfirmDelete = useCallback(() => { const closeConfirmDelete = useCallback(() => {
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
handleSetDeleteCredentialId(null)
handleSetDeleteModel(null)
}, []) }, [])
const [doingAction, setDoingAction] = useState(false) const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction) const doingActionRef = useRef(doingAction)
type: 'success', type: 'success',
message: t('common.api.actionSuccess'), message: t('common.api.actionSuccess'),
}) })
onUpdate?.()
handleRefreshModel(provider, configurationMethod, undefined) handleRefreshModel(provider, configurationMethod, undefined)
} }
finally { finally {
handleSetDoingAction(false) handleSetDoingAction(false)
} }
}, [getActiveCredentialService, onUpdate, notify, t, handleSetDoingAction])
}, [getActiveCredentialService, notify, t, handleSetDoingAction])
const handleConfirmDelete = useCallback(async () => { const handleConfirmDelete = useCallback(async () => {
if (doingActionRef.current) if (doingActionRef.current)
return return
if (!pendingOperationCredentialId.current) {
setDeleteCredentialId(null)
if (!pendingOperationCredentialId.current && !pendingOperationModel.current) {
closeConfirmDelete()
return return
} }
try { try {
handleSetDoingAction(true) handleSetDoingAction(true)
await getDeleteCredentialService(!!isModelCredential)({
credential_id: pendingOperationCredentialId.current,
model: pendingOperationModel.current?.model,
model_type: pendingOperationModel.current?.model_type,
})
let payload: any = {}
if (pendingOperationCredentialId.current) {
payload = {
credential_id: pendingOperationCredentialId.current,
model: pendingOperationModel.current?.model,
model_type: pendingOperationModel.current?.model_type,
}
await getDeleteCredentialService(!!isModelCredential)(payload)
}
if (!pendingOperationCredentialId.current && pendingOperationModel.current) {
payload = {
model: pendingOperationModel.current.model,
model_type: pendingOperationModel.current.model_type,
}
await deleteModelService(payload)
}
notify({ notify({
type: 'success', type: 'success',
message: t('common.api.actionSuccess'), message: t('common.api.actionSuccess'),
}) })
onUpdate?.()
handleRefreshModel(provider, configurationMethod, undefined) handleRefreshModel(provider, configurationMethod, undefined)
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
pendingOperationModel.current = null
onRemove?.(pendingOperationCredentialId.current ?? '')
closeConfirmDelete()
} }
finally { finally {
handleSetDoingAction(false) handleSetDoingAction(false)
} }
}, [onUpdate, notify, t, handleSetDoingAction, getDeleteCredentialService, isModelCredential])
const handleAddCredential = useCallback((model?: CustomModel) => {
if (model)
pendingOperationModel.current = model
}, [])
}, [notify, t, handleSetDoingAction, getDeleteCredentialService, isModelCredential, closeConfirmDelete, handleRefreshModel, provider, configurationMethod, deleteModelService])
const handleSaveCredential = useCallback(async (payload: Record<string, any>) => { const handleSaveCredential = useCallback(async (payload: Record<string, any>) => {
if (doingActionRef.current) if (doingActionRef.current)
return return


if (res.result === 'success') { if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
onUpdate?.()
handleRefreshModel(provider, configurationMethod, undefined)
} }
} }
finally { finally {
handleSetDoingAction(false) handleSetDoingAction(false)
} }
}, [onUpdate, notify, t, handleSetDoingAction, getEditCredentialService, getAddCredentialService])
}, [notify, t, handleSetDoingAction, getEditCredentialService, getAddCredentialService])
const handleOpenModal = useCallback((credential?: Credential, model?: CustomModel) => { const handleOpenModal = useCallback((credential?: Credential, model?: CustomModel) => {
handleOpenModelModal( handleOpenModelModal(
provider, provider,
configurationMethod, configurationMethod,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
isModelCredential,
credential,
model,
onUpdate,
{
isModelCredential,
credential,
model,
onUpdate,
mode,
},
) )
}, [handleOpenModelModal, provider, configurationMethod, currentCustomConfigurationModelFixedFields, isModelCredential, onUpdate])
}, [
handleOpenModelModal,
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
isModelCredential,
onUpdate,
mode,
])


return { return {
pendingOperationCredentialId, pendingOperationCredentialId,
doingAction, doingAction,
handleActiveCredential, handleActiveCredential,
handleConfirmDelete, handleConfirmDelete,
handleAddCredential,
deleteCredentialId, deleteCredentialId,
deleteModel,
handleSaveCredential, handleSaveCredential,
handleOpenModal, handleOpenModal,
} }

+ 6
- 0
web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-custom-models.ts Переглянути файл



return custom_models || [] return custom_models || []
} }

export const useCanAddedModels = (provider: ModelProvider) => {
const { can_added_models } = provider.custom_configuration

return can_added_models || []
}

+ 27
- 15
web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-model-form-schemas.ts Переглянути файл

import type { import type {
Credential, Credential,
CustomModelCredential, CustomModelCredential,
ModelLoadBalancingConfig,
ModelProvider, ModelProvider,
} from '../../declarations' } from '../../declarations'
import { import {
credentials?: Record<string, any>, credentials?: Record<string, any>,
credential?: Credential, credential?: Credential,
model?: CustomModelCredential, model?: CustomModelCredential,
draftConfig?: ModelLoadBalancingConfig,
) => { ) => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
model_credential_schema, model_credential_schema,
} = provider } = provider
const formSchemas = useMemo(() => { const formSchemas = useMemo(() => {
const modelTypeSchema = genModelTypeFormSchema(supported_model_types)
const modelNameSchema = genModelNameFormSchema(model_credential_schema?.model)
if (!!model) {
modelTypeSchema.disabled = true
modelNameSchema.disabled = true
}
return providerFormSchemaPredefined return providerFormSchemaPredefined
? provider_credential_schema.credential_form_schemas ? provider_credential_schema.credential_form_schemas
: [
modelTypeSchema,
modelNameSchema,
...(draftConfig?.enabled ? [] : model_credential_schema.credential_form_schemas),
]
: model_credential_schema.credential_form_schemas
}, [ }, [
providerFormSchemaPredefined, providerFormSchemaPredefined,
provider_credential_schema?.credential_form_schemas, provider_credential_schema?.credential_form_schemas,
supported_model_types, supported_model_types,
model_credential_schema?.credential_form_schemas, model_credential_schema?.credential_form_schemas,
model_credential_schema?.model, model_credential_schema?.model,
draftConfig?.enabled,
model, model,
]) ])


type: FormTypeEnum.textInput, type: FormTypeEnum.textInput,
variable: '__authorization_name__', variable: '__authorization_name__',
label: t('plugin.auth.authorizationName'), label: t('plugin.auth.authorizationName'),
required: true,
required: false,
} }


return [ return [
return result return result
}, [credentials, credential, model, formSchemas]) }, [credentials, credential, model, formSchemas])


const modelNameAndTypeFormSchemas = useMemo(() => {
if (providerFormSchemaPredefined)
return []

const modelNameSchema = genModelNameFormSchema(model_credential_schema?.model)
const modelTypeSchema = genModelTypeFormSchema(supported_model_types)
return [
modelNameSchema,
modelTypeSchema,
]
}, [supported_model_types, model_credential_schema?.model, providerFormSchemaPredefined])

const modelNameAndTypeFormValues = useMemo(() => {
let result = {}
if (providerFormSchemaPredefined)
return result

if (model)
result = { ...result, __model_name: model?.model, __model_type: model?.model_type }

return result
}, [model, providerFormSchemaPredefined])

return { return {
formSchemas: formSchemasWithAuthorizationName, formSchemas: formSchemasWithAuthorizationName,
formValues, formValues,
modelNameAndTypeFormSchemas,
modelNameAndTypeFormValues,
} }
} }

+ 2
- 0
web/app/components/header/account-setting/model-provider-page/model-auth/index.tsx Переглянути файл

export { default as AddCustomModel } from './add-custom-model' export { default as AddCustomModel } from './add-custom-model'
export { default as ConfigProvider } from './config-provider' export { default as ConfigProvider } from './config-provider'
export { default as ConfigModel } from './config-model' export { default as ConfigModel } from './config-model'
export { default as ManageCustomModelCredentials } from './manage-custom-model-credentials'
export { default as CredentialSelector } from './credential-selector'

+ 82
- 0
web/app/components/header/account-setting/model-provider-page/model-auth/manage-custom-model-credentials.tsx Переглянути файл

import {
memo,
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
Button,
} from '@/app/components/base/button'
import type {
CustomConfigurationModelFixedFields,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
ConfigurationMethodEnum,
ModelModalModeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import Authorized from './authorized'
import {
useCustomModels,
} from './hooks'
import cn from '@/utils/classnames'

type ManageCustomModelCredentialsProps = {
provider: ModelProvider,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
}
const ManageCustomModelCredentials = ({
provider,
currentCustomConfigurationModelFixedFields,
}: ManageCustomModelCredentialsProps) => {
const { t } = useTranslation()
const customModels = useCustomModels(provider)
const noModels = !customModels.length

const renderTrigger = useCallback((open?: boolean) => {
const Item = (
<Button
variant='ghost'
size='small'
className={cn(
'mr-0.5 text-text-tertiary',
open && 'bg-components-button-ghost-bg-hover',
)}
>
{t('common.modelProvider.auth.manageCredentials')}
</Button>
)
return Item
}, [t])

if (noModels)
return null

return (
<Authorized
provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel}
currentCustomConfigurationModelFixedFields={currentCustomConfigurationModelFixedFields}
items={customModels.map(model => ({
model,
credentials: model.available_model_credentials ?? [],
selectedCredential: model.current_credential_id ? {
credential_id: model.current_credential_id,
credential_name: model.current_credential_name,
} : undefined,
}))}
renderTrigger={renderTrigger}
authParams={{
isModelCredential: true,
mode: ModelModalModeEnum.configModelCredential,
}}
hideAddAction
disableItemClick
popupTitle={t('common.modelProvider.auth.customModelCredentials')}
showModelTitle
disableDeleteButShowAction
disableDeleteTip={t('common.modelProvider.auth.customModelCredentialsDeleteTip')}
/>
)
}

export default memo(ManageCustomModelCredentials)

+ 20
- 12
web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx Переглянути файл

CustomModel, CustomModel,
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Badge from '@/app/components/base/badge' import Badge from '@/app/components/base/badge'
credentials?: Credential[] credentials?: Credential[]
customModelCredential?: Credential customModelCredential?: Credential
setCustomModelCredential: Dispatch<SetStateAction<Credential | undefined>> setCustomModelCredential: Dispatch<SetStateAction<Credential | undefined>>
onUpdate?: (payload?: any, formValues?: Record<string, any>) => void
onRemove?: (credentialId: string) => void
} }
const SwitchCredentialInLoadBalancing = ({ const SwitchCredentialInLoadBalancing = ({
provider, provider,
customModelCredential, customModelCredential,
setCustomModelCredential, setCustomModelCredential,
credentials, credentials,
onUpdate,
onRemove,
}: SwitchCredentialInLoadBalancingProps) => { }: SwitchCredentialInLoadBalancingProps) => {
const { t } = useTranslation() const { t } = useTranslation()


<Authorized <Authorized
provider={provider} provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel} configurationMethod={ConfigurationMethodEnum.customizableModel}
currentCustomConfigurationModelFixedFields={model ? {
__model_name: model.model,
__model_type: model.model_type,
} : undefined}
authParams={{
isModelCredential: true,
mode: ModelModalModeEnum.configModelCredential,
onUpdate,
onRemove,
}}
items={[ items={[
{ {
title: t('common.modelProvider.auth.modelCredentials'),
model, model,
credentials: credentials || [], credentials: credentials || [],
selectedCredential: customModelCredential ? {
credential_id: customModelCredential?.credential_id || '',
credential_name: customModelCredential?.credential_name || '',
} : undefined,
}, },
]} ]}
renderTrigger={renderTrigger} renderTrigger={renderTrigger}
onItemClick={handleItemClick} onItemClick={handleItemClick}
isModelCredential
enableAddModelCredential enableAddModelCredential
bottomAddModelCredentialText={t('common.modelProvider.auth.addModelCredential')}
selectedCredential={
customModelCredential
? {
credential_id: customModelCredential?.credential_id || '',
credential_name: customModelCredential?.credential_name || '',
}
: undefined
}
showItemSelectedIcon showItemSelectedIcon
popupTitle={t('common.modelProvider.auth.modelCredentials')}
/> />
) )
} }

+ 239
- 119
web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx Переглянути файл

useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState,
} from 'react' } from 'react'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
ConfigurationMethodEnum, ConfigurationMethodEnum,
FormTypeEnum, FormTypeEnum,
ModelModalModeEnum,
} from '../declarations' } from '../declarations'
import { import {
useLanguage, useLanguage,
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon' import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
import Badge from '@/app/components/base/badge' import Badge from '@/app/components/base/badge'
import { useRenderI18nObject } from '@/hooks/use-i18n' import { useRenderI18nObject } from '@/hooks/use-i18n'
import { CredentialSelector } from '../model-auth'


type ModelModalProps = { type ModelModalProps = {
provider: ModelProvider provider: ModelProvider
configurateMethod: ConfigurationMethodEnum configurateMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
onCancel: () => void onCancel: () => void
onSave: () => void
onSave: (formValues?: Record<string, any>) => void
onRemove: (formValues?: Record<string, any>) => void
model?: CustomModel model?: CustomModel
credential?: Credential credential?: Credential
isModelCredential?: boolean isModelCredential?: boolean
mode?: ModelModalModeEnum
} }


const ModelModal: FC<ModelModalProps> = ({ const ModelModal: FC<ModelModalProps> = ({
model, model,
credential, credential,
isModelCredential, isModelCredential,
mode = ModelModalModeEnum.configProviderCredential,
}) => { }) => {
const renderI18nObject = useRenderI18nObject() const renderI18nObject = useRenderI18nObject()
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
closeConfirmDelete, closeConfirmDelete,
openConfirmDelete, openConfirmDelete,
doingAction, doingAction,
} = useAuth(provider, configurateMethod, currentCustomConfigurationModelFixedFields, isModelCredential, onSave)
handleActiveCredential,
} = useAuth(
provider,
configurateMethod,
currentCustomConfigurationModelFixedFields,
{
isModelCredential,
mode,
},
)
const { const {
credentials: formSchemasValue, credentials: formSchemasValue,
available_credentials,
} = credentialData as any } = credentialData as any


const { isCurrentWorkspaceManager } = useAppContext() const { isCurrentWorkspaceManager } = useAppContext()
const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager
const { t } = useTranslation() const { t } = useTranslation()
const language = useLanguage() const language = useLanguage()
const { const {
formSchemas, formSchemas,
formValues, formValues,
modelNameAndTypeFormSchemas,
modelNameAndTypeFormValues,
} = useModelFormSchemas(provider, providerFormSchemaPredefined, formSchemasValue, credential, model) } = useModelFormSchemas(provider, providerFormSchemaPredefined, formSchemasValue, credential, model)
const formRef = useRef<FormRefObject>(null)
const formRef1 = useRef<FormRefObject>(null)
const [selectedCredential, setSelectedCredential] = useState<Credential & { addNewCredential?: boolean } | undefined>()
const formRef2 = useRef<FormRefObject>(null)
const isEditMode = !!Object.keys(formValues).filter((key) => {
return key !== '__model_name' && key !== '__model_type'
}).length && isCurrentWorkspaceManager


const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
if (mode === ModelModalModeEnum.addCustomModelToModelList && selectedCredential && !selectedCredential?.addNewCredential) {
handleActiveCredential(selectedCredential, model)
onCancel()
return
}

let modelNameAndTypeIsCheckValidated = true
let modelNameAndTypeValues: Record<string, any> = {}

if (mode === ModelModalModeEnum.configCustomModel) {
const formResult = formRef1.current?.getFormValues({
needCheckValidatedValues: true,
}) || { isCheckValidated: false, values: {} }
modelNameAndTypeIsCheckValidated = formResult.isCheckValidated
modelNameAndTypeValues = formResult.values
}

if (mode === ModelModalModeEnum.configModelCredential && model) {
modelNameAndTypeValues = {
__model_name: model.model,
__model_type: model.model_type,
}
}

if (mode === ModelModalModeEnum.addCustomModelToModelList && selectedCredential?.addNewCredential && model) {
modelNameAndTypeValues = {
__model_name: model.model,
__model_type: model.model_type,
}
}
const { const {
isCheckValidated, isCheckValidated,
values, values,
} = formRef.current?.getFormValues({
} = formRef2.current?.getFormValues({
needCheckValidatedValues: true, needCheckValidatedValues: true,
needTransformWhenSecretFieldIsPristine: true, needTransformWhenSecretFieldIsPristine: true,
}) || { isCheckValidated: false, values: {} } }) || { isCheckValidated: false, values: {} }
if (!isCheckValidated)
if (!isCheckValidated || !modelNameAndTypeIsCheckValidated)
return return


const { const {
__authorization_name__,
__model_name, __model_name,
__model_type, __model_type,
} = modelNameAndTypeValues
const {
__authorization_name__,
...rest ...rest
} = values } = values
if (__model_name && __model_type) {
handleSaveCredential({
if (__model_name && __model_type && __authorization_name__) {
await handleSaveCredential({
credential_id: credential?.credential_id, credential_id: credential?.credential_id,
credentials: rest, credentials: rest,
name: __authorization_name__, name: __authorization_name__,
}) })
} }
else { else {
handleSaveCredential({
await handleSaveCredential({
credential_id: credential?.credential_id, credential_id: credential?.credential_id,
credentials: rest, credentials: rest,
name: __authorization_name__, name: __authorization_name__,
}) })
} }
}, [handleSaveCredential, credential?.credential_id, model])
onSave(values)
}, [handleSaveCredential, credential?.credential_id, model, onSave, mode, selectedCredential, handleActiveCredential])


const modalTitle = useMemo(() => { const modalTitle = useMemo(() => {
if (!providerFormSchemaPredefined && !model) {
return (
<div className='flex items-center'>
<ModelIcon
className='mr-2 h-10 w-10 shrink-0'
iconClassName='h-10 w-10'
provider={provider}
/>
<div>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t('common.modelProvider.auth.apiKeyModal.addModel')}</div>
<div className='system-md-semibold text-text-primary'>{renderI18nObject(provider.label)}</div>
</div>
</div>
)
}
let label = t('common.modelProvider.auth.apiKeyModal.title') let label = t('common.modelProvider.auth.apiKeyModal.title')


if (model)
label = t('common.modelProvider.auth.addModelCredential')
if (mode === ModelModalModeEnum.configCustomModel || mode === ModelModalModeEnum.addCustomModelToModelList)
label = t('common.modelProvider.auth.addModel')
if (mode === ModelModalModeEnum.configModelCredential) {
if (credential)
label = t('common.modelProvider.auth.editModelCredential')
else
label = t('common.modelProvider.auth.addModelCredential')
}


return ( return (
<div className='title-2xl-semi-bold text-text-primary'> <div className='title-2xl-semi-bold text-text-primary'>
{label} {label}
</div> </div>
) )
}, [providerFormSchemaPredefined, t, model, renderI18nObject])
}, [t, mode, credential])


const modalDesc = useMemo(() => { const modalDesc = useMemo(() => {
if (providerFormSchemaPredefined) { if (providerFormSchemaPredefined) {
}, [providerFormSchemaPredefined, t]) }, [providerFormSchemaPredefined, t])


const modalModel = useMemo(() => { const modalModel = useMemo(() => {
if (model) {
if (mode === ModelModalModeEnum.configCustomModel) {
return (
<div className='mt-2 flex items-center'>
<ModelIcon
className='mr-2 h-4 w-4 shrink-0'
provider={provider}
/>
<div className='system-md-regular mr-1 text-text-secondary'>{renderI18nObject(provider.label)}</div>
</div>
)
}
if (model && (mode === ModelModalModeEnum.configModelCredential || mode === ModelModalModeEnum.addCustomModelToModelList)) {
return ( return (
<div className='mt-2 flex items-center'> <div className='mt-2 flex items-center'>
<ModelIcon <ModelIcon
} }


return null return null
}, [model, provider])
}, [model, provider, mode, renderI18nObject])

const showCredentialLabel = useMemo(() => {
if (mode === ModelModalModeEnum.configCustomModel)
return true
if (mode === ModelModalModeEnum.addCustomModelToModelList)
return selectedCredential?.addNewCredential
}, [mode, selectedCredential])
const showCredentialForm = useMemo(() => {
if (mode !== ModelModalModeEnum.addCustomModelToModelList)
return true
return selectedCredential?.addNewCredential
}, [mode, selectedCredential])
const saveButtonText = useMemo(() => {
if (mode === ModelModalModeEnum.addCustomModelToModelList || mode === ModelModalModeEnum.configCustomModel)
return t('common.operation.add')
return t('common.operation.save')
}, [mode, t])

const handleDeleteCredential = useCallback(() => {
handleConfirmDelete()
onCancel()
}, [handleConfirmDelete])

const handleModelNameAndTypeChange = useCallback((field: string, value: any) => {
const {
getForm,
} = formRef2.current as FormRefObject || {}
if (getForm())
getForm()?.setFieldValue(field, value)
}, [])
const notAllowCustomCredential = provider.allow_custom_token === false


useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
> >
<RiCloseLine className='h-4 w-4 text-text-tertiary' /> <RiCloseLine className='h-4 w-4 text-text-tertiary' />
</div> </div>
<div className='px-6 pt-6'>
<div className='pb-3'>
{modalTitle}
{modalDesc}
{modalModel}
</div>

<div className='max-h-[calc(100vh-320px)] overflow-y-auto'>
{
isLoading && (
<div className='flex items-center justify-center'>
<Loading />
</div>
<div className='p-6 pb-3'>
{modalTitle}
{modalDesc}
{modalModel}
</div>
<div className='max-h-[calc(100vh-320px)] overflow-y-auto px-6 py-3'>
{
mode === ModelModalModeEnum.configCustomModel && (
<AuthForm
formSchemas={modelNameAndTypeFormSchemas.map((formSchema) => {
return {
...formSchema,
name: formSchema.variable,
}
}) as FormSchema[]}
defaultValues={modelNameAndTypeFormValues}
inputClassName='justify-start'
ref={formRef1}
onChange={handleModelNameAndTypeChange}
/>
)
}
{
mode === ModelModalModeEnum.addCustomModelToModelList && (
<CredentialSelector
credentials={available_credentials || []}
onSelect={setSelectedCredential}
selectedCredential={selectedCredential}
disabled={isLoading}
notAllowAddNewCredential={notAllowCustomCredential}
/>
)
}
{
showCredentialLabel && (
<div className='system-xs-medium-uppercase mb-3 mt-6 flex items-center text-text-tertiary'>
{t('common.modelProvider.auth.modelCredential')}
<div className='ml-2 h-px grow bg-gradient-to-r from-divider-regular to-background-gradient-mask-transparent' />
</div>
)
}
{
isLoading && (
<div className='mt-3 flex items-center justify-center'>
<Loading />
</div>
)
}
{
!isLoading
&& showCredentialForm
&& (
<AuthForm
formSchemas={formSchemas.map((formSchema) => {
return {
...formSchema,
name: formSchema.variable,
showRadioUI: formSchema.type === FormTypeEnum.radio,
}
}) as FormSchema[]}
defaultValues={formValues}
inputClassName='justify-start'
ref={formRef2}
/>
)
}
</div>
<div className='flex justify-between p-6 pt-5'>
{
(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='system-xs-regular mt-2 inline-flex items-center text-text-accent'
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 h-3 w-3' />
</a>
) )
}
: <div />
}
<div className='flex items-center justify-end space-x-2'>
{ {
!isLoading && (
<AuthForm
formSchemas={formSchemas.map((formSchema) => {
return {
...formSchema,
name: formSchema.variable,
showRadioUI: formSchema.type === FormTypeEnum.radio,
}
}) as FormSchema[]}
defaultValues={formValues}
inputClassName='justify-start'
ref={formRef}
/>
isEditMode && (
<Button
variant='warning'
onClick={() => openConfirmDelete(credential, model)}
>
{t('common.operation.remove')}
</Button>
) )
} }
</div>

<div className='sticky bottom-0 -mx-2 mt-2 flex flex-wrap items-center justify-between gap-y-2 bg-components-panel-bg px-2 pb-6 pt-4'>
{
(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 h-3 w-3' />
</a>
)
: <div />
}
<div>
{
isEditMode && (
<Button
variant='warning'
size='large'
className='mr-2'
onClick={() => openConfirmDelete(credential, model)}
>
{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={isLoading || doingAction}
>
{t('common.operation.save')}
</Button>
</div>
</div>
</div>
<div className='border-t-[0.5px] border-t-divider-regular'>
<div className='flex items-center justify-center rounded-b-2xl bg-background-section-burn py-3 text-xs text-text-tertiary'>
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
{t('common.modelProvider.encrypted.front')}
<a
className='mx-1 text-text-accent'
target='_blank' rel='noopener noreferrer'
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
<Button
onClick={onCancel}
>
{t('common.operation.cancel')}
</Button>
<Button
variant='primary'
onClick={handleSave}
disabled={isLoading || doingAction}
> >
PKCS1_OAEP
</a>
{t('common.modelProvider.encrypted.back')}
{saveButtonText}
</Button>
</div> </div>
</div> </div>
{
(mode === ModelModalModeEnum.configCustomModel || mode === ModelModalModeEnum.configProviderCredential) && (
<div className='border-t-[0.5px] border-t-divider-regular'>
<div className='flex items-center justify-center rounded-b-2xl bg-background-section-burn py-3 text-xs text-text-tertiary'>
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
{t('common.modelProvider.encrypted.front')}
<a
className='mx-1 text-text-accent'
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> </div>
{ {
deleteCredentialId && ( deleteCredentialId && (
title={t('common.modelProvider.confirmDelete')} title={t('common.modelProvider.confirmDelete')}
isDisabled={doingAction} isDisabled={doingAction}
onCancel={closeConfirmDelete} onCancel={closeConfirmDelete}
onConfirm={handleConfirmDelete}
onConfirm={handleDeleteCredential}
/> />
) )
} }

+ 0
- 1
web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx Переглянути файл

<div className='flex items-center gap-0.5'> <div className='flex items-center gap-0.5'>
<ConfigProvider <ConfigProvider
provider={provider} provider={provider}
configurationMethod={ConfigurationMethodEnum.predefinedModel}
/> />
{ {
systemConfig.enabled && isCustomConfigured && ( systemConfig.enabled && isCustomConfigured && (

+ 15
- 5
web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx Переглянути файл

import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { AddCustomModel } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import {
AddCustomModel,
ManageCustomModelCredentials,
} from '@/app/components/header/account-setting/model-provider-page/model-auth'


export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST' export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
type ProviderAddedCardProps = { type ProviderAddedCardProps = {
)} )}
{ {
configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && isCurrentWorkspaceManager && ( configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && isCurrentWorkspaceManager && (
<AddCustomModel
provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel}
/>
<div className='flex grow justify-end'>
<ManageCustomModelCredentials
provider={provider}
currentCustomConfigurationModelFixedFields={undefined}
/>
<AddCustomModel
provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel}
currentCustomConfigurationModelFixedFields={undefined}
/>
</div>
) )
} }
</div> </div>

+ 8
- 1
web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx Переглянути файл

import ModelListItem from './model-list-item' import ModelListItem from './model-list-item'
import { useModalContextSelector } from '@/context/modal-context' import { useModalContextSelector } from '@/context/modal-context'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { AddCustomModel } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import {
AddCustomModel,
ManageCustomModelCredentials,
} from '@/app/components/header/account-setting/model-provider-page/model-auth'


type ModelListProps = { type ModelListProps = {
provider: ModelProvider provider: ModelProvider
{ {
isConfigurable && isCurrentWorkspaceManager && ( isConfigurable && isCurrentWorkspaceManager && (
<div className='flex grow justify-end'> <div className='flex grow justify-end'>
<ManageCustomModelCredentials
provider={provider}
currentCustomConfigurationModelFixedFields={undefined}
/>
<AddCustomModel <AddCustomModel
provider={provider} provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel} configurationMethod={ConfigurationMethodEnum.customizableModel}

+ 26
- 35
web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx Переглянути файл

import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiDeleteBinLine,
RiEqualizer2Line,
RiIndeterminateCircleLine,
} from '@remixicon/react' } from '@remixicon/react'
import type { import type {
Credential, Credential,
import { useProviderContextSelector } from '@/context/provider-context' import { useProviderContextSelector } from '@/context/provider-context'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import { AddCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth' import { AddCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import { useModelModalHandler } from '@/app/components/header/account-setting/model-provider-page/hooks'
import Badge from '@/app/components/base/badge/index' import Badge from '@/app/components/base/badge/index'


export type ModelLoadBalancingConfigsProps = { export type ModelLoadBalancingConfigsProps = {
withSwitch?: boolean withSwitch?: boolean
className?: string className?: string
modelCredential: ModelCredential modelCredential: ModelCredential
onUpdate?: () => void
onUpdate?: (payload?: any, formValues?: Record<string, any>) => void
onRemove?: (credentialId: string) => void
model: CustomModelCredential model: CustomModelCredential
} }


className, className,
modelCredential, modelCredential,
onUpdate, onUpdate,
onRemove,
}: ModelLoadBalancingConfigsProps) => { }: ModelLoadBalancingConfigsProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const providerFormSchemaPredefined = configurationMethod === ConfigurationMethodEnum.predefinedModel const providerFormSchemaPredefined = configurationMethod === ConfigurationMethodEnum.predefinedModel
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled) const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
const handleOpenModal = useModelModalHandler()


const updateConfigEntry = useCallback( const updateConfigEntry = useCallback(
( (
return draftConfig.configs return draftConfig.configs
}, [draftConfig]) }, [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) if (!draftConfig)
return null return null


</Tooltip> </Tooltip>
)} )}
</div> </div>
<div className='mr-1 text-[13px]'>
<div className='mr-1 text-[13px] text-text-secondary'>
{isProviderManaged ? t('common.modelProvider.defaultConfig') : config.name} {isProviderManaged ? t('common.modelProvider.defaultConfig') : config.name}
</div> </div>
{isProviderManaged && providerFormSchemaPredefined && ( {isProviderManaged && providerFormSchemaPredefined && (
{!isProviderManaged && ( {!isProviderManaged && (
<> <>
<div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'> <div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
{
config.credential_id && !credential?.not_allowed_to_use && !credential?.from_enterprise && (
<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={() => {
handleOpenModal(
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
configurationMethod === ConfigurationMethodEnum.customizableModel,
(config.credential_id && config.name) ? {
credential_id: config.credential_id,
credential_name: config.name,
} : undefined,
model,
)
}}
>
<RiEqualizer2Line className='h-4 w-4' />
</span>
)
}
<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)}
>
<RiDeleteBinLine className='h-4 w-4' />
</span>
<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> </div>
</> </>
)} )}
configurationMethod={configurationMethod} configurationMethod={configurationMethod}
modelCredential={modelCredential} modelCredential={modelCredential}
onSelectCredential={addConfigEntry} onSelectCredential={addConfigEntry}
onUpdate={onUpdate}
onUpdate={handleUpdate}
onRemove={handleRemove}
/> />
</div> </div>
)} )}

+ 213
- 104
web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx Переглянути файл

import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { import type {
Credential, Credential,
CustomConfigurationModelFixedFields,
ModelItem, ModelItem,
ModelLoadBalancingConfig, ModelLoadBalancingConfig,
ModelLoadBalancingConfigEntry, ModelLoadBalancingConfigEntry,
useGetModelCredential, useGetModelCredential,
useUpdateModelLoadBalancingConfig, useUpdateModelLoadBalancingConfig,
} from '@/service/use-models' } 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 = { export type ModelLoadBalancingModalProps = {
provider: ModelProvider provider: ModelProvider
configurateMethod: ConfigurationMethodEnum configurateMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
model: ModelItem model: ModelItem
credential?: Credential credential?: Credential
open?: boolean open?: boolean
const ModelLoadBalancingModal = ({ const ModelLoadBalancingModal = ({
provider, provider,
configurateMethod, configurateMethod,
currentCustomConfigurationModelFixedFields,
model, model,
credential, credential,
open = false, open = false,
}: ModelLoadBalancingModalProps) => { }: ModelLoadBalancingModalProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()

const {
doingAction,
deleteModel,
openConfirmDelete,
closeConfirmDelete,
handleConfirmDelete,
} = useAuth(
provider,
configurateMethod,
currentCustomConfigurationModelFixedFields,
{
isModelCredential: true,
},
)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
const configFrom = providerFormSchemaPredefined ? 'predefined-model' : 'custom-model' const configFrom = providerFormSchemaPredefined ? 'predefined-model' : 'custom-model'
} }
}, [current_credential_id, current_credential_name]) }, [current_credential_id, current_credential_name])
const [customModelCredential, setCustomModelCredential] = useState<Credential | undefined>(initialCustomModelCredential) const [customModelCredential, setCustomModelCredential] = useState<Credential | undefined>(initialCustomModelCredential)
const { handleRefreshModel } = useRefreshModel()
const handleSave = async () => { const handleSave = async () => {
try { try {
setLoading(true) setLoading(true)
) )
if (res.result === 'success') { if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
handleRefreshModel(provider, configurateMethod, currentCustomConfigurationModelFixedFields)
onSave?.(provider.provider) onSave?.(provider.provider)
onClose?.() onClose?.()
} }
setLoading(false) setLoading(false)
} }
} }
const handleDeleteModel = useCallback(async () => {
await handleConfirmDelete()
onClose?.()
}, [handleConfirmDelete, 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>
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,
}],
}
})
} }
>
{!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>
}
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>
</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 && ( !providerFormSchemaPredefined && (
<SwitchCredentialInLoadBalancing
provider={provider}
customModelCredential={initialCustomModelCredential ?? customModelCredential}
setCustomModelCredential={setCustomModelCredential}
model={model}
credentials={available_credentials}
/>
<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>
<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> </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: refetch,
model: {
model: model.model,
model_type: model.model_type,
},
}} />
)
}
</div>

<div className='mt-6 flex items-center justify-end gap-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>
</>
</>
)
}
</Modal >
{
deleteModel && (
<Confirm
isShow
title={t('common.modelProvider.confirmDelete')}
onCancel={closeConfirmDelete}
onConfirm={handleDeleteModel}
isDisabled={doingAction}
/>
) )
} }
</Modal >
</>
) )
} }



+ 1
- 1
web/app/components/header/account-setting/model-provider-page/utils.ts Переглянути файл



export const genModelTypeFormSchema = (modelTypes: ModelTypeEnum[]) => { export const genModelTypeFormSchema = (modelTypes: ModelTypeEnum[]) => {
return { return {
type: FormTypeEnum.radio,
type: FormTypeEnum.select,
label: { label: {
zh_Hans: '模型类型', zh_Hans: '模型类型',
en_US: 'Model Type', en_US: 'Model Type',

+ 14
- 9
web/context/modal-context.tsx Переглянути файл

Credential, Credential,
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
CustomModel, CustomModel,
ModelLoadBalancingConfigEntry,
ModelProvider, ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { import {
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal' import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal'
import type { ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'


const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), { const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), {
ssr: false, ssr: false,
export type ModalState<T> = { export type ModalState<T> = {
payload: T payload: T
onCancelCallback?: () => void onCancelCallback?: () => void
onSaveCallback?: (newPayload: T) => void
onRemoveCallback?: (newPayload: T) => void
onSaveCallback?: (newPayload?: T, formValues?: Record<string, any>) => void
onRemoveCallback?: (newPayload?: T, formValues?: Record<string, any>) => void
onEditCallback?: (newPayload: T) => void onEditCallback?: (newPayload: T) => void
onValidateBeforeSaveCallback?: (newPayload: T) => boolean onValidateBeforeSaveCallback?: (newPayload: T) => boolean
isEditMode?: boolean isEditMode?: boolean
isModelCredential?: boolean isModelCredential?: boolean
credential?: Credential credential?: Credential
model?: CustomModel model?: CustomModel
}
export type LoadBalancingEntryModalType = ModelModalType & {
entry?: ModelLoadBalancingConfigEntry
index?: number
mode?: ModelModalModeEnum
} }


export type ModalContextState = { export type ModalContextState = {
showModelModal.onCancelCallback() showModelModal.onCancelCallback()
}, [showModelModal]) }, [showModelModal])


const handleSaveModelModal = useCallback(() => {
const handleSaveModelModal = useCallback((formValues?: Record<string, any>) => {
if (showModelModal?.onSaveCallback) if (showModelModal?.onSaveCallback)
showModelModal.onSaveCallback(showModelModal.payload)
showModelModal.onSaveCallback(showModelModal.payload, formValues)
setShowModelModal(null)
}, [showModelModal])

const handleRemoveModelModal = useCallback((formValues?: Record<string, any>) => {
if (showModelModal?.onRemoveCallback)
showModelModal.onRemoveCallback(showModelModal.payload, formValues)
setShowModelModal(null) setShowModelModal(null)
}, [showModelModal]) }, [showModelModal])


isModelCredential={showModelModal.payload.isModelCredential} isModelCredential={showModelModal.payload.isModelCredential}
credential={showModelModal.payload.credential} credential={showModelModal.payload.credential}
model={showModelModal.payload.model} model={showModelModal.payload.model}
mode={showModelModal.payload.mode}
onCancel={handleCancelModelModal} onCancel={handleCancelModelModal}
onSave={handleSaveModelModal} onSave={handleSaveModelModal}
onRemove={handleRemoveModelModal}
/> />
) )
} }

+ 9
- 0
web/i18n/en-US/common.ts Переглянути файл

authRemoved: 'Auth removed', authRemoved: 'Auth removed',
apiKeys: 'API Keys', apiKeys: 'API Keys',
addApiKey: 'Add API Key', addApiKey: 'Add API Key',
addModel: 'Add model',
addNewModel: 'Add new model', addNewModel: 'Add new model',
addCredential: 'Add credential', addCredential: 'Add credential',
addModelCredential: 'Add model credential', addModelCredential: 'Add model credential',
editModelCredential: 'Edit model credential',
modelCredentials: 'Model credentials', modelCredentials: 'Model credentials',
modelCredential: 'Model credential',
configModel: 'Config model', configModel: 'Config model',
configLoadBalancing: 'Config Load Balancing', configLoadBalancing: 'Config Load Balancing',
authorizationError: 'Authorization error', authorizationError: 'Authorization error',
desc: 'After configuring credentials, all members within the workspace can use this model when orchestrating applications.', desc: 'After configuring credentials, all members within the workspace can use this model when orchestrating applications.',
addModel: 'Add model', addModel: 'Add model',
}, },
manageCredentials: 'Manage Credentials',
customModelCredentials: 'Custom Model Credentials',
addNewModelCredential: 'Add new model credential',
removeModel: 'Remove Model',
selectModelCredential: 'Select a model credential',
customModelCredentialsDeleteTip: 'Credential is in use and cannot be deleted',
}, },
}, },
dataSource: { dataSource: {

+ 9
- 0
web/i18n/zh-Hans/common.ts Переглянути файл

authRemoved: '授权已移除', authRemoved: '授权已移除',
apiKeys: 'API 密钥', apiKeys: 'API 密钥',
addApiKey: '添加 API 密钥', addApiKey: '添加 API 密钥',
addModel: '添加模型',
addNewModel: '添加新模型', addNewModel: '添加新模型',
addCredential: '添加凭据', addCredential: '添加凭据',
addModelCredential: '添加模型凭据', addModelCredential: '添加模型凭据',
editModelCredential: '编辑模型凭据',
modelCredentials: '模型凭据', modelCredentials: '模型凭据',
modelCredential: '模型凭据',
configModel: '配置模型', configModel: '配置模型',
configLoadBalancing: '配置负载均衡', configLoadBalancing: '配置负载均衡',
authorizationError: '授权错误', authorizationError: '授权错误',
desc: '配置凭据后,工作空间中的所有成员都可以在编排应用时使用此模型。', desc: '配置凭据后,工作空间中的所有成员都可以在编排应用时使用此模型。',
addModel: '添加模型', addModel: '添加模型',
}, },
manageCredentials: '管理凭据',
customModelCredentials: '自定义模型凭据',
addNewModelCredential: '添加模型新凭据',
removeModel: '移除模型',
selectModelCredential: '选择模型凭据',
customModelCredentialsDeleteTip: '模型凭据正在使用中,无法删除',
}, },
}, },
dataSource: { dataSource: {

+ 1
- 1
web/service/use-models.ts Переглянути файл

mutationFn: (data: { mutationFn: (data: {
model: string model: string
model_type: ModelTypeEnum model_type: ModelTypeEnum
}) => del<{ result: string }>(`/workspaces/current/model-providers/${provider}/models/credentials`, {
}) => del<{ result: string }>(`/workspaces/current/model-providers/${provider}/models`, {
body: data, body: data,
}), }),
}) })

Завантаження…
Відмінити
Зберегти