feat(web): workflow import & export
This commit is contained in:
@@ -1,3 +1,15 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-30 13:59:36
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-28 16:19:26
|
||||
*/
|
||||
/**
|
||||
* ChatVariableModal Component
|
||||
*
|
||||
* This component provides a modal for adding or editing chat variables in workflows.
|
||||
* It supports various variable types and provides appropriate input fields based on the selected type.
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Select, InputNumber } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -8,54 +20,86 @@ import RbModal from '@/components/RbModal'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
/**
|
||||
* Props for ChatVariableModal component
|
||||
*/
|
||||
interface ChatVariableModalProps {
|
||||
/**
|
||||
* Callback function to refresh variable list
|
||||
* @param {ChatVariable} value - The variable data
|
||||
* @param {number} [editIndex] - Optional index for editing existing variable
|
||||
*/
|
||||
refresh: (value: ChatVariable, editIndex?: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported variable types
|
||||
*/
|
||||
const types = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'string', // String type
|
||||
'number', // Number type
|
||||
'boolean', // Boolean type
|
||||
'object', // Object type
|
||||
'array[string]', // Array of strings
|
||||
'array[number]', // Array of numbers
|
||||
'array[boolean]', // Array of booleans
|
||||
'array[object]', // Array of objects
|
||||
]
|
||||
|
||||
/**
|
||||
* ChatVariableModal component
|
||||
*/
|
||||
const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<ChatVariable>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editIndex, setEditIndex] = useState<number | undefined>(undefined)
|
||||
const type = Form.useWatch('type', form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
// State management
|
||||
const [visible, setVisible] = useState(false); // Modal visibility
|
||||
const [form] = Form.useForm<ChatVariable>(); // Form instance
|
||||
const [loading, setLoading] = useState(false); // Loading state
|
||||
const [editIndex, setEditIndex] = useState<number | undefined>(undefined); // Index of variable being edited
|
||||
const type = Form.useWatch('type', form); // Current selected type
|
||||
|
||||
/**
|
||||
* Handle modal close
|
||||
*/
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setEditIndex(undefined)
|
||||
setLoading(false);
|
||||
setEditIndex(undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle modal open
|
||||
*/
|
||||
const handleOpen = (variable?: ChatVariable, index?: number) => {
|
||||
setVisible(true);
|
||||
if (variable) {
|
||||
const { default: _, ...rest } = variable
|
||||
form.setFieldsValue({ ...rest })
|
||||
setEditIndex(index)
|
||||
// Exclude 'default' property and set form values
|
||||
const { default: _, ...rest } = variable;
|
||||
form.setFieldsValue({ ...rest });
|
||||
setEditIndex(index);
|
||||
} else {
|
||||
// Reset form for new variable
|
||||
form.resetFields();
|
||||
setEditIndex(undefined)
|
||||
setEditIndex(undefined);
|
||||
}
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
|
||||
/**
|
||||
* Handle save/submit action
|
||||
*/
|
||||
const handleSave = () => {
|
||||
form.validateFields().then((values) => {
|
||||
refresh({ ...values, default: values.defaultValue }, editIndex)
|
||||
handleClose()
|
||||
})
|
||||
}
|
||||
// Create variable with 'default' property mapped from 'defaultValue'
|
||||
refresh({ ...values, default: values.defaultValue }, editIndex);
|
||||
handleClose();
|
||||
});
|
||||
};
|
||||
|
||||
// 暴露给父组件的方法
|
||||
// Expose handleOpen method to parent component via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen
|
||||
}));
|
||||
@@ -74,6 +118,7 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
||||
layout="vertical"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
{/* Variable name field */}
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('workflow.config.parameter-extractor.name')}
|
||||
@@ -84,6 +129,8 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
{/* Variable type field */}
|
||||
<FormItem
|
||||
name="type"
|
||||
label={t('workflow.config.parameter-extractor.type')}
|
||||
@@ -98,6 +145,8 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
||||
}))}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
{/* Default value field - dynamic based on type */}
|
||||
<Form.Item
|
||||
name="defaultValue"
|
||||
label={t('workflow.config.parameter-extractor.default')}
|
||||
@@ -119,6 +168,8 @@ const ChatVariableModal = forwardRef<ChatVariableModalRef, ChatVariableModalProp
|
||||
: <Input placeholder={t('common.enter')} />
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
{/* Variable description field */}
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('workflow.config.parameter-extractor.desc')}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-06 21:10:56
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-27 09:58:30
|
||||
* @Last Modified time: 2026-02-28 16:43:06
|
||||
*/
|
||||
/**
|
||||
* Workflow Chat Component
|
||||
@@ -97,6 +97,8 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
setConversationId(null)
|
||||
setMessage(undefined)
|
||||
setFileList([])
|
||||
setLoading(false)
|
||||
setStreamLoading(false)
|
||||
}
|
||||
/**
|
||||
* Opens the variable configuration modal
|
||||
@@ -179,6 +181,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
cycle_idx: number;
|
||||
node_id: string;
|
||||
node_name?: string;
|
||||
node_type?: string;
|
||||
input?: any;
|
||||
output?: any;
|
||||
elapsed_time?: string;
|
||||
@@ -188,7 +191,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
};
|
||||
|
||||
const node = graphRef.current?.getNodes().find(n => n.id === node_id);
|
||||
const { name, icon } = node?.getData() || {}
|
||||
const { name, icon, type } = node?.getData() || {}
|
||||
|
||||
switch(item.event) {
|
||||
// Append streaming text chunks to assistant message
|
||||
@@ -218,6 +221,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
...newSubContent[filterIndex],
|
||||
node_id: node_id,
|
||||
node_name: name,
|
||||
node_type: type,
|
||||
icon,
|
||||
content: {},
|
||||
}
|
||||
@@ -226,6 +230,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
id: node_id,
|
||||
node_id: node_id,
|
||||
node_name: name,
|
||||
node_type: type,
|
||||
icon,
|
||||
content: {},
|
||||
})
|
||||
@@ -282,6 +287,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
cycle_idx,
|
||||
node_id,
|
||||
node_name: name,
|
||||
node_type: type,
|
||||
icon,
|
||||
content: {
|
||||
cycle_idx,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-24 17:57:08
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-24 17:57:08
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-28 16:48:09
|
||||
*/
|
||||
/*
|
||||
* Runtime Component
|
||||
@@ -105,7 +105,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
||||
if (Array.isArray(list)) {
|
||||
return <Space size={8} direction="vertical" className="rb:w-full!">
|
||||
{list?.map(vo => {
|
||||
const isLoop = vo.node_id.startsWith('loop');
|
||||
const isLoop = vo.node_type === 'loop';
|
||||
// Render cycle variables for loop nodes without node_name
|
||||
if (typeof vo.cycle_idx === 'number' && isLoop && !vo.node_name) {
|
||||
return <div className="rb:bg-[#F0F3F8] rb:rounded-md">
|
||||
@@ -165,7 +165,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
||||
}
|
||||
{/* Display navigation to nested cycles if subContent exists */}
|
||||
{vo.subContent?.length > 0 && (
|
||||
<Flex justify="space-between" className="rb:bg-[#F0F3F8] rb:rounded-md rb:py-2! rb:px-3! rb:cursor-pointer" onClick={() => handleViewDetail(vo, vo.node_id.startsWith('loop'))}>
|
||||
<Flex justify="space-between" className="rb:bg-[#F0F3F8] rb:rounded-md rb:py-2! rb:px-3! rb:cursor-pointer" onClick={() => handleViewDetail(vo, vo.node_type === 'loop')}>
|
||||
<span>{Math.max(...vo.subContent.map((itemVo: any) => itemVo.cycle_idx + 1))} {t(`workflow.${isLoop ? 'loopNum' : 'iterationNum'}`)}</span>
|
||||
<RightOutlined />
|
||||
</Flex>
|
||||
@@ -217,7 +217,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
||||
children: (
|
||||
detail
|
||||
? (
|
||||
<div className="rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 rb:mt-2">
|
||||
<div className="rb:bg-[#FBFDFF] rb:rounded-md">
|
||||
<Button type="link" icon={<ArrowLeftOutlined />} onClick={() => setDetail(null)} className="rb:px-0! rb:text-[12px]!">
|
||||
{t('common.return')}
|
||||
</Button>
|
||||
|
||||
@@ -28,9 +28,6 @@
|
||||
background-color: transparent;
|
||||
border-top: none;
|
||||
}
|
||||
:global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
||||
padding-top: 0;
|
||||
}
|
||||
.collapse-item :global(.ant-collapse) {
|
||||
/* background-color: #F0F3F8; */
|
||||
background-color: #FBFDFF;
|
||||
@@ -41,5 +38,5 @@
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
.collapse-item :global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
||||
padding: 0 4px 4px 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-09 18:24:53
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-09 18:24:53
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-28 17:49:28
|
||||
*/
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
@@ -292,7 +292,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type?.toLocaleLowerCase() : undefined;
|
||||
return (
|
||||
<div key={conditionField.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
|
||||
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
@@ -330,7 +330,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
||||
options={[{ value: 'variable', label: 'Variable' }, { value: 'constant', label: 'Constant' }]}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)}
|
||||
@@ -339,7 +339,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
</Form.Item>
|
||||
<Divider type="vertical" />
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{inputType === 'Variable'
|
||||
{inputType === 'variable'
|
||||
?
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
|
||||
@@ -118,7 +118,8 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type?.toLocaleLowerCase() : undefined;
|
||||
console.log('inputType', inputType)
|
||||
|
||||
return (
|
||||
<div key={field.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
|
||||
@@ -160,7 +161,7 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
<Form.Item name={[field.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
||||
options={[{ value: 'variable', label: 'Variable' }, { value: 'constant', label: 'Constant' }]}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
@@ -169,7 +170,7 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
</Form.Item>
|
||||
<Divider type="vertical" />
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
{inputType === 'Variable'
|
||||
{inputType === 'variable'
|
||||
?
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-01-19 17:00:26
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-28 16:24:31
|
||||
*/
|
||||
/**
|
||||
* useVariableList Hook
|
||||
*
|
||||
* This hook provides functionality for managing and retrieving variables in workflow nodes.
|
||||
* It handles variable extraction from different node types, including:
|
||||
* - Node-specific output variables
|
||||
* - Chat variables
|
||||
* - Loop and iteration variables
|
||||
* - Connected node variables
|
||||
*/
|
||||
import { useMemo, useEffect, useState } from 'react';
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
|
||||
import type { ChatVariable } from '../../../types';
|
||||
|
||||
/**
|
||||
* Node variable definitions
|
||||
*
|
||||
* Maps node types to their available output variables
|
||||
*/
|
||||
const NODE_VARIABLES = {
|
||||
llm: [{ label: 'output', dataType: 'string', field: 'output' }],
|
||||
'jinja-render': [{ label: 'output', dataType: 'string', field: 'output' }],
|
||||
@@ -23,6 +44,18 @@ const NODE_VARIABLES = {
|
||||
]
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Add variable to list if not already present
|
||||
*
|
||||
* @param {Suggestion[]} list - List of suggestions to add to
|
||||
* @param {Set<string>} keys - Set of existing keys to check for duplicates
|
||||
* @param {string} key - Unique key for the variable
|
||||
* @param {string} label - Human-readable label for the variable
|
||||
* @param {string} dataType - Data type of the variable
|
||||
* @param {string} value - Variable value/expression
|
||||
* @param {any} nodeData - Node data associated with the variable
|
||||
* @param {Partial<Suggestion>} [extra] - Additional suggestion properties
|
||||
*/
|
||||
const addVariable = (
|
||||
list: Suggestion[],
|
||||
keys: Set<string>,
|
||||
@@ -39,6 +72,14 @@ const addVariable = (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Process node variables based on node type
|
||||
*
|
||||
* @param {any} nodeData - Node data object
|
||||
* @param {string} dataNodeId - Node ID
|
||||
* @param {Suggestion[]} variableList - List to add variables to
|
||||
* @param {Set<string>} addedKeys - Set of already added keys
|
||||
*/
|
||||
const processNodeVariables = (
|
||||
nodeData: any,
|
||||
dataNodeId: string,
|
||||
@@ -47,29 +88,35 @@ const processNodeVariables = (
|
||||
) => {
|
||||
const { type, config } = nodeData;
|
||||
|
||||
// Add node-specific variables
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// Process special node types
|
||||
switch (type) {
|
||||
case 'start':
|
||||
// Add start node variables
|
||||
[...(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);
|
||||
});
|
||||
// Add system variables
|
||||
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':
|
||||
// Add extracted parameters
|
||||
(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':
|
||||
// Add aggregated variables
|
||||
if (config.group.defaultValue) {
|
||||
(config.group_variables.defaultValue || []).forEach((gv: any) => {
|
||||
if (gv?.key) {
|
||||
@@ -93,6 +140,7 @@ const processNodeVariables = (
|
||||
break;
|
||||
|
||||
case 'iteration':
|
||||
// Add iteration output variable
|
||||
let dt = 'string';
|
||||
if (nodeData.output) {
|
||||
const sv = variableList.find(v => v.value === nodeData.output);
|
||||
@@ -102,11 +150,14 @@ const processNodeVariables = (
|
||||
break;
|
||||
|
||||
case 'loop':
|
||||
// Add loop cycle variables
|
||||
(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;
|
||||
|
||||
case 'code':
|
||||
// Add code node output variables
|
||||
(config.output_variables.defaultValue || []).forEach((cv: any) => {
|
||||
if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData);
|
||||
});
|
||||
@@ -114,6 +165,9 @@ const processNodeVariables = (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Node types that have output variables
|
||||
*/
|
||||
const hasOutputNodeTypes = [
|
||||
'llm',
|
||||
'knowledge-retrieval',
|
||||
@@ -123,7 +177,15 @@ const hasOutputNodeTypes = [
|
||||
'http-request',
|
||||
'tool',
|
||||
'jinja-render'
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Get variables for the current node
|
||||
*
|
||||
* @param {any} nodeData - Node data object
|
||||
* @param {any} values - Additional values to merge with node config
|
||||
* @returns {Suggestion[]} List of node variables
|
||||
*/
|
||||
export const getCurrentNodeVariables = (nodeData: any, values: any): Suggestion[] => {
|
||||
if (!nodeData || !hasOutputNodeTypes.includes(nodeData.type)) return [];
|
||||
const list: Suggestion[] = [];
|
||||
@@ -137,9 +199,18 @@ export const getCurrentNodeVariables = (nodeData: any, values: any): Suggestion[
|
||||
...values
|
||||
}
|
||||
}, dataNodeId, list, keys);
|
||||
|
||||
// Special case: var-aggregator without group enabled returns no variables
|
||||
return nodeData.type === 'var-aggregator' && !nodeData.config.group.defaultValue ? [] : list;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get variables from child nodes in a loop/iteration
|
||||
*
|
||||
* @param {Node} selectedNode - Selected node
|
||||
* @param {React.MutableRefObject<Graph | undefined>} graphRef - Graph reference
|
||||
* @returns {Suggestion[]} List of child node variables
|
||||
*/
|
||||
export const getChildNodeVariables = (
|
||||
selectedNode: Node,
|
||||
graphRef: React.MutableRefObject<Graph | undefined>
|
||||
@@ -152,8 +223,15 @@ export const getChildNodeVariables = (
|
||||
const edges = graph.getEdges();
|
||||
const keys = new Set<string>();
|
||||
|
||||
// Find child nodes in the same cycle
|
||||
const childNodes = nodes.filter(node => node.getData()?.cycle === selectedNode.id);
|
||||
|
||||
/**
|
||||
* Get all connected nodes recursively
|
||||
* @param {string} nodeId - Node ID to start from
|
||||
* @param {Set<string>} visited - Set of visited node IDs
|
||||
* @returns {string[]} List of connected node IDs
|
||||
*/
|
||||
const getConnectedNodes = (nodeId: string, visited = new Set<string>()): string[] => {
|
||||
if (visited.has(nodeId)) return [];
|
||||
visited.add(nodeId);
|
||||
@@ -161,12 +239,14 @@ export const getChildNodeVariables = (
|
||||
return [...prev, ...prev.flatMap(id => getConnectedNodes(id, visited))];
|
||||
};
|
||||
|
||||
// Collect all relevant node IDs
|
||||
const relevantIds = new Set<string>();
|
||||
childNodes.forEach(child => {
|
||||
relevantIds.add(child.id);
|
||||
getConnectedNodes(child.id).forEach(id => relevantIds.add(id));
|
||||
});
|
||||
|
||||
// Process each relevant node
|
||||
relevantIds.forEach(id => {
|
||||
const node = nodes.find(n => n.id === id);
|
||||
if (!node) return;
|
||||
@@ -175,6 +255,7 @@ export const getChildNodeVariables = (
|
||||
const nodeId = nodeData.id;
|
||||
const { type } = nodeData;
|
||||
|
||||
// Add node-specific variables
|
||||
if (type in NODE_VARIABLES) {
|
||||
NODE_VARIABLES[type as keyof typeof NODE_VARIABLES].forEach(({ label, dataType, field }) => {
|
||||
const varKey = `${nodeId}_${label}`;
|
||||
@@ -192,6 +273,7 @@ export const getChildNodeVariables = (
|
||||
});
|
||||
}
|
||||
|
||||
// Add parameter-extractor variables
|
||||
if (type === 'parameter-extractor') {
|
||||
(nodeData.config?.params?.defaultValue || []).forEach((p: any) => {
|
||||
if (p?.name && !keys.has(`${nodeId}_${p.name}`)) {
|
||||
@@ -207,11 +289,36 @@ export const getChildNodeVariables = (
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add code node variables
|
||||
if (type === 'code') {
|
||||
(nodeData.config?.output_variables?.defaultValue || []).forEach((p: any) => {
|
||||
if (p?.name && !keys.has(`${nodeId}_${p.name}`)) {
|
||||
keys.add(`${nodeId}_${p.name}`);
|
||||
list.push({
|
||||
key: `${nodeId}_${p.name}`,
|
||||
label: p.name,
|
||||
type: 'variable',
|
||||
dataType: p.type || 'string',
|
||||
value: `${nodeId}.${p.name}`,
|
||||
nodeData,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return list;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for managing workflow variable list
|
||||
*
|
||||
* @param {Node | null | undefined} selectedNode - Currently selected node
|
||||
* @param {React.MutableRefObject<Graph | undefined>} graphRef - Graph reference
|
||||
* @param {ChatVariable[]} chatVariables - List of chat variables
|
||||
* @returns {Suggestion[]} List of available variables
|
||||
*/
|
||||
export const useVariableList = (
|
||||
selectedNode: Node | null | undefined,
|
||||
graphRef: React.MutableRefObject<Graph | undefined>,
|
||||
@@ -228,6 +335,12 @@ export const useVariableList = (
|
||||
const nodes = graph.getNodes();
|
||||
const keys = new Set<string>();
|
||||
|
||||
/**
|
||||
* Get all previous connected nodes recursively
|
||||
* @param {string} nodeId - Node ID to start from
|
||||
* @param {Set<string>} visited - Set of visited node IDs
|
||||
* @returns {string[]} List of previous node IDs
|
||||
*/
|
||||
const getPreviousNodes = (nodeId: string, visited = new Set<string>()): string[] => {
|
||||
if (visited.has(nodeId)) return [];
|
||||
visited.add(nodeId);
|
||||
@@ -235,6 +348,11 @@ export const useVariableList = (
|
||||
return [...prev, ...prev.flatMap(id => getPreviousNodes(id, visited))];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get parent loop/iteration node
|
||||
* @param {string} nodeId - Node ID to check
|
||||
* @returns {Node | null} Parent loop/iteration node or null
|
||||
*/
|
||||
const getParentLoop = (nodeId: string): Node | null => {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
const cycle = node?.getData()?.cycle;
|
||||
@@ -245,17 +363,21 @@ export const useVariableList = (
|
||||
return null;
|
||||
};
|
||||
|
||||
// Collect relevant node IDs
|
||||
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) : [])];
|
||||
|
||||
// Add chat variables
|
||||
chatVariables?.forEach(v => addVariable(list, keys, `CONVERSATION_${v.name}`, v.name, v.type, `conv.${v.name}`, { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' }, { group: 'CONVERSATION' }));
|
||||
|
||||
// Process each relevant node
|
||||
relevantIds.forEach(id => {
|
||||
const node = nodes.find(n => n.id === id);
|
||||
if (node) processNodeVariables(node.getData(), node.getData().id, list, keys);
|
||||
});
|
||||
|
||||
// Add parent loop variables
|
||||
if (parentLoop) {
|
||||
const pd = parentLoop.getData();
|
||||
const pid = pd.id;
|
||||
@@ -270,7 +392,9 @@ export const useVariableList = (
|
||||
} 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');}
|
||||
if (iv?.dataType.startsWith('array[')) {
|
||||
itemType = iv.dataType.replace(/^array\[(.+)\]$/, '$1');
|
||||
}
|
||||
addVariable(list, keys, `${pid}_item`, 'item', 'string', `${pid}.item`, pd);
|
||||
addVariable(list, keys, `${pid}_index`, 'index', 'number', `${pid}.index`, pd);
|
||||
}
|
||||
@@ -279,6 +403,7 @@ export const useVariableList = (
|
||||
return list;
|
||||
}, [selectedNode, graphRef, trigger, chatVariables]);
|
||||
|
||||
// Refresh variable list when graph changes
|
||||
useEffect(() => {
|
||||
if (!graphRef?.current) return;
|
||||
const graph = graphRef.current;
|
||||
|
||||
Reference in New Issue
Block a user