index.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useMemo, useState } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import Link from 'next/link'
  6. import {
  7. RiArrowLeftLine,
  8. RiArrowRightUpLine,
  9. } from '@remixicon/react'
  10. import {
  11. PortalToFollowElem,
  12. PortalToFollowElemContent,
  13. PortalToFollowElemTrigger,
  14. } from '@/app/components/base/portal-to-follow-elem'
  15. import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-trigger'
  16. import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item'
  17. import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
  18. import Button from '@/app/components/base/button'
  19. import Indicator from '@/app/components/header/indicator'
  20. import ToolCredentialForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form'
  21. import Toast from '@/app/components/base/toast'
  22. import Textarea from '@/app/components/base/textarea'
  23. import Divider from '@/app/components/base/divider'
  24. import TabSlider from '@/app/components/base/tab-slider-plain'
  25. import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form'
  26. import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
  27. import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
  28. import { useAppContext } from '@/context/app-context'
  29. import {
  30. useAllBuiltInTools,
  31. useAllCustomTools,
  32. useAllWorkflowTools,
  33. useInvalidateAllBuiltInTools,
  34. useUpdateProviderCredentials,
  35. } from '@/service/use-tools'
  36. import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
  37. import { usePluginInstalledCheck } from '@/app/components/plugins/plugin-detail-panel/tool-selector/hooks'
  38. import { CollectionType } from '@/app/components/tools/types'
  39. import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
  40. import type {
  41. OffsetOptions,
  42. Placement,
  43. } from '@floating-ui/react'
  44. import { MARKETPLACE_API_PREFIX } from '@/config'
  45. import type { Node } from 'reactflow'
  46. import type { NodeOutPutVar } from '@/app/components/workflow/types'
  47. import cn from '@/utils/classnames'
  48. type Props = {
  49. disabled?: boolean
  50. placement?: Placement
  51. offset?: OffsetOptions
  52. scope?: string
  53. value?: ToolValue
  54. selectedTools?: ToolValue[]
  55. onSelect: (tool: {
  56. provider_name: string
  57. tool_name: string
  58. tool_label: string
  59. settings?: Record<string, any>
  60. parameters?: Record<string, any>
  61. extra?: Record<string, any>
  62. }) => void
  63. onDelete?: () => void
  64. supportEnableSwitch?: boolean
  65. supportAddCustomTool?: boolean
  66. trigger?: React.ReactNode
  67. controlledState?: boolean
  68. onControlledStateChange?: (state: boolean) => void
  69. panelShowState?: boolean
  70. onPanelShowStateChange?: (state: boolean) => void
  71. nodeOutputVars: NodeOutPutVar[],
  72. availableNodes: Node[],
  73. nodeId?: string,
  74. }
  75. const ToolSelector: FC<Props> = ({
  76. value,
  77. selectedTools,
  78. disabled,
  79. placement = 'left',
  80. offset = 4,
  81. onSelect,
  82. onDelete,
  83. scope,
  84. supportEnableSwitch,
  85. trigger,
  86. controlledState,
  87. onControlledStateChange,
  88. panelShowState,
  89. onPanelShowStateChange,
  90. nodeOutputVars,
  91. availableNodes,
  92. nodeId = '',
  93. }) => {
  94. const { t } = useTranslation()
  95. const [isShow, onShowChange] = useState(false)
  96. const handleTriggerClick = () => {
  97. if (disabled) return
  98. onShowChange(true)
  99. }
  100. const { data: buildInTools } = useAllBuiltInTools()
  101. const { data: customTools } = useAllCustomTools()
  102. const { data: workflowTools } = useAllWorkflowTools()
  103. const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools()
  104. const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
  105. // plugin info check
  106. const { inMarketPlace, manifest } = usePluginInstalledCheck(value?.provider_name)
  107. const currentProvider = useMemo(() => {
  108. const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
  109. return mergedTools.find((toolWithProvider) => {
  110. return toolWithProvider.id === value?.provider_name
  111. })
  112. }, [value, buildInTools, customTools, workflowTools])
  113. const [isShowChooseTool, setIsShowChooseTool] = useState(false)
  114. const handleSelectTool = (tool: ToolDefaultValue) => {
  115. const settingValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any))
  116. const paramValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form === 'llm') as any), true)
  117. const toolValue = {
  118. provider_name: tool.provider_id,
  119. type: tool.provider_type,
  120. tool_name: tool.tool_name,
  121. tool_label: tool.tool_label,
  122. settings: settingValues,
  123. parameters: paramValues,
  124. enabled: tool.is_team_authorization,
  125. extra: {
  126. description: '',
  127. },
  128. schemas: tool.paramSchemas,
  129. }
  130. onSelect(toolValue)
  131. // setIsShowChooseTool(false)
  132. }
  133. const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
  134. onSelect({
  135. ...value,
  136. extra: {
  137. ...value?.extra,
  138. description: e.target.value || '',
  139. },
  140. } as any)
  141. }
  142. // tool settings & params
  143. const currentToolSettings = useMemo(() => {
  144. if (!currentProvider) return []
  145. return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form !== 'llm') || []
  146. }, [currentProvider, value])
  147. const currentToolParams = useMemo(() => {
  148. if (!currentProvider) return []
  149. return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form === 'llm') || []
  150. }, [currentProvider, value])
  151. const [currType, setCurrType] = useState('settings')
  152. const showTabSlider = currentToolSettings.length > 0 && currentToolParams.length > 0
  153. const userSettingsOnly = currentToolSettings.length > 0 && !currentToolParams.length
  154. const reasoningConfigOnly = currentToolParams.length > 0 && !currentToolSettings.length
  155. const settingsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolSettings), [currentToolSettings])
  156. const paramsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams])
  157. const handleSettingsFormChange = (v: Record<string, any>) => {
  158. const newValue = getStructureValue(v)
  159. const toolValue = {
  160. ...value,
  161. settings: newValue,
  162. }
  163. onSelect(toolValue as any)
  164. }
  165. const handleParamsFormChange = (v: Record<string, any>) => {
  166. const toolValue = {
  167. ...value,
  168. parameters: v,
  169. }
  170. onSelect(toolValue as any)
  171. }
  172. const handleEnabledChange = (state: boolean) => {
  173. onSelect({
  174. ...value,
  175. enabled: state,
  176. } as any)
  177. }
  178. // authorization
  179. const { isCurrentWorkspaceManager } = useAppContext()
  180. const [isShowSettingAuth, setShowSettingAuth] = useState(false)
  181. const handleCredentialSettingUpdate = () => {
  182. invalidateAllBuiltinTools()
  183. Toast.notify({
  184. type: 'success',
  185. message: t('common.api.actionSuccess'),
  186. })
  187. setShowSettingAuth(false)
  188. onShowChange(false)
  189. }
  190. const { mutate: updatePermission } = useUpdateProviderCredentials({
  191. onSuccess: handleCredentialSettingUpdate,
  192. })
  193. // install from marketplace
  194. const currentTool = useMemo(() => {
  195. return currentProvider?.tools.find(tool => tool.name === value?.tool_name)
  196. }, [currentProvider?.tools, value?.tool_name])
  197. const manifestIcon = useMemo(() => {
  198. if (!manifest)
  199. return ''
  200. return `${MARKETPLACE_API_PREFIX}/plugins/${(manifest as any).plugin_id}/icon`
  201. }, [manifest])
  202. const handleInstall = async () => {
  203. invalidateAllBuiltinTools()
  204. invalidateInstalledPluginList()
  205. }
  206. return (
  207. <>
  208. <PortalToFollowElem
  209. placement={placement}
  210. offset={offset}
  211. open={trigger ? controlledState : isShow}
  212. onOpenChange={trigger ? onControlledStateChange : onShowChange}
  213. >
  214. <PortalToFollowElemTrigger
  215. className='w-full'
  216. onClick={() => {
  217. if (!currentProvider || !currentTool) return
  218. handleTriggerClick()
  219. }}
  220. >
  221. {trigger}
  222. {!trigger && !value?.provider_name && (
  223. <ToolTrigger
  224. isConfigure
  225. open={isShow}
  226. value={value}
  227. provider={currentProvider}
  228. />
  229. )}
  230. {!trigger && value?.provider_name && (
  231. <ToolItem
  232. open={isShow}
  233. icon={currentProvider?.icon || manifestIcon}
  234. providerName={value.provider_name}
  235. toolLabel={value.tool_label || value.tool_name}
  236. showSwitch={supportEnableSwitch}
  237. switchValue={value.enabled}
  238. onSwitchChange={handleEnabledChange}
  239. onDelete={onDelete}
  240. noAuth={currentProvider && currentTool && !currentProvider.is_team_authorization}
  241. onAuth={() => setShowSettingAuth(true)}
  242. uninstalled={!currentProvider && inMarketPlace}
  243. versionMismatch={currentProvider && inMarketPlace && !currentTool}
  244. installInfo={manifest?.latest_package_identifier}
  245. onInstall={() => handleInstall()}
  246. isError={(!currentProvider || !currentTool) && !inMarketPlace}
  247. errorTip={
  248. <div className='space-y-1 max-w-[240px] text-xs'>
  249. <h3 className='text-text-primary font-semibold'>{currentTool ? t('plugin.detailPanel.toolSelector.uninstalledTitle') : t('plugin.detailPanel.toolSelector.unsupportedTitle')}</h3>
  250. <p className='text-text-secondary tracking-tight'>{currentTool ? t('plugin.detailPanel.toolSelector.uninstalledContent') : t('plugin.detailPanel.toolSelector.unsupportedContent')}</p>
  251. <p>
  252. <Link href={'/plugins'} className='text-text-accent tracking-tight'>{t('plugin.detailPanel.toolSelector.uninstalledLink')}</Link>
  253. </p>
  254. </div>
  255. }
  256. />
  257. )}
  258. </PortalToFollowElemTrigger>
  259. <PortalToFollowElemContent className='z-[1000]'>
  260. <div className={cn('relative w-[361px] min-h-20 max-h-[642px] pb-4 rounded-xl backdrop-blur-sm bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg', !isShowSettingAuth && 'overflow-y-auto pb-2')}>
  261. {!isShowSettingAuth && (
  262. <>
  263. <div className='px-4 pt-3.5 pb-1 text-text-primary system-xl-semibold'>{t('plugin.detailPanel.toolSelector.title')}</div>
  264. {/* base form */}
  265. <div className='px-4 py-2 flex flex-col gap-3'>
  266. <div className='flex flex-col gap-1'>
  267. <div className='h-6 flex items-center system-sm-semibold text-text-secondary'>{t('plugin.detailPanel.toolSelector.toolLabel')}</div>
  268. <ToolPicker
  269. panelClassName='w-[328px]'
  270. placement='bottom'
  271. offset={offset}
  272. trigger={
  273. <ToolTrigger
  274. open={panelShowState || isShowChooseTool}
  275. value={value}
  276. provider={currentProvider}
  277. />
  278. }
  279. isShow={panelShowState || isShowChooseTool}
  280. onShowChange={trigger ? onPanelShowStateChange as any : setIsShowChooseTool}
  281. disabled={false}
  282. supportAddCustomTool
  283. onSelect={handleSelectTool}
  284. scope={scope}
  285. selectedTools={selectedTools}
  286. />
  287. </div>
  288. <div className='flex flex-col gap-1'>
  289. <div className='h-6 flex items-center system-sm-semibold text-text-secondary'>{t('plugin.detailPanel.toolSelector.descriptionLabel')}</div>
  290. <Textarea
  291. className='resize-none'
  292. placeholder={t('plugin.detailPanel.toolSelector.descriptionPlaceholder')}
  293. value={value?.extra?.description || ''}
  294. onChange={handleDescriptionChange}
  295. disabled={!value?.provider_name}
  296. />
  297. </div>
  298. </div>
  299. {/* authorization */}
  300. {currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && (
  301. <>
  302. <Divider className='my-1 w-full' />
  303. <div className='px-4 py-2'>
  304. {!currentProvider.is_team_authorization && (
  305. <Button
  306. variant='primary'
  307. className={cn('shrink-0 w-full')}
  308. onClick={() => setShowSettingAuth(true)}
  309. disabled={!isCurrentWorkspaceManager}
  310. >
  311. {t('tools.auth.unauthorized')}
  312. </Button>
  313. )}
  314. {currentProvider.is_team_authorization && (
  315. <Button
  316. variant='secondary'
  317. className={cn('shrink-0 w-full')}
  318. onClick={() => setShowSettingAuth(true)}
  319. disabled={!isCurrentWorkspaceManager}
  320. >
  321. <Indicator className='mr-2' color={'green'} />
  322. {t('tools.auth.authorized')}
  323. </Button>
  324. )}
  325. </div>
  326. </>
  327. )}
  328. {/* tool settings */}
  329. {(currentToolSettings.length > 0 || currentToolParams.length > 0) && currentProvider?.is_team_authorization && (
  330. <>
  331. <Divider className='my-1 w-full' />
  332. {/* tabs */}
  333. {nodeId && showTabSlider && (
  334. <TabSlider
  335. className='shrink-0 mt-1 px-4'
  336. itemClassName='py-3'
  337. noBorderBottom
  338. smallItem
  339. value={currType}
  340. onChange={(value) => {
  341. setCurrType(value)
  342. }}
  343. options={[
  344. { value: 'settings', text: t('plugin.detailPanel.toolSelector.settings')! },
  345. { value: 'params', text: t('plugin.detailPanel.toolSelector.params')! },
  346. ]}
  347. />
  348. )}
  349. {nodeId && showTabSlider && currType === 'params' && (
  350. <div className='px-4 py-2'>
  351. <div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div>
  352. <div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
  353. </div>
  354. )}
  355. {/* user settings only */}
  356. {userSettingsOnly && (
  357. <div className='p-4 pb-1'>
  358. <div className='text-text-primary system-sm-semibold-uppercase'>{t('plugin.detailPanel.toolSelector.settings')}</div>
  359. </div>
  360. )}
  361. {/* reasoning config only */}
  362. {nodeId && reasoningConfigOnly && (
  363. <div className='mb-1 p-4 pb-1'>
  364. <div className='text-text-primary system-sm-semibold-uppercase'>{t('plugin.detailPanel.toolSelector.params')}</div>
  365. <div className='pb-1'>
  366. <div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div>
  367. <div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
  368. </div>
  369. </div>
  370. )}
  371. {/* user settings form */}
  372. {(currType === 'settings' || userSettingsOnly) && (
  373. <div className='px-4 py-2'>
  374. <Form
  375. value={getPlainValue(value?.settings || {})}
  376. onChange={handleSettingsFormChange}
  377. formSchemas={settingsFormSchemas as any}
  378. isEditMode={true}
  379. showOnVariableMap={{}}
  380. validating={false}
  381. inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
  382. fieldMoreInfo={item => item.url
  383. ? (<a
  384. href={item.url}
  385. target='_blank' rel='noopener noreferrer'
  386. className='inline-flex items-center text-xs text-text-accent'
  387. >
  388. {t('tools.howToGet')}
  389. <RiArrowRightUpLine className='ml-1 w-3 h-3' />
  390. </a>)
  391. : null}
  392. />
  393. </div>
  394. )}
  395. {/* reasoning config form */}
  396. {nodeId && (currType === 'params' || reasoningConfigOnly) && (
  397. <ReasoningConfigForm
  398. value={value?.parameters || {}}
  399. onChange={handleParamsFormChange}
  400. schemas={paramsFormSchemas as any}
  401. nodeOutputVars={nodeOutputVars}
  402. availableNodes={availableNodes}
  403. nodeId={nodeId}
  404. />
  405. )}
  406. </>
  407. )}
  408. </>
  409. )}
  410. {/* authorization panel */}
  411. {isShowSettingAuth && currentProvider && (
  412. <>
  413. <div className='relative pt-3.5 flex flex-col gap-1'>
  414. <div className='absolute -top-2 left-2 w-[345px] pt-2 rounded-t-xl backdrop-blur-sm bg-components-panel-bg-blur border-[0.5px] border-components-panel-border'></div>
  415. <div
  416. className='px-3 h-6 flex items-center gap-1 text-text-accent-secondary system-xs-semibold-uppercase cursor-pointer'
  417. onClick={() => setShowSettingAuth(false)}
  418. >
  419. <RiArrowLeftLine className='w-4 h-4' />
  420. BACK
  421. </div>
  422. <div className='px-4 text-text-primary system-xl-semibold'>{t('tools.auth.setupModalTitle')}</div>
  423. <div className='px-4 text-text-tertiary system-xs-regular'>{t('tools.auth.setupModalTitleDescription')}</div>
  424. </div>
  425. <ToolCredentialForm
  426. collection={currentProvider}
  427. onCancel={() => setShowSettingAuth(false)}
  428. onSaved={async value => updatePermission({
  429. providerName: currentProvider.name,
  430. credentials: value,
  431. })}
  432. />
  433. </>
  434. )}
  435. </div>
  436. </PortalToFollowElemContent>
  437. </PortalToFollowElem>
  438. </>
  439. )
  440. }
  441. export default React.memo(ToolSelector)