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 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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 { useCheckInstalled, 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: checkedInstalledData } = useCheckInstalled({
  78. pluginIds: currentProvider?.plugin_id ? [currentProvider.plugin_id] : [],
  79. enabled: !!currentProvider?.plugin_id,
  80. })
  81. const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
  82. const currentPluginDetail = useMemo(() => {
  83. return checkedInstalledData?.plugins?.[0]
  84. }, [checkedInstalledData])
  85. const toolListTailRef = useRef<HTMLDivElement>(null)
  86. const showMarketplacePanel = useCallback(() => {
  87. containerRef.current?.scrollTo({
  88. top: toolListTailRef.current
  89. ? toolListTailRef.current?.offsetTop - 80
  90. : 0,
  91. behavior: 'smooth',
  92. })
  93. }, [toolListTailRef])
  94. const marketplaceContext = useMarketplace(keywords, tagFilterValue)
  95. const {
  96. handleScroll,
  97. } = marketplaceContext
  98. const [isMarketplaceArrowVisible, setIsMarketplaceArrowVisible] = useState(true)
  99. const onContainerScroll = useMemo(() => {
  100. return (e: Event) => {
  101. handleScroll(e)
  102. if (containerRef.current && toolListTailRef.current)
  103. setIsMarketplaceArrowVisible(containerRef.current.scrollTop < (toolListTailRef.current?.offsetTop - 80))
  104. }
  105. }, [handleScroll, containerRef, toolListTailRef, setIsMarketplaceArrowVisible])
  106. useEffect(() => {
  107. const container = containerRef.current
  108. if (container)
  109. container.addEventListener('scroll', onContainerScroll)
  110. return () => {
  111. if (container)
  112. container.removeEventListener('scroll', onContainerScroll)
  113. }
  114. }, [onContainerScroll])
  115. return (
  116. <>
  117. <div className='relative flex h-0 shrink-0 grow overflow-hidden'>
  118. <div
  119. ref={containerRef}
  120. className='relative flex grow flex-col overflow-y-auto bg-background-body'
  121. >
  122. <div className={cn(
  123. '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]',
  124. currentProviderId && 'pr-6',
  125. )}>
  126. <TabSliderNew
  127. value={activeTab}
  128. onChange={(state) => {
  129. setActiveTab(state)
  130. if (state !== activeTab)
  131. setCurrentProviderId(undefined)
  132. }}
  133. options={options}
  134. />
  135. <div className='flex items-center gap-2'>
  136. {activeTab !== 'mcp' && (
  137. <LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
  138. )}
  139. <Input
  140. showLeftIcon
  141. showClearIcon
  142. wrapperClassName='w-[200px]'
  143. value={keywords}
  144. onChange={e => handleKeywordsChange(e.target.value)}
  145. onClear={() => handleKeywordsChange('')}
  146. />
  147. </div>
  148. </div>
  149. {activeTab !== 'mcp' && (
  150. <div className={cn(
  151. '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',
  152. !filteredCollectionList.length && activeTab === 'workflow' && 'grow',
  153. )}>
  154. {activeTab === 'api' && <CustomCreateCard onRefreshData={refetch} />}
  155. {filteredCollectionList.map(collection => (
  156. <div
  157. key={collection.id}
  158. onClick={() => setCurrentProviderId(collection.id)}
  159. >
  160. <Card
  161. className={cn(
  162. 'cursor-pointer border-[1.5px] border-transparent',
  163. currentProviderId === collection.id && 'border-components-option-card-option-selected-border',
  164. )}
  165. hideCornerMark
  166. payload={{
  167. ...collection,
  168. brief: collection.description,
  169. org: collection.plugin_id ? collection.plugin_id.split('/')[0] : '',
  170. name: collection.plugin_id ? collection.plugin_id.split('/')[1] : collection.name,
  171. } as any}
  172. footer={
  173. <CardMoreInfo
  174. tags={collection.labels}
  175. />
  176. }
  177. />
  178. </div>
  179. ))}
  180. {!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>}
  181. </div>
  182. )}
  183. {!filteredCollectionList.length && activeTab === 'builtin' && (
  184. <Empty lightCard text={t('tools.noTools')} className='h-[224px] shrink-0 px-12' />
  185. )}
  186. <div ref={toolListTailRef} />
  187. {enable_marketplace && activeTab === 'builtin' && (
  188. <Marketplace
  189. searchPluginText={keywords}
  190. filterPluginTags={tagFilterValue}
  191. isMarketplaceArrowVisible={isMarketplaceArrowVisible}
  192. showMarketplacePanel={showMarketplacePanel}
  193. marketplaceContext={marketplaceContext}
  194. />
  195. )}
  196. {activeTab === 'mcp' && (
  197. <MCPList searchText={keywords} />
  198. )}
  199. </div>
  200. </div>
  201. {currentProvider && !currentProvider.plugin_id && (
  202. <ProviderDetail
  203. collection={currentProvider}
  204. onHide={() => setCurrentProviderId(undefined)}
  205. onRefreshData={refetch}
  206. />
  207. )}
  208. <PluginDetailPanel
  209. detail={currentPluginDetail}
  210. onUpdate={() => invalidateInstalledPluginList()}
  211. onHide={() => setCurrentProviderId(undefined)}
  212. />
  213. </>
  214. )
  215. }
  216. ProviderList.displayName = 'ToolProviderList'
  217. export default ProviderList