mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-09-05 11:01:09 +00:00
feat(awel): Support simple templates
This commit is contained in:
@@ -39,12 +39,20 @@ export const SaveFlowModal: React.FC<Props> = ({
|
||||
setId(router.query.id || '');
|
||||
}, [router.query.id]);
|
||||
|
||||
// function onLabelChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
// const label = e.target.value;
|
||||
// // replace spaces with underscores, convert uppercase letters to lowercase, remove characters other than digits, letters, _, and -.
|
||||
// const result = label
|
||||
// .replace(/\s+/g, '_')
|
||||
// .replace(/[^a-z0-9_-]/g, '')
|
||||
// .toLowerCase();
|
||||
// form.setFieldsValue({ name: result });
|
||||
// }
|
||||
function onLabelChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const label = e.target.value;
|
||||
// replace spaces with underscores, convert uppercase letters to lowercase, remove characters other than digits, letters, _, and -.
|
||||
// replace spaces with underscores, convert letters to lowercase, but preserve Chinese and other characters
|
||||
const result = label
|
||||
.replace(/\s+/g, '_')
|
||||
.replace(/[^a-z0-9_-]/g, '')
|
||||
.replace(/\s+/g, '_') // replace spaces with underscores
|
||||
.toLowerCase();
|
||||
form.setFieldsValue({ name: result });
|
||||
}
|
||||
|
@@ -140,7 +140,7 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
|
||||
<TypeLabel label='Outputs' />
|
||||
{outputs?.map((output, index) => (
|
||||
<NodeHandler
|
||||
key={`${data.id}_input_${index}`}
|
||||
key={`${data.id}_output_${index}`}
|
||||
node={data}
|
||||
data={output}
|
||||
type='source'
|
||||
@@ -159,6 +159,7 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { IFlowNode, IFlowNodeInput, IFlowNodeOutput, IFlowNodeParameter } from '@/types/flow';
|
||||
import { FLOW_NODES_KEY } from '@/utils';
|
||||
import { InfoCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Popconfirm, Tooltip, Typography, message } from 'antd';
|
||||
import { InfoCircleOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Popconfirm, Tooltip, Typography, message } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -23,6 +23,20 @@ const NodeHandler: React.FC<NodeHandlerProps> = ({ node, data, type, label, inde
|
||||
const reactflow = useReactFlow();
|
||||
const [relatedNodes, setRelatedNodes] = React.useState<IFlowNode[]>([]);
|
||||
|
||||
const dynamic = data.dynamic || false;
|
||||
const dynamicMinimum = data.dynamic_minimum || 0;
|
||||
|
||||
// Determine if input is optional based on dynamic and dynamicMinimum
|
||||
const isOptional = () => {
|
||||
if (dynamic) {
|
||||
// When dynamic is true, it's optional if dynamicMinimum is 0
|
||||
return dynamicMinimum === 0;
|
||||
} else {
|
||||
// When dynamic is false, use the original logic
|
||||
return data.optional;
|
||||
}
|
||||
};
|
||||
|
||||
function isValidConnection(connection: Connection) {
|
||||
const { sourceHandle, targetHandle, source, target } = connection;
|
||||
const sourceNode = reactflow.getNode(source!);
|
||||
@@ -49,15 +63,20 @@ const NodeHandler: React.FC<NodeHandlerProps> = ({ node, data, type, label, inde
|
||||
return false;
|
||||
}
|
||||
|
||||
function showRelatedNodes() {
|
||||
function showRelatedNodes(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// find all nodes that can be connected to this node
|
||||
const cache = localStorage.getItem(FLOW_NODES_KEY);
|
||||
if (!cache) {
|
||||
return;
|
||||
}
|
||||
|
||||
const staticNodes = JSON.parse(cache);
|
||||
const typeCls = data.type_cls;
|
||||
let nodes: IFlowNode[] = [];
|
||||
|
||||
if (label === 'inputs') {
|
||||
// find other operators and outputs matching this input type_cls
|
||||
nodes = staticNodes
|
||||
@@ -89,9 +108,115 @@ const NodeHandler: React.FC<NodeHandlerProps> = ({ node, data, type, label, inde
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setRelatedNodes(nodes);
|
||||
}
|
||||
|
||||
// Add new dynamic field
|
||||
function addDynamicField(e: React.MouseEvent) {
|
||||
console.log('addDynamicField clicked', e);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
console.log(`Adding dynamic field for node ${node.id}, label=${label}, current field name=${data.name}`);
|
||||
|
||||
// Get current IO array
|
||||
const ioArray = [...node[label]];
|
||||
// Get the original field template
|
||||
const fieldTemplate = { ...data };
|
||||
|
||||
// CHECK: How many dynamic fields of this type already exist
|
||||
const dynamicFieldsCount = ioArray.filter(
|
||||
item => item.type_cls === data.type_cls && item.name.startsWith(data.name),
|
||||
).length;
|
||||
|
||||
// Create a new field based on the template
|
||||
const newField = {
|
||||
...fieldTemplate,
|
||||
name: `${data.name}_${dynamicFieldsCount}`,
|
||||
// keep the dynamic flag but reset the value
|
||||
value: null,
|
||||
};
|
||||
|
||||
// Push the new field to the array
|
||||
ioArray.push(newField);
|
||||
|
||||
// Update the nodes in the flow
|
||||
reactflow.setNodes(nodes => {
|
||||
return nodes.map(n => {
|
||||
if (n.id === node.id) {
|
||||
return {
|
||||
...n,
|
||||
data: {
|
||||
...n.data,
|
||||
[label]: ioArray,
|
||||
},
|
||||
};
|
||||
}
|
||||
return n;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Remove dynamic field
|
||||
function removeDynamicField(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Get the count of dynamic fields of this type
|
||||
const ioArray = [...node[label]];
|
||||
const dynamicFields = ioArray.filter(item => item.type_cls === data.type_cls && item.name.startsWith(data.name));
|
||||
|
||||
console.log(
|
||||
`Removing dynamic field at index ${index}, total count: ${dynamicFields.length}, minimum: ${dynamicMinimum}`,
|
||||
);
|
||||
|
||||
// Make sure we don't go below the minimum
|
||||
if (dynamicFields.length <= dynamicMinimum) {
|
||||
console.log(`Cannot remove: already at minimum (${dynamicMinimum})`);
|
||||
message.warning(t('minimum_dynamic_fields_warning', { count: dynamicMinimum }));
|
||||
return;
|
||||
}
|
||||
// Remove the field at the current index
|
||||
const updatedArray = ioArray.filter((_, idx) => idx !== index);
|
||||
|
||||
// Update the node data in the flow
|
||||
reactflow.setNodes(nodes => {
|
||||
return nodes.map(n => {
|
||||
if (n.id === node.id) {
|
||||
return {
|
||||
...n,
|
||||
data: {
|
||||
...n.data,
|
||||
[label]: updatedArray,
|
||||
},
|
||||
};
|
||||
}
|
||||
return n;
|
||||
});
|
||||
});
|
||||
|
||||
// Update the edges connected to this handle
|
||||
const handleId = `${node.id}|${label}|${index}`;
|
||||
reactflow.setEdges(edges => {
|
||||
return edges.filter(
|
||||
edge =>
|
||||
(type === 'source' && edge.sourceHandle !== handleId) ||
|
||||
(type === 'target' && edge.targetHandle !== handleId),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Check if this field is the last one of this type (for dynamic fields)
|
||||
const isLastDynamicField = () => {
|
||||
if (!dynamic) return false;
|
||||
|
||||
const ioArray = node[label];
|
||||
const dynamicFields = ioArray.filter(item => item.type_cls === data.type_cls && item.name.startsWith(data.name));
|
||||
|
||||
return index === ioArray.indexOf(dynamicFields[dynamicFields.length - 1]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('relative flex items-center', {
|
||||
@@ -124,16 +249,36 @@ const NodeHandler: React.FC<NodeHandlerProps> = ({ node, data, type, label, inde
|
||||
}
|
||||
>
|
||||
{['inputs', 'parameters'].includes(label) && (
|
||||
<PlusOutlined className='cursor-pointer' onClick={showRelatedNodes} />
|
||||
<PlusOutlined
|
||||
className='cursor-pointer mr-1'
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showRelatedNodes(e);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Popconfirm>
|
||||
{label !== 'outputs' && <RequiredIcon optional={data.optional} />}
|
||||
|
||||
{['inputs', 'parameters'].includes(label) && dynamic && index >= dynamicMinimum && (
|
||||
<MinusCircleOutlined
|
||||
className='cursor-pointer text-red-500 mr-1'
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
removeDynamicField(e);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{label !== 'outputs' && <RequiredIcon optional={isOptional()} />}
|
||||
{data.type_name}
|
||||
{data.description && (
|
||||
<Tooltip title={data.description}>
|
||||
<InfoCircleOutlined className='ml-2 cursor-pointer' />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Popconfirm
|
||||
placement='right'
|
||||
icon={null}
|
||||
@@ -146,8 +291,45 @@ const NodeHandler: React.FC<NodeHandlerProps> = ({ node, data, type, label, inde
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{['outputs'].includes(label) && <PlusOutlined className='ml-2 cursor-pointer' onClick={showRelatedNodes} />}
|
||||
{['outputs'].includes(label) && (
|
||||
<PlusOutlined
|
||||
className='ml-2 cursor-pointer'
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showRelatedNodes(e);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Popconfirm>
|
||||
|
||||
{['outputs'].includes(label) && dynamic && index >= dynamicMinimum && (
|
||||
<MinusCircleOutlined
|
||||
className='ml-2 cursor-pointer text-red-500'
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
removeDynamicField(e);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Add dynamic field button */}
|
||||
{dynamic && isLastDynamicField() && (
|
||||
<Button
|
||||
type='primary'
|
||||
size='small'
|
||||
className='ml-2'
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
addDynamicField(e);
|
||||
}}
|
||||
style={{ float: label === 'outputs' ? 'left' : 'right' }}
|
||||
>
|
||||
add
|
||||
</Button>
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
|
@@ -26,4 +26,5 @@ export const FlowEn = {
|
||||
Template_Name: 'Template Name',
|
||||
Template_Label: 'Label',
|
||||
Template_Action: 'Action',
|
||||
minimum_dynamic_fields_warning: 'Please keep more than one dynamic field',
|
||||
};
|
||||
|
@@ -26,4 +26,5 @@ export const FlowZn = {
|
||||
Template_Name: '模版名称',
|
||||
Template_Label: '标签',
|
||||
Template_Action: '操作',
|
||||
minimum_dynamic_fields_warning: '请保留一个以上的动态字段',
|
||||
};
|
||||
|
@@ -70,6 +70,8 @@ export type IFlowNodeParameter = {
|
||||
options?: any;
|
||||
value: any;
|
||||
is_list?: boolean;
|
||||
dynamic?: boolean;
|
||||
dynamic_minimum?: number;
|
||||
ui: IFlowNodeParameterUI;
|
||||
};
|
||||
|
||||
@@ -101,6 +103,8 @@ export type IFlowNodeInput = {
|
||||
optional?: boolean | undefined;
|
||||
value: any;
|
||||
is_list?: boolean;
|
||||
dynamic?: boolean;
|
||||
dynamic_minimum?: number;
|
||||
};
|
||||
|
||||
export type IFlowNodeOutput = {
|
||||
@@ -112,6 +116,8 @@ export type IFlowNodeOutput = {
|
||||
id: string;
|
||||
optional?: boolean | undefined;
|
||||
is_list?: boolean;
|
||||
dynamic?: boolean;
|
||||
dynamic_minimum?: number;
|
||||
};
|
||||
|
||||
export type IFlowNode = Node & {
|
||||
|
@@ -76,23 +76,157 @@ export const mapUnderlineToHump = (flowData: IFlowData) => {
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to check if a dynamic input/output has enough connections
|
||||
const checkDynamicConnections = (
|
||||
nodeId: string,
|
||||
fieldType: string,
|
||||
// @typescript-eslint/no-unused-vars
|
||||
fieldIndex: number,
|
||||
edges: any[],
|
||||
dynamicMinimum: number,
|
||||
): boolean => {
|
||||
// Count connections for this specific field type
|
||||
const handlePrefix = `${nodeId}|${fieldType}|`;
|
||||
const connectionCount = edges.filter(edge => {
|
||||
// For inputs, check targetHandle; for outputs, check sourceHandle
|
||||
const handle = fieldType === 'inputs' ? edge.targetHandle : edge.sourceHandle;
|
||||
if (!handle) return false;
|
||||
|
||||
// Check if the handle belongs to this node and field type
|
||||
return handle.startsWith(handlePrefix);
|
||||
}).length;
|
||||
|
||||
// Return true if we have at least the minimum required connections
|
||||
return connectionCount >= dynamicMinimum;
|
||||
};
|
||||
|
||||
// Helper function to identify dynamic field groups
|
||||
const getDynamicFieldGroups = (fields: any[]) => {
|
||||
const groups: Record<string, any[]> = {};
|
||||
|
||||
fields.forEach(field => {
|
||||
if (field.dynamic) {
|
||||
// Extract base name (remove _X suffix if present)
|
||||
const baseName = field.name.replace(/_\d+$/, '');
|
||||
if (!groups[baseName]) {
|
||||
groups[baseName] = [];
|
||||
}
|
||||
groups[baseName].push(field);
|
||||
}
|
||||
});
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
// Helper function to validate dynamic parameters
|
||||
const validateDynamicParameters = (node: IFlowDataNode): [boolean, string] => {
|
||||
if (!node.data.parameters || node.data.parameters.length === 0) {
|
||||
return [true, ''];
|
||||
}
|
||||
|
||||
// Find all dynamic parameter groups
|
||||
const dynamicParamGroups = getDynamicFieldGroups(node.data.parameters);
|
||||
|
||||
// Check each group
|
||||
for (const [baseName, fields] of Object.entries(dynamicParamGroups)) {
|
||||
const minimumRequired = fields[0].dynamic_minimum || 0;
|
||||
|
||||
// Skip if minimum is 0
|
||||
if (minimumRequired === 0) continue;
|
||||
|
||||
// For dynamic parameters, we check if we have at least the minimum number
|
||||
if (fields.length < minimumRequired) {
|
||||
return [
|
||||
false,
|
||||
`The dynamic parameter ${baseName} of node ${node.data.label} requires at least ${minimumRequired} parameters`,
|
||||
];
|
||||
}
|
||||
|
||||
// Check if any required parameters are missing values
|
||||
const requiredFields = fields.filter(field => !field.optional);
|
||||
for (const field of requiredFields) {
|
||||
if (field.value === undefined || field.value === null) {
|
||||
return [false, `The parameter ${field.name} of node ${node.data.label} is required`];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [true, ''];
|
||||
};
|
||||
|
||||
export const checkFlowDataRequied = (flowData: IFlowData) => {
|
||||
const { nodes, edges } = flowData;
|
||||
// check the input, parameters that are required
|
||||
let result: [boolean, IFlowDataNode, string] = [true, nodes[0], ''];
|
||||
|
||||
outerLoop: for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i].data;
|
||||
const { inputs = [], parameters = [] } = node;
|
||||
// check inputs
|
||||
|
||||
// Check dynamic input groups first
|
||||
const dynamicInputGroups = getDynamicFieldGroups(inputs);
|
||||
for (const [baseName, fields] of Object.entries(dynamicInputGroups)) {
|
||||
const minimumRequired = fields[0].dynamic_minimum || 0;
|
||||
if (minimumRequired > 0) {
|
||||
// For dynamic fields, we check connections across all fields of this type
|
||||
const hasEnoughConnections = checkDynamicConnections(nodes[i].id, 'inputs', 0, edges, minimumRequired);
|
||||
if (!hasEnoughConnections) {
|
||||
result = [
|
||||
false,
|
||||
nodes[i],
|
||||
`The dynamic input ${baseName} of node ${node.label} requires at least ${minimumRequired} connections`,
|
||||
];
|
||||
break outerLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check individual inputs
|
||||
for (let j = 0; j < inputs.length; j++) {
|
||||
if (!edges.some(edge => edge.targetHandle === `${nodes[i].id}|inputs|${j}`)) {
|
||||
const input = inputs[j];
|
||||
|
||||
// Skip dynamic inputs that were checked above
|
||||
if (input.dynamic) continue;
|
||||
|
||||
const isRequired = !input.optional;
|
||||
if (isRequired && !edges.some(edge => edge.targetHandle === `${nodes[i].id}|inputs|${j}`)) {
|
||||
result = [false, nodes[i], `The input ${inputs[j].type_name} of node ${node.label} is required`];
|
||||
break outerLoop;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate dynamic parameters
|
||||
const [paramsValid, errorMessage] = validateDynamicParameters(nodes[i]);
|
||||
if (!paramsValid) {
|
||||
result = [false, nodes[i], errorMessage];
|
||||
break outerLoop;
|
||||
}
|
||||
|
||||
// Check dynamic parameter groups
|
||||
const dynamicParamGroups = getDynamicFieldGroups(parameters);
|
||||
for (const [baseName, fields] of Object.entries(dynamicParamGroups)) {
|
||||
const minimumRequired = fields[0].dynamic_minimum || 0;
|
||||
if (minimumRequired > 0 && fields[0].category === 'resource') {
|
||||
// For dynamic params, check connections across all params of this type
|
||||
const hasEnoughConnections = checkDynamicConnections(nodes[i].id, 'parameters', 0, edges, minimumRequired);
|
||||
if (!hasEnoughConnections) {
|
||||
result = [
|
||||
false,
|
||||
nodes[i],
|
||||
`The dynamic parameter ${baseName} of node ${node.label} requires at least ${minimumRequired} connections`,
|
||||
];
|
||||
break outerLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check parameters
|
||||
for (let k = 0; k < parameters.length; k++) {
|
||||
const parameter = parameters[k];
|
||||
|
||||
// Skip dynamic parameters that were checked above
|
||||
if (parameter.dynamic) continue;
|
||||
|
||||
if (
|
||||
!parameter.optional &&
|
||||
parameter.category === 'resource' &&
|
||||
@@ -109,7 +243,26 @@ export const checkFlowDataRequied = (flowData: IFlowData) => {
|
||||
break outerLoop;
|
||||
}
|
||||
}
|
||||
|
||||
// Check dynamic output groups
|
||||
const dynamicOutputGroups = getDynamicFieldGroups(node.outputs || []);
|
||||
for (const [baseName, fields] of Object.entries(dynamicOutputGroups)) {
|
||||
const minimumRequired = fields[0].dynamic_minimum || 0;
|
||||
if (minimumRequired > 0) {
|
||||
// For dynamic outputs, check connections across all outputs of this type
|
||||
const hasEnoughConnections = checkDynamicConnections(nodes[i].id, 'outputs', 0, edges, minimumRequired);
|
||||
if (!hasEnoughConnections) {
|
||||
result = [
|
||||
false,
|
||||
nodes[i],
|
||||
`The dynamic output ${baseName} of node ${node.label} requires at least ${minimumRequired} connections`,
|
||||
];
|
||||
break outerLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user