Merge pull request #156 from SuanmoSuanyangTechnology/feature/ui_zy

refactor: extract useVariableList; properties add output variable
This commit is contained in:
yingzhao
2026-01-20 10:43:36 +08:00
committed by GitHub
14 changed files with 861 additions and 1008 deletions

View File

@@ -1,10 +1,13 @@
import { useEffect, useState, type FC, type Key } from 'react';
import { Select } from 'antd'
import type { SelectProps, DefaultOptionType } from 'antd/es/select'
import { Select } from 'antd';
import type { SelectProps, DefaultOptionType } from 'antd/es/select';
import { useTranslation } from 'react-i18next';
import { request } from '@/utils/request';
// 定义API响应类型
interface OptionType {
[key: string]: Key | string | number;
}
interface ApiResponse<T> {
items?: T[];
}
@@ -20,19 +23,16 @@ interface CustomSelectProps extends Omit<SelectProps, 'filterOption'> {
format?: (items: OptionType[]) => OptionType[];
showSearch?: boolean;
optionFilterProp?: string;
// 其他SelectProps属性
onChange?: SelectProps<Key, DefaultOptionType>['onChange'];
value?: SelectProps<Key, DefaultOptionType>['value'];
disabled?: boolean;
style?: React.CSSProperties;
className?: string;
filterOption?: (inputValue: string, option?: DefaultOptionType) => boolean;
}
interface OptionType {
[key: string]: Key | string | number;
}
const defaultFilterOption = (inputValue: string, option?: DefaultOptionType): boolean => {
if (!option || !inputValue) return true;
const label = String(option.children || option.label || '');
return label.toLowerCase().includes(inputValue.toLowerCase());
};
const CustomSelect: FC<CustomSelectProps> = ({
onChange,
url,
params,
valueKey = 'value',
@@ -42,42 +42,37 @@ const CustomSelect: FC<CustomSelectProps> = ({
allTitle,
format,
showSearch = false,
optionFilterProp = 'label',
filterOption,
...props
}) => {
const { t } = useTranslation();
const [options, setOptions] = useState<OptionType[]>([]);
// 默认模糊搜索函数
const defaultFilterOption = (inputValue: string, option?: DefaultOptionType) => {
if (!option || !inputValue) return true;
const label = String(option.children || option.label || '');
return label.toLowerCase().includes(inputValue.toLowerCase());
};
// 组件挂载时获取初始数据
useEffect(() => {
request.get<ApiResponse<OptionType>>(url, params).then((res) => {
const data = res;
setOptions(Array.isArray(data) ? data || [] : Array.isArray(data?.items) ? data.items || [] : []);
const data = Array.isArray(res) ? res : res?.items || [];
setOptions(data);
});
}, []);
}, [url, params]);
const displayOptions = format ? format(options) : options;
return (
<Select
placeholder={placeholder ? placeholder : t('common.select')}
onChange={onChange}
placeholder={placeholder || t('common.select')}
defaultValue={hasAll ? null : undefined}
showSearch={showSearch}
filterOption={filterOption || defaultFilterOption}
{...props}
>
{hasAll && (<Select.Option>{allTitle || t('common.all')}</Select.Option>)}
{(format ? format(options) : options)?.map(option => (
{hasAll && <Select.Option value={null}>{allTitle || t('common.all')}</Select.Option>}
{displayOptions.map((option) => (
<Select.Option key={option[valueKey]} value={option[valueKey]}>
{String(option[labelKey])}
</Select.Option>
))}
</Select>
);
}
};
export default CustomSelect;

View File

@@ -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',

View File

@@ -2061,6 +2061,7 @@ export const zh = {
value: '值',
addCase: '添加条件',
addVariable: '添加变量',
output: '输出变量'
},
clear: '清空',

View File

@@ -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<LexicalEditorProps> =({
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,6 +177,7 @@ const Editor: FC<LexicalEditorProps> =({
<div className="line-numbers">
<div>1</div>
</div>
<div className="editor-content-wrapper">
<ContentEditable
className="editor-content-with-numbers"
style={{
@@ -187,6 +191,7 @@ const Editor: FC<LexicalEditorProps> =({
}}
/>
</div>
</div>
) : (
<ContentEditable
style={{
@@ -207,8 +212,8 @@ const Editor: FC<LexicalEditorProps> =({
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<LexicalEditorProps> =({
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
{enableJinja2 && <BlurPlugin />}
</div>
</LexicalComposer>
);

View File

@@ -36,7 +36,7 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({
return (
<span
onClick={handleClick}
className={clsx('rb:border rb:rounded-md rb:bg-white rb:text-[12px] rb:inline-flex rb:items-center rb:py-0.5 rb:px-1.5 rb:mx-0.5 rb:cursor-pointer', {
className={clsx('rb:border rb:rounded-md rb:bg-white rb:text-[10px] rb:inline-flex rb:items-center rb:py-0 rb:px-1.5 rb:mx-0.5 rb:cursor-pointer', {
'rb:border-[#155EEF]': isSelected,
'rb:border-[#DFE4ED]': !isSelected
})}

View File

@@ -1,6 +1,6 @@
import { useEffect, useState, type FC } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getSelection, $isRangeSelection } from 'lexical';
import { $getSelection, $isRangeSelection, $isTextNode } from 'lexical';
import { INSERT_VARIABLE_COMMAND } from '../commands';
import type { NodeProperties } from '../../../types'
@@ -96,7 +96,9 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
const textAfter = nodeText.substring(anchorOffset);
const newText = textBefore + `{{${suggestion.value}}}` + textAfter;
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 (
<div
data-autocomplete-popup="true"
onMouseDown={(e) => e.preventDefault()}
style={{
position: 'fixed',
top: popupPosition.top,

View File

@@ -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;
}

View File

@@ -33,6 +33,7 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
useEffect(() => {
if (value !== prevValueRef.current && !isUserInputRef.current) {
queueMicrotask(() => {
editor.update(() => {
const root = $getRoot();
root.clear();
@@ -99,6 +100,7 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
root.append(paragraph);
}
}, { discrete: true });
});
}
prevValueRef.current = value;

View File

@@ -17,7 +17,7 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
name,
options = [],
isCanAdd = false,
size = "middle"
size = "small"
}) => {
const { t } = useTranslation();
const form = Form.useFormInstance();
@@ -37,16 +37,10 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
}
return (
<div className="rb:mb-4">
<Row gutter={12} className="rb:mb-2!">
<Col span={12}>
<Form.Item
noStyle
>
<div>
<div className="rb:font-medium rb:text-[12px] rb:mb-1">
{t('workflow.config.var-aggregator.variable')}
</Form.Item>
</Col>
</Row>
</div>
<Form.Item
name={name}

View File

@@ -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<JinjaRenderProps> = ({ selectedNode, options, templateOptions }) => {
const { t } = useTranslation()
const form = Form.useFormInstance()
const values = Form.useWatch([], form) || {}
console.log('JinjaRender values', values)
const prevMappingNamesRef = useRef<string[]>([])
const prevTemplateVarsRef = useRef<string[]>([])
const syncTimeoutRef = useRef<number | null>(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<string>()
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 (
<>
<Form.Item name="mapping" noStyle>
<MappingList name="mapping" options={options} />
</Form.Item>
<Form.Item name="template">
<MessageEditor
title={t('workflow.config.jinja-render.template')}
isArray={false}
parentName="template"
enableJinja2={true}
options={templateOptions}
titleVariant="borderless"
size="small"
/>
</Form.Item>
</>
)
}
export default JinjaRender

View File

@@ -34,9 +34,9 @@ const VariableSelect: FC<VariableSelectProps> = ({
if (filterOption) {
return (
<span
className={clsx("rb:w-full rb:wrap-break-word rb:line-clamp-1 rb:border rb:border-[#DFE4ED] rb:rounded-md rb:bg-white rb:text-[12px] rb:inline-flex rb:items-center rb:px-1.5 rb:cursor-pointer", {
className={clsx("rb:max-w-full rb:wrap-break-word rb:line-clamp-1 rb:border rb:border-[#DFE4ED] rb:rounded-md rb:bg-white rb:text-[12px] rb:inline-flex rb:items-center rb:px-1.5 rb:cursor-pointer", {
'rb:leading-5.5!': size !== 'small',
'rb:leading-4!': size === 'small'
'rb:leading-4! rb:text-[10px]!': size === 'small'
})}
contentEditable={false}
>

View File

@@ -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<string>,
key: string,
label: string,
dataType: string,
value: string,
nodeData: any,
extra?: Partial<Suggestion>
) => {
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<string>
) => {
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<string>();
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<Graph | undefined>,
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<string>();
const getPreviousNodes = (nodeId: string, visited = new Set<string>()): 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;
};

View File

@@ -1,8 +1,9 @@
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';
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';
@@ -16,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'
@@ -24,10 +24,11 @@ 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'
import JinjaRender from './JinjaRender'
interface PropertiesProps {
selectedNode?: Node | null;
@@ -49,135 +50,16 @@ const Properties: FC<PropertiesProps> = ({
const [form] = Form.useForm<NodeConfig>();
const [configs, setConfigs] = useState<Record<string,NodeConfig>>({} as Record<string,NodeConfig>)
const values = Form.useWatch([], form);
const [graphUpdateTrigger, setGraphUpdateTrigger] = useState(0)
const prevMappingNamesRef = useRef<string[]>([])
const prevTemplateVarsRef = useRef<string[]>([])
const syncTimeoutRef = useRef<number | null>(null)
const isSyncingRef = useRef(false)
const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null)
const variableList = useVariableList(selectedNode, graphRef, chatVariables)
useEffect(() => {
if (selectedNode?.getData()?.id) {
setOutputCollapsed(true)
} else {
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<string>()
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() || {}
@@ -195,6 +77,8 @@ const Properties: FC<PropertiesProps> = ({
...initialValue,
})
setConfigs(config || {})
} else {
form.resetFields()
}
}, [selectedNode, form])
@@ -244,513 +128,7 @@ const Properties: FC<PropertiesProps> = ({
}
}, [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<string>();
// Find all connected previous nodes (recursive)
const getAllPreviousNodes = (nodeId: string, visited = new Set<string>()): 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,14 +372,25 @@ const Properties: FC<PropertiesProps> = ({
// 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 (
<div className={clsx("rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3 rb:pb-6", styles.properties)}>
<div className="rb:font-medium rb:leading-5 rb:pb-3 rb:mb-3 rb:border-b rb:border-b-[#DFE4ED]">{t('workflow.nodeProperties')}</div>
{!selectedNode
? <Empty url={emptyIcon} size={140} className="rb:h-full rb:mx-15" title={t('workflow.empty')} />
: <Form form={form} size="small" layout="vertical" className="rb:h-[calc(100%-20px)] rb:overflow-x-hidden rb:overflow-y-auto">
: <div className="rb:h-[calc(100%-20px)] rb:overflow-x-hidden rb:overflow-y-auto">
<Form form={form} size="small" layout="vertical">
<Form.Item name="name" label={t('workflow.nodeName')}>
<Input
placeholder={t('common.pleaseEnter')}
@@ -1022,6 +411,12 @@ const Properties: FC<PropertiesProps> = ({
/>
: selectedNode?.data?.type === 'tool'
? <ToolConfig options={variableList} />
: selectedNode?.data.type === 'jinja-render'
? <JinjaRender
selectedNode={selectedNode}
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
templateOptions={getFilteredVariableList(selectedNode?.data?.type, 'template')}
/>
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
const config = configs[key] || {}
@@ -1139,15 +534,6 @@ const Properties: FC<PropertiesProps> = ({
</Form.Item>
)
}
if (config.type === 'mappingList') {
return (
<Form.Item key={key} name={key} noStyle>
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type, key)} />
</Form.Item>
)
}
if (config.type === 'cycleVarsList') {
return (
<Form.Item key={key} name={key}>
@@ -1311,7 +697,25 @@ const Properties: FC<PropertiesProps> = ({
})
}
</Form>
{currentNodeVariables.length > 0 && !(!values?.group && selectedNode.getData().type === 'var-aggregator') &&
<div className="rb:pb-3">
<Divider />
<Space size={8} direction="vertical" className="rb:max-w-full!">
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5 rb:cursor-pointer rb:ml-4" onClick={handleToggle}>
{t('workflow.config.output')}
{outputCollapsed ? <CaretRightOutlined /> : <CaretDownOutlined />}
</div>
{!outputCollapsed && currentNodeVariables.map(vo => (
<div key={vo.value} className="rb:ml-4 rb:text-[12px] rb:flex rb:gap-2">
<span className="rb:font-medium">{vo.label}</span>
<span className="rb:text-[#5B6167]">{vo.dataType}</span>
</div>
))}
</Space>
</div>
}
</div>}
</div>
);
};

View File

@@ -275,11 +275,6 @@ export const useWorkflowGraph = ({
}, 100)
}
if (edges.length) {
// 计算loop和iteration类型节点的数量
const loopIterationCount = nodes.filter(node =>
node.type === 'loop' || node.type === 'iteration'
).length;
// 去重处理对于if-else和question-classifier节点不同连接桩允许连接到相同节点
const uniqueEdges = edges.filter((edge, index, arr) => {
return arr.findIndex(e => {
@@ -805,6 +800,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;