You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

provider-list.tsx 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. 'use client'
  2. import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import type { Collection } from './types'
  5. import Marketplace from './marketplace'
  6. import cn from '@/utils/classnames'
  7. import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
  8. import TabSliderNew from '@/app/components/base/tab-slider-new'
  9. import LabelFilter from '@/app/components/tools/labels/filter'
  10. import Input from '@/app/components/base/input'
  11. import ProviderDetail from '@/app/components/tools/provider/detail'
  12. import Empty from '@/app/components/plugins/marketplace/empty'
  13. import CustomCreateCard from '@/app/components/tools/provider/custom-create-card'
  14. import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty'
  15. import Card from '@/app/components/plugins/card'
  16. import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
  17. import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
  18. import MCPList from './mcp'
  19. import { useAllToolProviders } from '@/service/use-tools'
  20. import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
  21. import { useGlobalPublicStore } from '@/context/global-public-context'
  22. import { ToolTypeEnum } from '../workflow/block-selector/types'
  23. import { useMarketplace } from './marketplace/hooks'
  24. const getToolType = (type: string) => {
  25. switch (type) {
  26. case 'builtin':
  27. return ToolTypeEnum.BuiltIn
  28. case 'api':
  29. return ToolTypeEnum.Custom
  30. case 'workflow':
  31. return ToolTypeEnum.Workflow
  32. case 'mcp':
  33. return ToolTypeEnum.MCP
  34. default:
  35. return ToolTypeEnum.BuiltIn
  36. }
  37. }
  38. const ProviderList = () => {
  39. // const searchParams = useSearchParams()
  40. // searchParams.get('category') === 'workflow'
  41. const { t } = useTranslation()
  42. const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
  43. const containerRef = useRef<HTMLDivElement>(null)
  44. const [activeTab, setActiveTab] = useTabSearchParams({
  45. defaultTab: 'builtin',
  46. })
  47. const options = [
  48. { value: 'builtin', text: t('tools.type.builtIn') },
  49. { value: 'api', text: t('tools.type.custom') },
  50. { value: 'workflow', text: t('tools.type.workflow') },
  51. { value: 'mcp', text: 'MCP' },
  52. ]
  53. const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
  54. const handleTagsChange = (value: string[]) => {
  55. setTagFilterValue(value)
  56. }
  57. const [keywords, setKeywords] = useState<string>('')
  58. const handleKeywordsChange = (value: string) => {
  59. setKeywords(value)
  60. }
  61. const { data: collectionList = [], refetch } = useAllToolProviders()
  62. const filteredCollectionList = useMemo(() => {
  63. return collectionList.filter((collection) => {
  64. if (collection.type !== activeTab)
  65. return false
  66. if (tagFilterValue.length > 0 && (!collection.labels || collection.labels.every(label => !tagFilterValue.includes(label))))
  67. return false
  68. if (keywords)
  69. return Object.values(collection.label).some(value => value.toLowerCase().includes(keywords.toLowerCase()))
  70. return true
  71. })
  72. }, [activeTab, tagFilterValue, keywords, collectionList])
  73. const [currentProviderId, setCurrentProviderId] = useState<string | undefined>()
  74. const currentProvider = useMemo<Collection | undefined>(() => {
  75. return filteredCollectionList.find(collection => collection.id === currentProviderId)
  76. }, [currentProviderId, filteredCollectionList])
  77. const { data: pluginList } = useInstalledPluginList()
  78. const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
  79. const currentPluginDetail = useMemo(() => {
  80. const detail = pluginList?.plugins.find(plugin => plugin.plugin_id === currentProvider?.plugin_id)
  81. return detail
  82. }, [currentProvider?.plugin_id, pluginList?.plugins])
  83. const toolListTailRef = useRef<HTMLDivElement>(null)
  84. const showMarketplacePanel = useCallback(() => {
  85. containerRef.current?.scrollTo({
  86. top: toolListTailRef.current
  87. ? toolListTailRef.current?.offsetTop - 80
  88. : 0,
  89. behavior: 'smooth',
  90. })
  91. }, [toolListTailRef])
  92. const marketplaceContext = useMarketplace(keywords, tagFilterValue)
  93. const {
  94. handleScroll,
  95. } = marketplaceContext
  96. const [isMarketplaceArrowVisible, setIsMarketplaceArrowVisible] = useState(true)
  97. const onContainerScroll = useMemo(() => {
  98. return (e: Event) => {
  99. handleScroll(e)
  100. if (containerRef.current && toolListTailRef.current)
  101. setIsMarketplaceArrowVisible(containerRef.current.scrollTop < (toolListTailRef.current?.offsetTop - 80))
  102. }
  103. }, [handleScroll, containerRef, toolListTailRef, setIsMarketplaceArrowVisible])
  104. useEffect(() => {
  105. const container = containerRef.current
  106. if (container)
  107. container.addEventListener('scroll', onContainerScroll)
  108. return () => {
  109. if (container)
  110. container.removeEventListener('scroll', onContainerScroll)
  111. }
  112. }, [onContainerScroll])
  113. return (
  114. <>
  115. <div className='relative flex h-0 shrink-0 grow overflow-hidden'>
  116. <div
  117. ref={containerRef}
  118. className='relative flex grow flex-col overflow-y-auto bg-background-body'
  119. >
  120. <div className={cn(
  121. 'sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]',
  122. currentProviderId && 'pr-6',
  123. )}>
  124. <TabSliderNew
  125. value={activeTab}
  126. onChange={(state) => {
  127. setActiveTab(state)
  128. if (state !== activeTab)
  129. setCurrentProviderId(undefined)
  130. }}
  131. options={options}
  132. />
  133. <div className='flex items-center gap-2'>
  134. {activeTab !== 'mcp' && (
  135. <LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
  136. )}
  137. <Input
  138. showLeftIcon
  139. showClearIcon
  140. wrapperClassName='w-[200px]'
  141. value={keywords}
  142. onChange={e => handleKeywordsChange(e.target.value)}
  143. onClear={() => handleKeywordsChange('')}
  144. />
  145. </div>
  146. </div>
  147. {activeTab !== 'mcp' && (
  148. <div className={cn(
  149. 'relative grid shrink-0 grid-cols-1 content-start gap-4 px-12 pb-4 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
  150. !filteredCollectionList.length && activeTab === 'workflow' && 'grow',
  151. )}>
  152. {activeTab === 'api' && <CustomCreateCard onRefreshData={refetch} />}
  153. {filteredCollectionList.map(collection => (
  154. <div
  155. key={collection.id}
  156. onClick={() => setCurrentProviderId(collection.id)}
  157. >
  158. <Card
  159. className={cn(
  160. 'cursor-pointer border-[1.5px] border-transparent',
  161. currentProviderId === collection.id && 'border-components-option-card-option-selected-border',
  162. )}
  163. hideCornerMark
  164. payload={{
  165. ...collection,
  166. brief: collection.description,
  167. org: collection.plugin_id ? collection.plugin_id.split('/')[0] : '',
  168. name: collection.plugin_id ? collection.plugin_id.split('/')[1] : collection.name,
  169. } as any}
  170. footer={
  171. <CardMoreInfo
  172. tags={collection.labels}
  173. />
  174. }
  175. />
  176. </div>
  177. ))}
  178. {!filteredCollectionList.length && activeTab === 'workflow' && <div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'><WorkflowToolEmpty type={getToolType(activeTab)} /></div>}
  179. </div>
  180. )}
  181. {!filteredCollectionList.length && activeTab === 'builtin' && (
  182. <Empty lightCard text={t('tools.noTools')} className='h-[224px] shrink-0 px-12' />
  183. )}
  184. <div ref={toolListTailRef} />
  185. {enable_marketplace && activeTab === 'builtin' && (
  186. <Marketplace
  187. searchPluginText={keywords}
  188. filterPluginTags={tagFilterValue}
  189. isMarketplaceArrowVisible={isMarketplaceArrowVisible}
  190. showMarketplacePanel={showMarketplacePanel}
  191. marketplaceContext={marketplaceContext}
  192. />
  193. )}
  194. {activeTab === 'mcp' && (
  195. <MCPList searchText={keywords} />
  196. )}
  197. </div>
  198. </div>
  199. {currentProvider && !currentProvider.plugin_id && (
  200. <ProviderDetail
  201. collection={currentProvider}
  202. onHide={() => setCurrentProviderId(undefined)}
  203. onRefreshData={refetch}
  204. />
  205. )}
  206. <PluginDetailPanel
  207. detail={currentPluginDetail}
  208. onUpdate={() => invalidateInstalledPluginList()}
  209. onHide={() => setCurrentProviderId(undefined)}
  210. />
  211. </>
  212. )
  213. }
  214. ProviderList.displayName = 'ToolProviderList'
  215. export default ProviderList