'use client' import type { ReactNode } from 'react' import type { PluginDetail } from '../types' import type { PluginPageContentInset } from './content-inset' import type { FilterState } from './filter-management' import { cn } from '@langgenius/dify-ui/cn' import { useSuspenseQuery } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' import { useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { isSearchResultEmpty } from '@/app/components/base/search-input/search-state' import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' import ProviderDetail from '@/app/components/tools/provider/detail' import { useGetLanguage } from '@/context/i18n' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { renderI18nObject } from '@/i18n-config' import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' import { usePluginsWithLatestVersion } from '../hooks' import { PluginCategoryEnum } from '../types' import { pluginPageContentFrameClassNames, pluginPageContentInsetClassNames } from './content-inset' import { usePluginPageContext } from './context' import Empty from './empty' import FilterManagement from './filter-management' import PluginListSkeleton from './plugin-list-skeleton' import PluginsPanelResults from './plugins-panel-results' import { EMPTY_BUILTIN_TOOLS, filterBuiltinTools } from './plugins-panel-utils' const matchesSearchQuery = (plugin: PluginDetail & { latest_version: string }, query: string, locale: string): boolean => { if (!query) return true const lowerQuery = query.toLowerCase() const { declaration } = plugin // Match plugin_id if (plugin.plugin_id.toLowerCase().includes(lowerQuery)) return true // Match plugin name if (plugin.name?.toLowerCase().includes(lowerQuery)) return true // Match declaration name if (declaration.name?.toLowerCase().includes(lowerQuery)) return true // Match localized label const label = renderI18nObject(declaration.label, locale) if (label?.toLowerCase().includes(lowerQuery)) return true // Match localized description const description = renderI18nObject(declaration.description, locale) if (description?.toLowerCase().includes(lowerQuery)) return true return false } type PluginsPanelProps = { canInstall?: boolean contentInset?: PluginPageContentInset fixedCategory?: PluginCategoryEnum layout?: (parts: { body: ReactNode, toolbar: ReactNode }) => ReactNode onSwitchToMarketplace?: () => void toolbarAction?: ReactNode } const PluginsPanel = ({ canInstall = true, contentInset = 'default', fixedCategory, layout, onSwitchToMarketplace, toolbarAction, }: PluginsPanelProps) => { const { t } = useTranslation() const locale = useGetLanguage() const filters = usePluginPageContext(v => v.filters) as FilterState const setFilters = usePluginPageContext(v => v.setFilters) const isToolIntegrationPage = fixedCategory === PluginCategoryEnum.tool const isTriggerIntegrationPage = fixedCategory === PluginCategoryEnum.trigger const isAgentStrategyIntegrationPage = fixedCategory === PluginCategoryEnum.agent const isExtensionIntegrationPage = fixedCategory === PluginCategoryEnum.extension const isIntegrationCategoryPage = isToolIntegrationPage || isTriggerIntegrationPage || isAgentStrategyIntegrationPage || isExtensionIntegrationPage const supportsTagFilter = !fixedCategory || isToolIntegrationPage || isTriggerIntegrationPage const { data: enableMarketplace } = useSuspenseQuery({ ...systemFeaturesQueryOptions(), select: s => s.enable_marketplace, }) const { data: pluginList, isLoading: isPluginListLoading, isFetching, isLastPage, loadNextPage } = useInstalledPluginList( undefined, 100, fixedCategory ? { category: fixedCategory, refetchOnMount: isIntegrationCategoryPage ? 'always' : undefined, } : undefined, ) const pluginListWithLatestVersion = usePluginsWithLatestVersion(pluginList?.plugins) const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const currentPluginID = usePluginPageContext(v => v.currentPluginID) const setCurrentPluginID = usePluginPageContext(v => v.setCurrentPluginID) const [currentBuiltinToolID, setCurrentBuiltinToolID] = useState() const containerRef = useRef(null) const { run: handleFilterChange } = useDebounceFn((filters: FilterState) => { setFilters(filters) }, { wait: 500 }) const categoryList = useMemo(() => { if (!fixedCategory) return pluginListWithLatestVersion return pluginListWithLatestVersion.filter(plugin => plugin.declaration.category === fixedCategory) }, [fixedCategory, pluginListWithLatestVersion]) const filteredList = useMemo(() => { const { categories, searchQuery, tags } = filters const filteredList = categoryList.filter((plugin) => { return ( (fixedCategory || categories.length === 0 || categories.includes(plugin.declaration.category)) && (!supportsTagFilter || tags.length === 0 || tags.some(tag => plugin.declaration.tags.includes(tag))) && matchesSearchQuery(plugin, searchQuery, locale) ) }) return filteredList }, [categoryList, fixedCategory, supportsTagFilter, filters, locale]) const builtinTools = isToolIntegrationPage ? pluginList?.builtin_tools ?? EMPTY_BUILTIN_TOOLS : EMPTY_BUILTIN_TOOLS const filteredBuiltinTools = useMemo(() => { if (!isToolIntegrationPage) return [] return filterBuiltinTools(builtinTools, filters.searchQuery, locale, filters.tags) }, [builtinTools, filters.searchQuery, filters.tags, isToolIntegrationPage, locale]) const hasVisiblePlugins = (filteredList?.length ?? 0) > 0 const hasVisibleBuiltinTools = filteredBuiltinTools.length > 0 const isFilteringCategory = !!filters.searchQuery.trim() || (supportsTagFilter && filters.tags.length > 0) const isIntegrationCategorySearchEmpty = isIntegrationCategoryPage && isSearchResultEmpty({ hasActiveFilter: isFilteringCategory, isLoading: isPluginListLoading, resultCount: filteredList.length + filteredBuiltinTools.length, sourceCount: categoryList.length + builtinTools.length, }) const currentPluginDetail = useMemo(() => { const detail = pluginListWithLatestVersion.find(plugin => plugin.plugin_id === currentPluginID) return detail }, [currentPluginID, pluginListWithLatestVersion]) const currentBuiltinTool = useMemo(() => { return filteredBuiltinTools.find(collection => collection.id === currentBuiltinToolID) }, [currentBuiltinToolID, filteredBuiltinTools]) const handleHide = () => setCurrentPluginID(undefined) const handleBuiltinToolHide = () => setCurrentBuiltinToolID(undefined) const hasToolMarketplacePanel = enableMarketplace && isToolIntegrationPage const contentPaddingClassName = pluginPageContentInsetClassNames[contentInset] const contentFrameClassName = cn( pluginPageContentFrameClassNames[contentInset], contentPaddingClassName, ) const emptyVariant = isTriggerIntegrationPage ? 'integrationsTrigger' : isAgentStrategyIntegrationPage ? 'integrationsAgentStrategy' : isExtensionIntegrationPage ? 'integrationsExtension' : 'default' const scrollAreaLabel = isTriggerIntegrationPage ? t('categorySingle.trigger', { ns: 'plugin' }) : isAgentStrategyIntegrationPage ? t('categorySingle.agent', { ns: 'plugin' }) : isExtensionIntegrationPage ? t('categorySingle.extension', { ns: 'plugin' }) : isToolIntegrationPage ? t('categorySingle.tool', { ns: 'plugin' }) : undefined const toolbar = (
{!layout && !isIntegrationCategoryPage &&
}
) const body = ( <> {isPluginListLoading && } {!isPluginListLoading && ( <> {hasVisiblePlugins || hasVisibleBuiltinTools || hasToolMarketplacePanel ? ( ) : isIntegrationCategorySearchEmpty ? (
) : ( )} )} ) return ( <> {layout ? layout({ body, toolbar }) : ( <> {toolbar} {body} )} invalidateInstalledPluginList()} onHide={handleHide} /> {currentBuiltinTool && !currentBuiltinTool.plugin_id && ( )} ) } export default PluginsPanel