| 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 && ( |
| 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 |
| 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', | |||||
| } |
| 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) | |||||
| }, | }, | ||||
| }) | }) | ||||
| } | } |
| 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') : ''} | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } |
| 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> | |||||
| ) | ) | ||||
| } | } | ||||
| 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} | |||||
| /> | /> | ||||
| )) | )) | ||||
| } | } |
| 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> | ||||
| ) | ) |
| 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()} |
| 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' /> |
| 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} | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } |
| 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) |
| 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 | ||||
| } | } | ||||
| 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, | ||||
| } | } |
| return custom_models || [] | return custom_models || [] | ||||
| } | } | ||||
| export const useCanAddedModels = (provider: ModelProvider) => { | |||||
| const { can_added_models } = provider.custom_configuration | |||||
| return can_added_models || [] | |||||
| } |
| 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, | |||||
| } | } | ||||
| } | } |
| 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' |
| 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) |
| 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')} | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } |
| 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} | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } |
| <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 && ( |
| 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> |
| 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} |
| 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> | ||||
| )} | )} |
| 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 > | |||||
| </> | |||||
| ) | ) | ||||
| } | } | ||||
| 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', |
| 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} | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } |
| 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: { |
| 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: { |
| 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, | ||||
| }), | }), | ||||
| }) | }) |