index.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import {
  2. Fragment,
  3. memo,
  4. useCallback,
  5. useState,
  6. } from 'react'
  7. import {
  8. RiAddLine,
  9. } from '@remixicon/react'
  10. import { useTranslation } from 'react-i18next'
  11. import {
  12. PortalToFollowElem,
  13. PortalToFollowElemContent,
  14. PortalToFollowElemTrigger,
  15. } from '@/app/components/base/portal-to-follow-elem'
  16. import type {
  17. PortalToFollowElemOptions,
  18. } from '@/app/components/base/portal-to-follow-elem'
  19. import Button from '@/app/components/base/button'
  20. import cn from '@/utils/classnames'
  21. import Confirm from '@/app/components/base/confirm'
  22. import type {
  23. ConfigurationMethodEnum,
  24. Credential,
  25. CustomConfigurationModelFixedFields,
  26. CustomModel,
  27. ModelModalModeEnum,
  28. ModelProvider,
  29. } from '../../declarations'
  30. import { useAuth } from '../hooks'
  31. import AuthorizedItem from './authorized-item'
  32. type AuthorizedProps = {
  33. provider: ModelProvider,
  34. configurationMethod: ConfigurationMethodEnum,
  35. currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
  36. authParams?: {
  37. isModelCredential?: boolean
  38. onUpdate?: (newPayload?: any, formValues?: Record<string, any>) => void
  39. onRemove?: (credentialId: string) => void
  40. mode?: ModelModalModeEnum
  41. }
  42. items: {
  43. title?: string
  44. model?: CustomModel
  45. selectedCredential?: Credential
  46. credentials: Credential[]
  47. }[]
  48. disabled?: boolean
  49. renderTrigger: (open?: boolean) => React.ReactNode
  50. isOpen?: boolean
  51. onOpenChange?: (open: boolean) => void
  52. offset?: PortalToFollowElemOptions['offset']
  53. placement?: PortalToFollowElemOptions['placement']
  54. triggerPopupSameWidth?: boolean
  55. popupClassName?: string
  56. showItemSelectedIcon?: boolean
  57. onItemClick?: (credential: Credential, model?: CustomModel) => void
  58. enableAddModelCredential?: boolean
  59. triggerOnlyOpenModal?: boolean
  60. hideAddAction?: boolean
  61. disableItemClick?: boolean
  62. popupTitle?: string
  63. showModelTitle?: boolean
  64. disableDeleteButShowAction?: boolean
  65. disableDeleteTip?: string
  66. }
  67. const Authorized = ({
  68. provider,
  69. configurationMethod,
  70. currentCustomConfigurationModelFixedFields,
  71. items,
  72. authParams,
  73. disabled,
  74. renderTrigger,
  75. isOpen,
  76. onOpenChange,
  77. offset = 8,
  78. placement = 'bottom-end',
  79. triggerPopupSameWidth = false,
  80. popupClassName,
  81. showItemSelectedIcon,
  82. onItemClick,
  83. triggerOnlyOpenModal,
  84. hideAddAction,
  85. disableItemClick,
  86. popupTitle,
  87. showModelTitle,
  88. disableDeleteButShowAction,
  89. disableDeleteTip,
  90. }: AuthorizedProps) => {
  91. const { t } = useTranslation()
  92. const [isLocalOpen, setIsLocalOpen] = useState(false)
  93. const mergedIsOpen = isOpen ?? isLocalOpen
  94. const setMergedIsOpen = useCallback((open: boolean) => {
  95. if (onOpenChange)
  96. onOpenChange(open)
  97. setIsLocalOpen(open)
  98. }, [onOpenChange])
  99. const {
  100. isModelCredential,
  101. onUpdate,
  102. onRemove,
  103. mode,
  104. } = authParams || {}
  105. const {
  106. openConfirmDelete,
  107. closeConfirmDelete,
  108. doingAction,
  109. handleActiveCredential,
  110. handleConfirmDelete,
  111. deleteCredentialId,
  112. handleOpenModal,
  113. } = useAuth(
  114. provider,
  115. configurationMethod,
  116. currentCustomConfigurationModelFixedFields,
  117. {
  118. isModelCredential,
  119. onUpdate,
  120. onRemove,
  121. mode,
  122. },
  123. )
  124. const handleEdit = useCallback((credential?: Credential, model?: CustomModel) => {
  125. handleOpenModal(credential, model)
  126. setMergedIsOpen(false)
  127. }, [handleOpenModal, setMergedIsOpen])
  128. const handleItemClick = useCallback((credential: Credential, model?: CustomModel) => {
  129. if (disableItemClick)
  130. return
  131. if (onItemClick)
  132. onItemClick(credential, model)
  133. else
  134. handleActiveCredential(credential, model)
  135. setMergedIsOpen(false)
  136. }, [handleActiveCredential, onItemClick, setMergedIsOpen, disableItemClick])
  137. const notAllowCustomCredential = provider.allow_custom_token === false
  138. return (
  139. <>
  140. <PortalToFollowElem
  141. open={mergedIsOpen}
  142. onOpenChange={setMergedIsOpen}
  143. placement={placement}
  144. offset={offset}
  145. triggerPopupSameWidth={triggerPopupSameWidth}
  146. >
  147. <PortalToFollowElemTrigger
  148. onClick={() => {
  149. if (triggerOnlyOpenModal) {
  150. handleOpenModal()
  151. return
  152. }
  153. setMergedIsOpen(!mergedIsOpen)
  154. }}
  155. asChild
  156. >
  157. {renderTrigger(mergedIsOpen)}
  158. </PortalToFollowElemTrigger>
  159. <PortalToFollowElemContent className='z-[1060]'>
  160. <div className={cn(
  161. 'w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]',
  162. popupClassName,
  163. )}>
  164. {
  165. popupTitle && (
  166. <div className='system-xs-medium px-3 pb-0.5 pt-[10px] text-text-tertiary'>
  167. {popupTitle}
  168. </div>
  169. )
  170. }
  171. <div className='max-h-[304px] overflow-y-auto'>
  172. {
  173. items.map((item, index) => (
  174. <Fragment key={index}>
  175. <AuthorizedItem
  176. provider={provider}
  177. title={item.title}
  178. model={item.model}
  179. credentials={item.credentials}
  180. disabled={disabled}
  181. onDelete={openConfirmDelete}
  182. disableDeleteButShowAction={disableDeleteButShowAction}
  183. disableDeleteTip={disableDeleteTip}
  184. onEdit={handleEdit}
  185. showItemSelectedIcon={showItemSelectedIcon}
  186. selectedCredentialId={item.selectedCredential?.credential_id}
  187. onItemClick={handleItemClick}
  188. showModelTitle={showModelTitle}
  189. />
  190. {
  191. index !== items.length - 1 && (
  192. <div className='h-[1px] bg-divider-subtle'></div>
  193. )
  194. }
  195. </Fragment>
  196. ))
  197. }
  198. </div>
  199. <div className='h-[1px] bg-divider-subtle'></div>
  200. {
  201. isModelCredential && !notAllowCustomCredential && !hideAddAction && (
  202. <div
  203. onClick={() => handleEdit(
  204. undefined,
  205. currentCustomConfigurationModelFixedFields
  206. ? {
  207. model: currentCustomConfigurationModelFixedFields.__model_name,
  208. model_type: currentCustomConfigurationModelFixedFields.__model_type,
  209. }
  210. : undefined,
  211. )}
  212. className='system-xs-medium flex h-[40px] cursor-pointer items-center px-3 text-text-accent-light-mode-only'
  213. >
  214. <RiAddLine className='mr-1 h-4 w-4' />
  215. {t('common.modelProvider.auth.addModelCredential')}
  216. </div>
  217. )
  218. }
  219. {
  220. !isModelCredential && !notAllowCustomCredential && !hideAddAction && (
  221. <div className='p-2'>
  222. <Button
  223. onClick={() => handleEdit()}
  224. className='w-full'
  225. >
  226. {t('common.modelProvider.auth.addApiKey')}
  227. </Button>
  228. </div>
  229. )
  230. }
  231. </div>
  232. </PortalToFollowElemContent>
  233. </PortalToFollowElem>
  234. {
  235. deleteCredentialId && (
  236. <Confirm
  237. isShow
  238. title={t('common.modelProvider.confirmDelete')}
  239. isDisabled={doingAction}
  240. onCancel={closeConfirmDelete}
  241. onConfirm={handleConfirmDelete}
  242. />
  243. )
  244. }
  245. </>
  246. )
  247. }
  248. export default memo(Authorized)