feat(awel): Support simple templates

This commit is contained in:
Fangyin Cheng
2025-03-14 10:44:50 +08:00
parent 676d8ec70f
commit 8ccd4090b9
73 changed files with 1623 additions and 89 deletions

View File

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

View File

@@ -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 (

View File

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