This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
import {
memo,
useCallback,
} from 'react'
import { RiAddLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { Authorized } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import cn from '@/utils/classnames'
import type {
Credential,
CustomConfigurationModelFixedFields,
CustomModelCredential,
ModelCredential,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
type AddCredentialInLoadBalancingProps = {
provider: ModelProvider
model: CustomModelCredential
configurationMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
modelCredential: ModelCredential
onSelectCredential: (credential: Credential) => void
onUpdate?: (payload?: any, formValues?: Record<string, any>) => void
onRemove?: (credentialId: string) => void
}
const AddCredentialInLoadBalancing = ({
provider,
model,
configurationMethod,
modelCredential,
onSelectCredential,
onUpdate,
onRemove,
}: AddCredentialInLoadBalancingProps) => {
const { t } = useTranslation()
const {
available_credentials,
} = modelCredential
const isCustomModel = configurationMethod === ConfigurationMethodEnum.customizableModel
const notAllowCustomCredential = provider.allow_custom_token === false
const handleUpdate = useCallback((payload?: any, formValues?: Record<string, any>) => {
onUpdate?.(payload, formValues)
}, [onUpdate])
const renderTrigger = useCallback((open?: boolean) => {
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',
open && 'bg-state-base-hover',
)}>
<RiAddLine className='mr-2 h-4 w-4' />
{t('common.modelProvider.auth.addCredential')}
</div>
)
return Item
}, [t, isCustomModel])
return (
<Authorized
provider={provider}
renderTrigger={renderTrigger}
authParams={{
isModelCredential: isCustomModel,
mode: ModelModalModeEnum.configModelCredential,
onUpdate: handleUpdate,
onRemove,
}}
triggerOnlyOpenModal={!available_credentials?.length && !notAllowCustomCredential}
items={[
{
title: isCustomModel ? '' : t('common.modelProvider.auth.apiKeys'),
model: isCustomModel ? model : undefined,
credentials: available_credentials ?? [],
},
]}
showModelTitle={!isCustomModel}
configurationMethod={configurationMethod}
currentCustomConfigurationModelFixedFields={isCustomModel ? {
__model_name: model.model,
__model_type: model.model_type,
} : undefined}
onItemClick={onSelectCredential}
placement='bottom-start'
popupTitle={isCustomModel ? t('common.modelProvider.auth.modelCredentials') : ''}
/>
)
}
export default memo(AddCredentialInLoadBalancing)

View File

@@ -0,0 +1,167 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiAddCircleFill,
RiAddLine,
} from '@remixicon/react'
import {
Button,
} from '@/app/components/base/button'
import type {
ConfigurationMethodEnum,
CustomConfigurationModelFixedFields,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
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'
type AddCustomModelProps = {
provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
open?: boolean
onOpenChange?: (open: boolean) => void
}
const AddCustomModel = ({
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
}: AddCustomModelProps) => {
const { t } = useTranslation()
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 {
handleOpenModal: handleOpenModalForAddCustomModelToModelList,
} = useAuth(
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
{
isModelCredential: true,
mode: ModelModalModeEnum.addCustomModelToModelList,
},
)
const notAllowCustomCredential = provider.allow_custom_token === false
const renderTrigger = useCallback((open?: boolean) => {
const Item = (
<Button
variant='ghost'
size='small'
className={cn(
'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' />
{t('common.modelProvider.addModel')}
</Button>
)
if (notAllowCustomCredential && !!noModels) {
return (
<Tooltip asChild popupContent={t('plugin.auth.credentialUnavailable')}>
{Item}
</Tooltip>
)
}
return Item
}, [t, notAllowCustomCredential, noModels])
return (
<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>
)
}
export default memo(AddCustomModel)

View File

@@ -0,0 +1,101 @@
import {
memo,
useCallback,
} from 'react'
import CredentialItem from './credential-item'
import type {
Credential,
CustomModel,
CustomModelCredential,
ModelProvider,
} from '../../declarations'
import ModelIcon from '../../model-icon'
type AuthorizedItemProps = {
provider: ModelProvider
model?: CustomModelCredential
title?: string
disabled?: boolean
onDelete?: (credential?: Credential, model?: CustomModel) => void
onEdit?: (credential?: Credential, model?: CustomModel) => void
showItemSelectedIcon?: boolean
selectedCredentialId?: string
credentials: Credential[]
onItemClick?: (credential: Credential, model?: CustomModel) => void
enableAddModelCredential?: boolean
notAllowCustomCredential?: boolean
showModelTitle?: boolean
disableDeleteButShowAction?: boolean
disableDeleteTip?: string
}
export const AuthorizedItem = ({
provider,
model,
title,
credentials,
disabled,
onDelete,
onEdit,
showItemSelectedIcon,
selectedCredentialId,
onItemClick,
showModelTitle,
disableDeleteButShowAction,
disableDeleteTip,
}: AuthorizedItemProps) => {
const handleEdit = useCallback((credential?: Credential) => {
onEdit?.(credential, model)
}, [onEdit, model])
const handleDelete = useCallback((credential?: Credential) => {
onDelete?.(credential, model)
}, [onDelete, model])
const handleItemClick = useCallback((credential: Credential) => {
onItemClick?.(credential, model)
}, [onItemClick, model])
return (
<div className='p-1'>
{
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}
>
{title ?? model?.model}
</div>
</div>
)
}
{
credentials.map(credential => (
<CredentialItem
key={credential.credential_id}
credential={credential}
disabled={disabled}
onDelete={handleDelete}
onEdit={handleEdit}
showSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredentialId}
onItemClick={handleItemClick}
disableDeleteButShowAction={disableDeleteButShowAction}
disableDeleteTip={disableDeleteTip}
/>
))
}
</div>
)
}
export default memo(AuthorizedItem)

View File

@@ -0,0 +1,149 @@
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiCheckLine,
RiDeleteBinLine,
RiEqualizer2Line,
} from '@remixicon/react'
import Indicator from '@/app/components/header/indicator'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
import type { Credential } from '../../declarations'
import Badge from '@/app/components/base/badge'
type CredentialItemProps = {
credential: Credential
disabled?: boolean
onDelete?: (credential: Credential) => void
onEdit?: (credential?: Credential) => void
onItemClick?: (credential: Credential) => void
disableRename?: boolean
disableEdit?: boolean
disableDelete?: boolean
disableDeleteButShowAction?: boolean
disableDeleteTip?: string
showSelectedIcon?: boolean
selectedCredentialId?: string
}
const CredentialItem = ({
credential,
disabled,
onDelete,
onEdit,
onItemClick,
disableRename,
disableEdit,
disableDelete,
disableDeleteButShowAction,
disableDeleteTip,
showSelectedIcon,
selectedCredentialId,
}: CredentialItemProps) => {
const { t } = useTranslation()
const showAction = useMemo(() => {
return !(disableRename && disableEdit && disableDelete)
}, [disableRename, disableEdit, disableDelete])
const disableDeleteWhenSelected = useMemo(() => {
return disableDeleteButShowAction && selectedCredentialId === credential.credential_id
}, [disableDeleteButShowAction, selectedCredentialId, credential.credential_id])
const Item = (
<div
key={credential.credential_id}
className={cn(
'group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover',
(disabled || credential.not_allowed_to_use) && 'cursor-not-allowed opacity-50',
)}
onClick={() => {
if (disabled || credential.not_allowed_to_use)
return
onItemClick?.(credential)
}}
>
<div className='flex w-0 grow items-center space-x-1.5'>
{
showSelectedIcon && (
<div className='h-4 w-4'>
{
selectedCredentialId === credential.credential_id && (
<RiCheckLine className='h-4 w-4 text-text-accent' />
)
}
</div>
)
}
<Indicator className='ml-2 mr-1.5 shrink-0' />
<div
className='system-md-regular truncate text-text-secondary'
title={credential.credential_name}
>
{credential.credential_name}
</div>
</div>
{
credential.from_enterprise && (
<Badge className='shrink-0'>
Enterprise
</Badge>
)
}
{
showAction && !credential.from_enterprise && (
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
{
!disableEdit && !credential.not_allowed_to_use && (
<Tooltip popupContent={t('common.operation.edit')}>
<ActionButton
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
onEdit?.(credential)
}}
>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</Tooltip>
)
}
{
!disableDelete && (
<Tooltip popupContent={disableDeleteWhenSelected ? disableDeleteTip : t('common.operation.delete')}>
<ActionButton
className='hover:bg-transparent'
onClick={(e) => {
if (disabled || disableDeleteWhenSelected)
return
e.stopPropagation()
onDelete?.(credential)
}}
>
<RiDeleteBinLine className={cn(
'h-4 w-4 text-text-tertiary',
!disableDeleteWhenSelected && 'hover:text-text-destructive',
disableDeleteWhenSelected && 'opacity-50',
)} />
</ActionButton>
</Tooltip>
)
}
</div>
)
}
</div>
)
if (credential.not_allowed_to_use) {
return (
<Tooltip popupContent={t('plugin.auth.customCredentialUnavailable')}>
{Item}
</Tooltip>
)
}
return Item
}
export default memo(CredentialItem)

View File

@@ -0,0 +1,257 @@
import {
Fragment,
memo,
useCallback,
useState,
} from 'react'
import {
RiAddLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type {
PortalToFollowElemOptions,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
import Confirm from '@/app/components/base/confirm'
import type {
ConfigurationMethodEnum,
Credential,
CustomConfigurationModelFixedFields,
CustomModel,
ModelModalModeEnum,
ModelProvider,
} from '../../declarations'
import { useAuth } from '../hooks'
import AuthorizedItem from './authorized-item'
type AuthorizedProps = {
provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
authParams?: {
isModelCredential?: boolean
onUpdate?: (newPayload?: any, formValues?: Record<string, any>) => void
onRemove?: (credentialId: string) => void
mode?: ModelModalModeEnum
}
items: {
title?: string
model?: CustomModel
selectedCredential?: Credential
credentials: Credential[]
}[]
disabled?: boolean
renderTrigger: (open?: boolean) => React.ReactNode
isOpen?: boolean
onOpenChange?: (open: boolean) => void
offset?: PortalToFollowElemOptions['offset']
placement?: PortalToFollowElemOptions['placement']
triggerPopupSameWidth?: boolean
popupClassName?: string
showItemSelectedIcon?: boolean
onItemClick?: (credential: Credential, model?: CustomModel) => void
enableAddModelCredential?: boolean
triggerOnlyOpenModal?: boolean
hideAddAction?: boolean
disableItemClick?: boolean
popupTitle?: string
showModelTitle?: boolean
disableDeleteButShowAction?: boolean
disableDeleteTip?: string
}
const Authorized = ({
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
items,
authParams,
disabled,
renderTrigger,
isOpen,
onOpenChange,
offset = 8,
placement = 'bottom-end',
triggerPopupSameWidth = false,
popupClassName,
showItemSelectedIcon,
onItemClick,
triggerOnlyOpenModal,
hideAddAction,
disableItemClick,
popupTitle,
showModelTitle,
disableDeleteButShowAction,
disableDeleteTip,
}: AuthorizedProps) => {
const { t } = useTranslation()
const [isLocalOpen, setIsLocalOpen] = useState(false)
const mergedIsOpen = isOpen ?? isLocalOpen
const setMergedIsOpen = useCallback((open: boolean) => {
if (onOpenChange)
onOpenChange(open)
setIsLocalOpen(open)
}, [onOpenChange])
const {
isModelCredential,
onUpdate,
onRemove,
mode,
} = authParams || {}
const {
openConfirmDelete,
closeConfirmDelete,
doingAction,
handleActiveCredential,
handleConfirmDelete,
deleteCredentialId,
handleOpenModal,
} = useAuth(
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
{
isModelCredential,
onUpdate,
onRemove,
mode,
},
)
const handleEdit = useCallback((credential?: Credential, model?: CustomModel) => {
handleOpenModal(credential, model)
setMergedIsOpen(false)
}, [handleOpenModal, setMergedIsOpen])
const handleItemClick = useCallback((credential: Credential, model?: CustomModel) => {
if (disableItemClick)
return
if (onItemClick)
onItemClick(credential, model)
else
handleActiveCredential(credential, model)
setMergedIsOpen(false)
}, [handleActiveCredential, onItemClick, setMergedIsOpen, disableItemClick])
const notAllowCustomCredential = provider.allow_custom_token === false
return (
<>
<PortalToFollowElem
open={mergedIsOpen}
onOpenChange={setMergedIsOpen}
placement={placement}
offset={offset}
triggerPopupSameWidth={triggerPopupSameWidth}
>
<PortalToFollowElemTrigger
onClick={() => {
if (triggerOnlyOpenModal) {
handleOpenModal()
return
}
setMergedIsOpen(!mergedIsOpen)
}}
asChild
>
{renderTrigger(mergedIsOpen)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[100]'>
<div className={cn(
'w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]',
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'>
{
items.map((item, index) => (
<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 className='h-[1px] bg-divider-subtle'></div>
{
isModelCredential && !notAllowCustomCredential && !hideAddAction && (
<div
onClick={() => handleEdit(
undefined,
currentCustomConfigurationModelFixedFields
? {
model: currentCustomConfigurationModelFixedFields.__model_name,
model_type: currentCustomConfigurationModelFixedFields.__model_type,
}
: undefined,
)}
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' />
{t('common.modelProvider.auth.addModelCredential')}
</div>
)
}
{
!isModelCredential && !notAllowCustomCredential && !hideAddAction && (
<div className='p-2'>
<Button
onClick={() => handleEdit()}
className='w-full'
>
{t('common.modelProvider.auth.addApiKey')}
</Button>
</div>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
{
deleteCredentialId && (
<Confirm
isShow
title={t('common.modelProvider.confirmDelete')}
isDisabled={doingAction}
onCancel={closeConfirmDelete}
onConfirm={handleConfirmDelete}
/>
)
}
</>
)
}
export default memo(Authorized)

View File

@@ -0,0 +1,76 @@
import { memo } from 'react'
import {
RiEqualizer2Line,
RiScales3Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
type ConfigModelProps = {
onClick?: () => void
loadBalancingEnabled?: boolean
loadBalancingInvalid?: boolean
credentialRemoved?: boolean
}
const ConfigModel = ({
onClick,
loadBalancingEnabled,
loadBalancingInvalid,
credentialRemoved,
}: ConfigModelProps) => {
const { t } = useTranslation()
if (loadBalancingInvalid) {
return (
<div
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}
>
<RiScales3Line className='mr-0.5 h-3 w-3' />
{t('common.modelProvider.auth.authorizationError')}
<Indicator color='orange' className='absolute right-[-1px] top-[-1px] h-1.5 w-1.5' />
</div>
)
}
return (
<Button
variant='secondary'
size='small'
className={cn(
'hidden shrink-0 group-hover:flex',
credentialRemoved && 'flex',
)}
onClick={onClick}
>
{
credentialRemoved && (
<>
{t('common.modelProvider.auth.credentialRemoved')}
<Indicator color='red' className='ml-2' />
</>
)
}
{
!loadBalancingEnabled && !credentialRemoved && !loadBalancingInvalid && (
<>
<RiEqualizer2Line className='mr-1 h-4 w-4' />
{t('common.operation.config')}
</>
)
}
{
loadBalancingEnabled && !credentialRemoved && !loadBalancingInvalid && (
<>
<RiScales3Line className='mr-1 h-4 w-4' />
{t('common.modelProvider.auth.configLoadBalancing')}
</>
)
}
</Button>
)
}
export default memo(ConfigModel)

View File

@@ -0,0 +1,90 @@
import {
memo,
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiEqualizer2Line,
} from '@remixicon/react'
import {
Button,
} from '@/app/components/base/button'
import type {
CustomConfigurationModelFixedFields,
ModelProvider,
} 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 { useCredentialStatus } from './hooks'
import Tooltip from '@/app/components/base/tooltip'
type ConfigProviderProps = {
provider: ModelProvider,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
}
const ConfigProvider = ({
provider,
currentCustomConfigurationModelFixedFields,
}: ConfigProviderProps) => {
const { t } = useTranslation()
const {
hasCredential,
authorized,
current_credential_id,
current_credential_name,
available_credentials,
} = useCredentialStatus(provider)
const notAllowCustomCredential = provider.allow_custom_token === false
const renderTrigger = useCallback(() => {
const text = hasCredential ? t('common.operation.config') : t('common.operation.setup')
const Item = (
<Button
className='flex grow'
size='small'
variant={!authorized ? 'secondary-accent' : 'secondary'}
title={text}
>
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5 shrink-0' />
<span className='w-0 grow truncate text-left'>
{text}
</span>
</Button>
)
if (notAllowCustomCredential && !hasCredential) {
return (
<Tooltip
asChild
popupContent={t('plugin.auth.credentialUnavailable')}
>
{Item}
</Tooltip>
)
}
return Item
}, [authorized, hasCredential, notAllowCustomCredential, t])
return (
<Authorized
provider={provider}
configurationMethod={ConfigurationMethodEnum.predefinedModel}
currentCustomConfigurationModelFixedFields={currentCustomConfigurationModelFixedFields}
items={[
{
title: t('common.modelProvider.auth.apiKeys'),
credentials: available_credentials ?? [],
selectedCredential: {
credential_id: current_credential_id ?? '',
credential_name: current_credential_name ?? '',
},
},
]}
showItemSelectedIcon
showModelTitle
renderTrigger={renderTrigger}
triggerOnlyOpenModal={!hasCredential && !notAllowCustomCredential}
/>
)
}
export default memo(ConfigProvider)

View File

@@ -0,0 +1,115 @@
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)

View File

@@ -0,0 +1,6 @@
export * from './use-model-form-schemas'
export * from './use-credential-status'
export * from './use-custom-models'
export * from './use-auth'
export * from './use-auth-service'
export * from './use-credential-data'

View File

@@ -0,0 +1,57 @@
import { useCallback } from 'react'
import {
useActiveModelCredential,
useActiveProviderCredential,
useAddModelCredential,
useAddProviderCredential,
useDeleteModelCredential,
useDeleteProviderCredential,
useEditModelCredential,
useEditProviderCredential,
useGetModelCredential,
useGetProviderCredential,
} from '@/service/use-models'
import type {
CustomModel,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
export const useGetCredential = (provider: string, isModelCredential?: boolean, credentialId?: string, model?: CustomModel, configFrom?: string) => {
const providerData = useGetProviderCredential(!isModelCredential && !!credentialId, provider, credentialId)
const modelData = useGetModelCredential(!!isModelCredential && (!!credentialId || !!model), provider, credentialId, model?.model, model?.model_type, configFrom)
return isModelCredential ? modelData : providerData
}
export const useAuthService = (provider: string) => {
const { mutateAsync: addProviderCredential } = useAddProviderCredential(provider)
const { mutateAsync: editProviderCredential } = useEditProviderCredential(provider)
const { mutateAsync: deleteProviderCredential } = useDeleteProviderCredential(provider)
const { mutateAsync: activeProviderCredential } = useActiveProviderCredential(provider)
const { mutateAsync: addModelCredential } = useAddModelCredential(provider)
const { mutateAsync: activeModelCredential } = useActiveModelCredential(provider)
const { mutateAsync: deleteModelCredential } = useDeleteModelCredential(provider)
const { mutateAsync: editModelCredential } = useEditModelCredential(provider)
const getAddCredentialService = useCallback((isModel: boolean) => {
return isModel ? addModelCredential : addProviderCredential
}, [addModelCredential, addProviderCredential])
const getEditCredentialService = useCallback((isModel: boolean) => {
return isModel ? editModelCredential : editProviderCredential
}, [editModelCredential, editProviderCredential])
const getDeleteCredentialService = useCallback((isModel: boolean) => {
return isModel ? deleteModelCredential : deleteProviderCredential
}, [deleteModelCredential, deleteProviderCredential])
const getActiveCredentialService = useCallback((isModel: boolean) => {
return isModel ? activeModelCredential : activeProviderCredential
}, [activeModelCredential, activeProviderCredential])
return {
getAddCredentialService,
getEditCredentialService,
getDeleteCredentialService,
getActiveCredentialService,
}
}

View File

@@ -0,0 +1,193 @@
import {
useCallback,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useToastContext } from '@/app/components/base/toast'
import { useAuthService } from './use-auth-service'
import type {
ConfigurationMethodEnum,
Credential,
CustomConfigurationModelFixedFields,
CustomModel,
ModelModalModeEnum,
ModelProvider,
} from '../../declarations'
import {
useModelModalHandler,
useRefreshModel,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useDeleteModel } from '@/service/use-models'
export const useAuth = (
provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
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 { notify } = useToastContext()
const {
getDeleteCredentialService,
getActiveCredentialService,
getEditCredentialService,
getAddCredentialService,
} = useAuthService(provider.provider)
const { mutateAsync: deleteModelService } = useDeleteModel(provider.provider)
const handleOpenModelModal = useModelModalHandler()
const { handleRefreshModel } = useRefreshModel()
const pendingOperationCredentialId = useRef<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) => {
if (credential)
handleSetDeleteCredentialId(credential.credential_id)
if (model)
handleSetDeleteModel(model)
}, [])
const closeConfirmDelete = useCallback(() => {
handleSetDeleteCredentialId(null)
handleSetDeleteModel(null)
}, [])
const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction)
const handleSetDoingAction = useCallback((doing: boolean) => {
doingActionRef.current = doing
setDoingAction(doing)
}, [])
const handleActiveCredential = useCallback(async (credential: Credential, model?: CustomModel) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await getActiveCredentialService(!!model)({
credential_id: credential.credential_id,
model: model?.model,
model_type: model?.model_type,
})
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
handleRefreshModel(provider, undefined, true)
}
finally {
handleSetDoingAction(false)
}
}, [getActiveCredentialService, notify, t, handleSetDoingAction])
const handleConfirmDelete = useCallback(async () => {
if (doingActionRef.current)
return
if (!pendingOperationCredentialId.current && !pendingOperationModel.current) {
closeConfirmDelete()
return
}
try {
handleSetDoingAction(true)
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({
type: 'success',
message: t('common.api.actionSuccess'),
})
handleRefreshModel(provider, undefined, true)
onRemove?.(pendingOperationCredentialId.current ?? '')
closeConfirmDelete()
}
finally {
handleSetDoingAction(false)
}
}, [notify, t, handleSetDoingAction, getDeleteCredentialService, isModelCredential, closeConfirmDelete, handleRefreshModel, provider, configurationMethod, deleteModelService])
const handleSaveCredential = useCallback(async (payload: Record<string, any>) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
let res: { result?: string } = {}
if (payload.credential_id)
res = await getEditCredentialService(!!isModelCredential)(payload as any)
else
res = await getAddCredentialService(!!isModelCredential)(payload as any)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
handleRefreshModel(provider, undefined, !payload.credential_id)
}
}
finally {
handleSetDoingAction(false)
}
}, [notify, t, handleSetDoingAction, getEditCredentialService, getAddCredentialService])
const handleOpenModal = useCallback((credential?: Credential, model?: CustomModel) => {
handleOpenModelModal(
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
{
isModelCredential,
credential,
model,
onUpdate,
mode,
},
)
}, [
handleOpenModelModal,
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
isModelCredential,
onUpdate,
mode,
])
return {
pendingOperationCredentialId,
pendingOperationModel,
openConfirmDelete,
closeConfirmDelete,
doingAction,
handleActiveCredential,
handleConfirmDelete,
deleteCredentialId,
deleteModel,
handleSaveCredential,
handleOpenModal,
}
}

View File

@@ -0,0 +1,24 @@
import { useMemo } from 'react'
import { useGetCredential } from './use-auth-service'
import type {
Credential,
CustomModelCredential,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
export const useCredentialData = (provider: ModelProvider, providerFormSchemaPredefined: boolean, isModelCredential?: boolean, credential?: Credential, model?: CustomModelCredential) => {
const configFrom = useMemo(() => {
if (providerFormSchemaPredefined)
return 'predefined-model'
return 'custom-model'
}, [providerFormSchemaPredefined])
const {
isLoading,
data: credentialData = {},
} = useGetCredential(provider.provider, isModelCredential, credential?.credential_id, model, configFrom)
return {
isLoading,
credentialData,
}
}

View File

@@ -0,0 +1,26 @@
import { useMemo } from 'react'
import type {
ModelProvider,
} from '../../declarations'
export const useCredentialStatus = (provider: ModelProvider) => {
const {
current_credential_id,
current_credential_name,
available_credentials,
} = provider.custom_configuration
const hasCredential = !!available_credentials?.length
const authorized = current_credential_id && current_credential_name
const authRemoved = hasCredential && !current_credential_id && !current_credential_name
const currentCredential = available_credentials?.find(credential => credential.credential_id === current_credential_id)
return useMemo(() => ({
hasCredential,
authorized,
authRemoved,
current_credential_id,
current_credential_name,
available_credentials,
notAllowedToUse: currentCredential?.not_allowed_to_use,
}), [hasCredential, authorized, authRemoved, current_credential_id, current_credential_name, available_credentials])
}

View File

@@ -0,0 +1,15 @@
import type {
ModelProvider,
} from '../../declarations'
export const useCustomModels = (provider: ModelProvider) => {
const { custom_models } = provider.custom_configuration
return custom_models || []
}
export const useCanAddedModels = (provider: ModelProvider) => {
const { can_added_models } = provider.custom_configuration
return can_added_models || []
}

View File

@@ -0,0 +1,98 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import type {
Credential,
CustomModelCredential,
ModelProvider,
} from '../../declarations'
import {
genModelNameFormSchema,
genModelTypeFormSchema,
} from '../../utils'
import { FormTypeEnum } from '@/app/components/base/form/types'
export const useModelFormSchemas = (
provider: ModelProvider,
providerFormSchemaPredefined: boolean,
credentials?: Record<string, any>,
credential?: Credential,
model?: CustomModelCredential,
) => {
const { t } = useTranslation()
const {
provider_credential_schema,
supported_model_types,
model_credential_schema,
} = provider
const formSchemas = useMemo(() => {
return providerFormSchemaPredefined
? provider_credential_schema.credential_form_schemas
: model_credential_schema.credential_form_schemas
}, [
providerFormSchemaPredefined,
provider_credential_schema?.credential_form_schemas,
supported_model_types,
model_credential_schema?.credential_form_schemas,
model_credential_schema?.model,
model,
])
const formSchemasWithAuthorizationName = useMemo(() => {
const authorizationNameSchema = {
type: FormTypeEnum.textInput,
variable: '__authorization_name__',
label: t('plugin.auth.authorizationName'),
required: false,
}
return [
authorizationNameSchema,
...formSchemas,
]
}, [formSchemas, t])
const formValues = useMemo(() => {
let result: any = {}
formSchemas.forEach((schema) => {
result[schema.variable] = schema.default
})
if (credential) {
result = { ...result, __authorization_name__: credential?.credential_name }
if (credentials)
result = { ...result, ...credentials }
}
if (model)
result = { ...result, __model_name: model?.model, __model_type: model?.model_type }
return result
}, [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 {
formSchemas: formSchemasWithAuthorizationName,
formValues,
modelNameAndTypeFormSchemas,
modelNameAndTypeFormValues,
}
}

View File

@@ -0,0 +1,8 @@
export { default as Authorized } from './authorized'
export { default as SwitchCredentialInLoadBalancing } from './switch-credential-in-load-balancing'
export { default as AddCredentialInLoadBalancing } from './add-credential-in-load-balancing'
export { default as AddCustomModel } from './add-custom-model'
export { default as ConfigProvider } from './config-provider'
export { default as ConfigModel } from './config-model'
export { default as ManageCustomModelCredentials } from './manage-custom-model-credentials'
export { default as CredentialSelector } from './credential-selector'

View File

@@ -0,0 +1,82 @@
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)

View File

@@ -0,0 +1,137 @@
import type { Dispatch, SetStateAction } from 'react'
import {
memo,
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import Authorized from './authorized'
import type {
Credential,
CustomModel,
ModelProvider,
} from '../declarations'
import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import cn from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip'
import Badge from '@/app/components/base/badge'
type SwitchCredentialInLoadBalancingProps = {
provider: ModelProvider
model: CustomModel
credentials?: Credential[]
customModelCredential?: Credential
setCustomModelCredential: Dispatch<SetStateAction<Credential | undefined>>
onUpdate?: (payload?: any, formValues?: Record<string, any>) => void
onRemove?: (credentialId: string) => void
}
const SwitchCredentialInLoadBalancing = ({
provider,
model,
customModelCredential,
setCustomModelCredential,
credentials,
onUpdate,
onRemove,
}: SwitchCredentialInLoadBalancingProps) => {
const { t } = useTranslation()
const notAllowCustomCredential = provider.allow_custom_token === false
const handleItemClick = useCallback((credential: Credential) => {
setCustomModelCredential(credential)
}, [setCustomModelCredential])
const renderTrigger = useCallback(() => {
const selectedCredentialId = customModelCredential?.credential_id
const currentCredential = credentials?.find(c => c.credential_id === selectedCredentialId)
const empty = !credentials?.length
const authRemoved = selectedCredentialId && !currentCredential && !empty
const unavailable = currentCredential?.not_allowed_to_use
let color = 'green'
if (authRemoved || unavailable)
color = 'red'
const Item = (
<Button
variant='secondary'
className={cn(
'shrink-0 space-x-1',
(authRemoved || unavailable) && 'text-components-button-destructive-secondary-text',
empty && 'cursor-not-allowed opacity-50',
)}
>
{
!empty && (
<Indicator
className='mr-2'
color={color as any}
/>
)
}
{
authRemoved && t('common.modelProvider.auth.authRemoved')
}
{
(unavailable || empty) && t('plugin.auth.credentialUnavailableInButton')
}
{
!authRemoved && !unavailable && !empty && customModelCredential?.credential_name
}
{
currentCredential?.from_enterprise && (
<Badge className='ml-2'>Enterprise</Badge>
)
}
<RiArrowDownSLine className='h-4 w-4' />
</Button>
)
if (empty && notAllowCustomCredential) {
return (
<Tooltip
asChild
popupContent={t('plugin.auth.credentialUnavailable')}
>
{Item}
</Tooltip>
)
}
return Item
}, [customModelCredential, t, credentials, notAllowCustomCredential])
return (
<Authorized
provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel}
currentCustomConfigurationModelFixedFields={model ? {
__model_name: model.model,
__model_type: model.model_type,
} : undefined}
authParams={{
isModelCredential: true,
mode: ModelModalModeEnum.configModelCredential,
onUpdate,
onRemove,
}}
items={[
{
model,
credentials: credentials || [],
selectedCredential: customModelCredential ? {
credential_id: customModelCredential?.credential_id || '',
credential_name: customModelCredential?.credential_name || '',
} : undefined,
},
]}
renderTrigger={renderTrigger}
onItemClick={handleItemClick}
enableAddModelCredential
showItemSelectedIcon
popupTitle={t('common.modelProvider.auth.modelCredentials')}
triggerOnlyOpenModal={!credentials?.length}
/>
)
}
export default memo(SwitchCredentialInLoadBalancing)