import { type FC, useEffect, useState, useRef, useMemo } from "react"; import { useTranslation } from 'react-i18next' import { Graph, Node } from '@antv/x6'; import { Form, Input, Button, Select, InputNumber, Slider, Space, Divider, App, Switch } from 'antd' import type { NodeConfig, NodeProperties, StartVariableItem, VariableEditModalRef } from '../../types' import Empty from '@/components/Empty'; import emptyIcon from '@/assets/images/workflow/empty.png' import CustomSelect from "@/components/CustomSelect"; import VariableEditModal from './VariableEditModal'; import MessageEditor from './MessageEditor' import Knowledge from './Knowledge/Knowledge'; import type { Suggestion } from '../Editor/plugin/AutocompletePlugin' import VariableSelect from './VariableSelect'; 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' import AssignmentList from './AssignmentList' import ToolConfig from './ToolConfig' import MemoryConfig from './MemoryConfig' // import { calculateVariableList } from './utils/variableListCalculator' interface PropertiesProps { selectedNode?: Node | null; setSelectedNode: (node: Node | null) => void; graphRef: React.MutableRefObject; blankClick: () => void; deleteEvent: () => void; copyEvent: () => void; parseEvent: () => void; config?: any; } const Properties: FC = ({ selectedNode, graphRef, config: workflowConfig, }) => { const { t } = useTranslation() const { modal } = App.useApp() const [form] = Form.useForm(); const [configs, setConfigs] = useState>({} as Record) const values = Form.useWatch([], form); const variableModalRef = useRef(null) const [editIndex, setEditIndex] = useState(null) const prevMappingNamesRef = useRef([]) const prevTemplateVarsRef = useRef([]) const syncTimeoutRef = useRef(null) const isSyncingRef = useRef(false) const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null) useEffect(() => { if (selectedNode?.getData()?.id) { form.resetFields() 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) ? 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() || {} const initialValue: Record = {} Object.keys(config || {}).forEach(key => { if (config && config[key] && 'defaultValue' in config[key]) { initialValue[key] = config[key].defaultValue } }) form.setFieldsValue({ type, id: selectedNode.id, name, ...initialValue, }) setConfigs(config || {}) } }, [selectedNode, form]) const updateNodeLabel = (newLabel: string) => { if (selectedNode && form) { const nodeData = selectedNode.data as NodeProperties; selectedNode.setAttrByPath('text/text', `${nodeData.icon} ${newLabel}`); selectedNode.setData({ ...selectedNode.data, name: newLabel }); } }; useEffect(() => { if (values && selectedNode) { const { id, knowledge_retrieval, group, group_variables, ...rest } = values const { knowledge_bases = [], ...restKnowledgeConfig } = (knowledge_retrieval as any) || {} let allRest = { ...rest, ...restKnowledgeConfig, } if (knowledge_bases?.length) { allRest.knowledge_bases = knowledge_bases?.map((vo: any) => ({ id: vo.id, ...vo.config })) } Object.keys(values).forEach(key => { if (selectedNode.data?.config?.[key]) { // Create a deep copy to avoid reference sharing between nodes if (!selectedNode.data.config[key]) { selectedNode.data.config[key] = {}; } selectedNode.data.config[key] = { ...selectedNode.data.config[key], defaultValue: values[key] }; } }) selectedNode?.setData({ ...selectedNode.data, ...allRest, }) } }, [values, selectedNode, form]) const handleAddVariable = () => { variableModalRef.current?.handleOpen() } const handleEditVariable = (index: number, vo: StartVariableItem) => { variableModalRef.current?.handleOpen(vo) setEditIndex(index) } const handleRefreshVariable = (value: StartVariableItem) => { if (!selectedNode) return if (editIndex !== null) { const defaultValue = selectedNode.data.config.variables.defaultValue ?? [] defaultValue[editIndex] = value selectedNode.data.config.variables.defaultValue = [...defaultValue] } else { const defaultValue = selectedNode.data.config.variables.defaultValue ?? [] selectedNode.data.config.variables.defaultValue = [...defaultValue, value] } selectedNode?.setData({ ...selectedNode.data}) setConfigs({ ...selectedNode.data.config}) } const handleDeleteVariable = (index: number, vo: StartVariableItem) => { if (!selectedNode) return modal.confirm({ title: t('common.confirmDeleteDesc', { name: vo.name }), okText: t('common.delete'), cancelText: t('common.cancel'), okType: 'danger', onOk: () => { const defaultValue = selectedNode.data.config.variables.defaultValue ?? [] defaultValue.splice(index, 1) selectedNode.data.config.variables.defaultValue = [...defaultValue] selectedNode?.setData({ ...selectedNode.data }) setConfigs({ ...selectedNode.data.config }) } }) } 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 = workflowConfig?.variables || []; 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, workflowConfig?.variables]); // Filter out boolean type variables for loop and llm nodes const getFilteredVariableList = (nodeType?: string, key?: string) => { // Check if current node is a child of iteration node const parentIterationNode = selectedNode ? (() => { const nodes = graphRef.current?.getNodes() || []; const nodeData = selectedNode.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 === 'iteration') { return parentNode; } } } return null; })() : null; // Helper function to add parent iteration variables const addParentIterationVars = (filteredList: any[]) => { if (parentIterationNode) { const parentData = parentIterationNode.getData(); const parentNodeId = parentData.id; if (parentData.config?.input?.defaultValue) { const itemKey = `${parentNodeId}_item`; const indexKey = `${parentNodeId}_index`; const existingItemVar = filteredList.find(v => v.key === itemKey); const existingIndexVar = filteredList.find(v => v.key === indexKey); if (!existingItemVar) { // Determine item dataType from input variable let itemDataType = 'object'; const inputVariable = variableList.find(v => `{{${v.value}}}` === parentData.config.input.defaultValue); if (inputVariable && inputVariable.dataType.startsWith('array[')) { itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1'); } filteredList.push({ key: itemKey, label: 'item', type: 'variable', dataType: itemDataType, value: `${parentNodeId}.item`, nodeData: parentData, }); } if (!existingIndexVar) { filteredList.push({ key: indexKey, label: 'index', type: 'variable', dataType: 'number', value: `${parentNodeId}.index`, nodeData: parentData, }); } } } return filteredList; }; if (nodeType === 'llm') { // For LLM nodes that are children of iteration or loop nodes, include parent variables const parentLoopNode = selectedNode ? (() => { const nodes = graphRef.current?.getNodes() || []; const nodeData = selectedNode.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; })() : null; let filteredList = variableList.filter(variable => variable.dataType !== 'boolean'); // If this LLM node is a child of iteration/loop, ensure parent variables are included if (parentLoopNode) { const parentData = parentLoopNode.getData(); const parentNodeId = parentData.id; // Ensure parent loop/iteration variables are included if (parentData.type === 'loop') { const cycleVars = parentData.cycle_vars || []; cycleVars.forEach((cycleVar: any) => { const key = `${parentNodeId}_cycle_${cycleVar.name}`; const existingVar = filteredList.find(v => v.key === key); if (!existingVar && cycleVar.name && cycleVar.type !== 'boolean') { filteredList.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 if (parentData.config?.input?.defaultValue) { const itemKey = `${parentNodeId}_item`; const indexKey = `${parentNodeId}_index`; const existingItemVar = filteredList.find(v => v.key === itemKey); const existingIndexVar = filteredList.find(v => v.key === indexKey); if (!existingItemVar) { // Determine item dataType from input variable let itemDataType = 'object'; const inputVariable = variableList.find(v => `{{${v.value}}}` === parentData.config.input.defaultValue); if (inputVariable && inputVariable.dataType.startsWith('array[')) { itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1'); } filteredList.push({ key: itemKey, label: 'item', type: 'variable', dataType: itemDataType, value: `${parentNodeId}.item`, nodeData: parentData, }); } if (!existingIndexVar) { filteredList.push({ key: indexKey, label: 'index', type: 'variable', dataType: 'Number', value: `${parentNodeId}.index`, nodeData: parentData, }); } } } } return filteredList; } if (nodeType === 'knowledge-retrieval' || nodeType === 'parameter-extractor' && key !== 'prompt' || nodeType === 'memory-read' || nodeType === 'memory-write' || nodeType === 'question-classifier') { let filteredList = addParentIterationVars(variableList).filter(variable => variable.dataType === 'string'); return filteredList; } if (nodeType === 'parameter-extractor' && key === 'prompt') { let filteredList = addParentIterationVars(variableList).filter(variable => variable.dataType === 'string' || variable.dataType === 'number'); return filteredList; } if (nodeType === 'iteration' && key === 'output') { return variableList.filter(variable => variable.value.includes('sys.')); } if (nodeType === 'iteration') { return variableList.filter(variable => variable.dataType.includes('array')); } if (nodeType === 'loop' && key === 'condition') { let filteredList = addParentIterationVars(variableList).filter(variable => variable.nodeData.type !== 'loop'); // Add child node output variables for loop nodes if (selectedNode) { const graph = graphRef.current; if (graph) { const nodes = graph.getNodes(); const childNodes = nodes.filter(node => { const nodeData = node.getData(); return nodeData?.cycle === selectedNode.id; }); // Add output variables from child nodes childNodes.forEach(childNode => { const childData = childNode.getData(); const childNodeId = childData.id; // Add child node output variables based on their type switch(childData.type) { case 'llm': case 'jinja-render': case 'tool': const outputKey = `${childNodeId}_output`; const existingOutput = filteredList.find(v => v.key === outputKey); if (!existingOutput) { filteredList.push({ key: outputKey, label: 'output', type: 'variable', dataType: 'string', value: `${childNodeId}.output`, nodeData: childData, }); } break; case 'http-request': const bodyKey = `${childNodeId}_body`; const statusKey = `${childNodeId}_status_code`; if (!filteredList.find(v => v.key === bodyKey)) { filteredList.push({ key: bodyKey, label: 'body', type: 'variable', dataType: 'string', value: `${childNodeId}.body`, nodeData: childData, }); } if (!filteredList.find(v => v.key === statusKey)) { filteredList.push({ key: statusKey, label: 'status_code', type: 'variable', dataType: 'number', value: `${childNodeId}.status_code`, nodeData: childData, }); } break; } }); } } return filteredList; } // For all other node types, add parent iteration variables if applicable let baseList = variableList; return addParentIterationVars(baseList); }; // const defaultVariableList = calculateVariableList(selectedNode as Node, graphRef, workflowConfig ) console.log('values', values) console.log('variableList', variableList) return (
{t('workflow.nodeProperties')}
{!selectedNode ? :
{ updateNodeLabel(e.target.value); }} /> {selectedNode?.data?.type === 'http-request' ? : 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 (
{t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
{Array.isArray(config.defaultValue) && config.defaultValue?.map((vo, index) =>
{vo.name}·{vo.description}
{vo.required && {t('workflow.config.start.required')}} {vo.type}
handleEditVariable(index, vo)} >
handleDeleteVariable(index, vo)} >
)} {config.sys?.map((vo, index) =>
sys.{vo.name}
{vo.type}
)}
) } 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} /> ) } if (selectedNode?.data?.type === 'end' && key === 'output') { return ( variable.nodeData?.type !== 'knowledge-retrieval')} /> ) } 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 ( ) } return ( {config.type === 'input' ? : config.type === 'textarea' ? : config.type === 'select' ?