1207 lines
35 KiB
TypeScript
1207 lines
35 KiB
TypeScript
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,
|
||
};
|
||
};
|