'use client' import type { Tool as ToolType } from '../../../tools/types' import type { ToolWithProvider } from '../../types' import type { ToolDefaultValue, ToolValue } from '../types' import type { ToolActionPreviewCardHandle } from './action-item' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible' import * as React from 'react' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability' import { useGetLanguage } from '@/context/i18n' import useTheme from '@/hooks/use-theme' import { Theme } from '@/types/app' import { basePath } from '@/utils/var' import { CollectionType } from '../../../tools/types' import BlockIcon from '../../block-icon' import McpToolNotSupportTooltip from '../../nodes/_base/components/mcp-tool-not-support-tooltip' import { BlockEnum } from '../../types' import { ViewType } from '../types' import ActionItem from './action-item' const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => { if (!icon) return icon! if ( typeof icon === 'string' && basePath && icon.startsWith('/') && !icon.startsWith(`${basePath}/`) ) return `${basePath}${icon}` return icon } type Props = Readonly<{ className?: string payload: ToolWithProvider previewCardHandle: ToolActionPreviewCardHandle viewType: ViewType hasSearchText: boolean onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void canNotSelectMultiple?: boolean onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void selectedTools?: ToolValue[] isShowLetterIndex?: boolean }> function Tool({ className, payload, previewCardHandle, viewType, hasSearchText, onSelect, canNotSelectMultiple, onSelectMultiple, selectedTools, }: Props) { const { t } = useTranslation() const { allowed: isMCPToolAllowed } = useMCPToolAvailability() const language = useGetLanguage() const isFlatView = viewType === ViewType.flat const notShowProvider = payload.type === CollectionType.workflow const actions = payload.tools const panelId = React.useId() const [open, setOpen] = React.useState(hasSearchText) const [previousHasSearchText, setPreviousHasSearchText] = React.useState(hasSearchText) const isMCPTool = payload.type === CollectionType.mcp const isShowCanNotChooseMCPTip = !isMCPToolAllowed && isMCPTool const { theme } = useTheme() const normalizedIcon = useMemo(() => { return normalizeProviderIcon(payload.icon) ?? payload.icon }, [payload.icon]) const normalizedIconDark = useMemo(() => { if (!payload.icon_dark) return undefined! return normalizeProviderIcon(payload.icon_dark) ?? payload.icon_dark }, [payload.icon_dark]) const providerIcon = useMemo(() => { if (theme === Theme.dark && normalizedIconDark) return normalizedIconDark return normalizedIcon }, [theme, normalizedIcon, normalizedIconDark]) const getIsDisabled = useCallback( (tool: ToolType) => { if (!selectedTools || !selectedTools.length) return false return selectedTools.some( (selectedTool) => (selectedTool.provider_name === payload.name || selectedTool.provider_name === payload.id) && selectedTool.tool_name === tool.name, ) }, [payload.id, payload.name, selectedTools], ) const totalToolsNum = actions.length const selectedToolsNum = actions.filter((action) => getIsDisabled(action)).length const isAllSelected = selectedToolsNum === totalToolsNum if (previousHasSearchText !== hasSearchText) { setPreviousHasSearchText(hasSearchText) setOpen(hasSearchText) } const groupName = useMemo(() => { if (payload.type === CollectionType.builtIn) return payload.author if (payload.type === CollectionType.custom) return t(($) => $['tabs.customTool'], { ns: 'workflow' }) if (payload.type === CollectionType.workflow) return t(($) => $['tabs.workflowTool'], { ns: 'workflow' }) return '' }, [payload.author, payload.type, t]) const providerDetails = (
{notShowProvider ? actions[0]?.label[language] : payload.label[language]} {isFlatView && groupName && ( {groupName} )} {isMCPTool && ( )}
) const selectedStatus = selectedToolsNum ? isAllSelected ? t(($) => $['tabs.allAdded'], { ns: 'workflow' }) : `${selectedToolsNum} / ${totalToolsNum}` : undefined const handleSelectAll = () => { onSelectMultiple?.( BlockEnum.Tool, actions .filter((action) => !getIsDisabled(action)) .map((tool) => { const params: Record = {} tool.parameters?.forEach((item) => { params[item.name] = '' }) return { provider_id: payload.id, provider_type: payload.type, provider_name: payload.name, provider_show_name: payload.label[language], plugin_id: payload.plugin_id!, plugin_unique_identifier: payload.plugin_unique_identifier!, provider_icon: normalizedIcon, provider_icon_dark: normalizedIconDark, tool_name: tool.name, tool_label: tool.label[language]!, tool_description: tool.description[language], title: tool.label[language]!, is_team_authorization: payload.is_team_authorization, paramSchemas: tool.parameters, params, } }), ) } const handleWorkflowToolSelect = () => { const tool = actions[0]! const params: Record = {} tool.parameters?.forEach((item) => { params[item.name] = '' }) onSelect(BlockEnum.Tool, { provider_id: payload.id, provider_type: payload.type, provider_name: payload.name, provider_show_name: payload.label[language], plugin_id: payload.plugin_id, plugin_unique_identifier: payload.plugin_unique_identifier, provider_icon: normalizedIcon, provider_icon_dark: normalizedIconDark, tool_name: tool.name, tool_label: tool.label[language]!, tool_description: tool.description[language], title: tool.label[language]!, is_team_authorization: payload.is_team_authorization, paramSchemas: tool.parameters, params, }) } if (notShowProvider) { return (
) } return (
{providerDetails} {!isShowCanNotChooseMCPTip && !canNotSelectMultiple && selectedStatus && ( {selectedStatus} )} {!isShowCanNotChooseMCPTip && !canNotSelectMultiple && !isAllSelected && onSelectMultiple && ( )} {isShowCanNotChooseMCPTip && (
)}
{actions.map((action) => ( ))}
) } export default React.memo(Tool)