diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx index 6c2c81a916..f7b9e8624e 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx @@ -11,26 +11,20 @@ import Link from 'next/link' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import Divider from '@/app/components/base/divider' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' -import TabSlider from '@/app/components/base/tab-slider-plain' import Textarea from '@/app/components/base/textarea' -import { - AuthCategory, - PluginAuthInAgent, -} from '@/app/components/plugins/plugin-auth' import { usePluginInstalledCheck } from '@/app/components/plugins/plugin-detail-panel/tool-selector/hooks' -import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form' +import ToolAuthorizationSection from '@/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-authorization-section' +import ToolSettingsSection from '@/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-settings-section' import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item' import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-trigger' import { CollectionType } from '@/app/components/tools/types' -import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' +import { generateFormValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' -import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form' import { MARKETPLACE_API_PREFIX } from '@/config' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { @@ -151,41 +145,6 @@ const ToolSelector: FC = ({ } as any) } - // tool settings & params - const currentToolSettings = useMemo(() => { - if (!currentProvider) - return [] - return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form !== 'llm') || [] - }, [currentProvider, value]) - const currentToolParams = useMemo(() => { - if (!currentProvider) - return [] - return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form === 'llm') || [] - }, [currentProvider, value]) - const [currType, setCurrType] = useState('settings') - const showTabSlider = currentToolSettings.length > 0 && currentToolParams.length > 0 - const userSettingsOnly = currentToolSettings.length > 0 && !currentToolParams.length - const reasoningConfigOnly = currentToolParams.length > 0 && !currentToolSettings.length - - const settingsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolSettings), [currentToolSettings]) - const paramsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams]) - - const handleSettingsFormChange = (v: Record) => { - const newValue = getStructureValue(v) - const toolValue = { - ...value, - settings: newValue, - } - onSelect(toolValue as any) - } - const handleParamsFormChange = (v: Record) => { - const toolValue = { - ...value, - parameters: v, - } - onSelect(toolValue as any) - } - const handleEnabledChange = (state: boolean) => { onSelect({ ...value, @@ -311,92 +270,21 @@ const ToolSelector: FC = ({ {/* authorization */} - {currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && ( - <> - -
- -
- - )} + {/* tool settings */} - {(currentToolSettings.length > 0 || currentToolParams.length > 0) && currentProvider?.is_team_authorization && ( - <> - - {/* tabs */} - {nodeId && showTabSlider && ( - { - setCurrType(value) - }} - options={[ - { value: 'settings', text: t('detailPanel.toolSelector.settings', { ns: 'plugin' })! }, - { value: 'params', text: t('detailPanel.toolSelector.params', { ns: 'plugin' })! }, - ]} - /> - )} - {nodeId && showTabSlider && currType === 'params' && ( -
-
{t('detailPanel.toolSelector.paramsTip1', { ns: 'plugin' })}
-
{t('detailPanel.toolSelector.paramsTip2', { ns: 'plugin' })}
-
- )} - {/* user settings only */} - {userSettingsOnly && ( -
-
{t('detailPanel.toolSelector.settings', { ns: 'plugin' })}
-
- )} - {/* reasoning config only */} - {nodeId && reasoningConfigOnly && ( -
-
{t('detailPanel.toolSelector.params', { ns: 'plugin' })}
-
-
{t('detailPanel.toolSelector.paramsTip1', { ns: 'plugin' })}
-
{t('detailPanel.toolSelector.paramsTip2', { ns: 'plugin' })}
-
-
- )} - {/* user settings form */} - {(currType === 'settings' || userSettingsOnly) && ( -
- -
- )} - {/* reasoning config form */} - {nodeId && (currType === 'params' || reasoningConfigOnly) && ( - - )} - - )} + diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-authorization-section.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-authorization-section.tsx new file mode 100644 index 0000000000..83208c4177 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-authorization-section.tsx @@ -0,0 +1,46 @@ +'use client' + +import type { FC } from 'react' +import type { ToolWithProvider } from '@/app/components/workflow/types' +import * as React from 'react' +import Divider from '@/app/components/base/divider' +import { + AuthCategory, + PluginAuthInAgent, +} from '@/app/components/plugins/plugin-auth' +import { CollectionType } from '@/app/components/tools/types' + +type ToolAuthorizationSectionProps = { + currentProvider?: ToolWithProvider + credentialId?: string + onAuthorizationItemClick?: (id: string) => void +} + +const ToolAuthorizationSection: FC = ({ + currentProvider, + credentialId, + onAuthorizationItemClick, +}) => { + if (!currentProvider || currentProvider.type !== CollectionType.builtIn || !currentProvider.allow_delete) + return null + + return ( + <> + +
+ +
+ + ) +} + +export default React.memo(ToolAuthorizationSection) diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-settings-section.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-settings-section.tsx new file mode 100644 index 0000000000..260bed4bc3 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-settings-section.tsx @@ -0,0 +1,154 @@ +'use client' + +import type { FC } from 'react' +import type { Node } from 'reactflow' +import type { Tool } from '@/app/components/tools/types' +import type { ToolValue } from '@/app/components/workflow/block-selector/types' +import type { NodeOutPutVar, ToolWithProvider } from '@/app/components/workflow/types' +import * as React from 'react' +import { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Divider from '@/app/components/base/divider' +import TabSlider from '@/app/components/base/tab-slider-plain' +import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form' +import { getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' +import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form' + +type ToolSettingsSectionProps = { + currentProvider?: ToolWithProvider + currentTool?: Tool + value?: ToolValue + nodeId?: string + nodeOutputVars?: NodeOutPutVar[] + availableNodes?: Node[] + onChange?: (value: ToolValue) => void +} + +const ToolSettingsSection: FC = ({ + currentProvider, + currentTool, + value, + nodeId, + nodeOutputVars = [], + availableNodes = [], + onChange, +}) => { + const { t } = useTranslation() + const [currType, setCurrType] = useState<'settings' | 'params'>('settings') + const safeNodeId = nodeId ?? '' + + const currentToolSettings = useMemo(() => { + if (!currentTool) + return [] + return currentTool.parameters?.filter(param => param.form !== 'llm') || [] + }, [currentTool]) + const currentToolParams = useMemo(() => { + if (!currentTool) + return [] + return currentTool.parameters?.filter(param => param.form === 'llm') || [] + }, [currentTool]) + + const settingsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolSettings), [currentToolSettings]) + const paramsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams]) + + const allowReasoning = !!safeNodeId + const showTabSlider = allowReasoning && currentToolSettings.length > 0 && currentToolParams.length > 0 + const userSettingsOnly = currentToolSettings.length > 0 && (!allowReasoning || !currentToolParams.length) + const reasoningConfigOnly = allowReasoning && currentToolParams.length > 0 && currentToolSettings.length === 0 + + const handleSettingsFormChange = (v: Record) => { + if (!value || !onChange) + return + const newValue = getStructureValue(v) + onChange({ + ...value, + settings: newValue, + }) + } + + const handleParamsFormChange = (v: Record) => { + if (!value || !onChange) + return + onChange({ + ...value, + parameters: v, + }) + } + + if (!currentProvider?.is_team_authorization) + return null + + if (!currentToolSettings.length && !currentToolParams.length) + return null + + return ( + <> + + {/* tabs */} + {showTabSlider && ( + { + setCurrType(value as 'settings' | 'params') + }} + options={[ + { value: 'settings', text: t('detailPanel.toolSelector.settings', { ns: 'plugin' })! }, + { value: 'params', text: t('detailPanel.toolSelector.params', { ns: 'plugin' })! }, + ]} + /> + )} + {showTabSlider && currType === 'params' && ( +
+
{t('detailPanel.toolSelector.paramsTip1', { ns: 'plugin' })}
+
{t('detailPanel.toolSelector.paramsTip2', { ns: 'plugin' })}
+
+ )} + {/* user settings only */} + {userSettingsOnly && ( +
+
{t('detailPanel.toolSelector.settings', { ns: 'plugin' })}
+
+ )} + {/* reasoning config only */} + {reasoningConfigOnly && ( +
+
{t('detailPanel.toolSelector.params', { ns: 'plugin' })}
+
+
{t('detailPanel.toolSelector.paramsTip1', { ns: 'plugin' })}
+
{t('detailPanel.toolSelector.paramsTip2', { ns: 'plugin' })}
+
+
+ )} + {/* user settings form */} + {(currType === 'settings' || userSettingsOnly) && ( +
+ +
+ )} + {/* reasoning config form */} + {allowReasoning && (currType === 'params' || reasoningConfigOnly) && ( + + )} + + ) +} + +export default React.memo(ToolSettingsSection) diff --git a/web/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/component.tsx b/web/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/component.tsx index eaba2de055..802a673fa2 100644 --- a/web/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/component.tsx +++ b/web/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/component.tsx @@ -1,10 +1,20 @@ import type { FC } from 'react' import type { Emoji } from '@/app/components/tools/types' +import type { ToolValue } from '@/app/components/workflow/block-selector/types' import type { ToolWithProvider } from '@/app/components/workflow/types' import * as React from 'react' -import { useMemo } from 'react' +import { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' import { useSelectOrDelete } from '@/app/components/base/prompt-editor/hooks' +import ToolAuthorizationSection from '@/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-authorization-section' +import ToolSettingsSection from '@/app/components/plugins/plugin-detail-panel/tool-selector/sections/tool-settings-section' +import { generateFormValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { useGetLanguage } from '@/context/i18n' import useTheme from '@/hooks/use-theme' import { @@ -23,6 +33,7 @@ type ToolBlockComponentProps = { nodeKey: string provider: string tool: string + configId: string label?: string icon?: string | Emoji iconDark?: string | Emoji @@ -40,35 +51,79 @@ const ToolBlockComponent: FC = ({ nodeKey, provider, tool, + configId, label, icon, iconDark, }) => { const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_TOOL_BLOCK_COMMAND) const language = useGetLanguage() + const { t } = useTranslation() const { theme } = useTheme() + const [isSettingOpen, setIsSettingOpen] = useState(false) + const [toolValue, setToolValue] = useState(null) const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() const { data: mcpTools } = useAllMCPTools() - const toolMeta = useMemo(() => { - const collections = [buildInTools, customTools, workflowTools, mcpTools].filter(Boolean) as ToolWithProvider[][] - for (const collection of collections) { + const mergedTools = useMemo(() => { + return [buildInTools, customTools, workflowTools, mcpTools].filter(Boolean) as ToolWithProvider[][] + }, [buildInTools, customTools, workflowTools, mcpTools]) + + const currentProvider = useMemo(() => { + for (const collection of mergedTools) { const providerItem = collection.find(item => item.name === provider || item.id === provider || canFindTool(item.id, provider)) - if (!providerItem) - continue - const toolItem = providerItem.tools?.find(item => item.name === tool) - if (!toolItem) - continue - return { - label: toolItem.label?.[language] || tool, - icon: providerItem.icon, - iconDark: providerItem.icon_dark, - } + if (providerItem) + return providerItem } - return null - }, [buildInTools, customTools, workflowTools, mcpTools, language, provider, tool]) + return undefined + }, [mergedTools, provider]) + + const currentTool = useMemo(() => { + if (!currentProvider) + return undefined + return currentProvider.tools?.find(item => item.name === tool) + }, [currentProvider, tool]) + + const toolMeta = useMemo(() => { + if (!currentProvider || !currentTool) + return null + return { + label: currentTool.label?.[language] || tool, + icon: currentProvider.icon, + iconDark: currentProvider.icon_dark, + } + }, [currentProvider, currentTool, language, tool]) + + const defaultToolValue = useMemo(() => { + if (!currentProvider || !currentTool) + return null + const settingsSchemas = toolParametersToFormSchemas(currentTool.parameters?.filter(param => param.form !== 'llm') || []) + const paramsSchemas = toolParametersToFormSchemas(currentTool.parameters?.filter(param => param.form === 'llm') || []) + const toolLabel = currentTool.label?.[language] || tool + const toolDescription = typeof currentTool.description === 'object' + ? (currentTool.description?.[language] || '') + : (currentTool.description || '') + return { + provider_name: currentProvider.id, + provider_show_name: currentProvider.name, + tool_name: currentTool.name, + tool_label: toolLabel, + tool_description: toolDescription, + settings: generateFormValue({}, settingsSchemas as any), + parameters: generateFormValue({}, paramsSchemas as any, true), + enabled: true, + extra: { description: toolDescription }, + } as ToolValue + }, [currentProvider, currentTool, language, tool]) + + useEffect(() => { + if (!defaultToolValue) + return + if (!toolValue || toolValue.tool_name !== defaultToolValue.tool_name || toolValue.provider_name !== defaultToolValue.provider_name) + setToolValue(defaultToolValue) + }, [defaultToolValue, toolValue]) const displayLabel = label || toolMeta?.label || tool const resolvedIcon = (() => { @@ -109,20 +164,69 @@ const ToolBlockComponent: FC = ({ ) } + const handleToolValueChange = (nextValue: ToolValue) => { + setToolValue(nextValue) + } + + const handleAuthorizationItemClick = (id: string) => { + setToolValue(prev => (prev ? { ...prev, credential_id: id } : prev)) + } + return ( - - {renderIcon()} - - {displayLabel} - - + { + if (!currentProvider || !currentTool) + return + setIsSettingOpen(true) + }} + > + + {renderIcon()} + + {displayLabel} + + + + +
+
{t('detailPanel.toolSelector.toolSetting', { ns: 'plugin' })}
+ {currentProvider && currentTool && toolValue && ( + <> +
{displayLabel}
+ + + + )} +
+
+ ) } diff --git a/web/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/node.tsx b/web/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/node.tsx index 21c07fea51..b7d596be88 100644 --- a/web/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/node.tsx +++ b/web/app/components/workflow/skill/editor/skill-editor/plugins/tool-block/node.tsx @@ -71,6 +71,7 @@ export class ToolBlockNode extends DecoratorNode { nodeKey={this.getKey()} provider={this.__provider} tool={this.__tool} + configId={this.__configId} label={this.__label} icon={this.__icon} iconDark={this.__iconDark}