'use client' import type { PluginPageTab } from './context' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiBookOpenLine, RiBugLine, RiDragDropLine, RiEqualizer2Line, } from '@remixicon/react' import { useSuspenseQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import { noop } from 'es-toolkit/function' import { cloneElement, isValidElement, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import TabSlider from '@/app/components/base/tab-slider' import ReferenceSettingModal from '@/app/components/plugins/reference-setting-modal' import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config' import { useDocLink } from '@/context/i18n' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useDocumentTitle from '@/hooks/use-document-title' import Link from '@/next/link' import { useRouter } from '@/next/navigation' import { PLUGIN_PAGE_TABS_MAP } from '../hooks' import { PluginInstallPermissionProvider } from '../install-plugin/components/plugin-install-permission-provider' import InstallFromLocalPackage from '../install-plugin/install-from-local-package' import InstallFromMarketplaceQuery from '../install-plugin/install-from-marketplace-query' import { PLUGIN_TYPE_SEARCH_MAP } from '../marketplace/constants' import { getInstallRedirectPathByPluginCategory } from '../plugin-routes' import { PluginCategoryEnum } from '../types' import { usePluginPageContext } from './context' import { PluginPageContextProvider } from './context-provider' import DebugInfo from './debug-info' import InstallPluginDropdown from './install-plugin-dropdown' import PluginTasks from './plugin-tasks' import useReferenceSetting from './use-reference-setting' import { useUploader } from './use-uploader' const pluginPageTabSet = new Set([ PLUGIN_PAGE_TABS_MAP.plugins, PLUGIN_PAGE_TABS_MAP.marketplace, ...Object.values(PLUGIN_TYPE_SEARCH_MAP), ]) const isPluginPageTab = (value: string): value is PluginPageTab => { return pluginPageTabSet.has(value) } const getCurrentInstallSearchParams = (packageId: string) => { const searchParams: Record = {} new URLSearchParams(window.location.search).forEach((value, key) => { const existing = searchParams[key] if (existing === undefined) { searchParams[key] = value return } if (Array.isArray(existing)) { existing.push(value) return } searchParams[key] = [existing, value] }) searchParams['package-ids'] ??= JSON.stringify([packageId]) return searchParams } export type PluginPageProps = { plugins: React.ReactNode marketplace: React.ReactNode } type PluginPanelPermissionProps = { canInstall?: boolean canDeletePlugin?: boolean canUpdatePlugin?: boolean } const PluginPage = ({ plugins, marketplace, }: PluginPageProps) => { const { t } = useTranslation() const docLink = useDocLink() const { replace } = useRouter() const { referenceSetting, canInstallPlugin, canUpdatePlugin, canDeletePlugin, canDebugger, canSetPermissions, canSetPluginPreferences, currentDifyVersion, isPermissionLoading, isReferenceSettingLoading, setReferenceSettings, } = useReferenceSetting(PluginCategoryEnum.tool) const handlePackageCategoryResolved = useCallback((category: string | undefined, packageId: string) => { const installRedirectPath = getInstallRedirectPathByPluginCategory(category, getCurrentInstallSearchParams(packageId)) if (!installRedirectPath) return false replace(installRedirectPath) return true }, [replace]) const [showPluginSettingModal, { setTrue: setShowPluginSettingModal, setFalse: setHidePluginSettingModal, }] = useBoolean(false) const [currentFile, setCurrentFile] = useState(null) const containerRef = usePluginPageContext(v => v.containerRef) const options = usePluginPageContext(v => v.options) const activeTab = usePluginPageContext(v => v.activeTab) const setActiveTab = usePluginPageContext(v => v.setActiveTab) const { data: enable_marketplace } = useSuspenseQuery({ ...systemFeaturesQueryOptions(), select: s => s.enable_marketplace, }) const isPluginsTab = useMemo(() => activeTab === PLUGIN_PAGE_TABS_MAP.plugins, [activeTab]) const isExploringMarketplace = useMemo(() => { const values = Object.values(PLUGIN_TYPE_SEARCH_MAP) return activeTab === PLUGIN_PAGE_TABS_MAP.marketplace || values.includes(activeTab) }, [activeTab]) useDocumentTitle(isExploringMarketplace ? t('mainNav.marketplace', { ns: 'common' }) : t('metadata.title', { ns: 'plugin' })) const handleFileChange = (file: File | null) => { if (!canInstallPlugin) { setCurrentFile(null) return } if (!file || !file.name.endsWith('.difypkg')) { setCurrentFile(null) return } setCurrentFile(file) } const uploaderProps = useUploader({ onFileChange: handleFileChange, containerRef, enabled: isPluginsTab && canInstallPlugin, }) const { dragging, fileUploader, fileChangeHandle, removeFile } = uploaderProps const pluginsWithPermission = useMemo(() => { if (!isValidElement(plugins) || typeof plugins.type === 'string') return plugins return cloneElement(plugins as React.ReactElement, { canInstall: canInstallPlugin, canDeletePlugin, canUpdatePlugin, }) }, [canInstallPlugin, canDeletePlugin, canUpdatePlugin, plugins]) return (
{ if (isPluginPageTab(nextTab)) setActiveTab(nextTab) }} options={options} />
{ isExploringMarketplace && ( <>
) } {(canInstallPlugin || isPermissionLoading) && ( setActiveTab('discover')} /> )} { canDebugger && ( ) } {isPermissionLoading && ( )} { (canSetPermissions || canSetPluginPreferences) && ( ) }
{isPluginsTab && ( <> {pluginsWithPermission} {dragging && (
)} {canInstallPlugin && (
{t('installModal.dropPluginToInstall', { ns: 'plugin' })}
)} {currentFile && ( )} )} { isExploringMarketplace && enable_marketplace && ( {marketplace} ) } {showPluginSettingModal && referenceSetting && ( )}
) } const PluginPageWithContext = (props: PluginPageProps) => { return ( ) } export default PluginPageWithContext