'use client' import type { Placement } from '@langgenius/dify-ui/popover' import type { ReactElement } from 'react' import type { ToolDefaultValue, ToolValue } from './types' import type { CustomCollectionBackend } from '@/app/components/tools/types' import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover' import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import SearchBox from '@/app/components/plugins/marketplace/search-box' import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' import { useCanManageTools } from '@/app/components/tools/hooks/use-tool-permissions' import ToolBrowser from '@/app/components/workflow/block-selector/tool-browser' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { createCustomCollection } from '@/service/tools' import { useFeaturedToolsRecommendations } from '@/service/use-plugins' import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools, useInvalidateAllCustomTools, useInvalidateAllMCPTools, useInvalidateAllWorkflowTools, } from '@/service/use-tools' type Props = Readonly<{ disabled: boolean trigger: ReactElement placement?: Placement sideOffset?: number isShow: boolean onShowChange: (isShow: boolean) => void }> & ToolPickerContentProps export type ToolPickerContentProps = Readonly<{ focusSearchOnMount?: boolean panelClassName?: string onSelect: (tool: ToolDefaultValue) => void onSelectMultiple: (tools: ToolDefaultValue[]) => void supportAddCustomTool?: boolean scope?: ToolPickerScope selectedTools?: ToolValue[] }> export type ToolPickerScope = 'all' | 'plugins' | 'custom' | 'workflow' export function ToolPickerContent({ focusSearchOnMount = false, onSelect, onSelectMultiple, supportAddCustomTool, scope = 'all', selectedTools, panelClassName, }: ToolPickerContentProps) { const { t } = useTranslation() const [searchText, setSearchText] = useState('') const [tags, setTags] = useState([]) const canManageTools = useCanManageTools() const { data: enable_marketplace } = useSuspenseQuery({ ...systemFeaturesQueryOptions(), select: (s) => s.enable_marketplace, }) const { data: buildInTools } = useAllBuiltInTools() const shouldFetchCustomTools = scope !== 'plugins' && scope !== 'workflow' const { data: customTools } = useAllCustomTools(shouldFetchCustomTools) const invalidateCustomTools = useInvalidateAllCustomTools() const shouldFetchWorkflowTools = scope !== 'plugins' && scope !== 'custom' const { data: workflowTools } = useAllWorkflowTools(shouldFetchWorkflowTools) const { data: mcpTools } = useAllMCPTools() const invalidateBuiltInTools = useInvalidateAllBuiltInTools() const invalidateWorkflowTools = useInvalidateAllWorkflowTools() const invalidateMcpTools = useInvalidateAllMCPTools() const { plugins: featuredPlugins = [], isLoading: isFeaturedLoading } = useFeaturedToolsRecommendations(enable_marketplace) const { builtinToolList, customToolList, workflowToolList } = useMemo(() => { if (scope === 'plugins') { return { builtinToolList: buildInTools, customToolList: [], workflowToolList: [], } } if (scope === 'custom') { return { builtinToolList: [], customToolList: customTools, workflowToolList: [], } } if (scope === 'workflow') { return { builtinToolList: [], customToolList: [], workflowToolList: workflowTools, } } return { builtinToolList: buildInTools, customToolList: customTools, workflowToolList: workflowTools, } }, [scope, buildInTools, customTools, workflowTools]) const handleSelect = (_type: BlockEnum, tool?: ToolDefaultValue) => { onSelect(tool!) } const handleSelectMultiple = (_type: BlockEnum, tools: ToolDefaultValue[]) => { onSelectMultiple(tools) } const [ isShowEditCollectionToolModal, { setFalse: hideEditCustomCollectionModal, setTrue: showEditCustomCollectionModal }, ] = useBoolean(false) const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => { if (!canManageTools) return await createCustomCollection(data) toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) hideEditCustomCollectionModal() invalidateCustomTools() } if (isShowEditCollectionToolModal && canManageTools) { return ( ) } return (
$.searchTools, { ns: 'plugin' })!} supportAddCustomTool={supportAddCustomTool && canManageTools} onShowAddCustomCollectionModal={showEditCustomCollectionModal} // The picker replaces the focused menu item inside an already-open popover. // Focusing search keeps keyboard users in the same add-tool workflow. /* oxlint-disable-next-line jsx-a11y/no-autofocus */ autoFocus={focusSearchOnMount} inputClassName="grow" />
{ invalidateBuiltInTools() invalidateCustomTools() invalidateWorkflowTools() invalidateMcpTools() }} />
) } function ToolPicker({ disabled, trigger, placement = 'right-start', sideOffset = 0, isShow, onShowChange, ...contentProps }: Props) { const { t } = useTranslation() const handleOpenChange = (nextOpen: boolean) => { if (nextOpen && disabled) return onShowChange(nextOpen) } return ( {t(($) => $['detailPanel.toolSelector.title'], { ns: 'plugin' })} ) } export default ToolPicker