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());
};
// 组件挂载时获取初始数据
const [options, setOptions] = useState<OptionType[]>([]);
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}
<Select
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,18 +177,20 @@ const Editor: FC<LexicalEditorProps> =({
<div className="line-numbers">
<div>1</div>
</div>
<ContentEditable
className="editor-content-with-numbers"
style={{
minHeight: minheight,
padding: '4px 0',
outline: 'none',
resize: 'none',
fontSize: fontSize,
lineHeight: lineHeight,
border: 'none',
}}
/>
<div className="editor-content-wrapper">
<ContentEditable
className="editor-content-with-numbers"
style={{
minHeight: minheight,
padding: '4px 0',
outline: 'none',
resize: 'none',
fontSize: fontSize,
lineHeight: lineHeight,
border: 'none',
}}
/>
</div>
</div>
) : (
<ContentEditable
@@ -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;
anchorNode.setTextContent(newText);
if ($isTextNode(anchorNode)) {
anchorNode.setTextContent(newText);
}
// 设置光标位置到插入文本之后
const newOffset = textBefore.length + `{{${suggestion.value}}}`.length;
@@ -129,6 +131,8 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
}
return (
<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,7 +33,8 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ 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<InitialValuePluginProps> = ({ value, options
});
root.append(paragraph);
}
}, { discrete: true });
}, { 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
>
{t('workflow.config.var-aggregator.variable')}
</Form.Item>
</Col>
</Row>
<div>
<div className="rb:font-medium rb:text-[12px] rb:mb-1">
{t('workflow.config.var-aggregator.variable')}
</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;
};

File diff suppressed because it is too large Load Diff

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;