Files
bishengWeb/src/util/reactflowUtils.ts
zhangkai 8b8f4111e2 1
2024-06-25 14:55:48 +08:00

1207 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { cloneDeep } from "lodash";
import {
Connection,
Edge,
Node,
OnSelectionChangeParams,
ReactFlowJsonObject,
XYPosition,
} from "reactflow";
// import { downloadFlowsFromDatabase } from "../controllers/API";
import {
APIKindType,
APIObjectType,
APITemplateType,
TemplateVariableType
} from "../types/api";
import {
FlowType,
NodeDataType,
NodeType,
sourceHandleType,
targetHandleType,
} from "../types/flow";
import {
findLastNodeType,
generateFlowType,
unselectAllNodesType,
updateEdgesHandleIdsType,
} from "../types/utils/reactflowUtils";
import { generateUUID } from "../utils";
import {
getFieldTitle,
toTitleCase
} from "./utils";
export const LANGFLOW_SUPPORTED_TYPES = new Set([
"str",
"bool",
"float",
"code",
"prompt",
"file",
"int",
"dict",
"NestedDict",
]);
// edges (线)文档
export function cleanEdges(nodes: Node[], edges: Edge[]) {
let newEdges = cloneDeep(edges);
edges.forEach((edge) => {
// check if the source and target node still exists
const sourceNode = nodes.find((node) => node.id === edge.source);
const targetNode = nodes.find((node) => node.id === edge.target);
if (!sourceNode || !targetNode) {
newEdges = newEdges.filter((edg) => edg.id !== edge.id);
return;
}
// check if the source and target handle still exists
const sourceHandle = edge.sourceHandle; //right
const targetHandle = edge.targetHandle; //left
if (targetHandle) {
const field = targetHandle.split('|')[1]
const targetNodeTargetHandle =
targetNode.data.node!.template[field]?.input_types.join(';')
+ '|' +
field + '|' +
targetNode.data.id
// if (targetNode.data.node!.template[field]?.proxy) {
// id.proxy = targetNode.data.node!.template[field]?.proxy;
// }
if (targetNodeTargetHandle !== targetHandle) {
newEdges = newEdges.filter((e) => e.id !== edge.id);
}
}
if (sourceHandle) {
const sourceNodeSourceHandle =
sourceNode.data.type + '|' +
sourceNode.data.id + '|' +
sourceNode.data.node!.base_classes.join('|')
if (sourceNodeSourceHandle !== sourceHandle) {
newEdges = newEdges.filter((e) => e.id !== edge.id);
}
}
});
return newEdges;
}
export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) {
let newNodes = cloneDeep(data);
newNodes.forEach((node: Node) => {
node.selected = false;
});
updateNodes(newNodes!);
}
// utils中的新方法
export function isValidConnection(
{ source, target, sourceHandle, targetHandle }: Connection,
nodes: Node[],
edges: Edge[]
) {
const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle!);
const sourceHandleObject: sourceHandleType = scapeJSONParse(sourceHandle!);
if (
targetHandleObject.inputTypes?.some(
(n) => n === sourceHandleObject.dataType
) ||
sourceHandleObject.baseClasses.some(
(t) =>
targetHandleObject.inputTypes?.some((n) => n === t) ||
t === targetHandleObject.type
)
) {
let targetNode = nodes.find((node) => node.id === target!)?.data?.node;
if (!targetNode) {
if (!edges.find((e) => e.targetHandle === targetHandle)) {
return true;
}
} else if (
(!targetNode.template[targetHandleObject.fieldName].list &&
!edges.find((e) => e.targetHandle === targetHandle)) ||
targetNode.template[targetHandleObject.fieldName].list
) {
return true;
}
}
return false;
}
export function removeApiKeys(flow: FlowType): FlowType {
let cleanFLow = cloneDeep(flow);
cleanFLow.data!.nodes.forEach((node) => {
for (const key in node.data.node.template) {
if (node.data.node.template[key].password) {
node.data.node.template[key].value = "";
}
}
});
return cleanFLow;
}
export function updateTemplate(
reference: APITemplateType,
objectToUpdate: APITemplateType
): APITemplateType {
let clonedObject: APITemplateType = cloneDeep(reference);
// Loop through each key in the reference object
for (const key in clonedObject) {
// If the key is not in the object to update, add it
if (objectToUpdate[key] && objectToUpdate[key].value) {
clonedObject[key].value = objectToUpdate[key].value;
}
if (
objectToUpdate[key] &&
objectToUpdate[key].advanced !== null &&
objectToUpdate[key].advanced !== undefined
) {
clonedObject[key].advanced = objectToUpdate[key].advanced;
}
}
return clonedObject;
}
export const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
let data = flow?.data ? flow.data : null;
if (data) {
processFlowEdges(flow);
//prevent node update for now
// processFlowNodes(flow);
//add animation to text type edges
updateEdges(data.edges);
// updateNodes(data.nodes, data.edges);
if (refreshIds) updateIds(data); // Assuming updateIds is defined elsewhere
}
return data;
};
// utils中的新方法
export function updateIds(newFlow: ReactFlowJsonObject) {
let idsMap = {};
if (newFlow.nodes)
newFlow.nodes.forEach((node: NodeType) => {
// Generate a unique node ID
let newId = getNodeId(
node.data.node?.flow ? "GroupNode" : node.data.type
);
idsMap[node.id] = newId;
node.id = newId;
node.data.id = newId;
// Add the new node to the list of nodes in state
});
if (newFlow.edges)
newFlow.edges.forEach((edge: Edge) => {
edge.source = idsMap[edge.source];
edge.target = idsMap[edge.target];
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
edge.sourceHandle = scapedJSONStringfy({
...sourceHandleObject,
id: edge.source,
});
if (edge.data?.sourceHandle?.id) {
edge.data.sourceHandle.id = edge.source;
}
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
edge.targetHandle = scapedJSONStringfy({
...targetHandleObject,
id: edge.target,
});
if (edge.data?.targetHandle?.id) {
edge.data.targetHandle.id = edge.target;
}
edge.id =
"reactflow__edge-" +
edge.source +
edge.sourceHandle +
"-" +
edge.target +
edge.targetHandle;
});
return idsMap;
}
export function buildTweaks(flow: FlowType) {
return flow.data!.nodes.reduce((acc, node) => {
acc[node.data.id] = {};
return acc;
}, {});
}
export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
if (!node.data?.node?.template || !Object.keys(node.data.node.template)) {
return [
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
];
}
const {
type,
node: { template },
} = node.data;
return Object.keys(template).reduce((errors: Array<string>, t) => {
if (
template[t].required &&
template[t].show &&
(template[t].value === undefined ||
template[t].value === null ||
template[t].value === "") &&
!edges.some(
(edge) =>
(scapeJSONParse(edge.targetHandle!) as targetHandleType).fieldName ===
t &&
(scapeJSONParse(edge.targetHandle!) as targetHandleType).id ===
node.id
)
) {
errors.push(`${type} is missing ${getFieldTitle(template, t)}.`);
} else if (
template[t].type === "dict" &&
template[t].required &&
template[t].show &&
(template[t].value !== undefined ||
template[t].value !== null ||
template[t].value !== "")
) {
if (hasDuplicateKeys(template[t].value))
errors.push(
`${type} (${getFieldTitle(
template,
t
)}) contains duplicate keys with the same values.`
);
if (hasEmptyKey(template[t].value))
errors.push(
`${type} (${getFieldTitle(template, t)}) field must not be empty.`
);
}
return errors;
}, [] as string[]);
}
export function validateNodes(nodes: Node[], edges: Edge[]) {
if (nodes.length === 0) {
return [
"No nodes found in the flow. Please add at least one node to the flow.",
];
}
return nodes.flatMap((n: NodeType) => validateNode(n, edges));
}
export function updateEdges(edges: Edge[]) {
if (edges)
edges.forEach((edge) => {
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
edge.className =
(targetHandleObject.type === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ") + " stroke-connection";
edge.animated = targetHandleObject.type === "Text";
});
}
export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
const existingNames = flows.map((item) => item.name);
let newName = flow.name;
let count = 1;
while (existingNames.includes(newName)) {
newName = `${flow.name} (${count})`;
count++;
}
return newName;
}
export function updateEdgesHandleIds({
edges,
nodes,
}: updateEdgesHandleIdsType): Edge[] {
let newEdges = cloneDeep(edges);
newEdges.forEach((edge) => {
const sourceNodeId = edge.source;
const targetNodeId = edge.target;
const sourceNode = nodes.find((node) => node.id === sourceNodeId);
const targetNode = nodes.find((node) => node.id === targetNodeId);
let source = edge.sourceHandle;
let target = edge.targetHandle;
//right
let newSource: sourceHandleType;
//left
let newTarget: targetHandleType;
if (target && targetNode) {
let field = target.split("|")[1];
newTarget = {
type: targetNode.data.node!.template[field].type,
fieldName: field,
id: targetNode.data.id,
inputTypes: targetNode.data.node!.template[field].input_types,
};
}
if (source && sourceNode) {
newSource = {
id: sourceNode.data.id,
baseClasses: sourceNode.data.node!.base_classes,
dataType: sourceNode.data.type,
};
}
edge.sourceHandle = scapedJSONStringfy(newSource!);
edge.targetHandle = scapedJSONStringfy(newTarget!);
const newData = {
sourceHandle: scapeJSONParse(edge.sourceHandle),
targetHandle: scapeJSONParse(edge.targetHandle),
};
edge.data = newData;
});
return newEdges;
}
export function handleKeyDown(
e:
| React.KeyboardEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLTextAreaElement>,
inputValue: string | string[] | null,
block: string
) {
//condition to fix bug control+backspace on Windows/Linux
if (
(typeof inputValue === "string" &&
(e.metaKey === true || e.ctrlKey === true) &&
e.key === "Backspace" &&
(inputValue === block ||
inputValue?.charAt(inputValue?.length - 1) === " " ||
/[!@#$%^&*()\-_=+[\]{}|;:'",.<>/?\\`´]/.test(inputValue?.charAt(inputValue?.length - 1)))) ||
(navigator.userAgent.toUpperCase().includes("MAC") &&
e.ctrlKey === true &&
e.key === "Backspace")
) {
e.preventDefault();
e.stopPropagation();
}
if (e.ctrlKey === true && e.key === "Backspace" && inputValue === block) {
e.preventDefault();
e.stopPropagation();
}
}
export function handleOnlyIntegerInput(
event: React.KeyboardEvent<HTMLInputElement>
) {
if (
event.key === "." ||
event.key === "-" ||
event.key === "," ||
event.key === "e" ||
event.key === "E" ||
event.key === "+"
) {
event.preventDefault();
}
}
export function getConnectedNodes(
edge: Edge,
nodes: Array<NodeType>
): Array<NodeType> {
const sourceId = edge.source;
const targetId = edge.target;
return nodes.filter((node) => node.id === targetId || node.id === sourceId);
}
export function convertObjToArray(singleObject: object | string) {
if (typeof singleObject === "string") {
singleObject = JSON.parse(singleObject);
}
if (Array.isArray(singleObject)) return singleObject;
let arrConverted: any[] = [];
if (typeof singleObject === "object") {
for (const key in singleObject) {
if (Object.prototype.hasOwnProperty.call(singleObject, key)) {
const newObj = {};
newObj[key] = singleObject[key];
arrConverted.push(newObj);
}
}
}
return arrConverted;
}
export function convertArrayToObj(arrayOfObjects) {
if (!Array.isArray(arrayOfObjects)) return arrayOfObjects;
let objConverted = {};
for (const obj of arrayOfObjects) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
objConverted[key] = obj[key];
}
}
}
return objConverted;
}
export function hasDuplicateKeys(array) {
const keys = {};
// Transforms an empty object into an object array without opening the 'editNode' modal to prevent the flow build from breaking.
if (!Array.isArray(array)) array = [{ "": "" }];
for (const obj of array) {
for (const key in obj) {
if (keys[key]) {
return true;
}
keys[key] = true;
}
}
return false;
}
export function hasEmptyKey(objArray) {
// Transforms an empty object into an array without opening the 'editNode' modal to prevent the flow build from breaking.
if (!Array.isArray(objArray)) objArray = [];
for (const obj of objArray) {
for (const key in obj) {
if (obj.hasOwnProperty(key) && key === "") {
return true; // Found an empty key
}
}
}
return false; // No empty keys found
}
export function convertValuesToNumbers(arr) {
return arr.map((obj) => {
const newObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
let value = obj[key];
if (/^\d+$/.test(value)) {
value = value?.toString().trim();
}
newObj[key] =
value === "" || isNaN(value) ? value.toString() : Number(value);
}
}
return newObj;
});
}
export function scapedJSONStringfy(json: object): string {
return customStringify(json).replace(/"/g, "œ");
}
export function scapeJSONParse(json: string): any {
let parsed = json.replace(/œ/g, '"');
return JSON.parse(parsed);
}
// this function receives an array of edges and return true if any of the handles are not a json string
// 数据结构转新版本
export function checkOldEdgesHandles(edges: Edge[]): boolean {
return edges.some(
(edge) =>
!edge.sourceHandle ||
!edge.targetHandle ||
!edge.sourceHandle.includes("{") ||
!edge.targetHandle.includes("{")
);
}
export function customStringify(obj: any): string {
if (typeof obj === "undefined") {
return "null";
}
if (obj === null || typeof obj !== "object") {
if (obj instanceof Date) {
return `"${obj.toISOString()}"`;
}
return JSON.stringify(obj);
}
if (Array.isArray(obj)) {
const arrayItems = obj.map((item) => customStringify(item)).join(",");
return `[${arrayItems}]`;
}
const keys = Object.keys(obj).sort();
const keyValuePairs = keys.map(
(key) => `"${key}":${customStringify(obj[key])}`
);
return `{${keyValuePairs.join(",")}}`;
}
export function getMiddlePoint(nodes: Node[]) {
let middlePointX = 0;
let middlePointY = 0;
nodes.forEach((node) => {
middlePointX += node.position.x;
middlePointY += node.position.y;
});
const totalNodes = nodes.length;
const averageX = middlePointX / totalNodes;
const averageY = middlePointY / totalNodes;
return { x: averageX, y: averageY };
}
export function getNodeId(nodeType: string) {
return nodeType + "-" + generateUUID(5);
}
export function getHandleId(
source: string,
sourceHandle: string,
target: string,
targetHandle: string
) {
return (
"reactflow__edge-" + source + sourceHandle + "-" + target + targetHandle
);
}
export function generateFlow(
selection: OnSelectionChangeParams,
nodes: Node[],
edges: Edge[],
name: string
): generateFlowType {
const newFlowData = { nodes, edges, viewport: { zoom: 1, x: 0, y: 0 } };
/* remove edges that are not connected to selected nodes on both ends
*/
newFlowData.edges = selection.edges.filter(
(edge) =>
selection.nodes.some((node) => node.id === edge.target) &&
selection.nodes.some((node) => node.id === edge.source)
);
newFlowData.nodes = selection.nodes;
const newFlow: FlowType = {
data: newFlowData,
is_component: false,
name: name,
description: "",
//generating local id instead of using the id from the server, can change in the future
id: generateUUID(5),
status: 0,
write: false,
guide_word: ""
};
// filter edges that are not connected to selected nodes on both ends
// using O(n²) aproach because the number of edges is small
// in the future we can use a better aproach using a set
return {
newFlow,
removedEdges: edges.filter(
(edge) =>
(selection.nodes.some((node) => node.id === edge.target) ||
selection.nodes.some((node) => node.id === edge.source)) &&
newFlowData.edges.every((e) => e.id !== edge.id)
),
};
}
export function reconnectEdges(groupNode: NodeType, excludedEdges: Edge[]) {
let newEdges = cloneDeep(excludedEdges);
if (!groupNode.data.node!.flow) return [];
const { nodes, edges } = groupNode.data.node!.flow!.data!;
const lastNode = findLastNode(groupNode.data.node!.flow!.data!);
newEdges.forEach((edge) => {
// 选中的 node 有链接其他 node
if (lastNode && edge.source === lastNode.id) {
edge.source = groupNode.id;
const sourceHandleArr = edge.sourceHandle.split('|')
sourceHandleArr[1] = groupNode.id
edge.sourceHandle = sourceHandleArr.join('|');
// edge.data.sourceHandle = newSourceHandle;
}
// 选中的 node 有被链接的 node
if (nodes.some((node) => node.id === edge.target)) {
const targetNode = nodes.find((node) => node.id === edge.target)!;
const targetHandleArr = edge.targetHandle.split('|')
targetHandleArr[targetHandleArr.length - 1] = groupNode.id;
targetHandleArr[1] = targetHandleArr[1] + "_" + targetNode.id;
// const proxy = { id: targetNode.id, field: targetHandle.fieldName };
// newTargetHandle.proxy = proxy;
edge.target = groupNode.id;
edge.targetHandle = targetHandleArr.join('|');
// edge.data.targetHandle = newTargetHandle;
}
});
return newEdges;
}
export function filterFlow(
selection: OnSelectionChangeParams,
setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void,
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void
) {
setNodes((nodes) => nodes.filter((node) => !selection.nodes.includes(node)));
setEdges((edges) => edges.filter((edge) => !selection.edges.includes(edge)));
}
export function findLastNode({ nodes, edges }: findLastNodeType) {
/*
this function receives a flow and return the last node
*/
let lastNode = nodes.find((n) => !edges.some((e) => e.source === n.id));
return lastNode;
}
export function updateFlowPosition(NewPosition: XYPosition, flow: FlowType) {
const middlePoint = getMiddlePoint(flow.data!.nodes);
let deltaPosition = {
x: NewPosition.x - middlePoint.x,
y: NewPosition.y - middlePoint.y,
};
return {
...flow,
data: {
...flow.data!,
nodes: flow.data!.nodes.map((node) => ({
...node,
position: {
x: node.position.x + deltaPosition.x,
y: node.position.y + deltaPosition.y,
},
})),
},
};
}
export function concatFlows(
flow: FlowType,
setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void,
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void
) {
const { nodes, edges } = flow.data!;
setNodes((old) => [...old, ...nodes]);
setEdges((old) => [...old, ...edges]);
}
export function validateSelection(
selection: OnSelectionChangeParams,
edges: Edge[]
): Array<string> {
//add edges to selection if selection mode selected only nodes
if (selection.edges.length === 0) {
selection.edges = edges;
}
// get only edges that are connected to the nodes in the selection
// first creates a set of all the nodes ids
let nodesSet = new Set(selection.nodes.map((n) => n.id));
// then filter the edges that are connected to the nodes in the set
let connectedEdges = selection.edges.filter(
(e) => nodesSet.has(e.source) && nodesSet.has(e.target)
);
// add the edges to the selection
selection.edges = connectedEdges;
let errorsArray: Array<string> = [];
// check if there is more than one node
if (selection.nodes.length < 2) {
errorsArray.push("Please select more than one node");
}
//check if there are two or more nodes with free outputs
if (
selection.nodes.filter(
(n) => !selection.edges.some((e) => e.source === n.id)
).length > 1
) {
errorsArray.push("请仅选择一个具有自由输出的节点");
// errorsArray.push("Please select only one node with free outputs");
}
// check if there is any node that does not have any connection
if (
selection.nodes.some(
(node) =>
!selection.edges.some((edge) => edge.target === node.id) &&
!selection.edges.some((edge) => edge.source === node.id)
)
) {
errorsArray.push("请仅选择已连接的节点");
// errorsArray.push("Please select only nodes that are connected");
}
return errorsArray;
}
function updateGroupNodeTemplate(template: APITemplateType) {
/*this function receives a template, iterates for it's items
updating the visibility of all basic types setting it to advanced true*/
Object.keys(template).forEach((key) => {
let type = template[key].type;
let input_types = template[key].input_types;
if (
LANGFLOW_SUPPORTED_TYPES.has(type) &&
!template[key].required && // 非必填项group 中不展示
!input_types
) {
template[key].advanced = true;
}
//prevent code fields from showing on the group node
if (type === "code") {
template[key].show = false;
}
});
return template;
}
export function mergeNodeTemplates({
nodes,
edges,
}: {
nodes: NodeType[];
edges: Edge[];
}): APITemplateType {
/* this function receives a flow and iterate throw each node
and merge the templates with only the visible fields
if there are two keys with the same name in the flow, we will update the display name of each one
to show from which node it came from
*/
let template: APITemplateType = {};
nodes.forEach((node) => {
let nodeTemplate = cloneDeep(node.data.node!.template);
Object.keys(nodeTemplate)
.filter((field_name) => field_name.charAt(0) !== "_")
.forEach((key) => {
if (!isHandleConnected(edges, key, nodeTemplate[key], node.id)) {
template[key + "_" + node.id] = nodeTemplate[key];
template[key + "_" + node.id].proxy = { id: node.id, field: key };
if (node.type === "groupNode") {
template[key + "_" + node.id].display_name =
node.data.node!.flow!.name + " - " + nodeTemplate[key].name;
} else {
template[key + "_" + node.id].display_name =
//data id already has the node name on it
nodeTemplate[key].display_name
? nodeTemplate[key].display_name
: nodeTemplate[key].name
? toTitleCase(nodeTemplate[key].name)
: toTitleCase(key);
}
}
});
});
return template;
}
function isHandleConnected(
edges: Edge[],
key: string,
field: TemplateVariableType,
nodeId: string
) {
/*
this function receives a flow and a handleId and check if there is a connection with this handle
*/
if (field.proxy) {
if (
edges.some(
(e) =>
e.targetHandle ===
scapedJSONStringfy({
type: field.type,
fieldName: key,
id: nodeId,
proxy: { id: field.proxy!.id, field: field.proxy!.field },
inputTypes: field.input_types,
} as targetHandleType)
)
) {
return true;
}
} else {
if (edges.some(
(e) => e.targetHandle === `${field.type}|${key}|${nodeId}`
)) {
return true;
}
}
return false;
}
export function generateNodeTemplate(Flow: FlowType) {
/*
this function receives a flow and generate a template for the group node
*/
let template = mergeNodeTemplates({
nodes: Flow.data!.nodes,
edges: Flow.data!.edges,
});
updateGroupNodeTemplate(template);
return template;
}
export function generateNodeFromFlow(
flow: FlowType,
getNodeId: (type: string) => string
): NodeType {
const { nodes } = flow.data!;
const outputNode = cloneDeep(findLastNode(flow.data!));
const position = getMiddlePoint(nodes);
let data = cloneDeep(flow);
const id = getNodeId(outputNode?.data.type!);
// 检查是否有 fileinput
const hasFileInput = flow.data.nodes.some((node) => node.data.type === "InputFileNode")
const newGroupNode: NodeType = {
data: {
id,
type: hasFileInput ? 'InputFileNode' : outputNode?.data.type!,
node: {
output_types: outputNode!.data.node!.output_types,
display_name: "Group",
documentation: "",
base_classes: outputNode!.data.node!.base_classes,
description: outputNode.data.node.description,
template: generateNodeTemplate(data),
flow: data,
},
},
id,
position,
type: "genericNode",
};
return newGroupNode;
}
export function connectedInputNodesOnHandle(
nodeId: string,
handleId: string,
{ nodes, edges }: { nodes: NodeType[]; edges: Edge[] }
) {
const connectedNodes: Array<{ name: string; id: string; isGroup: boolean }> =
[];
// return the nodes connected to the input handle of the node
const TargetEdges = edges.filter((e) => e.target === nodeId);
TargetEdges.forEach((edge) => {
if (edge.targetHandle === handleId) {
const sourceNode = nodes.find((n) => n.id === edge.source);
if (sourceNode) {
if (sourceNode.type === "groupNode") {
let lastNode = findLastNode(sourceNode.data.node!.flow!.data!);
while (lastNode && lastNode.type === "groupNode") {
lastNode = findLastNode(lastNode.data.node!.flow!.data!);
}
if (lastNode) {
connectedNodes.push({
name: sourceNode.data.node!.flow!.name,
id: lastNode.id,
isGroup: true,
});
}
} else {
connectedNodes.push({
name: sourceNode.data.type,
id: sourceNode.id,
isGroup: false,
});
}
}
}
});
return connectedNodes;
}
function updateProxyIdsOnTemplate(
template: APITemplateType,
idsMap: { [key: string]: string }
) {
Object.keys(template).forEach((key) => {
if (template[key].proxy && idsMap[template[key].proxy!.id]) {
template[key].proxy!.id = idsMap[template[key].proxy!.id];
}
});
}
function updateEdgesIds(edges: Edge[], idsMap: { [key: string]: string }) {
edges.forEach((edge) => {
let targetHandle: targetHandleType = edge.data.targetHandle;
if (targetHandle.proxy && idsMap[targetHandle.proxy!.id]) {
targetHandle.proxy!.id = idsMap[targetHandle.proxy!.id];
}
edge.data.targetHandle = targetHandle;
edge.targetHandle = scapedJSONStringfy(targetHandle);
});
}
// (新)
export function processFlowEdges(flow: FlowType) {
if (!flow.data || !flow.data.edges) return;
if (checkOldEdgesHandles(flow.data.edges)) {
const newEdges = updateEdgesHandleIds(flow.data);
flow.data.edges = newEdges;
}
//update edges colors
flow.data.edges.forEach((edge) => {
edge.className = "";
edge.style = { stroke: "#555" };
});
}
export function expandGroupNode(
id: string,
flow: FlowType,
template: APITemplateType,
nodes: Node[],
edges: Edge[],
setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void,
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void
) {
// const idsMap = updateIds(flow!.data!);
// updateProxyIdsOnTemplate(template, idsMap);
let flowEdges = edges;
// updateEdgesIds(flowEdges, idsMap);
const gNodes: NodeType[] = cloneDeep(flow?.data?.nodes!);
const gEdges = cloneDeep(flow!.data!.edges);
//redirect edges to correct proxy node
let updatedEdges: Edge[] = [];
flowEdges.forEach((edge) => {
let newEdge = cloneDeep(edge);
// group 组件输入线
if (newEdge.target === id) {
const targetHandleArr = newEdge.targetHandle.split("|");
const _index = targetHandleArr[1].lastIndexOf('_');
const tempField = targetHandleArr[1].slice(0, _index)
const nodeId = targetHandleArr[1].slice(_index + 1)
targetHandleArr[1] = tempField
targetHandleArr[2] = nodeId
newEdge.target = nodeId
newEdge.targetHandle = targetHandleArr.join('|')
// const targetHandle: targetHandleType = newEdge.targetHandle;
// if (targetHandle.proxy) {
// let type = targetHandle.type;
// let field = targetHandle.proxy.field;
// let proxyId = targetHandle.proxy.id;
// let inputTypes = targetHandle.inputTypes;
// let node: NodeType = gNodes.find((n) => n.id === proxyId)!;
// if (node) {
// newEdge.target = proxyId;
// let newTargetHandle: targetHandleType = {
// fieldName: field,
// type,
// id: proxyId,
// inputTypes: inputTypes,
// };
// if (node.data.node?.flow) {
// newTargetHandle.proxy = {
// field: node.data.node.template[field].proxy?.field!,
// id: node.data.node.template[field].proxy?.id!,
// };
// }
// newEdge.data.targetHandle = newTargetHandle;
// newEdge.targetHandle = scapedJSONStringfy(newTargetHandle);
// }
// }
}
// group 组件输出线
if (newEdge.source === id) {
const lastNode = cloneDeep(findLastNode(flow!.data!));
newEdge.source = lastNode!.id;
const sourceHandleArr = newEdge.sourceHandle.split('|')
sourceHandleArr[1] = lastNode!.id;
// newEdge.data.sourceHandle = newSourceHandle;
newEdge.sourceHandle = sourceHandleArr.join('|');
}
if (edge.target === id || edge.source === id) {
updatedEdges.push(newEdge);
}
});
//update template values
Object.keys(template).forEach((key) => {
let { field, id } = template[key].proxy!;
let nodeIndex = gNodes.findIndex((n) => n.id === id);
if (nodeIndex !== -1) {
let proxy: { id: string; field: string } | undefined;
let display_name: string | undefined;
let show = gNodes[nodeIndex].data.node!.template[field].show;
let advanced = gNodes[nodeIndex].data.node!.template[field].advanced;
if (gNodes[nodeIndex].data.node!.template[field].display_name) {
display_name =
gNodes[nodeIndex].data.node!.template[field].display_name;
} else {
display_name = gNodes[nodeIndex].data.node!.template[field].name;
}
if (gNodes[nodeIndex].data.node!.template[field].proxy) {
proxy = gNodes[nodeIndex].data.node!.template[field].proxy;
}
gNodes[nodeIndex].data.node!.template[field] = template[key];
gNodes[nodeIndex].data.node!.template[field].show = show;
gNodes[nodeIndex].data.node!.template[field].advanced = advanced;
gNodes[nodeIndex].data.node!.template[field].display_name = display_name;
// keep the nodes selected after ungrouping
// gNodes[nodeIndex].selected = false;
if (proxy) {
gNodes[nodeIndex].data.node!.template[field].proxy = proxy;
} else {
delete gNodes[nodeIndex].data.node!.template[field].proxy;
}
}
});
const filteredNodes = [...nodes.filter((n) => n.id !== id), ...gNodes];
const filteredEdges = [
...edges.filter((e) => e.target !== id && e.source !== id),
...gEdges,
...updatedEdges,
];
setNodes(filteredNodes);
setEdges(filteredEdges);
}
export function getGroupStatus(
flow: FlowType,
ssData: { [key: string]: { valid: boolean; params: string } }
) {
let status = { valid: true, params: "Built sucessfully ✨" };
const { nodes } = flow.data!;
const ids = nodes.map((n: NodeType) => n.data.id);
ids.forEach((id) => {
if (!ssData[id]) {
status = ssData[id];
return;
}
if (!ssData[id].valid) {
status = { valid: false, params: ssData[id].params };
}
});
return status;
}
export function createFlowComponent(
nodeData: NodeDataType,
version: string
): FlowType {
const flowNode: FlowType = {
data: {
edges: [],
nodes: [
{
data: { ...nodeData, node: { ...nodeData.node, official: false } },
id: nodeData.id,
position: { x: 0, y: 0 },
type: "genericNode",
},
],
viewport: { x: 1, y: 1, zoom: 1 },
},
description: nodeData.node?.description || "",
name: nodeData.node?.display_name || nodeData.type || "",
id: nodeData.id || "",
is_component: true,
last_tested_version: version,
status: 0,
write: false,
guide_word: ""
};
return flowNode;
}
export function downloadNode(NodeFLow: any) {
const element = document.createElement("a");
const file = new Blob([JSON.stringify(NodeFLow)], {
type: "application/json",
});
element.href = URL.createObjectURL(file);
element.download = `${NodeFLow.name || NodeFLow.node.display_name}.json`;
element.click();
}
export function updateComponentNameAndType(
data: any,
component: NodeDataType
) { }
export function removeFileNameFromComponents(flow: FlowType) {
flow.data!.nodes.forEach((node: NodeType) => {
Object.keys(node.data.node!.template).forEach((field) => {
if (node.data.node?.template[field].type === "file") {
node.data.node!.template[field].value = "";
}
});
if (node.data.node?.flow) {
removeFileNameFromComponents(node.data.node.flow);
}
});
}
export function typesGenerator(data: APIObjectType) {
return Object.keys(data)
.reverse()
.reduce((acc, curr) => {
Object.keys(data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = curr;
// Add the base classes to the accumulator as well.
data[curr][c].base_classes?.forEach((b) => {
acc[b] = curr;
});
});
return acc;
}, {});
}
export function templatesGenerator(data: APIObjectType) {
return Object.keys(data).reduce((acc, curr) => {
Object.keys(data[curr]).forEach((c: keyof APIKindType) => {
//prevent wrong overwriting of the component template by a group of the same type
if (!data[curr][c].flow) acc[c] = data[curr][c];
});
return acc;
}, {});
}
export function downloadFlow(
flow: FlowType,
flowName: string,
flowDescription?: string
) {
let clonedFlow = cloneDeep(flow);
removeFileNameFromComponents(clonedFlow);
// create a data URI with the current flow data
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify({
...clonedFlow,
name: flowName,
description: flowDescription,
})
)}`;
// create a link element and set its properties
const link = document.createElement("a");
link.href = jsonString;
link.download = `${flowName && flowName != "" ? flowName : flow.name}.json`;
// simulate a click on the link element to trigger the download
link.click();
}
export const createNewFlow = (
flowData: ReactFlowJsonObject,
flow: FlowType
) => {
return {
description: flow?.description ?? '',
name: flow?.name ? flow.name : "Untitled document",
data: flowData,
id: "",
is_component: flow?.is_component ?? false,
};
};