From 2d90b0c7527f288f71492198d86bcf4642614b1a Mon Sep 17 00:00:00 2001 From: zhaoying Date: Mon, 19 Jan 2026 17:00:26 +0800 Subject: [PATCH 1/4] refactor: extract useVariableList; properties add output variable --- web/src/i18n/en.ts | 1 + web/src/i18n/zh.ts | 1 + .../Editor/plugin/InitialValuePlugin.tsx | 6 +- .../Properties/GroupVariableList/index.tsx | 16 +- .../components/Properties/VariableSelect.tsx | 4 +- .../Properties/hooks/useVariableList.ts | 209 +++ .../Workflow/components/Properties/index.tsx | 1155 +++++------------ .../views/Workflow/hooks/useWorkflowGraph.ts | 5 - 8 files changed, 562 insertions(+), 835 deletions(-) create mode 100644 web/src/views/Workflow/components/Properties/hooks/useVariableList.ts diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 1341bf55..bc757797 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1967,6 +1967,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re value: 'Value', addCase: 'Add Condition', addVariable: 'Add Variables', + output: 'Output Variable' }, clear: 'Clear', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index b6834fea..eeee6bc9 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -2061,6 +2061,7 @@ export const zh = { value: '值', addCase: '添加条件', addVariable: '添加变量', + output: '输出变量' }, clear: '清空', diff --git a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx index 5ad18dcd..22de9592 100644 --- a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx @@ -33,7 +33,8 @@ const InitialValuePlugin: React.FC = ({ value, options useEffect(() => { if (value !== prevValueRef.current && !isUserInputRef.current) { - editor.update(() => { + queueMicrotask(() => { + editor.update(() => { const root = $getRoot(); root.clear(); @@ -98,7 +99,8 @@ const InitialValuePlugin: React.FC = ({ value, options }); root.append(paragraph); } - }, { discrete: true }); + }, { discrete: true }); + }); } prevValueRef.current = value; diff --git a/web/src/views/Workflow/components/Properties/GroupVariableList/index.tsx b/web/src/views/Workflow/components/Properties/GroupVariableList/index.tsx index 06ea9e86..81eac38e 100644 --- a/web/src/views/Workflow/components/Properties/GroupVariableList/index.tsx +++ b/web/src/views/Workflow/components/Properties/GroupVariableList/index.tsx @@ -17,7 +17,7 @@ const GroupVariableList: FC = ({ name, options = [], isCanAdd = false, - size = "middle" + size = "small" }) => { const { t } = useTranslation(); const form = Form.useFormInstance(); @@ -37,16 +37,10 @@ const GroupVariableList: FC = ({ } return ( -
- - - - {t('workflow.config.var-aggregator.variable')} - - - +
+
+ {t('workflow.config.var-aggregator.variable')} +
= ({ if (filterOption) { return ( diff --git a/web/src/views/Workflow/components/Properties/hooks/useVariableList.ts b/web/src/views/Workflow/components/Properties/hooks/useVariableList.ts new file mode 100644 index 00000000..ab37fec9 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/hooks/useVariableList.ts @@ -0,0 +1,209 @@ +import { useMemo, useEffect, useState } from 'react'; +import { Graph, Node } from '@antv/x6'; +import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'; +import type { ChatVariable } from '../../../types'; + +const NODE_VARIABLES = { + llm: [{ label: 'output', dataType: 'string', field: 'output' }], + 'jinja-render': [{ label: 'output', dataType: 'string', field: 'output' }], + tool: [{ label: 'data', dataType: 'string', field: 'data' }], + 'knowledge-retrieval': [{ label: 'output', dataType: 'array[object]', field: 'output' }], + 'parameter-extractor': [ + { label: '__is_success', dataType: 'number', field: '__is_success' }, + { label: '__reason', dataType: 'string', field: '__reason' } + ], + 'http-request': [ + { label: 'body', dataType: 'string', field: 'body' }, + { label: 'status_code', dataType: 'number', field: 'status_code' } + ], + 'question-classifier': [{ label: 'class_name', dataType: 'string', field: 'class_name' }], + 'memory-read': [ + { label: 'answer', dataType: 'string', field: 'answer' }, + { label: 'intermediate_outputs', dataType: 'array[object]', field: 'intermediate_outputs' } + ] +} as const; + +const addVariable = ( + list: Suggestion[], + keys: Set, + key: string, + label: string, + dataType: string, + value: string, + nodeData: any, + extra?: Partial +) => { + if (!keys.has(key)) { + keys.add(key); + list.push({ key, label, type: 'variable', dataType, value, nodeData, ...extra }); + } +}; + +const processNodeVariables = ( + nodeData: any, + dataNodeId: string, + variableList: Suggestion[], + addedKeys: Set +) => { + const { type, config } = nodeData; + + if (type in NODE_VARIABLES) { + NODE_VARIABLES[type as keyof typeof NODE_VARIABLES].forEach(({ label, dataType, field }) => { + addVariable(variableList, addedKeys, `${dataNodeId}_${label}`, label, dataType, `${dataNodeId}.${field}`, nodeData); + }); + } + + switch (type) { + case 'start': + [...(config?.variables?.defaultValue ?? []), ...(config?.variables?.value ?? [])].forEach((v: any) => { + if (v?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${v.name}`, v.name, v.type, `${dataNodeId}.${v.name}`, nodeData); + }); + config?.variables?.sys?.forEach((v: any) => { + if (v?.name) addVariable(variableList, addedKeys, `${dataNodeId}_sys_${v.name}`, `sys.${v.name}`, v.type, `sys.${v.name}`, nodeData); + }); + break; + + case 'parameter-extractor': + (config?.params?.defaultValue || []).forEach((p: any) => { + if (p?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${p.name}`, p.name, p.type || 'string', `${dataNodeId}.${p.name}`, nodeData); + }); + break; + + case 'var-aggregator': + if (config.group.defaultValue) { + (config.group_variables.defaultValue || []).forEach((gv: any) => { + if (gv?.key) { + let dt = 'string'; + if (gv.value?.[0]) { + const fv = variableList.find(v => `{{${v.value}}}` === gv.value[0]); + if (fv) dt = fv.dataType; + } + addVariable(variableList, addedKeys, `${dataNodeId}_${gv.key}`, gv.key, dt, `${dataNodeId}.${gv.key}`, nodeData); + } + }); + } else { + const fv = (config.group_variables.defaultValue || [])[0]; + let dt = 'any'; + if (fv) { + const found = variableList.find(v => `{{${v.value}}}` === fv); + if (found) dt = found.dataType; + } + addVariable(variableList, addedKeys, `${dataNodeId}_output`, 'output', dt, `${dataNodeId}.output`, nodeData); + } + break; + + case 'iteration': + let dt = 'string'; + if (nodeData.output) { + const sv = variableList.find(v => v.value === nodeData.output); + if (sv) dt = sv.dataType; + } + addVariable(variableList, addedKeys, `${dataNodeId}_output`, 'output', `array[${dt}]`, `${dataNodeId}.output`, nodeData); + break; + + case 'loop': + (config.cycle_vars.defaultValue || []).forEach((cv: any) => { + if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData); + }); + break; + } +}; + +const hasOutputNodeTypes = [ + 'llm', + 'knowledge-retrieval', + 'memory-read', + 'question-classifier', + 'var-aggregator', + 'http-request', + 'tool', + 'jinja-render' +] +export const getCurrentNodeVariables = (nodeData: any, values: any): Suggestion[] => { + if (!nodeData || !hasOutputNodeTypes.includes(nodeData.type)) return []; + const list: Suggestion[] = []; + const keys = new Set(); + const dataNodeId = nodeData.id; + + processNodeVariables({ + ...nodeData, + config: { + ...nodeData.config, + ...values + } + }, dataNodeId, list, keys); + return nodeData.type === 'var-aggregator' && !nodeData.config.group.defaultValue ? [] : list; +}; + +export const useVariableList = ( + selectedNode: Node | null | undefined, + graphRef: React.MutableRefObject, + chatVariables: ChatVariable[] +) => { + const [trigger, setTrigger] = useState(0); + + const variableList = useMemo(() => { + if (!selectedNode || !graphRef?.current) return []; + + const list: Suggestion[] = []; + const graph = graphRef.current; + const edges = graph.getEdges(); + const nodes = graph.getNodes(); + const keys = new Set(); + + const getPreviousNodes = (nodeId: string, visited = new Set()): string[] => { + if (visited.has(nodeId)) return []; + visited.add(nodeId); + const prev = edges.filter(e => e.getTargetCellId() === nodeId).map(e => e.getSourceCellId()); + return [...prev, ...prev.flatMap(id => getPreviousNodes(id, visited))]; + }; + + const getParentLoop = (nodeId: string): Node | null => { + const node = nodes.find(n => n.id === nodeId); + const cycle = node?.getData()?.cycle; + if (cycle) { + const parent = nodes.find(n => n.getData().id === cycle); + if (parent?.getData()?.type === 'loop' || parent?.getData()?.type === 'iteration') return parent; + } + return null; + }; + + const childIds = nodes.filter(n => n.getData()?.cycle === selectedNode.id).map(n => n.id); + const parentLoop = getParentLoop(selectedNode.id); + const relevantIds = [...getPreviousNodes(selectedNode.id), ...childIds, ...(parentLoop ? getPreviousNodes(parentLoop.id) : [])]; + + chatVariables?.forEach(v => addVariable(list, keys, `CONVERSATION_${v.name}`, v.name, v.type, `conv.${v.name}`, { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' }, { group: 'CONVERSATION' })); + + relevantIds.forEach(id => { + const node = nodes.find(n => n.id === id); + if (node) processNodeVariables(node.getData(), node.getData().id, list, keys); + }); + + if (parentLoop) { + const pd = parentLoop.getData(); + const pid = pd.id; + if (pd.type === 'loop') { + (pd.cycle_vars || []).forEach((cv: any) => addVariable(list, keys, `${pid}_cycle_${cv.name}`, cv.name, cv.type || 'String', `${pid}.${cv.name}`, pd)); + } else if (pd.type === 'iteration' && pd.config.input.defaultValue) { + let itemType = 'object'; + const iv = list.find(v => `{{${v.value}}}` === pd.config.input.defaultValue); + if (iv?.dataType.startsWith('array[')) itemType = iv.dataType.replace(/^array\[(.+)\]$/, '$1'); + addVariable(list, keys, `${pid}_item`, 'item', itemType, `${pid}.item`, pd); + addVariable(list, keys, `${pid}_index`, 'index', 'number', `${pid}.index`, pd); + } + } + + return list; + }, [selectedNode, graphRef, trigger, chatVariables]); + + useEffect(() => { + if (!graphRef?.current) return; + const graph = graphRef.current; + const handler = () => setTrigger(p => p + 1); + const events = ['edge:added', 'edge:removed', 'edge:changed', 'edge:connected', 'node:added', 'node:removed', 'node:change:data']; + events.forEach(e => graph.on(e, handler)); + return () => events.forEach(e => graph.off(e, handler)); + }, [graphRef]); + + return variableList; +}; diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx index 0ea5e284..6d4571dc 100644 --- a/web/src/views/Workflow/components/Properties/index.tsx +++ b/web/src/views/Workflow/components/Properties/index.tsx @@ -2,7 +2,8 @@ import { type FC, useEffect, useState, useRef, useMemo } from "react"; import clsx from 'clsx' import { useTranslation } from 'react-i18next' import { Graph, Node } from '@antv/x6'; -import { Form, Input, Select, InputNumber, Switch } from 'antd' +import { Form, Input, Select, InputNumber, Switch, Divider, Space } from 'antd' +import { CaretDownOutlined, CaretRightOutlined } from '@ant-design/icons'; import type { NodeConfig, NodeProperties, ChatVariable } from '../../types' import Empty from '@/components/Empty'; @@ -24,7 +25,7 @@ import AssignmentList from './AssignmentList' import ToolConfig from './ToolConfig' import MemoryConfig from './MemoryConfig' import VariableList from './VariableList' -// import { calculateVariableList } from './utils/variableListCalculator' +import { useVariableList, getCurrentNodeVariables } from './hooks/useVariableList' import styles from './properties.module.css' import Editor from "../Editor"; import RbSlider from './RbSlider' @@ -49,12 +50,12 @@ const Properties: FC = ({ const [form] = Form.useForm(); const [configs, setConfigs] = useState>({} as Record) const values = Form.useWatch([], form); - const [graphUpdateTrigger, setGraphUpdateTrigger] = useState(0) const prevMappingNamesRef = useRef([]) const prevTemplateVarsRef = useRef([]) const syncTimeoutRef = useRef(null) const isSyncingRef = useRef(false) const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null) + const variableList = useVariableList(selectedNode, graphRef, chatVariables) useEffect(() => { if (selectedNode?.getData()?.id) { @@ -62,6 +63,7 @@ const Properties: FC = ({ prevMappingNamesRef.current = [] prevTemplateVarsRef.current = [] lastSyncSourceRef.current = null + setOutputCollapsed(true) } }, [selectedNode?.getData()?.id]) @@ -244,513 +246,7 @@ const Properties: FC = ({ } }, [values, selectedNode, form]) - const variableList = useMemo(() => { - if (!selectedNode || !graphRef?.current) return []; - - const variableList: Suggestion[] = []; - const graph = graphRef.current; - const edges = graph.getEdges(); - const nodes = graph.getNodes(); - const addedKeys = new Set(); - - // Find all connected previous nodes (recursive) - const getAllPreviousNodes = (nodeId: string, visited = new Set()): string[] => { - if (visited.has(nodeId)) return []; - visited.add(nodeId); - - const directPrevious = edges - .filter(edge => edge.getTargetCellId() === nodeId) - .map(edge => edge.getSourceCellId()); - - const allPrevious = [...directPrevious]; - directPrevious.forEach(prevNodeId => { - allPrevious.push(...getAllPreviousNodes(prevNodeId, visited)); - }); - - return allPrevious; - }; - - // Find child nodes (nodes whose cycle field equals current node's ID) - const getChildNodes = (nodeId: string): string[] => { - return nodes - .filter(node => node.getData()?.cycle === nodeId) - .map(node => node.id); - }; - - // Find parent loop/iteration node if current node is a child - const getParentLoopNode = (nodeId: string): Node | null => { - const node = nodes.find(n => n.id === nodeId); - if (!node) return null; - - const nodeData = node.getData(); - const cycle = nodeData?.cycle; - - if (cycle) { - const parentNode = nodes.find(n => n.getData().id === cycle); - if (parentNode) { - const parentData = parentNode.getData(); - if (parentData?.type === 'loop' || parentData?.type === 'iteration') { - return parentNode; - } - } - } - return null; - }; - - const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id); - const childNodeIds = getChildNodes(selectedNode.id); - const parentLoopNode = getParentLoopNode(selectedNode.id); - - console.log('childNodeIds', selectedNode, childNodeIds) - let allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds]; - - // Add variables from nodes preceding the parent loop/iteration node if current node is a child - if (parentLoopNode) { - const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id); - allRelevantNodeIds.push(...parentPreviousNodeIds); - } - // Add conversation variables from global config - const conversationVariables = chatVariables || []; - - conversationVariables.forEach((variable: any) => { - const key = `CONVERSATION_${variable.name}`; - if (!addedKeys.has(key)) { - addedKeys.add(key); - variableList.push({ - key, - label: variable.name, - type: 'variable', - dataType: variable.type, - value: `conv.${variable.name}`, - nodeData: { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' }, - group: 'CONVERSATION' - }); - } - }); - - allRelevantNodeIds.forEach(nodeId => { - const node = nodes.find(n => n.id === nodeId); - if (!node) return; - - const nodeData = node.getData(); - const dataNodeId = nodeData.id; // Use the data.id instead of node.id for consistency - - switch(nodeData.type) { - case 'start': - const list = [ - ...(nodeData.config?.variables?.defaultValue ?? []), - ...(nodeData.config?.variables?.value ?? []) - ] - list.forEach((variable: any) => { - if (!variable || !variable?.name) return; - const key = `${dataNodeId}_${variable.name}`; - if (!addedKeys.has(key)) { - addedKeys.add(key); - variableList.push({ - key, - label: variable.name, - type: 'variable', - dataType: variable.type, - value: `${dataNodeId}.${variable.name}`, - nodeData: nodeData, - }); - } - }); - nodeData.config?.variables?.sys?.forEach((variable: any) => { - if (!variable || !variable?.name) return; - const key = `${dataNodeId}_sys_${variable.name}`; - if (!addedKeys.has(key)) { - addedKeys.add(key); - variableList.push({ - key, - label: `sys.${variable.name}`, - type: 'variable', - dataType: variable.type, - value: `sys.${variable.name}`, - nodeData: nodeData, - }); - } - }); - break - case 'llm': - const llmKey = `${dataNodeId}_output`; - if (!addedKeys.has(llmKey)) { - addedKeys.add(llmKey); - variableList.push({ - key: llmKey, - label: 'output', - type: 'variable', - dataType: 'string', - value: `${dataNodeId}.output`, - nodeData: nodeData, - }); - } - break - case 'knowledge-retrieval': - const knowledgeKey = `${dataNodeId}_output`; - if (!addedKeys.has(knowledgeKey)) { - addedKeys.add(knowledgeKey); - variableList.push({ - key: knowledgeKey, - label: 'output', - type: 'variable', - dataType: 'array[object]', - value: `${dataNodeId}.output`, - nodeData: nodeData, - }); - } - break - case 'parameter-extractor': - const successKey = `${dataNodeId}___is_success`; - const reasonKey = `${dataNodeId}___reason`; - if (!addedKeys.has(successKey)) { - addedKeys.add(successKey); - variableList.push({ - key: successKey, - label: '__is_success', - type: 'variable', - dataType: 'number', - value: `${dataNodeId}.__is_success`, - nodeData: nodeData, - }); - } - if (!addedKeys.has(reasonKey)) { - addedKeys.add(reasonKey); - variableList.push({ - key: reasonKey, - label: '__reason', - type: 'variable', - dataType: 'string', - value: `${dataNodeId}.__reason`, - nodeData: nodeData, - }); - } - // Add params variables - const paramsList = nodeData.config?.params?.defaultValue || []; - paramsList.forEach((param: any) => { - if (!param || !param?.name) return; - const paramKey = `${dataNodeId}_${param.name}`; - if (!addedKeys.has(paramKey)) { - addedKeys.add(paramKey); - variableList.push({ - key: paramKey, - label: param.name, - type: 'variable', - dataType: param.type || 'string', - value: `${dataNodeId}.${param.name}`, - nodeData: nodeData, - }); - } - }); - break - case 'var-aggregator': - if (nodeData.config.group.defaultValue) { - // If group=true, add variables from group_variables with key as variable name - const groupVariables = nodeData.config.group_variables.defaultValue || []; - groupVariables?.forEach((groupVar: any) => { - if (!groupVar || !groupVar.key) return; - - // Determine dataType from first variable in the group - let groupDataType = 'string'; - if (groupVar.value && Array.isArray(groupVar.value) && groupVar.value.length > 0) { - const firstVariableValue = groupVar.value[0]; - const firstVariable = variableList.find(v => `{{${v.value}}}` === firstVariableValue); - if (firstVariable) { - groupDataType = firstVariable.dataType; - } - } - - const groupVarKey = `${dataNodeId}_${groupVar.key}`; - if (!addedKeys.has(groupVarKey)) { - addedKeys.add(groupVarKey); - variableList.push({ - key: groupVarKey, - label: groupVar.key, - type: 'variable', - dataType: groupDataType, - value: `${dataNodeId}.${groupVar.key}`, - nodeData: nodeData, - }); - } - }); - } else { - // If group=false, add output variable with type from first group_variable - const groupVariables = nodeData.config.group_variables.defaultValue || []; - const firstVariable = groupVariables[0]; - let outputDataType: string = 'any'; - if (firstVariable) { - const filterVo = [...variableList].find(v => { - return `{{${v.value}}}` === firstVariable - }) - if (filterVo) { - outputDataType = filterVo?.dataType - } - } - - const varAggregatorKey = `${dataNodeId}_output`; - if (!addedKeys.has(varAggregatorKey)) { - addedKeys.add(varAggregatorKey); - variableList.push({ - key: varAggregatorKey, - label: 'output', - type: 'variable', - dataType: outputDataType, - value: `${dataNodeId}.output`, - nodeData: nodeData, - }); - } - } - break - case 'http-request': - const httpBodyKey = `${dataNodeId}_body`; - const httpStatusKey = `${dataNodeId}_status_code`; - if (!addedKeys.has(httpBodyKey)) { - addedKeys.add(httpBodyKey); - variableList.push({ - key: httpBodyKey, - label: 'body', - type: 'variable', - dataType: 'string', - value: `${dataNodeId}.body`, - nodeData: nodeData, - }); - } - if (!addedKeys.has(httpStatusKey)) { - addedKeys.add(httpStatusKey); - variableList.push({ - key: httpStatusKey, - label: 'status_code', - type: 'variable', - dataType: 'number', - value: `${dataNodeId}.status_code`, - nodeData: nodeData, - }); - } - break - case 'jinja-render': - const jinjaOutputKey = `${dataNodeId}_output`; - if (!addedKeys.has(jinjaOutputKey)) { - addedKeys.add(jinjaOutputKey); - variableList.push({ - key: jinjaOutputKey, - label: 'output', - type: 'variable', - dataType: 'string', - value: `${dataNodeId}.output`, - nodeData: nodeData, - }); - } - break - case 'question-classifier': - const classNameKey = `${dataNodeId}_class_name`; - // const outputKey = `${dataNodeId}_output`; - if (!addedKeys.has(classNameKey)) { - addedKeys.add(classNameKey); - variableList.push({ - key: classNameKey, - label: 'class_name', - type: 'variable', - dataType: 'string', - value: `${dataNodeId}.class_name`, - nodeData: nodeData, - }); - } - // if (!addedKeys.has(outputKey)) { - // addedKeys.add(outputKey); - // variableList.push({ - // key: outputKey, - // label: 'output', - // type: 'variable', - // dataType: 'string', - // value: `${dataNodeId}.output`, - // nodeData: nodeData, - // }); - // } - break - case 'iteration': - const iterationOutputKey = `${dataNodeId}_output`; - if (!addedKeys.has(iterationOutputKey)) { - addedKeys.add(iterationOutputKey); - // Get the data type from the output configuration, default to string - const outputConfig = nodeData.output; - let outputDataType = 'string'; - if (outputConfig) { - // Find the selected variable from variableList to get its type - const selectedVariable = variableList.find(v => v.value === outputConfig); - if (selectedVariable) { - outputDataType = selectedVariable.dataType; - } - } - variableList.push({ - key: iterationOutputKey, - label: 'output', - type: 'variable', - dataType: `array[${outputDataType}]`, - value: `${dataNodeId}.output`, - nodeData: nodeData, - }); - } - break - case 'loop': - const cycleVars = nodeData.config.cycle_vars.defaultValue || []; - console.log('cycleVars', cycleVars) - cycleVars.forEach((cycleVar: any) => { - const cycleVarKey = `${dataNodeId}_cycle_${cycleVar.name}`; - if (!addedKeys.has(cycleVarKey)) { - addedKeys.add(cycleVarKey); - if (cycleVar.name && cycleVar.name.trim() !== '') { - variableList.push({ - key: cycleVarKey, - label: cycleVar.name, - type: 'variable', - dataType: cycleVar.type || 'string', - value: `${dataNodeId}.${cycleVar.name}`, - nodeData: nodeData, - }); - } - } - }); - break - case 'tool': - const toolDataKey = `${dataNodeId}_data`; - if (!addedKeys.has(toolDataKey)) { - addedKeys.add(toolDataKey); - variableList.push({ - key: toolDataKey, - label: 'data', - type: 'variable', - dataType: 'string', - value: `${dataNodeId}.data`, - nodeData: nodeData, - }); - } - break - case 'memory-read': - const memoryReadAnswerKey = `${dataNodeId}_answer`; - const memoryReadIntermediateOutputs = `${dataNodeId}_intermediate_outputs`; - if (!addedKeys.has(memoryReadAnswerKey)) { - addedKeys.add(memoryReadAnswerKey); - variableList.push({ - key: memoryReadAnswerKey, - label: 'answer', - type: 'variable', - dataType: 'string', - value: `${dataNodeId}.answer`, - nodeData: nodeData, - }); - } - if (!addedKeys.has(memoryReadIntermediateOutputs)) { - addedKeys.add(memoryReadIntermediateOutputs); - variableList.push({ - key: memoryReadIntermediateOutputs, - label: 'intermediate_outputs', - type: 'variable', - dataType: 'array[object]', - value: `${dataNodeId}.intermediate_outputs`, - nodeData: nodeData, - }); - } - break - } - }); - - - // Add parent loop/iteration node variables if current node is a child - if (parentLoopNode) { - const parentData = parentLoopNode.getData(); - const parentNodeId = parentLoopNode.getData().id; - - if (parentData.type === 'loop') { - const cycleVars = parentData.cycle_vars || []; - cycleVars.forEach((cycleVar: any) => { - const key = `${parentNodeId}_cycle_${cycleVar.name}`; - if (!addedKeys.has(key)) { - addedKeys.add(key); - variableList.push({ - key, - label: cycleVar.name, - type: 'variable', - dataType: cycleVar.type || 'String', - value: `${parentNodeId}.${cycleVar.name}`, - nodeData: parentData, - }); - } - }); - } else if (parentData.type === 'iteration') { - // Add item and index variables for iteration parent only if input has value - if (parentData.config.input.defaultValue) { - const itemKey = `${parentNodeId}_item`; - const indexKey = `${parentNodeId}_index`; - - // Determine item dataType from input variable - let itemDataType = 'object'; - const inputVariable = variableList.find(v => `{{${v.value}}}` === parentData.config.input.defaultValue); - console.log('itemDataType defaultValue', parentData.config.input.defaultValue, variableList, inputVariable) - if (inputVariable && inputVariable.dataType.startsWith('array[')) { - itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1'); - console.log('itemDataType', itemDataType) - } - - - if (!addedKeys.has(itemKey)) { - addedKeys.add(itemKey); - variableList.push({ - key: itemKey, - label: 'item', - type: 'variable', - dataType: itemDataType, - value: `${parentNodeId}.item`, - nodeData: parentData, - }); - } - - if (!addedKeys.has(indexKey)) { - addedKeys.add(indexKey); - variableList.push({ - key: indexKey, - label: 'index', - type: 'variable', - dataType: 'number', - value: `${parentNodeId}.index`, - nodeData: parentData, - }); - } - } - } - } - - return variableList; - }, [selectedNode, graphRef, graphUpdateTrigger, chatVariables]); - - // Trigger variableList update when graph edges or nodes change - useEffect(() => { - if (!graphRef?.current) return; - - const graph = graphRef.current; - const handleGraphChange = () => { - console.log('handleGraphChange') - // Force variableList recalculation by updating trigger - setGraphUpdateTrigger(prev => prev + 1); - }; - - // Listen to graph changes - graph.on('edge:added', handleGraphChange); - graph.on('edge:removed', handleGraphChange); - graph.on('edge:changed', handleGraphChange); - graph.on('node:added', handleGraphChange); - graph.on('node:removed', handleGraphChange); - graph.on('node:change:data', handleGraphChange); - - return () => { - graph.off('edge:added', handleGraphChange); - graph.off('edge:removed', handleGraphChange); - graph.off('edge:changed', handleGraphChange); - graph.off('node:added', handleGraphChange); - graph.off('node:removed', handleGraphChange); - graph.off('node:change:data', handleGraphChange); - }; - }, [graphRef]); // Filter out boolean type variables for loop and llm nodes const getFilteredVariableList = (nodeType?: string, key?: string) => { @@ -994,324 +490,353 @@ const Properties: FC = ({ // const defaultVariableList = calculateVariableList(selectedNode as Node, graphRef, workflowConfig ) console.log('values', values) - console.log('variableList', variableList) + + const currentNodeVariables = useMemo(() => { + if (!selectedNode) return [] + return getCurrentNodeVariables(selectedNode?.getData(), values) + }, [selectedNode?.getData(), values]) + + const [outputCollapsed, setOutputCollapsed] = useState(true) + const handleToggle = () => { + setOutputCollapsed((prev: boolean) => !prev) + } + console.log('variableList', variableList, currentNodeVariables) return (
{t('workflow.nodeProperties')}
{!selectedNode ? - :
- - { - updateNodeLabel(e.target.value); - }} + :
+ + + { + updateNodeLabel(e.target.value); + }} + /> + + + + + + {selectedNode?.data?.type === 'http-request' + ? - - - - - - {selectedNode?.data?.type === 'http-request' - ? - : selectedNode?.data?.type === 'tool' - ? - : configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => { - const config = configs[key] || {} + : selectedNode?.data?.type === 'tool' + ? + : configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => { + const config = configs[key] || {} - if (config.dependsOn && (values as any)?.[config.dependsOn as string] !== config.dependsOnValue) { - return null - } - - if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') { - return ( - - - - ) - } - - if (selectedNode?.data?.type === 'llm' && key === 'messages' && config.type === 'define') { - // 为llm节点且isArray=true时添加context变量支持 - let contextVariableList = [...getFilteredVariableList('llm')]; - const isArrayMode = config.isArray !== false; // 默认为true - - if (isArrayMode) { - const contextKey = `${selectedNode.id}_context`; - const hasContextVariable = contextVariableList.some(v => v.key === contextKey); - - if (!hasContextVariable) { - contextVariableList.unshift({ - key: contextKey, - label: 'context', - type: 'variable', - dataType: 'String', - value: `context`, - nodeData: selectedNode.getData(), - isContext: true, - }); - } - } - return ( - - variable.nodeData?.type !== 'knowledge-retrieval')} - parentName={key} - placeholder={t(config.placeholder || 'common.pleaseSelect')} - size="small" - /> - - ) - } - if (config.type === 'define') { - return null - } - - if (config.type === 'knowledge') { - return ( - - - - ) - } - - if (config.type === 'messageEditor') { - return ( - - - - ) - } - - if (config.type === 'paramList') { - return ( - - - - - ) - } - if (config.type === 'groupVariableList') { - return ( - - - - ) - } - if (config.type === 'caseList') { - return ( - - - - ) - } - - if (config.type === 'mappingList') { - return ( - - - - - ) - } - if (config.type === 'cycleVarsList') { - return ( - - - - ) - } - if (config.type === 'assignmentList') { - return ( - - { - if (config.filterLoopIterationVars) { - const loopIterationVars: Suggestion[] = []; - - return [...getFilteredVariableList(selectedNode?.data?.type, key), ...loopIterationVars]; - } - return getFilteredVariableList(selectedNode?.data?.type, key); - })() - } - /> - - ) - } - if (config.type === 'memoryConfig') { - return ( - - - - ) - } - if (config.type === 'conditionList') { - return ( - - { - const cycleVars = values?.cycle_vars || []; - const cycleVarSuggestions: Suggestion[] = cycleVars.filter(vo => vo.name && vo.name.trim() !== '').map((cycleVar: any) => ({ - key: `${selectedNode.id}_cycle_${cycleVar.name}`, - label: cycleVar.name, - type: 'variable', - dataType: cycleVar.type || 'String', - value: `${selectedNode.getData().id}.${cycleVar.name}`, - nodeData: selectedNode.getData(), - })); - - return [...getFilteredVariableList(selectedNode?.data?.type, key), ...cycleVarSuggestions]; - })()} - selectedNode={selectedNode} - graphRef={graphRef} - addBtnText={t('workflow.config.addCase')} - /> - - ) - } + if (config.dependsOn && (values as any)?.[config.dependsOn as string] !== config.dependsOnValue) { + return null + } + if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') { return ( - {t(`workflow.config.${selectedNode?.data?.type}.${key}`)} : t(`workflow.config.${selectedNode?.data?.type}.${key}`)} - layout={config.type === 'switch' ? 'horizontal' : 'vertical'} - className={key === 'parallel_count' ? 'rb:-mt-3! rb:leading-3.5!' : ''} - > - {config.type === 'input' - ? - : config.type === 'textarea' - ? - : config.type === 'select' - ? + : config.type === 'textarea' + ? + : config.type === 'select' + ? - {hasAll && ({allTitle || t('common.all')})} - {(format ? format(options) : options)?.map(option => ( + {hasAll && {allTitle || t('common.all')}} + {displayOptions.map((option) => ( {String(option[labelKey])} ))} ); -} +}; + export default CustomSelect; \ No newline at end of file From cd1a50a1d1382a209f8ad6c8d9bfac3f9e75cb1a Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 20 Jan 2026 10:21:00 +0800 Subject: [PATCH 3/4] fix(web): node cannot be connected to itself --- web/src/views/Workflow/hooks/useWorkflowGraph.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index a7ebb29a..615cd3e5 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -729,6 +729,9 @@ export const useWorkflowGraph = ({ validateConnection({ sourceCell, targetCell, targetMagnet }) { if (!targetMagnet) return false; + // 节点不能与自己连线 + if (sourceCell?.id === targetCell?.id) return false; + const sourceType = sourceCell?.getData()?.type; const targetType = targetCell?.getData()?.type; From 804d87bca2f9a7b2fbd341efb851d7da23778949 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 20 Jan 2026 10:42:13 +0800 Subject: [PATCH 4/4] refactor: extract jinja render's form --- .../Workflow/components/Editor/index.tsx | 36 +-- .../components/Editor/nodes/VariableNode.tsx | 2 +- .../Editor/plugin/AutocompletePlugin.tsx | 8 +- .../components/Editor/plugin/BlurPlugin.tsx | 33 +++ .../Properties/JinjaRender/index.tsx | 206 ++++++++++++++++++ .../Workflow/components/Properties/index.tsx | 145 +----------- 6 files changed, 279 insertions(+), 151 deletions(-) create mode 100644 web/src/views/Workflow/components/Editor/plugin/BlurPlugin.tsx create mode 100644 web/src/views/Workflow/components/Properties/JinjaRender/index.tsx diff --git a/web/src/views/Workflow/components/Editor/index.tsx b/web/src/views/Workflow/components/Editor/index.tsx index ba2e3a41..fd3e937b 100644 --- a/web/src/views/Workflow/components/Editor/index.tsx +++ b/web/src/views/Workflow/components/Editor/index.tsx @@ -16,6 +16,7 @@ import InitialValuePlugin from './plugin/InitialValuePlugin'; import CommandPlugin from './plugin/CommandPlugin'; import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin'; import LineNumberPlugin from './plugin/LineNumberPlugin'; +import BlurPlugin from './plugin/BlurPlugin'; import { VariableNode } from './nodes/VariableNode' interface LexicalEditorProps { @@ -113,8 +114,10 @@ const Editor: FC =({ display: flex; align-items: flex-start; } - .editor-content-with-numbers { + .editor-content-wrapper { flex: 1; + } + .editor-content-with-numbers { white-space: pre-wrap; } .editor-content-with-numbers p { @@ -174,18 +177,20 @@ const Editor: FC =({
1
- +
+ +
) : ( =({ style={{ minHeight: placeHolderMinheight, position: 'absolute', - top: variant === 'borderless' ? '0' : '6px', - left: enableJinja2 ? '59px' : (variant === 'borderless' ? '0' : '11px'), + top: enableJinja2 ? '4px' : variant === 'borderless' ? '0' : '6px', + left: enableJinja2 ? '16px' : (variant === 'borderless' ? '0' : '11px'), color: '#A8A9AA', fontSize: fontSize, lineHeight: placeHolderMinheight, @@ -227,6 +232,7 @@ const Editor: FC =({ { setCount(count) }} onChange={onChange} /> + {enableJinja2 && }
); diff --git a/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx b/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx index 13d12ee1..d29fba4c 100644 --- a/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx +++ b/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx @@ -36,7 +36,7 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({ return ( const textAfter = nodeText.substring(anchorOffset); const newText = textBefore + `{{${suggestion.value}}}` + textAfter; - anchorNode.setTextContent(newText); + if ($isTextNode(anchorNode)) { + anchorNode.setTextContent(newText); + } // 设置光标位置到插入文本之后 const newOffset = textBefore.length + `{{${suggestion.value}}}`.length; @@ -129,6 +131,8 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> } return (
e.preventDefault()} style={{ position: 'fixed', top: popupPosition.top, diff --git a/web/src/views/Workflow/components/Editor/plugin/BlurPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/BlurPlugin.tsx new file mode 100644 index 00000000..b636605b --- /dev/null +++ b/web/src/views/Workflow/components/Editor/plugin/BlurPlugin.tsx @@ -0,0 +1,33 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { useEffect } from 'react'; +import { $setSelection } from 'lexical'; + +export default function BlurPlugin() { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return editor.registerRootListener((rootElement) => { + if (rootElement) { + const handleBlur = (e: FocusEvent) => { + // 检查是否点击了自动完成弹窗 + const target = e.target as HTMLElement; + console.log('target', target) + if (target?.closest('[data-autocomplete-popup="true"]')) { + return; + } + + editor.update(() => { + $setSelection(null); + }); + }; + + rootElement.addEventListener('blur', handleBlur); + return () => { + rootElement.removeEventListener('blur', handleBlur); + }; + } + }); + }, [editor]); + + return null; +} diff --git a/web/src/views/Workflow/components/Properties/JinjaRender/index.tsx b/web/src/views/Workflow/components/Properties/JinjaRender/index.tsx new file mode 100644 index 00000000..a2c9da37 --- /dev/null +++ b/web/src/views/Workflow/components/Properties/JinjaRender/index.tsx @@ -0,0 +1,206 @@ +import { type FC, useEffect, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { Form } from 'antd' +import { Node } from '@antv/x6' +import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' +import MappingList from '../MappingList' +import MessageEditor from '../MessageEditor' + +interface MappingItem { + name?: string + value?: string +} + +interface JinjaRenderProps { + options: Suggestion[] + templateOptions: Suggestion[] + selectedNode: Node +} + +const extractTemplateVars = (template: string): string[] => { + return (template.match(/{{\s*([\w.]+)\s*}}/g) || []) + .map(m => m.replace(/{{\s*|\s*}}/g, '')) +} + +const getMappingNames = (mapping: MappingItem[]): string[] => { + return mapping.filter(item => item?.name).map(item => item.name!) +} + +const JinjaRender: FC = ({ selectedNode, options, templateOptions }) => { + const { t } = useTranslation() + const form = Form.useFormInstance() + const values = Form.useWatch([], form) || {} + + console.log('JinjaRender values', values) + + const prevMappingNamesRef = useRef([]) + const prevTemplateVarsRef = useRef([]) + const syncTimeoutRef = useRef(null) + const isSyncingRef = useRef(false) + const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null) + + // Reset refs when node changes + useEffect(() => { + if (selectedNode?.getData()?.id) { + prevMappingNamesRef.current = [] + prevTemplateVarsRef.current = [] + lastSyncSourceRef.current = null + } + }, [selectedNode?.getData()?.id]) + + // Sync template when mapping names change + useEffect(() => { + if ( + isSyncingRef.current || + lastSyncSourceRef.current === 'mapping' || + selectedNode?.data?.type !== 'jinja-render' || + !values?.mapping || + !values?.template + ) return + + const currentMappingNames = Array.isArray(values.mapping) ? getMappingNames(values.mapping) : [] + const prevNames = prevMappingNamesRef.current + + if (prevNames.length === 0) { + prevMappingNamesRef.current = currentMappingNames + return + } + + if (JSON.stringify(prevNames) === JSON.stringify(currentMappingNames)) return + + if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current) + const activeElement = document.activeElement as HTMLElement + + syncTimeoutRef.current = setTimeout(() => { + let updatedTemplate = String(form.getFieldValue('template') || '') + + prevNames.forEach((oldName, index) => { + const newName = currentMappingNames[index] + if (newName && oldName !== newName) { + updatedTemplate = updatedTemplate.replace( + new RegExp(`{{\\s*${oldName}\\s*}}`, 'g'), + `{{${newName}}}` + ) + } + }) + + if (updatedTemplate !== form.getFieldValue('template')) { + isSyncingRef.current = true + lastSyncSourceRef.current = 'mapping' + + prevTemplateVarsRef.current = extractTemplateVars(updatedTemplate) + prevMappingNamesRef.current = currentMappingNames + form.setFieldValue('template', updatedTemplate) + + requestAnimationFrame(() => { + activeElement?.focus?.() + setTimeout(() => { + isSyncingRef.current = false + lastSyncSourceRef.current = null + }, 50) + }) + } else { + prevMappingNamesRef.current = currentMappingNames + } + }, 0) + }, [values?.mapping, selectedNode?.data?.type, form]) + + // Sync mapping when template variables change + useEffect(() => { + console.log('values?.template', values?.template) + if ( + isSyncingRef.current || + lastSyncSourceRef.current === 'template' || + selectedNode?.data?.type !== 'jinja-render' || + !values?.template || + !values?.mapping + ) return + + const templateVars = extractTemplateVars(String(values.template)) + if (JSON.stringify(prevTemplateVarsRef.current) === JSON.stringify(templateVars)) return + + const isTemplateEditor = document.activeElement?.closest('[data-editor-type="template"]') + if (!isTemplateEditor) { + prevTemplateVarsRef.current = templateVars + return + } + + const updatedMapping: MappingItem[] = Array.isArray(values.mapping) + ? [...values.mapping.filter((item: MappingItem) => item)] + : [] + const existingNames = getMappingNames(updatedMapping) + let updatedTemplate = String(values.template) + + // Update existing mapping names based on position + if (prevTemplateVarsRef.current.length > 0) { + prevTemplateVarsRef.current.forEach((oldVar, index) => { + const newVar = templateVars[index] + if (newVar && oldVar !== newVar && updatedMapping[index]) { + updatedMapping[index] = { ...updatedMapping[index], name: newVar } + } + }) + } + + // Add new mappings and normalize template + templateVars.forEach(varName => { + const existingMapping = updatedMapping.find(item => item.value === `{{${varName}}}`) + const regex = new RegExp(`{{\\s*${varName.replace(/\./g, '\\.')}\\s*}}`, 'g') + + if (existingMapping) { + updatedTemplate = updatedTemplate.replace(regex, `{{${existingMapping.name}}}`) + } else if (!existingNames.includes(varName)) { + const mappingName = varName.includes('.') ? varName.split('.').pop() || varName : varName + updatedMapping.push({ name: mappingName, value: `{{${varName}}}` }) + updatedTemplate = updatedTemplate.replace(regex, `{{${mappingName}}}`) + } + }) + + // Remove unused mappings and duplicates + const seenNames = new Set() + const finalMapping = updatedMapping.filter(item => { + const isUsed = templateVars.some(v => item.name === v || item.value === `{{${v}}}`) + if (!isUsed || !item.name || seenNames.has(item.name)) return false + seenNames.add(item.name) + return true + }) + + isSyncingRef.current = true + lastSyncSourceRef.current = 'template' + prevMappingNamesRef.current = getMappingNames(finalMapping) + prevTemplateVarsRef.current = templateVars + + if (JSON.stringify(finalMapping) !== JSON.stringify(values.mapping)) { + form.setFieldValue('mapping', finalMapping) + } + if (updatedTemplate !== String(values.template)) { + form.setFieldValue('template', updatedTemplate) + } + + setTimeout(() => { + isSyncingRef.current = false + lastSyncSourceRef.current = null + }, 50) + }, [values?.template, selectedNode?.data?.type, form]) + + return ( + <> + + + + + + + + + ) +} + +export default JinjaRender diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx index 6d4571dc..d55e1d9e 100644 --- a/web/src/views/Workflow/components/Properties/index.tsx +++ b/web/src/views/Workflow/components/Properties/index.tsx @@ -1,4 +1,4 @@ -import { type FC, useEffect, useState, useRef, useMemo } from "react"; +import { type FC, useEffect, useState, useMemo } from "react"; import clsx from 'clsx' import { useTranslation } from 'react-i18next' import { Graph, Node } from '@antv/x6'; @@ -17,7 +17,6 @@ import ParamsList from './ParamsList'; import GroupVariableList from './GroupVariableList' import CaseList from './CaseList' import HttpRequest from './HttpRequest'; -import MappingList from './MappingList' import CategoryList from './CategoryList' import ConditionList from './ConditionList' import CycleVarsList from './CycleVarsList' @@ -29,6 +28,7 @@ import { useVariableList, getCurrentNodeVariables } from './hooks/useVariableLis import styles from './properties.module.css' import Editor from "../Editor"; import RbSlider from './RbSlider' +import JinjaRender from './JinjaRender' interface PropertiesProps { selectedNode?: Node | null; @@ -50,136 +50,16 @@ const Properties: FC = ({ const [form] = Form.useForm(); const [configs, setConfigs] = useState>({} as Record) const values = Form.useWatch([], form); - const prevMappingNamesRef = useRef([]) - const prevTemplateVarsRef = useRef([]) - const syncTimeoutRef = useRef(null) - const isSyncingRef = useRef(false) - const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null) const variableList = useVariableList(selectedNode, graphRef, chatVariables) useEffect(() => { if (selectedNode?.getData()?.id) { - form.resetFields() - prevMappingNamesRef.current = [] - prevTemplateVarsRef.current = [] - lastSyncSourceRef.current = null setOutputCollapsed(true) + } else { + form.resetFields() } }, [selectedNode?.getData()?.id]) - // Sync template when mapping names change - useEffect(() => { - if (isSyncingRef.current || lastSyncSourceRef.current === 'mapping' || selectedNode?.data?.type !== 'jinja-render' || !values?.mapping || !values?.template) return - - const currentMappingNames = Array.isArray(values.mapping) ? values.mapping.filter(item => item && item.name).map((item: any) => item.name) : [] - const prevNames = prevMappingNamesRef.current - - if (prevNames.length === 0) { - prevMappingNamesRef.current = currentMappingNames - return - } - - if (JSON.stringify(prevNames) === JSON.stringify(currentMappingNames)) return - - if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current) - const activeElement = document.activeElement as HTMLElement - - syncTimeoutRef.current = setTimeout(() => { - let updatedTemplate = String(form.getFieldValue('template') || '') - - prevNames.forEach((oldName, index) => { - const newName = currentMappingNames[index] - if (newName && oldName !== newName) { - updatedTemplate = updatedTemplate.replace(new RegExp(`{{\\s*${oldName}\\s*}}`, 'g'), `{{${newName}}}`) - } - }) - - if (updatedTemplate !== form.getFieldValue('template')) { - isSyncingRef.current = true - lastSyncSourceRef.current = 'mapping' - const newTemplateVars = (updatedTemplate.match(/{{\s*([\w.]+)\s*}}/g) || []).map(m => m.replace(/{{\s*|\s*}}/g, '')) - prevTemplateVarsRef.current = newTemplateVars - prevMappingNamesRef.current = currentMappingNames - form.setFieldValue('template', updatedTemplate) - - requestAnimationFrame(() => { - activeElement?.focus?.() - setTimeout(() => { - isSyncingRef.current = false - lastSyncSourceRef.current = null - }, 50) - }) - } else { - prevMappingNamesRef.current = currentMappingNames - } - }, 0) - }, [values?.mapping, selectedNode?.data?.type, form]) - - // Sync mapping when template variables change - useEffect(() => { - if (isSyncingRef.current || lastSyncSourceRef.current === 'template' || selectedNode?.data?.type !== 'jinja-render' || !values?.template || !values?.mapping) return - - const templateVars = (String(values.template).match(/{{\s*([\w.]+)\s*}}/g) || []).map(m => m.replace(/{{\s*|\s*}}/g, '')) - if (JSON.stringify(prevTemplateVarsRef.current) === JSON.stringify(templateVars)) return - - const isTemplateEditor = document.activeElement?.closest('[data-editor-type="template"]') - if (!isTemplateEditor) { - prevTemplateVarsRef.current = templateVars - return - } - - const updatedMapping = Array.isArray(values.mapping) ? [...values.mapping.filter(item => item)] : [] - const existingNames = updatedMapping.filter(item => item && item.name).map(item => item.name) - let updatedTemplate = String(values.template) - - if (prevTemplateVarsRef.current.length > 0) { - prevTemplateVarsRef.current.forEach((oldVar, index) => { - const newVar = templateVars[index] - if (newVar && oldVar !== newVar && updatedMapping[index]) { - updatedMapping[index] = { ...updatedMapping[index], name: newVar } - } - }) - } - - templateVars.forEach(varName => { - const existingMapping = updatedMapping.find(item => item.value === `{{${varName}}}`) - const regex = new RegExp(`{{\\s*${varName.replace(/\./g, '\\.')}\\s*}}`, 'g') - - if (existingMapping) { - updatedTemplate = updatedTemplate.replace(regex, `{{${existingMapping.name}}}`) - } else if (!existingNames.includes(varName)) { - const mappingName = varName.includes('.') ? varName.split('.').pop() || varName : varName - updatedMapping.push({ name: mappingName, value: `{{${varName}}}` }) - updatedTemplate = updatedTemplate.replace(regex, `{{${mappingName}}}`) - } - }) - - const seenNames = new Set() - const finalMapping = updatedMapping.filter(item => { - const isUsed = templateVars.some(v => item.name === v || item.value === `{{${v}}}`) - if (!isUsed || seenNames.has(item.name)) return false - seenNames.add(item.name) - return true - }) - - isSyncingRef.current = true - lastSyncSourceRef.current = 'template' - prevMappingNamesRef.current = finalMapping.filter(item => item && item.name).map((item: any) => item.name) - prevTemplateVarsRef.current = templateVars - - if (JSON.stringify(finalMapping) !== JSON.stringify(values.mapping)) { - form.setFieldValue('mapping', finalMapping) - } - if (updatedTemplate !== String(values.template)) { - form.setFieldValue('template', updatedTemplate) - } - - setTimeout(() => { - isSyncingRef.current = false - lastSyncSourceRef.current = null - }, 50) - }, [values?.template, selectedNode?.data?.type, form]) - useEffect(() => { if (selectedNode && form) { const { type = 'default', name = '', config } = selectedNode.getData() || {} @@ -197,6 +77,8 @@ const Properties: FC = ({ ...initialValue, }) setConfigs(config || {}) + } else { + form.resetFields() } }, [selectedNode, form]) @@ -529,6 +411,12 @@ const Properties: FC = ({ /> : selectedNode?.data?.type === 'tool' ? + : selectedNode?.data.type === 'jinja-render' + ? : configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => { const config = configs[key] || {} @@ -646,15 +534,6 @@ const Properties: FC = ({ ) } - - if (config.type === 'mappingList') { - return ( - - - - - ) - } if (config.type === 'cycleVarsList') { return (