Fixed Copy and Paste, refactored tabsContext code

This commit is contained in:
Lucas Oliveira 2023-05-22 19:54:32 -03:00
commit 610beb18dd
4 changed files with 629 additions and 543 deletions

View file

@ -1,273 +1,335 @@
import { import {
createContext, createContext,
useEffect, useEffect,
useState, useState,
useRef, useRef,
ReactNode, ReactNode,
useContext, useContext,
} from "react"; } from "react";
import { FlowType } from "../types/flow"; import { FlowType, NodeType } from "../types/flow";
import { LangFlowState, TabsContextType } from "../types/tabs"; import { LangFlowState, TabsContextType } from "../types/tabs";
import { normalCaseToSnakeCase, updateObject, updateTemplate } from "../utils"; import { normalCaseToSnakeCase, updateObject, updateTemplate } from "../utils";
import { alertContext } from "./alertContext"; import { alertContext } from "./alertContext";
import { typesContext } from "./typesContext"; import { typesContext } from "./typesContext";
import { APITemplateType, TemplateVariableType } from "../types/api"; import { APITemplateType, TemplateVariableType } from "../types/api";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { addEdge } from "reactflow";
const TabsContextInitialValue: TabsContextType = { const TabsContextInitialValue: TabsContextType = {
save: () => {}, save: () => {},
tabIndex: 0, tabIndex: 0,
setTabIndex: (index: number) => {}, setTabIndex: (index: number) => {},
flows: [], flows: [],
removeFlow: (id: string) => {}, removeFlow: (id: string) => {},
addFlow: (flowData?: any) => {}, addFlow: (flowData?: any) => {},
updateFlow: (newFlow: FlowType) => {}, updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => 0, incrementNodeId: () => uuidv4(),
downloadFlow: (flow: FlowType) => {}, downloadFlow: (flow: FlowType) => {},
uploadFlow: () => {}, uploadFlow: () => {},
hardReset: () => {}, hardReset: () => {},
disableCP: false, disableCP:false,
setDisableCP: (state: boolean) => {}, setDisableCP:(state:boolean)=>{},
getNodeId: () => "",
paste: (selection: {nodes: any, edges: any}, position: {x: number, y: number}) => {},
}; };
export const TabsContext = createContext<TabsContextType>( export const TabsContext = createContext<TabsContextType>(
TabsContextInitialValue TabsContextInitialValue
); );
export function TabsProvider({ children }: { children: ReactNode }) { export function TabsProvider({ children }: { children: ReactNode }) {
const { setNoticeData } = useContext(alertContext); const { setNoticeData } = useContext(alertContext);
const [tabIndex, setTabIndex] = useState(0); const [tabIndex, setTabIndex] = useState(0);
const [flows, setFlows] = useState<Array<FlowType>>([]); const [flows, setFlows] = useState<Array<FlowType>>([]);
const [id, setId] = useState(uuidv4()); const [id, setId] = useState(uuidv4());
const { templates } = useContext(typesContext); const { templates, reactFlowInstance } = useContext(typesContext);
const newNodeId = useRef(0); const newNodeId = useRef(uuidv4());
function incrementNodeId() { function incrementNodeId() {
newNodeId.current = newNodeId.current + 1; newNodeId.current = uuidv4();
return newNodeId.current; return newNodeId.current;
} }
function save() { function save() {
if (flows.length !== 0) if (flows.length !== 0)
window.localStorage.setItem( window.localStorage.setItem(
"tabsData", "tabsData",
JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current }) JSON.stringify({ tabIndex, flows, id})
); );
} }
useEffect(() => { useEffect(() => {
//save tabs locally //save tabs locally
save(); // console.log(id)
}, [flows, id, tabIndex, newNodeId]); save();
}, [flows, id, tabIndex, newNodeId]);
useEffect(() => { useEffect(() => {
//get tabs locally saved //get tabs locally saved
let cookie = window.localStorage.getItem("tabsData"); let cookie = window.localStorage.getItem("tabsData");
if (cookie && Object.keys(templates).length > 0) { if (cookie && Object.keys(templates).length > 0) {
let cookieObject: LangFlowState = JSON.parse(cookie); let cookieObject: LangFlowState = JSON.parse(cookie);
cookieObject.flows.forEach((flow) => { cookieObject.flows.forEach((flow) => {
// check if flow.data is null flow.data.nodes.forEach((node) => {
if (flow.data) { if (Object.keys(templates[node.data.type]["template"]).length > 0) {
flow.data.nodes.forEach((node) => { node.data.node.template = updateTemplate(
// check if node.data.type is in templates templates[node.data.type][
if ( "template"
Object.keys(templates).includes(node.data.type) && ] as unknown as APITemplateType,
Object.keys(templates[node.data.type]["template"]).length > 0
) {
node.data.node.base_classes =
templates[node.data.type]["base_classes"];
flow.data.edges.forEach((edge) => {
if (edge.source === node.id) {
edge.sourceHandle = edge.sourceHandle
.split("|")
.slice(0, 2)
.concat(templates[node.data.type]["base_classes"])
.join("|");
}
});
node.data.node.description =
templates[node.data.type]["description"];
node.data.node.template = updateTemplate(
templates[node.data.type][
"template"
] as unknown as APITemplateType,
node.data.node.template as APITemplateType
);
}
});
}
});
setTabIndex(cookieObject.tabIndex);
setFlows(cookieObject.flows);
setId(cookieObject.id);
newNodeId.current = cookieObject.nodeId;
}
}, [templates]);
function hardReset() { node.data.node.template as APITemplateType
newNodeId.current = 0; );
setTabIndex(0); }
setFlows([]); });
setId(uuidv4()); });
} setTabIndex(cookieObject.tabIndex);
setFlows(cookieObject.flows);
setId(cookieObject.id);
}
}, [templates]);
/** function hardReset() {
* Downloads the current flow as a JSON file newNodeId.current = uuidv4();
*/ setTabIndex(0);
function downloadFlow(flow: FlowType) { setFlows([]);
// create a data URI with the current flow data setId(uuidv4());
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( }
JSON.stringify(flow)
)}`;
// create a link element and set its properties /**
const link = document.createElement("a"); * Downloads the current flow as a JSON file
link.href = jsonString; */
link.download = `${flows[tabIndex].name}.json`; function downloadFlow(flow: FlowType) {
// create a data URI with the current flow data
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify(flow)
)}`;
// simulate a click on the link element to trigger the download // create a link element and set its properties
link.click(); const link = document.createElement("a");
setNoticeData({ link.href = jsonString;
title: "Warning: Critical data,JSON file may including API keys.", link.download = `${flows[tabIndex].name}.json`;
});
}
/** // simulate a click on the link element to trigger the download
* Creates a file input and listens to a change event to upload a JSON flow file. link.click();
* If the file type is application/json, the file is read and parsed into a JSON object. setNoticeData({
* The resulting JSON object is passed to the addFlow function. title: "Warning: Critical data,JSON file may including API keys.",
*/ });
function uploadFlow() { }
// create a file input
const input = document.createElement("input");
input.type = "file";
// add a change event listener to the file input
input.onchange = (e: Event) => {
// check if the file type is application/json
if ((e.target as HTMLInputElement).files[0].type === "application/json") {
// get the file from the file input
const file = (e.target as HTMLInputElement).files[0];
// read the file as text
file.text().then((text) => {
// parse the text into a JSON object
let flow: FlowType = JSON.parse(text);
addFlow(flow); function getNodeId() {
}); return `dndnode_` + incrementNodeId();
} }
};
// trigger the file input click event to open the file dialog
input.click();
}
/**
* Removes a flow from an array of flows based on its id.
* Updates the state of flows and tabIndex using setFlows and setTabIndex hooks.
* @param {string} id - The id of the flow to remove.
*/
function removeFlow(id: string) {
setFlows((prevState) => {
const newFlows = [...prevState];
const index = newFlows.findIndex((flow) => flow.id === id);
if (index >= 0) {
if (index === tabIndex) {
setTabIndex(flows.length - 2);
newFlows.splice(index, 1);
} else {
let flowId = flows[tabIndex].id;
newFlows.splice(index, 1);
setTabIndex(newFlows.findIndex((flow) => flow.id === flowId));
}
}
return newFlows;
});
}
/**
* Add a new flow to the list of flows.
* @param flow Optional flow to add.
*/
function addFlow(flow?: FlowType) {
// Get data from the flow or set it to null if there's no flow provided.
const data = flow?.data ? flow.data : null;
const description = flow?.description ? flow.description : "";
if (data) { /**
data.nodes.forEach((node) => { * Creates a file input and listens to a change event to upload a JSON flow file.
if (Object.keys(templates[node.data.type]["template"]).length > 0) { * If the file type is application/json, the file is read and parsed into a JSON object.
node.data.node.base_classes = * The resulting JSON object is passed to the addFlow function.
templates[node.data.type]["base_classes"]; */
data.edges.forEach((edge) => { function uploadFlow() {
if (edge.source === node.id) { // create a file input
edge.sourceHandle = edge.sourceHandle const input = document.createElement("input");
.split("|") input.type = "file";
.slice(0, 2) // add a change event listener to the file input
.concat(templates[node.data.type]["base_classes"]) input.onchange = (e: Event) => {
.join("|"); // check if the file type is application/json
} if ((e.target as HTMLInputElement).files[0].type === "application/json") {
}); // get the file from the file input
node.data.node.description = templates[node.data.type]["description"]; const file = (e.target as HTMLInputElement).files[0];
node.data.node.template = updateTemplate( // read the file as text
templates[node.data.type]["template"] as unknown as APITemplateType, file.text().then((text) => {
node.data.node.template as APITemplateType // parse the text into a JSON object
); let flow: FlowType = JSON.parse(text);
}
});
console.log(data);
}
// Create a new flow with a default name if no flow is provided.
let newFlow: FlowType = {
description,
name: flow?.name ?? "New Flow",
id: uuidv4(),
data,
};
// Increment the ID counter. addFlow(flow);
setId(uuidv4()); });
}
};
// trigger the file input click event to open the file dialog
input.click();
}
/**
* Removes a flow from an array of flows based on its id.
* Updates the state of flows and tabIndex using setFlows and setTabIndex hooks.
* @param {string} id - The id of the flow to remove.
*/
function removeFlow(id: string) {
setFlows((prevState) => {
const newFlows = [...prevState];
const index = newFlows.findIndex((flow) => flow.id === id);
if (index >= 0) {
if (index === tabIndex) {
setTabIndex(flows.length - 2);
newFlows.splice(index, 1);
} else {
let flowId = flows[tabIndex].id;
newFlows.splice(index, 1);
setTabIndex(newFlows.findIndex((flow) => flow.id === flowId));
}
}
return newFlows;
});
}
/**
* Add a new flow to the list of flows.
* @param flow Optional flow to add.
*/
// Add the new flow to the list of flows. function paste(selectionInstance, position){
setFlows((prevState) => { console.log(position);
const newFlows = [...prevState, newFlow]; console.log(selectionInstance)
return newFlows; let minimumX = Infinity;
}); let minimumY = Infinity;
let idsMap = {};
let nodes = reactFlowInstance.getNodes();
let edges = reactFlowInstance.getEdges();
selectionInstance.nodes.forEach((n) => {
if (n.position.y < minimumY) {
minimumY = n.position.y;
}
if (n.position.x < minimumX) {
minimumX = n.position.x;
}
});
// Set the tab index to the new flow. const insidePosition = reactFlowInstance.project(position);
setTabIndex(flows.length);
} selectionInstance.nodes.forEach((n) => {
/** // Generate a unique node ID
* Updates an existing flow with new data let newId = getNodeId();
* @param newFlow - The new flow object containing the updated data idsMap[n.id] = newId;
*/
function updateFlow(newFlow: FlowType) { // Create a new node object
setFlows((prevState) => { const newNode: NodeType = {
const newFlows = [...prevState]; id: newId,
const index = newFlows.findIndex((flow) => flow.id === newFlow.id); type: "genericNode",
if (index !== -1) { position: {
newFlows[index].description = newFlow.description ?? ""; x: insidePosition.x + n.position.x - minimumX,
newFlows[index].data = newFlow.data; y: insidePosition.y + n.position.y - minimumY,
newFlows[index].name = newFlow.name; },
} data: {
return newFlows; ...n.data,
}); id: newId,
} },
const [disableCP, setDisableCP] = useState(false); };
// Add the new node to the list of nodes in state
nodes = nodes
.map((e) => ({ ...e, selected: false }))
.concat({ ...newNode, selected: false })
console.log(nodes);
});
reactFlowInstance.setNodes(nodes);
selectionInstance.edges.forEach((e) => {
let source = idsMap[e.source];
let target = idsMap[e.target];
let sourceHandleSplitted = e.sourceHandle.split("|");
let sourceHandle =
sourceHandleSplitted[0] +
"|" +
source +
"|" +
sourceHandleSplitted.slice(2).join("|");
let targetHandleSplitted = e.targetHandle.split("|");
let targetHandle =
targetHandleSplitted.slice(0, -1).join("|") + "|" + target;
let id =
"reactflow__edge-" +
source +
sourceHandle +
"-" +
target +
targetHandle;
edges = addEdge(
{
source,
target,
sourceHandle,
targetHandle,
id,
className: "animate-pulse",
selected: false,
},
edges.map((e) => ({ ...e, selected: false }))
);
console.log(edges);
});
reactFlowInstance.setEdges(edges);
};
function addFlow(flow?: FlowType) {
// Get data from the flow or set it to null if there's no flow provided.
const data = flow?.data ? flow.data : null;
const description = flow?.description ? flow.description : "";
return ( if (data) {
<TabsContext.Provider data.nodes.forEach((node) => {
value={{ if (Object.keys(templates[node.data.type]["template"]).length > 0) {
disableCP, node.data.node.template = updateTemplate(
setDisableCP, templates[node.data.type]["template"] as unknown as APITemplateType,
save, node.data.node.template as APITemplateType
hardReset, );
tabIndex, }
setTabIndex, });
flows, }
incrementNodeId, // Create a new flow with a default name if no flow is provided.
removeFlow, let newFlow: FlowType = {
addFlow, description,
updateFlow, name: flow?.name ?? "New Flow",
downloadFlow, id: uuidv4(),
uploadFlow, data,
}} };
>
{children} // Increment the ID counter.
</TabsContext.Provider> setId(uuidv4());
);
} // Add the new flow to the list of flows.
setFlows((prevState) => {
const newFlows = [...prevState, newFlow];
return newFlows;
});
// Set the tab index to the new flow.
setTabIndex(flows.length);
}
/**
* Updates an existing flow with new data
* @param newFlow - The new flow object containing the updated data
*/
function updateFlow(newFlow: FlowType) {
setFlows((prevState) => {
const newFlows = [...prevState];
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
if (index !== -1) {
newFlows[index].description = newFlow.description ?? "";
newFlows[index].data = newFlow.data;
newFlows[index].name = newFlow.name;
}
return newFlows;
});
}
const [disableCP, setDisableCP] = useState(false);
return (
<TabsContext.Provider
value={{
disableCP,
setDisableCP,
save,
hardReset,
tabIndex,
setTabIndex,
flows,
incrementNodeId,
removeFlow,
addFlow,
updateFlow,
downloadFlow,
uploadFlow,
getNodeId,
paste,
}}
>
{children}
</TabsContext.Provider>
);
}

View file

@ -47,8 +47,8 @@ export default function TabComponent({
className="bg-transparent focus:border-none active:outline hover:outline focus:outline outline-gray-300 rounded-md w-28" className="bg-transparent focus:border-none active:outline hover:outline focus:outline outline-gray-300 rounded-md w-28"
onBlur={() => { onBlur={() => {
setIsRename(false); setIsRename(false);
setDisableCP(false);
if (value !== "") { if (value !== "") {
setDisableCP(false);
let newFlow = _.cloneDeep(flow); let newFlow = _.cloneDeep(flow);
newFlow.name = value; newFlow.name = value;
updateFlow(newFlow); updateFlow(newFlow);

View file

@ -1,21 +1,23 @@
import { useCallback, useContext, useEffect, useRef, useState } from "react"; import { useCallback, useContext, useEffect, useRef, useState } from "react";
import ReactFlow, { import ReactFlow, {
Background, Background,
Controls, Controls,
addEdge, addEdge,
useEdgesState, useEdgesState,
useNodesState, useNodesState,
useReactFlow, useReactFlow,
updateEdge, updateEdge,
EdgeChange, EdgeChange,
Connection, Connection,
Edge, Edge,
useKeyPress, useKeyPress,
useOnSelectionChange, NodeDragHandler,
NodeDragHandler, OnEdgesDelete,
OnEdgesDelete, OnNodesDelete,
OnNodesDelete, SelectionDragHandler,
SelectionDragHandler, useOnViewportChange,
OnSelectionChangeParams,
OnNodesChange,
} from "reactflow"; } from "reactflow";
import _ from "lodash"; import _ from "lodash";
import { locationContext } from "../../contexts/locationContext"; import { locationContext } from "../../contexts/locationContext";
@ -28,311 +30,331 @@ import { typesContext } from "../../contexts/typesContext";
import ConnectionLineComponent from "./components/ConnectionLineComponent"; import ConnectionLineComponent from "./components/ConnectionLineComponent";
import { FlowType, NodeType } from "../../types/flow"; import { FlowType, NodeType } from "../../types/flow";
import { APIClassType } from "../../types/api"; import { APIClassType } from "../../types/api";
import { isValidConnection } from "../../utils"; import {
isValidConnection,
} from "../../utils";
import useUndoRedo from "./hooks/useUndoRedo"; import useUndoRedo from "./hooks/useUndoRedo";
const nodeTypes = { const nodeTypes = {
genericNode: GenericNode, genericNode: GenericNode,
}; };
export default function FlowPage({ flow }: { flow: FlowType }) { export default function FlowPage({ flow }: { flow: FlowType }) {
let { updateFlow, incrementNodeId, disableCP} = let { updateFlow, disableCP, addFlow, getNodeId, paste } =
useContext(TabsContext); useContext(TabsContext);
const { types, reactFlowInstance, setReactFlowInstance, templates } = const { types, reactFlowInstance, setReactFlowInstance, templates } =
useContext(typesContext); useContext(typesContext);
const reactFlowWrapper = useRef(null); const reactFlowWrapper = useRef(null);
const { undo, redo, canUndo, canRedo, takeSnapshot } = useUndoRedo(); const { undo, redo, canUndo, canRedo, takeSnapshot } = useUndoRedo();
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if ((event.ctrlKey || event.metaKey) && (event.key === 'c') && lastSelection && !disableCP) { const [position, setPosition] = useState({ x: 0, y: 0 });
event.preventDefault(); const [lastSelection, setLastSelection] =
setLastCopiedSelection(lastSelection); useState<OnSelectionChangeParams>(null);
}
if ((event.ctrlKey || event.metaKey) && (event.key === 'v') && lastCopiedSelection && !disableCP) { const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
event.preventDefault();
paste(); useEffect(() => {
} // this effect is used to attach the global event handlers
}
const onKeyDown = (event: KeyboardEvent) => {
const [lastSelection, setLastSelection] = useState(null); console.log("keydownou", lastCopiedSelection, position)
const [lastCopiedSelection, setLastCopiedSelection] = useState(null); if (
(event.ctrlKey || event.metaKey) &&
const [position, setPosition] = useState({ x: 0, y: 0 }); event.key === "c" &&
lastSelection &&
const handleMouseMove = (event) => { !disableCP
setPosition({ x: event.clientX, y: event.clientY }); ) {
}; event.preventDefault();
setLastCopiedSelection(_.cloneDeep(lastSelection));
useOnSelectionChange({ }
onChange: (flow) => { setLastSelection(flow); }, if (
}) (event.ctrlKey || event.metaKey) &&
event.key === "v" &&
let paste = () => { lastCopiedSelection &&
let minimumX = Infinity; !disableCP
let minimumY = Infinity; ) {
let idsMap = {}; event.preventDefault();
lastCopiedSelection.nodes.forEach((n) => { let bounds = reactFlowWrapper.current.getBoundingClientRect();
if (n.position.y < minimumY) { paste(lastCopiedSelection, {
minimumY = n.position.y x: position.x - bounds.left,
} y: position.y - bounds.top,
if (n.position.x < minimumX) { });
minimumX = n.position.x; }
} if (
}); (event.ctrlKey || event.metaKey) &&
event.key === "g" &&
const bounds = reactFlowWrapper.current.getBoundingClientRect(); lastSelection
const insidePosition = reactFlowInstance.project({ ) {
x: position.x - bounds.left, event.preventDefault();
y: position.y - bounds.top // addFlow(newFlow, false);
}); }
};
lastCopiedSelection.nodes.forEach((n) => { const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
// Generate a unique node ID };
let newId = getId();
idsMap[n.id] = newId; document.addEventListener('keydown', onKeyDown);
document.addEventListener('mousemove', handleMouseMove);
// Create a new node object
const newNode: NodeType = { return () => {
id: newId, document.removeEventListener('keydown', onKeyDown);
type: "genericNode", document.removeEventListener('mousemove', handleMouseMove);
position: { };
x: insidePosition.x + n.position.x - minimumX, }, [position, lastCopiedSelection, lastSelection]);
y: insidePosition.y + n.position.y - minimumY,
},
data: {
...n.data,
id: newId,
},
};
// Add the new node to the list of nodes in state
setNodes((nds) => nds.map((e) => ({ ...e, selected: false })).concat({ ...newNode, selected: false }));
})
lastCopiedSelection.edges.forEach((e) => {
let source = idsMap[e.source];
let target = idsMap[e.target];
let sourceHandleSplitted = e.sourceHandle.split('|');
let sourceHandle = sourceHandleSplitted[0] + '|' + source + '|' + sourceHandleSplitted.slice(2).join('|');
let targetHandleSplitted = e.targetHandle.split('|');
let targetHandle = targetHandleSplitted.slice(0, -1).join('|') + '|' + target;
let id = "reactflow__edge-" + source + sourceHandle + "-" + target + targetHandle;
setEdges((eds) =>
addEdge({ source, target, sourceHandle, targetHandle, id, className: "animate-pulse", selected: false }, eds.map((e) => ({ ...e, selected: false })))
);
})
}
const { setExtraComponent, setExtraNavigation } = useContext(locationContext); const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
const { setErrorData } = useContext(alertContext);
const [nodes, setNodes, onNodesChange] = useNodesState(
flow.data?.nodes ?? []
);
const [edges, setEdges, onEdgesChange] = useEdgesState(
flow.data?.edges ?? []
);
const { setViewport } = useReactFlow();
const edgeUpdateSuccessful = useRef(true);
function getId() {
return `dndnode_` + incrementNodeId();
}
useEffect(() => { const { setExtraComponent, setExtraNavigation } = useContext(locationContext);
if (reactFlowInstance && flow) { const { setErrorData } = useContext(alertContext);
flow.data = reactFlowInstance.toObject(); const [nodes, setNodes, onNodesChange] = useNodesState(
updateFlow(flow); flow.data?.nodes ?? []
} );
// eslint-disable-next-line react-hooks/exhaustive-deps const [edges, setEdges, onEdgesChange] = useEdgesState(
}, [nodes, edges]); flow.data?.edges ?? []
//update flow when tabs change );
useEffect(() => { const { setViewport } = useReactFlow();
setNodes(flow?.data?.nodes ?? []); const edgeUpdateSuccessful = useRef(true);
setEdges(flow?.data?.edges ?? []); useEffect(() => {
if (reactFlowInstance) { if (reactFlowInstance && flow) {
setViewport(flow?.data?.viewport ?? { x: 1, y: 0, zoom: 0.5 }); flow.data = reactFlowInstance.toObject();
} updateFlow(flow);
}, [flow, reactFlowInstance, setEdges, setNodes, setViewport]); }
//set extra sidebar // eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => { }, [nodes, edges]);
setExtraComponent(<ExtraSidebar />); //update flow when tabs change
setExtraNavigation({ title: "Components" }); useEffect(() => {
}, [setExtraComponent, setExtraNavigation]); setNodes(flow?.data?.nodes ?? []);
setEdges(flow?.data?.edges ?? []);
if (reactFlowInstance) {
setViewport(flow?.data?.viewport ?? { x: 1, y: 0, zoom: 0.5 });
}
}, [flow, reactFlowInstance, setEdges, setNodes, setViewport]);
//set extra sidebar
useEffect(() => {
setExtraComponent(<ExtraSidebar />);
setExtraNavigation({ title: "Components" });
}, [setExtraComponent, setExtraNavigation]);
const onEdgesChangeMod = useCallback( const onEdgesChangeMod = useCallback(
(s: EdgeChange[]) => { (s: EdgeChange[]) => {
onEdgesChange(s); onEdgesChange(s);
setNodes((x) => { setNodes((x) => {
let newX = _.cloneDeep(x); let newX = _.cloneDeep(x);
return newX; return newX;
}); });
}, },
[onEdgesChange, setNodes] [onEdgesChange, setNodes]
); );
const onConnect = useCallback( const onConnect = useCallback(
(params: Connection) => { (params: Connection) => {
takeSnapshot(); takeSnapshot();
setEdges((eds) => setEdges((eds) =>
addEdge({ ...params, className: "animate-pulse" }, eds) addEdge(
); {
setNodes((x) => { ...params,
let newX = _.cloneDeep(x); style:
return newX; params.targetHandle.split("|")[0] === "Text"
}); ? { stroke: "#333333", strokeWidth: 2 }
}, : { stroke: "#222222" },
[setEdges, setNodes, takeSnapshot] className:
); params.targetHandle.split("|")[0] === "Text"
? ""
: "animate-pulse",
animated: params.targetHandle.split("|")[0] === "Text",
},
eds
)
);
setNodes((x) => {
let newX = _.cloneDeep(x);
return newX;
});
},
[setEdges, setNodes, takeSnapshot]
);
const onNodeDragStart: NodeDragHandler = useCallback(() => { const onNodeDragStart: NodeDragHandler = useCallback(() => {
// 👇 make dragging a node undoable // 👇 make dragging a node undoable
takeSnapshot(); takeSnapshot();
// 👉 you can place your event handlers here // 👉 you can place your event handlers here
}, [takeSnapshot]); }, [takeSnapshot]);
const onSelectionDragStart: SelectionDragHandler = useCallback(() => { const onSelectionDragStart: SelectionDragHandler = useCallback(() => {
// 👇 make dragging a selection undoable // 👇 make dragging a selection undoable
takeSnapshot(); takeSnapshot();
}, [takeSnapshot]); }, [takeSnapshot]);
const onEdgesDelete: OnEdgesDelete = useCallback(() => {
// 👇 make deleting edges undoable
takeSnapshot();
}, [takeSnapshot]);
const onEdgesDelete: OnEdgesDelete = useCallback(() => { const onDragOver = useCallback((event: React.DragEvent) => {
// 👇 make deleting edges undoable event.preventDefault();
takeSnapshot(); event.dataTransfer.dropEffect = "move";
}, [takeSnapshot]); }, []);
const onDragOver = useCallback((event: React.DragEvent) => { const onDrop = useCallback(
event.preventDefault(); (event: React.DragEvent) => {
event.dataTransfer.dropEffect = "move"; event.preventDefault();
}, []); takeSnapshot();
const onDrop = useCallback( // Get the current bounds of the ReactFlow wrapper element
(event: React.DragEvent) => { const reactflowBounds = reactFlowWrapper.current.getBoundingClientRect();
event.preventDefault();
takeSnapshot();
// Get the current bounds of the ReactFlow wrapper element // Extract the data from the drag event and parse it as a JSON object
const reactflowBounds = reactFlowWrapper.current.getBoundingClientRect(); let data: { type: string; node?: APIClassType } = JSON.parse(
event.dataTransfer.getData("json")
);
// Extract the data from the drag event and parse it as a JSON object // If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
let data: { type: string; node?: APIClassType } = JSON.parse( // Calculate the position where the node should be created
event.dataTransfer.getData("json") const position = reactFlowInstance.project({
); x: event.clientX - reactflowBounds.left,
y: event.clientY - reactflowBounds.top,
});
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node // Generate a unique node ID
if ( let newId = getNodeId();
data.type !== "chatInput" || let newNode: NodeType;
(data.type === "chatInput" &&
!reactFlowInstance.getNodes().some((n) => n.type === "chatInputNode"))
) {
// Calculate the position where the node should be created
const position = reactFlowInstance.project({
x: event.clientX - reactflowBounds.left,
y: event.clientY - reactflowBounds.top,
});
// Generate a unique node ID if (data.type !== "groupNode") {
let newId = getId(); // Create a new node object
newNode = {
id: newId,
type: "genericNode",
position,
data: {
...data,
id: newId,
value: null,
},
};
} else {
// Create a new node object
newNode = {
id: newId,
type: "genericNode",
position,
data: {
...data,
id: newId,
value: null,
},
};
// Create a new node object // Add the new node to the list of nodes in state
const newNode: NodeType = { }
id: newId, setNodes((nds) => nds.concat(newNode));
type: "genericNode", },
position, // Specify dependencies for useCallback
data: { [getNodeId, reactFlowInstance, setErrorData, setNodes, takeSnapshot]
...data, );
id: newId,
value: null,
},
};
// Add the new node to the list of nodes in state const onDelete = useCallback(
setNodes((nds) => nds.concat(newNode)); (mynodes) => {
} else { takeSnapshot();
// If a chat input node already exists, set an error message setEdges(
setErrorData({ edges.filter(
title: "Error creating node", (ns) => !mynodes.some((n) => ns.source === n.id || ns.target === n.id)
list: ["There can't be more than one chat input."], )
}); );
} },
}, [takeSnapshot, edges, setEdges]
// Specify dependencies for useCallback );
[incrementNodeId, reactFlowInstance, setErrorData, setNodes, takeSnapshot]
);
const onDelete = useCallback((mynodes) => { const onEdgeUpdateStart = useCallback(() => {
takeSnapshot(); edgeUpdateSuccessful.current = false;
setEdges( }, []);
edges.filter(
(ns) => !mynodes.some((n) => ns.source === n.id || ns.target === n.id)
)
);
}, [takeSnapshot, edges, setEdges]);
const onEdgeUpdateStart = useCallback(() => { const onEdgeUpdate = useCallback(
edgeUpdateSuccessful.current = false; (oldEdge: Edge, newConnection: Connection) => {
}, []); if (isValidConnection(newConnection, reactFlowInstance)) {
edgeUpdateSuccessful.current = true;
setEdges((els) => updateEdge(oldEdge, newConnection, els));
}
},
[]
);
const onEdgeUpdate = useCallback( const onEdgeUpdateEnd = useCallback((_, edge) => {
(oldEdge: Edge, newConnection: Connection) => { if (!edgeUpdateSuccessful.current) {
if (isValidConnection(newConnection, reactFlowInstance)) { setEdges((eds) => eds.filter((e) => e.id !== edge.id));
edgeUpdateSuccessful.current = true; }
setEdges((els) => updateEdge(oldEdge, newConnection, els));
}
},
[]
);
const onEdgeUpdateEnd = useCallback((_, edge) => { edgeUpdateSuccessful.current = true;
if (!edgeUpdateSuccessful.current) { }, []);
setEdges((eds) => eds.filter((e) => e.id !== edge.id));
}
edgeUpdateSuccessful.current = true; const [selectionEnded, setSelectionEnded] = useState(false);
}, []);
return ( const onSelectionEnd = useCallback(() => {
<div className="w-full h-full" onMouseMove={handleMouseMove} ref={reactFlowWrapper}> setSelectionEnded(true);
{Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? ( }, []);
<> const onSelectionStart = useCallback(() => {
<ReactFlow setSelectionEnded(false);
nodes={nodes} }, []);
onMove={() =>
updateFlow({ ...flow, data: reactFlowInstance.toObject() }) // Workaround to show the menu only after the selection has ended.
} useEffect(() => {
edges={edges} if (selectionEnded && lastSelection && lastSelection.nodes.length > 1) {
onNodesChange={onNodesChange} setSelectionMenuVisible(true);
onEdgesChange={onEdgesChangeMod} } else {
onKeyDown={(e) => onKeyDown(e)} setSelectionMenuVisible(false);
onConnect={onConnect} }
onLoad={setReactFlowInstance} }, [selectionEnded, lastSelection]);
onInit={setReactFlowInstance}
nodeTypes={nodeTypes} const onSelectionChange = useCallback((flow) => {
onEdgeUpdate={onEdgeUpdate} setLastSelection(flow);
onEdgeUpdateStart={onEdgeUpdateStart} }, []);
onEdgeUpdateEnd={onEdgeUpdateEnd}
onNodeDragStart={onNodeDragStart} return (
onSelectionDragStart={onSelectionDragStart} <div
onEdgesDelete={onEdgesDelete} className="w-full h-full"
connectionLineComponent={ConnectionLineComponent} ref={reactFlowWrapper}
onDragOver={onDragOver} >
onDrop={onDrop} {Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? (
onNodesDelete={onDelete} <>
selectNodesOnDrag={false} <ReactFlow
> nodes={nodes}
<Background className="dark:bg-gray-900" /> onMove={() => {
<Controls className="[&>button]:text-black [&>button]:dark:bg-gray-800 hover:[&>button]:dark:bg-gray-700 [&>button]:dark:text-gray-400 [&>button]:dark:fill-gray-400 [&>button]:dark:border-gray-600"> updateFlow({ ...flow, data: reactFlowInstance.toObject() });
</Controls> }}
</ReactFlow> edges={edges}
<Chat flow={flow} reactFlowInstance={reactFlowInstance} /> onNodesChange={onNodesChange}
</> onEdgesChange={onEdgesChangeMod}
) : ( onConnect={onConnect}
<></> onLoad={setReactFlowInstance}
)} onInit={setReactFlowInstance}
</div> nodeTypes={nodeTypes}
); onEdgeUpdate={onEdgeUpdate}
} onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
onNodeDragStart={onNodeDragStart}
onSelectionDragStart={onSelectionDragStart}
onSelectionEnd={onSelectionEnd}
onSelectionStart={onSelectionStart}
onEdgesDelete={onEdgesDelete}
connectionLineComponent={ConnectionLineComponent}
onDragOver={onDragOver}
onDrop={onDrop}
onNodesDelete={onDelete}
onSelectionChange={onSelectionChange}
selectNodesOnDrag={false}
>
<Background className="dark:bg-gray-900" />
<Controls className="[&>button]:text-black [&>button]:dark:bg-gray-800 hover:[&>button]:dark:bg-gray-700 [&>button]:dark:text-gray-400 [&>button]:dark:fill-gray-400 [&>button]:dark:border-gray-600"></Controls>
</ReactFlow>
<Chat flow={flow} reactFlowInstance={reactFlowInstance} />
</>
) : (
<></>
)}
</div>
);
}

View file

@ -6,15 +6,17 @@ export type TabsContextType = {
setTabIndex: (index: number) => void; setTabIndex: (index: number) => void;
flows: Array<FlowType>; flows: Array<FlowType>;
removeFlow: (id: string) => void; removeFlow: (id: string) => void;
addFlow: (flowData?: FlowType) => void; addFlow: (flowData?: FlowType,newFlow?:boolean) => void;
updateFlow: (newFlow: FlowType) => void; updateFlow: (newFlow: FlowType) => void;
incrementNodeId: () => number; incrementNodeId: () => string;
downloadFlow: (flow: FlowType) => void; downloadFlow: (flow: FlowType) => void;
uploadFlow: () => void; uploadFlow: (newFlow?:boolean) => void;
hardReset: () => void; hardReset: () => void;
//disable CopyPaste //disable CopyPaste
disableCP: boolean; disableCP: boolean;
setDisableCP: (value: boolean) => void; setDisableCP: (value: boolean) => void;
getNodeId: () => string;
paste: (selection: {nodes: any, edges: any}, position: {x: number, y: number}) => void;
}; };
export type LangFlowState = { export type LangFlowState = {
@ -22,4 +24,4 @@ export type LangFlowState = {
flows: FlowType[]; flows: FlowType[];
id: string; id: string;
nodeId: number; nodeId: number;
}; };