diff --git a/web/app/components/base/prompt-editor/index.tsx b/web/app/components/base/prompt-editor/index.tsx index 05853d1b4d..bf69d682d8 100644 --- a/web/app/components/base/prompt-editor/index.tsx +++ b/web/app/components/base/prompt-editor/index.tsx @@ -5,6 +5,7 @@ import type { } from 'lexical' import type { FC } from 'react' import type { + AgentBlockType, ContextBlockType, CurrentBlockType, ErrorMessageBlockType, @@ -103,6 +104,7 @@ export type PromptEditorProps = { currentBlock?: CurrentBlockType errorMessageBlock?: ErrorMessageBlockType lastRunBlock?: LastRunBlockType + agentBlock?: AgentBlockType isSupportFileVar?: boolean } @@ -128,6 +130,7 @@ const PromptEditor: FC = ({ currentBlock, errorMessageBlock, lastRunBlock, + agentBlock, isSupportFileVar, }) => { const { eventEmitter } = useEventEmitterContextContext() @@ -139,6 +142,7 @@ const PromptEditor: FC = ({ { replace: TextNode, with: (node: TextNode) => new CustomTextNode(node.__text), + withKlass: CustomTextNode, }, ContextBlockNode, HistoryBlockNode, @@ -212,19 +216,22 @@ const PromptEditor: FC = ({ lastRunBlock={lastRunBlock} isSupportFileVar={isSupportFileVar} /> - + {(!agentBlock || agentBlock.show) && ( + + )} { const { eventEmitter } = useEventEmitterContextContext() @@ -151,12 +156,31 @@ const ComponentPicker = ({ editor.dispatchCommand(KEY_ESCAPE_COMMAND, escapeEvent) }, [editor]) + const handleSelectAgent = useCallback((agent: { id: string, title: string }) => { + editor.update(() => { + const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!) + if (needRemove) + needRemove.remove() + }) + agentBlock?.onSelect?.(agent) + handleClose() + }, [editor, checkForTriggerMatch, triggerString, agentBlock, handleClose]) + + const isAgentTrigger = triggerString === '@' && agentBlock?.show + const agentNodes = agentBlock?.agentNodes || [] + const renderMenu = useCallback>(( anchorElementRef, { options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }, ) => { - if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show))) - return null + if (isAgentTrigger) { + if (!(anchorElementRef.current && agentNodes.length)) + return null + } + else { + if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show))) + return null + } setTimeout(() => { if (anchorElementRef.current) @@ -167,9 +191,6 @@ const ComponentPicker = ({ <> { ReactDOM.createPortal( - // The `LexicalMenu` will try to calculate the position of the floating menu based on the first child. - // Since we use floating ui, we need to wrap it with a div to prevent the position calculation being affected. - // See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493
- { - workflowVariableBlock?.show && ( -
- { - handleSelectWorkflowVariable(variables) - }} - maxHeightClass="max-h-[34vh]" - isSupportFileVar={isSupportFileVar} + {isAgentTrigger + ? ( + ({ + ...node, + type: BlockEnum.Agent, + }))} + onSelect={handleSelectAgent} onClose={handleClose} onBlur={handleClose} - showManageInputField={workflowVariableBlock.showManageInputField} - onManageInputField={workflowVariableBlock.onManageInputField} + maxHeightClass="max-h-[34vh]" autoFocus={false} - isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code} /> -
- ) - } - { - workflowVariableBlock?.show && !!options.length && ( -
- ) - } -
- { - options.map((option, index) => ( - + ) + : ( + <> { - // Divider - index !== 0 && options.at(index - 1)?.group !== option.group && ( + workflowVariableBlock?.show && ( +
+ { + handleSelectWorkflowVariable(variables) + }} + maxHeightClass="max-h-[34vh]" + isSupportFileVar={isSupportFileVar} + onClose={handleClose} + onBlur={handleClose} + showManageInputField={workflowVariableBlock.showManageInputField} + onManageInputField={workflowVariableBlock.onManageInputField} + autoFocus={false} + isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code} + /> +
+ ) + } + { + workflowVariableBlock?.show && !!options.length && (
) } - {option.renderMenuOption({ - queryString, - isSelected: selectedIndex === index, - onSelect: () => { - selectOptionAndCleanUp(option) - }, - onSetHighlight: () => { - setHighlightedIndex(index) - }, - })} -
- )) - } -
+
+ { + options.map((option, index) => ( + + { + index !== 0 && options.at(index - 1)?.group !== option.group && ( +
+ ) + } + {option.renderMenuOption({ + queryString, + isSelected: selectedIndex === index, + onSelect: () => { + selectOptionAndCleanUp(option) + }, + onSetHighlight: () => { + setHighlightedIndex(index) + }, + })} +
+ )) + } +
+ + )}
, anchorElementRef.current, @@ -236,7 +274,7 @@ const ComponentPicker = ({ } ) - }, [allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField]) + }, [isAgentTrigger, agentNodes, allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, handleSelectAgent, handleClose, workflowVariableOptions, isSupportFileVar, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField]) return ( void } +export type AgentNode = { + id: string + title: string +} + +export type AgentBlockType = { + show?: boolean + agentNodes?: AgentNode[] + onSelect?: (agent: AgentNode) => void +} + export type MenuTextMatch = { leadOffset: number matchingString: string diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx index 8cd98bd3a3..1e891ea385 100644 --- a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx @@ -1,3 +1,4 @@ +import type { AgentBlockType } from '@/app/components/base/prompt-editor/types' import type { Node, NodeOutPutVar, @@ -6,6 +7,7 @@ import { memo, useCallback, useMemo, + useState, } from 'react' import { useTranslation } from 'react-i18next' import PromptEditor from '@/app/components/base/prompt-editor' @@ -74,19 +76,44 @@ const MixedVariableTextInput = ({ return null }, [value, nodesByIdMap]) + const [selectedAgent, setSelectedAgent] = useState<{ id: string, title: string } | null>(null) + + const agentNodes = useMemo(() => { + return availableNodes + .filter(node => node.data.type === BlockEnum.Agent) + .map(node => ({ + id: node.id, + title: node.data.title, + })) + }, [availableNodes]) + + const handleAgentSelect = useCallback((agent: { id: string, title: string }) => { + setSelectedAgent(agent) + if (onChange) { + const agentVar = `{{#${agent.id}.text#}}` + const newValue = value ? `${agentVar}${value}` : agentVar + onChange(newValue) + setControlPromptEditorRerenderKey(Date.now()) + } + }, [onChange, value, setControlPromptEditorRerenderKey]) + const handleAgentRemove = useCallback(() => { - if (!detectedAgentFromValue || !onChange) + const agentNodeId = detectedAgentFromValue?.nodeId || selectedAgent?.id + if (!agentNodeId || !onChange) return const pattern = /\{\{#([^#]+)#\}\}/g const valueWithoutAgentVars = value.replace(pattern, (match, variablePath) => { const nodeId = variablePath.split('.')[0] - return nodeId === detectedAgentFromValue.nodeId ? '' : match + return nodeId === agentNodeId ? '' : match }).trim() onChange(valueWithoutAgentVars) + setSelectedAgent(null) setControlPromptEditorRerenderKey(Date.now()) - }, [detectedAgentFromValue, value, onChange, setControlPromptEditorRerenderKey]) + }, [detectedAgentFromValue?.nodeId, selectedAgent?.id, value, onChange, setControlPromptEditorRerenderKey]) + + const displayedAgent = detectedAgentFromValue || (selectedAgent ? { nodeId: selectedAgent.id, name: selectedAgent.title } : null) return (
- {detectedAgentFromValue && ( + {displayedAgent && ( @@ -127,7 +154,12 @@ const MixedVariableTextInput = ({ showManageInputField, onManageInputField, }} - placeholder={} + agentBlock={{ + show: agentNodes.length > 0 && !displayedAgent, + agentNodes, + onSelect: handleAgentSelect, + } as AgentBlockType} + placeholder={} onChange={onChange} />
diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx index 03a623b1bc..02053861a3 100644 --- a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx @@ -7,9 +7,10 @@ import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/cust type PlaceholderProps = { disableVariableInsertion?: boolean + hasSelectedAgent?: boolean } -const Placeholder = ({ disableVariableInsertion = false }: PlaceholderProps) => { +const Placeholder = ({ disableVariableInsertion = false, hasSelectedAgent = false }: PlaceholderProps) => { const { t } = useTranslation() const [editor] = useLexicalComposerContext() @@ -44,17 +45,21 @@ const Placeholder = ({ disableVariableInsertion = false }: PlaceholderProps) => > {t('nodes.tool.insertPlaceholder2', { ns: 'workflow' })} -
@
-
{ - e.preventDefault() - e.stopPropagation() - handleInsert('@') - })} - > - {t('nodes.tool.insertPlaceholder3', { ns: 'workflow' })} -
+ {!hasSelectedAgent && ( + <> +
@
+
{ + e.preventDefault() + e.stopPropagation() + handleInsert('@') + })} + > + {t('nodes.tool.insertPlaceholder3', { ns: 'workflow' })} +
+ + )} )} diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 03d42cd1df..39bcee3050 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -173,6 +173,7 @@ "common.needConnectTip": "This step is not connected to anything", "common.needOutputNode": "The Output node must be added", "common.needStartNode": "At least one start node must be added", + "common.noAgentNodes": "No agent nodes available", "common.noHistory": "No History", "common.noVar": "No variable", "common.notRunning": "Not running yet", diff --git a/web/i18n/ja-JP/workflow.json b/web/i18n/ja-JP/workflow.json index 7669d378e1..ed7abb48a3 100644 --- a/web/i18n/ja-JP/workflow.json +++ b/web/i18n/ja-JP/workflow.json @@ -171,6 +171,7 @@ "common.needConnectTip": "接続されていないステップがあります", "common.needOutputNode": "出力ノードを追加する必要があります", "common.needStartNode": "少なくとも1つのスタートノードを追加する必要があります", + "common.noAgentNodes": "利用可能なエージェントノードがありません", "common.noHistory": "履歴がありません", "common.noVar": "変数がありません", "common.notRunning": "まだ実行されていません", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 1579da0620..24be2d6c67 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -171,6 +171,7 @@ "common.needConnectTip": "此节点尚未连接到其他节点", "common.needOutputNode": "必须添加输出节点", "common.needStartNode": "必须添加至少一个开始节点", + "common.noAgentNodes": "没有可用的代理节点", "common.noHistory": "没有历史版本", "common.noVar": "没有变量", "common.notRunning": "尚未运行", diff --git a/web/i18n/zh-Hant/workflow.json b/web/i18n/zh-Hant/workflow.json index bc9a3008b4..e6f8e88fc5 100644 --- a/web/i18n/zh-Hant/workflow.json +++ b/web/i18n/zh-Hant/workflow.json @@ -171,6 +171,7 @@ "common.needConnectTip": "此節點尚未連接到其他節點", "common.needOutputNode": "必須新增輸出節點", "common.needStartNode": "至少必須新增一個起始節點", + "common.noAgentNodes": "沒有可用的代理節點", "common.noHistory": "無歷史記錄", "common.noVar": "沒有變數", "common.notRunning": "尚未運行",