import { type FC, useEffect, useState, useMemo } from "react"; import clsx from 'clsx' import { useTranslation } from 'react-i18next' import { Graph, Node } from '@antv/x6'; 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'; import emptyIcon from '@/assets/images/workflow/empty.png' import CustomSelect from "@/components/CustomSelect"; 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 CategoryList from './CategoryList' import ConditionList from './ConditionList' import CycleVarsList from './CycleVarsList' import AssignmentList from './AssignmentList' import ToolConfig from './ToolConfig' import MemoryConfig from './MemoryConfig' import VariableList from './VariableList' import { useVariableList, getCurrentNodeVariables, getChildNodeVariables } from './hooks/useVariableList' import styles from './properties.module.css' import Editor from "../Editor"; import RbSlider from './RbSlider' import JinjaRender from './JinjaRender' interface PropertiesProps { selectedNode?: Node | null; setSelectedNode: (node: Node | null) => void; graphRef: React.MutableRefObject; blankClick: () => void; deleteEvent: () => void; copyEvent: () => void; parseEvent: () => void; config?: any; chatVariables: ChatVariable[]; } const Properties: FC = ({ selectedNode, graphRef, chatVariables }) => { const { t } = useTranslation() const [form] = Form.useForm(); const [configs, setConfigs] = useState>({} as Record) const values = Form.useWatch([], form); const variableList = useVariableList(selectedNode, graphRef, chatVariables) useEffect(() => { if (selectedNode?.getData()?.id) { setOutputCollapsed(true) } else { form.resetFields() } }, [selectedNode?.getData()?.id]) 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 || {}) } else { form.resetFields() } }, [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]) // 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' || nodeType === 'loop' && key === 'condition') { if (!selectedNode) return []; let filteredList = nodeType === 'iteration' ? variableList.filter(variable => variable.value.includes('sys.')) : addParentIterationVars(variableList).filter(variable => variable.nodeData.type !== 'loop'); const childVariables = getChildNodeVariables(selectedNode, graphRef); const existingKeys = new Set(filteredList.map(v => v.key)); childVariables.forEach(v => { if (!existingKeys.has(v.key)) { filteredList.push(v); existingKeys.add(v.key); } }); return filteredList; } if (nodeType === 'iteration') { return variableList.filter(variable => variable.dataType.includes('array')); } // 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) 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); }} /> {selectedNode?.data?.type === 'http-request' ? : selectedNode?.data?.type === 'tool' ? : selectedNode?.data?.type === 'jinja-render' ? : 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 === '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')} /> ) } 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' ?