| | |
| | | 'use client' |
| | | import { useMemo, useRef, useState } from 'react' |
| | | import { useEffect, useMemo, useState } from 'react' |
| | | import { useTranslation } from 'react-i18next' |
| | | import { RiCloseLine } from '@remixicon/react' |
| | | import type { Collection } from './types' |
| | | import Marketplace from './marketplace' |
| | | import cn from '@/utils/classnames' |
| | | import { useTabSearchParams } from '@/hooks/use-tab-searchparams' |
| | | import TabSliderNew from '@/app/components/base/tab-slider-new' |
| | | import LabelFilter from '@/app/components/tools/labels/filter' |
| | | import Input from '@/app/components/base/input' |
| | | import ProviderDetail from '@/app/components/tools/provider/detail' |
| | | import Empty from '@/app/components/plugins/marketplace/empty' |
| | | import { DotsGrid } from '@/app/components/base/icons/src/vender/line/general' |
| | | import { Colors } from '@/app/components/base/icons/src/vender/line/others' |
| | | import { Route } from '@/app/components/base/icons/src/vender/line/mapsAndTravel' |
| | | import CustomCreateCard from '@/app/components/tools/provider/custom-create-card' |
| | | import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty' |
| | | import Card from '@/app/components/plugins/card' |
| | | import CardMoreInfo from '@/app/components/plugins/card/card-more-info' |
| | | import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' |
| | | import { useSelector as useAppContextSelector } from '@/context/app-context' |
| | | import { useAllToolProviders } from '@/service/use-tools' |
| | | import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' |
| | | import ContributeCard from '@/app/components/tools/provider/contribute' |
| | | import ProviderCard from '@/app/components/tools/provider/card' |
| | | import ProviderDetail from '@/app/components/tools/provider/detail' |
| | | import Empty from '@/app/components/tools/add-tool-modal/empty' |
| | | import { fetchCollectionList } from '@/service/tools' |
| | | |
| | | const ProviderList = () => { |
| | | const { t } = useTranslation() |
| | | const containerRef = useRef<HTMLDivElement>(null) |
| | | const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) |
| | | |
| | | const [activeTab, setActiveTab] = useTabSearchParams({ |
| | | defaultTab: 'builtin', |
| | | }) |
| | | const options = [ |
| | | { value: 'builtin', text: t('tools.type.builtIn') }, |
| | | { value: 'api', text: t('tools.type.custom') }, |
| | | { value: 'workflow', text: t('tools.type.workflow') }, |
| | | { value: 'builtin', text: t('tools.type.builtIn'), icon: <DotsGrid className='w-[14px] h-[14px] mr-1' /> }, |
| | | { value: 'api', text: t('tools.type.custom'), icon: <Colors className='w-[14px] h-[14px] mr-1' /> }, |
| | | { value: 'workflow', text: t('tools.type.workflow'), icon: <Route className='w-[14px] h-[14px] mr-1' /> }, |
| | | ] |
| | | const [tagFilterValue, setTagFilterValue] = useState<string[]>([]) |
| | | const handleTagsChange = (value: string[]) => { |
| | |
| | | const handleKeywordsChange = (value: string) => { |
| | | setKeywords(value) |
| | | } |
| | | const { data: collectionList = [], refetch } = useAllToolProviders() |
| | | |
| | | const [collectionList, setCollectionList] = useState<Collection[]>([]) |
| | | const filteredCollectionList = useMemo(() => { |
| | | return collectionList.filter((collection) => { |
| | | if (collection.type !== activeTab) |
| | |
| | | return true |
| | | }) |
| | | }, [activeTab, tagFilterValue, keywords, collectionList]) |
| | | const getProviderList = async () => { |
| | | const list = await fetchCollectionList() |
| | | setCollectionList([...list]) |
| | | } |
| | | useEffect(() => { |
| | | getProviderList() |
| | | }, []) |
| | | |
| | | const [currentProviderId, setCurrentProviderId] = useState<string | undefined>() |
| | | const currentProvider = useMemo<Collection | undefined>(() => { |
| | | return filteredCollectionList.find(collection => collection.id === currentProviderId) |
| | | }, [currentProviderId, filteredCollectionList]) |
| | | const { data: pluginList } = useInstalledPluginList() |
| | | const invalidateInstalledPluginList = useInvalidateInstalledPluginList() |
| | | const currentPluginDetail = useMemo(() => { |
| | | const detail = pluginList?.plugins.find(plugin => plugin.plugin_id === currentProvider?.plugin_id) |
| | | return detail |
| | | }, [currentProvider?.plugin_id, pluginList?.plugins]) |
| | | const [currentProvider, setCurrentProvider] = useState<Collection | undefined>() |
| | | useEffect(() => { |
| | | if (currentProvider && collectionList.length > 0) { |
| | | const newCurrentProvider = collectionList.find(collection => collection.id === currentProvider.id) |
| | | setCurrentProvider(newCurrentProvider) |
| | | } |
| | | }, [collectionList, currentProvider]) |
| | | |
| | | return ( |
| | | <> |
| | | <div className='relative flex h-0 shrink-0 grow overflow-hidden'> |
| | | <div |
| | | ref={containerRef} |
| | | className='relative flex grow flex-col overflow-y-auto bg-background-body' |
| | | > |
| | | <div className={cn( |
| | | 'sticky top-0 z-20 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]', |
| | | currentProviderId && 'pr-6', |
| | | )}> |
| | | <TabSliderNew |
| | | value={activeTab} |
| | | onChange={(state) => { |
| | | setActiveTab(state) |
| | | if (state !== activeTab) |
| | | setCurrentProviderId(undefined) |
| | | }} |
| | | options={options} |
| | | <div className='relative flex overflow-hidden bg-gray-100 shrink-0 h-0 grow'> |
| | | <div className='relative flex flex-col overflow-y-auto bg-gray-100 grow'> |
| | | <div className={cn( |
| | | 'sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-20 flex-wrap gap-y-2', |
| | | currentProvider && 'pr-6', |
| | | )}> |
| | | <TabSliderNew |
| | | value={activeTab} |
| | | onChange={(state) => { |
| | | setActiveTab(state) |
| | | if (state !== activeTab) |
| | | setCurrentProvider(undefined) |
| | | }} |
| | | options={options} |
| | | /> |
| | | <div className='flex items-center gap-2'> |
| | | <LabelFilter value={tagFilterValue} onChange={handleTagsChange} /> |
| | | <Input |
| | | showLeftIcon |
| | | showClearIcon |
| | | wrapperClassName='w-[200px]' |
| | | value={keywords} |
| | | onChange={e => handleKeywordsChange(e.target.value)} |
| | | onClear={() => handleKeywordsChange('')} |
| | | /> |
| | | <div className='flex items-center gap-2'> |
| | | <LabelFilter value={tagFilterValue} onChange={handleTagsChange} /> |
| | | <Input |
| | | showLeftIcon |
| | | showClearIcon |
| | | wrapperClassName='w-[200px]' |
| | | value={keywords} |
| | | onChange={e => handleKeywordsChange(e.target.value)} |
| | | onClear={() => handleKeywordsChange('')} |
| | | /> |
| | | </div> |
| | | </div> |
| | | {(filteredCollectionList.length > 0 || activeTab !== 'builtin') && ( |
| | | <div className={cn( |
| | | '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', |
| | | !filteredCollectionList.length && activeTab === 'workflow' && 'grow', |
| | | )}> |
| | | {activeTab === 'api' && <CustomCreateCard onRefreshData={refetch} />} |
| | | {filteredCollectionList.map(collection => ( |
| | | <div |
| | | key={collection.id} |
| | | onClick={() => setCurrentProviderId(collection.id)} |
| | | > |
| | | <Card |
| | | className={cn( |
| | | 'cursor-pointer border-[1.5px] border-transparent', |
| | | currentProviderId === collection.id && 'border-components-option-card-option-selected-border', |
| | | )} |
| | | hideCornerMark |
| | | payload={{ |
| | | ...collection, |
| | | brief: collection.description, |
| | | org: collection.plugin_id ? collection.plugin_id.split('/')[0] : '', |
| | | name: collection.plugin_id ? collection.plugin_id.split('/')[1] : collection.name, |
| | | } as any} |
| | | footer={ |
| | | <CardMoreInfo |
| | | tags={collection.labels} |
| | | /> |
| | | } |
| | | /> |
| | | </div> |
| | | ))} |
| | | {!filteredCollectionList.length && activeTab === 'workflow' && <div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'><WorkflowToolEmpty /></div>} |
| | | </div> |
| | | )} |
| | | {!filteredCollectionList.length && activeTab === 'builtin' && ( |
| | | <Empty lightCard text={t('tools.noTools')} className='h-[224px] px-12' /> |
| | | )} |
| | | { |
| | | enable_marketplace && activeTab === 'builtin' && ( |
| | | <Marketplace |
| | | onMarketplaceScroll={() => { |
| | | containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' }) |
| | | }} |
| | | searchPluginText={keywords} |
| | | filterPluginTags={tagFilterValue} |
| | | /> |
| | | ) |
| | | } |
| | | </div> |
| | | <div className={cn( |
| | | 'relative grid content-start grid-cols-1 gap-4 px-12 pt-2 pb-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0', |
| | | currentProvider && 'pr-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3', |
| | | )}> |
| | | {activeTab === 'builtin' && <ContributeCard />} |
| | | {activeTab === 'api' && <CustomCreateCard onRefreshData={getProviderList} />} |
| | | {filteredCollectionList.map(collection => ( |
| | | <ProviderCard |
| | | active={currentProvider?.id === collection.id} |
| | | onSelect={() => setCurrentProvider(collection)} |
| | | key={collection.id} |
| | | collection={collection} |
| | | /> |
| | | ))} |
| | | {!filteredCollectionList.length && <div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'><Empty /></div>} |
| | | </div> |
| | | </div> |
| | | {currentProvider && !currentProvider.plugin_id && ( |
| | | <ProviderDetail |
| | | collection={currentProvider} |
| | | onHide={() => setCurrentProviderId(undefined)} |
| | | onRefreshData={refetch} |
| | | /> |
| | | )} |
| | | <PluginDetailPanel |
| | | detail={currentPluginDetail} |
| | | onUpdate={() => invalidateInstalledPluginList()} |
| | | onHide={() => setCurrentProviderId(undefined)} |
| | | /> |
| | | </> |
| | | <div className={cn( |
| | | 'shrink-0 w-0 border-l-[0.5px] border-black/8 overflow-y-auto transition-all duration-200 ease-in-out', |
| | | currentProvider && 'w-[420px]', |
| | | )}> |
| | | {currentProvider && <ProviderDetail collection={currentProvider} onRefreshData={getProviderList} />} |
| | | </div> |
| | | <div className='absolute top-5 right-5 p-1 cursor-pointer' onClick={() => setCurrentProvider(undefined)}><RiCloseLine className='w-4 h-4' /></div> |
| | | </div> |
| | | ) |
| | | } |
| | | ProviderList.displayName = 'ToolProviderList' |