mirror of
https://github.com/langgenius/dify.git
synced 2026-06-17 06:21:07 +08:00
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: fatelei <fatelei@gmail.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Co-authored-by: gigglewang <gigglewang@dify.ai> Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai> Co-authored-by: chariri <w@chariri.moe> Co-authored-by: Evan <2869018789@qq.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
275 lines
11 KiB
TypeScript
275 lines
11 KiB
TypeScript
'use client'
|
|
import type { ReactNode, RefObject } from 'react'
|
|
import type { ToolCategory } from '@/app/components/integrations/routes'
|
|
import type { ToolsContentInset } from '@/app/components/tools/content-inset'
|
|
import type { Collection } from '@/app/components/tools/types'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import {
|
|
ScrollAreaContent,
|
|
ScrollAreaRoot,
|
|
ScrollAreaScrollbar,
|
|
ScrollAreaThumb,
|
|
ScrollAreaViewport,
|
|
} from '@langgenius/dify-ui/scroll-area'
|
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { isSearchResultEmpty } from '@/app/components/base/search-input/search-state'
|
|
import { useTags } from '@/app/components/plugins/hooks'
|
|
import Empty from '@/app/components/plugins/marketplace/empty'
|
|
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
|
import { useCanSetPluginSettings } from '@/app/components/plugins/plugin-page/use-reference-setting'
|
|
import { toolsContentInsetClassNames, toolsUnifiedContentFrameClassName } from '@/app/components/tools/content-inset'
|
|
import Marketplace from '@/app/components/tools/marketplace'
|
|
import MCPList from '@/app/components/tools/mcp'
|
|
import ProviderDetail from '@/app/components/tools/provider/detail'
|
|
import { ToolProviderGrid } from '@/app/components/tools/tool-provider-grid'
|
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
|
import { useCheckInstalled, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
|
import { useAllToolProviders } from '@/service/use-tools'
|
|
import { useToolMarketplacePanel } from './hooks/use-tool-marketplace-panel'
|
|
import { useToolProviderCategory } from './hooks/use-tool-provider-category'
|
|
import ToolProviderCreateAction from './tool-provider-create-action'
|
|
import { ToolProviderToolbar } from './tool-provider-toolbar'
|
|
|
|
type ProviderListProps = {
|
|
category?: ToolCategory
|
|
contentInset?: ToolsContentInset
|
|
layout?: (parts: { body: ReactNode, toolbar: ReactNode }) => ReactNode
|
|
}
|
|
|
|
type BuiltinMarketplacePanelProps = {
|
|
containerRef: RefObject<HTMLDivElement | null>
|
|
contentInset: ToolsContentInset
|
|
keywords: string
|
|
tagFilterValue: string[]
|
|
}
|
|
|
|
const BuiltinMarketplacePanel = ({
|
|
containerRef,
|
|
contentInset,
|
|
keywords,
|
|
tagFilterValue,
|
|
}: BuiltinMarketplacePanelProps) => {
|
|
const {
|
|
isMarketplaceArrowVisible,
|
|
marketplaceContext,
|
|
showMarketplacePanel,
|
|
toolListTailRef,
|
|
} = useToolMarketplacePanel({
|
|
containerRef,
|
|
keywords,
|
|
tagFilterValue,
|
|
})
|
|
|
|
return (
|
|
<>
|
|
<div ref={toolListTailRef} />
|
|
<Marketplace
|
|
searchPluginText={keywords}
|
|
filterPluginTags={tagFilterValue}
|
|
isMarketplaceArrowVisible={isMarketplaceArrowVisible}
|
|
showMarketplacePanel={showMarketplacePanel}
|
|
marketplaceContext={marketplaceContext}
|
|
contentInset={contentInset}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const ProviderList = ({
|
|
category,
|
|
contentInset = 'default',
|
|
layout,
|
|
}: ProviderListProps) => {
|
|
// const searchParams = useSearchParams()
|
|
// searchParams.get('category') === 'workflow'
|
|
const { t } = useTranslation()
|
|
const { getTagLabel } = useTags()
|
|
const {
|
|
canSetPermissions,
|
|
} = useCanSetPluginSettings()
|
|
const { data: enable_marketplace } = useSuspenseQuery({
|
|
...systemFeaturesQueryOptions(),
|
|
select: s => s.enable_marketplace,
|
|
})
|
|
const { activeTab, handleCategoryChange, isRouteCategory } = useToolProviderCategory(category)
|
|
const contentPaddingClassName = toolsContentInsetClassNames[contentInset]
|
|
const toolListFrameClassName = cn(contentPaddingClassName, toolsUnifiedContentFrameClassName)
|
|
const showToolsUpdateSetting = activeTab === 'builtin' && canSetPermissions
|
|
const showLabelFilter = activeTab === 'builtin'
|
|
const options = [
|
|
{ value: 'builtin', text: t('type.builtIn', { ns: 'tools' }) },
|
|
{ value: 'api', text: t('type.custom', { ns: 'tools' }) },
|
|
{ value: 'workflow', text: t('type.workflow', { ns: 'tools' }) },
|
|
{ value: 'mcp', text: 'MCP' },
|
|
]
|
|
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
|
|
const handleTagsChange = (value: string[]) => {
|
|
setTagFilterValue(value)
|
|
}
|
|
const [keywords, setKeywords] = useState<string>('')
|
|
const handleKeywordsChange = (value: string) => {
|
|
setKeywords(value)
|
|
}
|
|
const [createdMCPProviderId, setCreatedMCPProviderId] = useState<string>()
|
|
const handleMCPProviderCreated = useCallback((providerId: string) => {
|
|
setCreatedMCPProviderId(providerId)
|
|
}, [])
|
|
const handleCreatedMCPProviderHandled = useCallback(() => {
|
|
setCreatedMCPProviderId(undefined)
|
|
}, [])
|
|
const { data: collectionList = [], isLoading: isCollectionListLoading, refetch } = useAllToolProviders()
|
|
const activeTabCollectionList = useMemo(() => {
|
|
return collectionList.filter(collection => collection.type === activeTab)
|
|
}, [activeTab, collectionList])
|
|
const hasCategoryCollections = activeTabCollectionList.length > 0
|
|
const shouldShowCustomToolCreateCard = !(activeTab === 'api' && !isCollectionListLoading && hasCategoryCollections)
|
|
const shouldShowMCPCreateCard = !(activeTab === 'mcp' && hasCategoryCollections)
|
|
const shouldShowToolbarCreateAction
|
|
= (activeTab === 'mcp' && hasCategoryCollections)
|
|
|| (activeTab === 'api' && !isCollectionListLoading && hasCategoryCollections)
|
|
const filteredCollectionList = useMemo(() => {
|
|
return activeTabCollectionList.filter((collection) => {
|
|
if (showLabelFilter && tagFilterValue.length > 0 && (!collection.labels || collection.labels.every(label => !tagFilterValue.includes(label))))
|
|
return false
|
|
if (keywords)
|
|
return Object.values(collection.label).some(value => value.toLowerCase().includes(keywords.toLowerCase()))
|
|
return true
|
|
})
|
|
}, [activeTabCollectionList, showLabelFilter, tagFilterValue, keywords])
|
|
const isFilteringCollections = !!keywords.trim() || (showLabelFilter && tagFilterValue.length > 0)
|
|
const isCollectionSearchEmpty = isSearchResultEmpty({
|
|
hasActiveFilter: isFilteringCollections,
|
|
isLoading: isCollectionListLoading,
|
|
resultCount: filteredCollectionList.length,
|
|
sourceCount: activeTabCollectionList.length,
|
|
})
|
|
|
|
const [currentProviderId, setCurrentProviderId] = useState<string | undefined>()
|
|
const currentProvider = useMemo<Collection | undefined>(() => {
|
|
return filteredCollectionList.find(collection => collection.id === currentProviderId)
|
|
}, [currentProviderId, filteredCollectionList])
|
|
const { data: checkedInstalledData } = useCheckInstalled({
|
|
pluginIds: currentProvider?.plugin_id ? [currentProvider.plugin_id] : [],
|
|
enabled: !!currentProvider?.plugin_id,
|
|
})
|
|
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
|
const currentPluginDetail = useMemo(() => {
|
|
return checkedInstalledData?.plugins?.[0]
|
|
}, [checkedInstalledData])
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
|
|
const toolbar = (
|
|
<ToolProviderToolbar
|
|
activeTab={activeTab}
|
|
currentProviderId={currentProviderId}
|
|
frameClassName={layout ? undefined : toolListFrameClassName}
|
|
isRouteCategory={isRouteCategory}
|
|
keywords={keywords}
|
|
options={options}
|
|
showLabelFilter={showLabelFilter}
|
|
showToolsUpdateSetting={showToolsUpdateSetting}
|
|
tagFilterValue={tagFilterValue}
|
|
toolbarAction={shouldShowToolbarCreateAction
|
|
? (
|
|
<ToolProviderCreateAction
|
|
activeTab={activeTab}
|
|
hasCategoryCollections={hasCategoryCollections}
|
|
isCollectionListLoading={isCollectionListLoading}
|
|
onCustomToolCreated={refetch}
|
|
onMCPProviderCreated={handleMCPProviderCreated}
|
|
/>
|
|
)
|
|
: undefined}
|
|
onCategoryChange={state => handleCategoryChange(state, () => setCurrentProviderId(undefined))}
|
|
onKeywordsChange={handleKeywordsChange}
|
|
onTagsChange={handleTagsChange}
|
|
/>
|
|
)
|
|
|
|
const body = (
|
|
<>
|
|
<div className="relative flex h-0 shrink-0 grow flex-col overflow-hidden bg-components-panel-bg">
|
|
<ScrollAreaRoot className="relative min-h-0 grow overflow-hidden bg-components-panel-bg">
|
|
<ScrollAreaViewport
|
|
ref={containerRef}
|
|
aria-label={t('menus.tools', { ns: 'common' })}
|
|
className="overscroll-contain"
|
|
role="region"
|
|
>
|
|
<ScrollAreaContent className="flex min-h-full flex-col">
|
|
{activeTab !== 'mcp' && (
|
|
<ToolProviderGrid
|
|
activeTab={activeTab}
|
|
collections={filteredCollectionList}
|
|
currentProviderId={currentProviderId}
|
|
frameClassName={toolListFrameClassName}
|
|
getTagLabel={getTagLabel}
|
|
hasCategoryCollections={activeTabCollectionList.length > 0}
|
|
isLoading={isCollectionListLoading}
|
|
useIntegrationsCard={contentInset === 'compact'}
|
|
isSearchResultEmpty={isCollectionSearchEmpty}
|
|
showCreateCard={shouldShowCustomToolCreateCard}
|
|
onRefreshData={refetch}
|
|
onSelectProvider={setCurrentProviderId}
|
|
/>
|
|
)}
|
|
{!isCollectionListLoading && !activeTabCollectionList.length && activeTab === 'builtin' && (
|
|
<Empty lightCard text={t('noTools', { ns: 'tools' })} className={cn('h-[224px] shrink-0', toolListFrameClassName)} />
|
|
)}
|
|
{isCollectionSearchEmpty && activeTab === 'builtin' && (
|
|
<div className={cn('h-[224px] shrink-0', toolListFrameClassName)} />
|
|
)}
|
|
{enable_marketplace && activeTab === 'builtin' && (
|
|
<BuiltinMarketplacePanel
|
|
containerRef={containerRef}
|
|
contentInset={contentInset}
|
|
keywords={keywords}
|
|
tagFilterValue={tagFilterValue}
|
|
/>
|
|
)}
|
|
{activeTab === 'mcp' && (
|
|
<MCPList
|
|
searchText={keywords}
|
|
contentInset={contentInset}
|
|
createdProviderId={createdMCPProviderId}
|
|
showCreateCard={shouldShowMCPCreateCard}
|
|
onCreatedProviderHandled={handleCreatedMCPProviderHandled}
|
|
/>
|
|
)}
|
|
</ScrollAreaContent>
|
|
</ScrollAreaViewport>
|
|
<ScrollAreaScrollbar className="data-[orientation=vertical]:my-1 data-[orientation=vertical]:me-1">
|
|
<ScrollAreaThumb />
|
|
</ScrollAreaScrollbar>
|
|
</ScrollAreaRoot>
|
|
</div>
|
|
{currentProvider && !currentProvider.plugin_id && (
|
|
<ProviderDetail
|
|
collection={currentProvider}
|
|
onHide={() => setCurrentProviderId(undefined)}
|
|
onRefreshData={refetch}
|
|
/>
|
|
)}
|
|
<PluginDetailPanel
|
|
detail={currentPluginDetail}
|
|
onUpdate={() => invalidateInstalledPluginList()}
|
|
onHide={() => setCurrentProviderId(undefined)}
|
|
/>
|
|
</>
|
|
)
|
|
|
|
if (layout)
|
|
return layout({ body, toolbar })
|
|
|
|
return (
|
|
<>
|
|
{toolbar}
|
|
{body}
|
|
</>
|
|
)
|
|
}
|
|
ProviderList.displayName = 'ToolProviderList'
|
|
export default ProviderList
|