From 649283df09481c83e37ffa782f19719b0c74b304 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 16 Jan 2026 15:05:16 +0800 Subject: [PATCH] fix: not popup and use new setting --- .../skill/editor/skill-editor/index.tsx | 5 +- .../plugins/tool-block/component.tsx | 131 +++++++++------ .../tool-setting/tool-settings-section.tsx | 154 ++++++++++++++++++ 3 files changed, 235 insertions(+), 55 deletions(-) create mode 100644 web/app/components/workflow/skill/editor/skill-editor/tool-setting/tool-settings-section.tsx diff --git a/web/app/components/workflow/skill/editor/skill-editor/index.tsx b/web/app/components/workflow/skill/editor/skill-editor/index.tsx index 00d6db583c..ad6769d101 100644 --- a/web/app/components/workflow/skill/editor/skill-editor/index.tsx +++ b/web/app/components/workflow/skill/editor/skill-editor/index.tsx @@ -88,7 +88,10 @@ const SkillEditor: FC = ({ return ( -
+
= ({ const { theme } = useTheme() const [isSettingOpen, setIsSettingOpen] = useState(false) const [toolValue, setToolValue] = useState(null) + const [portalContainer, setPortalContainer] = useState(null) const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() @@ -125,6 +122,35 @@ const ToolBlockComponent: FC = ({ setToolValue(defaultToolValue) }, [defaultToolValue, toolValue]) + useEffect(() => { + const containerFromRef = ref.current?.closest('[data-skill-editor-root="true"]') as HTMLElement | null + const fallbackContainer = document.querySelector('[data-skill-editor-root="true"]') as HTMLElement | null + const container = containerFromRef || fallbackContainer + if (container) + setPortalContainer(container) + }, [ref]) + + useEffect(() => { + if (!isSettingOpen) + return + + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node | null + const triggerEl = ref.current + const panelEl = portalContainer?.querySelector('[data-tool-setting-panel="true"]') + if (!target || !panelEl) + return + if (panelEl.contains(target)) + return + if (triggerEl && triggerEl.contains(target)) + return + setIsSettingOpen(false) + } + + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, [isSettingOpen, portalContainer, ref]) + const displayLabel = label || toolMeta?.label || tool const resolvedIcon = (() => { const fromNode = theme === Theme.dark ? iconDark : icon @@ -173,60 +199,57 @@ const ToolBlockComponent: FC = ({ } return ( - - { + <> + { if (!currentProvider || !currentTool) return setIsSettingOpen(true) }} > - - {renderIcon()} - - {displayLabel} - + {renderIcon()} + + {displayLabel} - - -
-
{t('detailPanel.toolSelector.toolSetting', { ns: 'plugin' })}
- {currentProvider && currentTool && toolValue && ( - <> -
{displayLabel}
- - - - )} -
-
-
+ + {portalContainer && isSettingOpen && createPortal( +
+
+
{t('detailPanel.toolSelector.toolSetting', { ns: 'plugin' })}
+ {currentProvider && currentTool && toolValue && ( + <> +
{displayLabel}
+ + + + )} +
+
, + portalContainer, + )} + ) } diff --git a/web/app/components/workflow/skill/editor/skill-editor/tool-setting/tool-settings-section.tsx b/web/app/components/workflow/skill/editor/skill-editor/tool-setting/tool-settings-section.tsx new file mode 100644 index 0000000000..260bed4bc3 --- /dev/null +++ b/web/app/components/workflow/skill/editor/skill-editor/tool-setting/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)