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' + ?