diff --git a/src/frontend/src/contexts/tabsContext.tsx b/src/frontend/src/contexts/tabsContext.tsx index 15fe9eaf6..6f8877d57 100644 --- a/src/frontend/src/contexts/tabsContext.tsx +++ b/src/frontend/src/contexts/tabsContext.tsx @@ -1,273 +1,335 @@ import { - createContext, - useEffect, - useState, - useRef, - ReactNode, - useContext, + createContext, + useEffect, + useState, + useRef, + ReactNode, + useContext, } from "react"; -import { FlowType } from "../types/flow"; +import { FlowType, NodeType } from "../types/flow"; import { LangFlowState, TabsContextType } from "../types/tabs"; import { normalCaseToSnakeCase, updateObject, updateTemplate } from "../utils"; import { alertContext } from "./alertContext"; import { typesContext } from "./typesContext"; import { APITemplateType, TemplateVariableType } from "../types/api"; import { v4 as uuidv4 } from "uuid"; +import { addEdge } from "reactflow"; const TabsContextInitialValue: TabsContextType = { - save: () => {}, - tabIndex: 0, - setTabIndex: (index: number) => {}, - flows: [], - removeFlow: (id: string) => {}, - addFlow: (flowData?: any) => {}, - updateFlow: (newFlow: FlowType) => {}, - incrementNodeId: () => 0, - downloadFlow: (flow: FlowType) => {}, - uploadFlow: () => {}, - hardReset: () => {}, - disableCP: false, - setDisableCP: (state: boolean) => {}, + save: () => {}, + tabIndex: 0, + setTabIndex: (index: number) => {}, + flows: [], + removeFlow: (id: string) => {}, + addFlow: (flowData?: any) => {}, + updateFlow: (newFlow: FlowType) => {}, + incrementNodeId: () => uuidv4(), + downloadFlow: (flow: FlowType) => {}, + uploadFlow: () => {}, + hardReset: () => {}, + disableCP:false, + setDisableCP:(state:boolean)=>{}, + getNodeId: () => "", + paste: (selection: {nodes: any, edges: any}, position: {x: number, y: number}) => {}, }; export const TabsContext = createContext( - TabsContextInitialValue + TabsContextInitialValue ); export function TabsProvider({ children }: { children: ReactNode }) { - const { setNoticeData } = useContext(alertContext); - const [tabIndex, setTabIndex] = useState(0); - const [flows, setFlows] = useState>([]); - const [id, setId] = useState(uuidv4()); - const { templates } = useContext(typesContext); + const { setNoticeData } = useContext(alertContext); + const [tabIndex, setTabIndex] = useState(0); + const [flows, setFlows] = useState>([]); + const [id, setId] = useState(uuidv4()); + const { templates, reactFlowInstance } = useContext(typesContext); - const newNodeId = useRef(0); - function incrementNodeId() { - newNodeId.current = newNodeId.current + 1; - return newNodeId.current; - } - function save() { - if (flows.length !== 0) - window.localStorage.setItem( - "tabsData", - JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current }) - ); - } - useEffect(() => { - //save tabs locally - save(); - }, [flows, id, tabIndex, newNodeId]); + const newNodeId = useRef(uuidv4()); + function incrementNodeId() { + newNodeId.current = uuidv4(); + return newNodeId.current; + } + function save() { + if (flows.length !== 0) + window.localStorage.setItem( + "tabsData", + JSON.stringify({ tabIndex, flows, id}) + ); + } + useEffect(() => { + //save tabs locally + // console.log(id) + save(); + }, [flows, id, tabIndex, newNodeId]); - useEffect(() => { - //get tabs locally saved - let cookie = window.localStorage.getItem("tabsData"); - if (cookie && Object.keys(templates).length > 0) { - let cookieObject: LangFlowState = JSON.parse(cookie); - cookieObject.flows.forEach((flow) => { - // check if flow.data is null - if (flow.data) { - flow.data.nodes.forEach((node) => { - // check if node.data.type is in templates - if ( - Object.keys(templates).includes(node.data.type) && - 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]); + useEffect(() => { + //get tabs locally saved + let cookie = window.localStorage.getItem("tabsData"); + if (cookie && Object.keys(templates).length > 0) { + let cookieObject: LangFlowState = JSON.parse(cookie); + cookieObject.flows.forEach((flow) => { + flow.data.nodes.forEach((node) => { + if (Object.keys(templates[node.data.type]["template"]).length > 0) { + node.data.node.template = updateTemplate( + templates[node.data.type][ + "template" + ] as unknown as APITemplateType, - function hardReset() { - newNodeId.current = 0; - setTabIndex(0); - setFlows([]); - setId(uuidv4()); - } + node.data.node.template as APITemplateType + ); + } + }); + }); + setTabIndex(cookieObject.tabIndex); + setFlows(cookieObject.flows); + setId(cookieObject.id); + } + }, [templates]); - /** - * Downloads the current flow as a JSON file - */ - 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) - )}`; + function hardReset() { + newNodeId.current = uuidv4(); + setTabIndex(0); + setFlows([]); + setId(uuidv4()); + } - // create a link element and set its properties - const link = document.createElement("a"); - link.href = jsonString; - link.download = `${flows[tabIndex].name}.json`; + /** + * Downloads the current flow as a JSON file + */ + 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 - link.click(); - setNoticeData({ - title: "Warning: Critical data,JSON file may including API keys.", - }); - } + // create a link element and set its properties + const link = document.createElement("a"); + link.href = jsonString; + link.download = `${flows[tabIndex].name}.json`; - /** - * Creates a file input and listens to a change event to upload a JSON flow file. - * If the file type is application/json, the file is read and parsed into a JSON object. - * The resulting JSON object is passed to the addFlow function. - */ - 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); + // simulate a click on the link element to trigger the download + link.click(); + setNoticeData({ + title: "Warning: Critical data,JSON file may including API keys.", + }); + } - addFlow(flow); - }); - } - }; - // 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 : ""; + function getNodeId() { + return `dndnode_` + incrementNodeId(); + } - if (data) { - data.nodes.forEach((node) => { - if (Object.keys(templates[node.data.type]["template"]).length > 0) { - node.data.node.base_classes = - templates[node.data.type]["base_classes"]; - 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 - ); - } - }); - 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, - }; + /** + * Creates a file input and listens to a change event to upload a JSON flow file. + * If the file type is application/json, the file is read and parsed into a JSON object. + * The resulting JSON object is passed to the addFlow function. + */ + 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); - // Increment the ID counter. - setId(uuidv4()); + addFlow(flow); + }); + } + }; + // 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. - setFlows((prevState) => { - const newFlows = [...prevState, newFlow]; - return newFlows; - }); + function paste(selectionInstance, position){ + console.log(position); + console.log(selectionInstance) + 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. - 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); + const insidePosition = reactFlowInstance.project(position); + + selectionInstance.nodes.forEach((n) => { + // Generate a unique node ID + let newId = getNodeId(); + idsMap[n.id] = newId; + + // Create a new node object + const newNode: NodeType = { + id: newId, + type: "genericNode", + position: { + x: insidePosition.x + n.position.x - minimumX, + y: insidePosition.y + n.position.y - minimumY, + }, + data: { + ...n.data, + id: newId, + }, + }; + + // 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 ( - - {children} - - ); -} + if (data) { + data.nodes.forEach((node) => { + if (Object.keys(templates[node.data.type]["template"]).length > 0) { + node.data.node.template = updateTemplate( + templates[node.data.type]["template"] as unknown as APITemplateType, + node.data.node.template as APITemplateType + ); + } + }); + } + // 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. + 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 ( + + {children} + + ); +} \ No newline at end of file diff --git a/src/frontend/src/pages/FlowPage/components/tabComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/tabComponent/index.tsx index 0bf68286b..3cd36dc3f 100644 --- a/src/frontend/src/pages/FlowPage/components/tabComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/tabComponent/index.tsx @@ -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" onBlur={() => { setIsRename(false); + setDisableCP(false); if (value !== "") { - setDisableCP(false); let newFlow = _.cloneDeep(flow); newFlow.name = value; updateFlow(newFlow); diff --git a/src/frontend/src/pages/FlowPage/index.tsx b/src/frontend/src/pages/FlowPage/index.tsx index 02ec95614..1d28f467e 100644 --- a/src/frontend/src/pages/FlowPage/index.tsx +++ b/src/frontend/src/pages/FlowPage/index.tsx @@ -1,21 +1,23 @@ import { useCallback, useContext, useEffect, useRef, useState } from "react"; import ReactFlow, { - Background, - Controls, - addEdge, - useEdgesState, - useNodesState, - useReactFlow, - updateEdge, - EdgeChange, - Connection, - Edge, - useKeyPress, - useOnSelectionChange, - NodeDragHandler, - OnEdgesDelete, - OnNodesDelete, - SelectionDragHandler, + Background, + Controls, + addEdge, + useEdgesState, + useNodesState, + useReactFlow, + updateEdge, + EdgeChange, + Connection, + Edge, + useKeyPress, + NodeDragHandler, + OnEdgesDelete, + OnNodesDelete, + SelectionDragHandler, + useOnViewportChange, + OnSelectionChangeParams, + OnNodesChange, } from "reactflow"; import _ from "lodash"; import { locationContext } from "../../contexts/locationContext"; @@ -28,311 +30,331 @@ import { typesContext } from "../../contexts/typesContext"; import ConnectionLineComponent from "./components/ConnectionLineComponent"; import { FlowType, NodeType } from "../../types/flow"; import { APIClassType } from "../../types/api"; -import { isValidConnection } from "../../utils"; +import { + isValidConnection, +} from "../../utils"; import useUndoRedo from "./hooks/useUndoRedo"; const nodeTypes = { - genericNode: GenericNode, + genericNode: GenericNode, }; - export default function FlowPage({ flow }: { flow: FlowType }) { - let { updateFlow, incrementNodeId, disableCP} = - useContext(TabsContext); - const { types, reactFlowInstance, setReactFlowInstance, templates } = - useContext(typesContext); - const reactFlowWrapper = useRef(null); - - const { undo, redo, canUndo, canRedo, takeSnapshot } = useUndoRedo(); - - const onKeyDown = (event: React.KeyboardEvent) => { - if ((event.ctrlKey || event.metaKey) && (event.key === 'c') && lastSelection && !disableCP) { - event.preventDefault(); - setLastCopiedSelection(lastSelection); - } - if ((event.ctrlKey || event.metaKey) && (event.key === 'v') && lastCopiedSelection && !disableCP) { - event.preventDefault(); - paste(); - } - } - - const [lastSelection, setLastSelection] = useState(null); - const [lastCopiedSelection, setLastCopiedSelection] = useState(null); - - const [position, setPosition] = useState({ x: 0, y: 0 }); - - const handleMouseMove = (event) => { - setPosition({ x: event.clientX, y: event.clientY }); - }; - - useOnSelectionChange({ - onChange: (flow) => { setLastSelection(flow); }, - }) - - let paste = () => { - let minimumX = Infinity; - let minimumY = Infinity; - let idsMap = {}; - lastCopiedSelection.nodes.forEach((n) => { - if (n.position.y < minimumY) { - minimumY = n.position.y - } - if (n.position.x < minimumX) { - minimumX = n.position.x; - } - }); - - const bounds = reactFlowWrapper.current.getBoundingClientRect(); - const insidePosition = reactFlowInstance.project({ - x: position.x - bounds.left, - y: position.y - bounds.top - }); - - lastCopiedSelection.nodes.forEach((n) => { - - // Generate a unique node ID - let newId = getId(); - idsMap[n.id] = newId; - - // Create a new node object - const newNode: NodeType = { - id: newId, - type: "genericNode", - position: { - x: insidePosition.x + n.position.x - minimumX, - 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 }))) - ); - }) - } + let { updateFlow, disableCP, addFlow, getNodeId, paste } = + useContext(TabsContext); + const { types, reactFlowInstance, setReactFlowInstance, templates } = + useContext(typesContext); + const reactFlowWrapper = useRef(null); + + const { undo, redo, canUndo, canRedo, takeSnapshot } = useUndoRedo(); + + + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [lastSelection, setLastSelection] = + useState(null); + + const [lastCopiedSelection, setLastCopiedSelection] = useState(null); + + useEffect(() => { + // this effect is used to attach the global event handlers + + const onKeyDown = (event: KeyboardEvent) => { + console.log("keydownou", lastCopiedSelection, position) + if ( + (event.ctrlKey || event.metaKey) && + event.key === "c" && + lastSelection && + !disableCP + ) { + event.preventDefault(); + setLastCopiedSelection(_.cloneDeep(lastSelection)); + } + if ( + (event.ctrlKey || event.metaKey) && + event.key === "v" && + lastCopiedSelection && + !disableCP + ) { + event.preventDefault(); + let bounds = reactFlowWrapper.current.getBoundingClientRect(); + paste(lastCopiedSelection, { + x: position.x - bounds.left, + y: position.y - bounds.top, + }); + } + if ( + (event.ctrlKey || event.metaKey) && + event.key === "g" && + lastSelection + ) { + event.preventDefault(); + // addFlow(newFlow, false); + } + }; + const handleMouseMove = (event) => { + setPosition({ x: event.clientX, y: event.clientY }); + }; + + document.addEventListener('keydown', onKeyDown); + document.addEventListener('mousemove', handleMouseMove); + + return () => { + document.removeEventListener('keydown', onKeyDown); + document.removeEventListener('mousemove', handleMouseMove); + }; + }, [position, lastCopiedSelection, lastSelection]); - const { setExtraComponent, setExtraNavigation } = useContext(locationContext); - 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); + const [selectionMenuVisible, setSelectionMenuVisible] = useState(false); - function getId() { - return `dndnode_` + incrementNodeId(); - } + - useEffect(() => { - if (reactFlowInstance && flow) { - flow.data = reactFlowInstance.toObject(); - updateFlow(flow); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nodes, edges]); - //update flow when tabs change - useEffect(() => { - 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(); - setExtraNavigation({ title: "Components" }); - }, [setExtraComponent, setExtraNavigation]); + const { setExtraComponent, setExtraNavigation } = useContext(locationContext); + 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); + useEffect(() => { + if (reactFlowInstance && flow) { + flow.data = reactFlowInstance.toObject(); + updateFlow(flow); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nodes, edges]); + //update flow when tabs change + useEffect(() => { + 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(); + setExtraNavigation({ title: "Components" }); + }, [setExtraComponent, setExtraNavigation]); - const onEdgesChangeMod = useCallback( - (s: EdgeChange[]) => { - onEdgesChange(s); - setNodes((x) => { - let newX = _.cloneDeep(x); - return newX; - }); - }, - [onEdgesChange, setNodes] - ); + const onEdgesChangeMod = useCallback( + (s: EdgeChange[]) => { + onEdgesChange(s); + setNodes((x) => { + let newX = _.cloneDeep(x); + return newX; + }); + }, + [onEdgesChange, setNodes] + ); - const onConnect = useCallback( - (params: Connection) => { - takeSnapshot(); - setEdges((eds) => - addEdge({ ...params, className: "animate-pulse" }, eds) - ); - setNodes((x) => { - let newX = _.cloneDeep(x); - return newX; - }); - }, - [setEdges, setNodes, takeSnapshot] - ); + const onConnect = useCallback( + (params: Connection) => { + takeSnapshot(); + setEdges((eds) => + addEdge( + { + ...params, + style: + params.targetHandle.split("|")[0] === "Text" + ? { stroke: "#333333", strokeWidth: 2 } + : { stroke: "#222222" }, + 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(() => { - // 👇 make dragging a node undoable - takeSnapshot(); - // 👉 you can place your event handlers here - }, [takeSnapshot]); + const onNodeDragStart: NodeDragHandler = useCallback(() => { + // 👇 make dragging a node undoable + takeSnapshot(); + // 👉 you can place your event handlers here + }, [takeSnapshot]); - const onSelectionDragStart: SelectionDragHandler = useCallback(() => { - // 👇 make dragging a selection undoable - takeSnapshot(); - }, [takeSnapshot]); + const onSelectionDragStart: SelectionDragHandler = useCallback(() => { + // 👇 make dragging a selection undoable + takeSnapshot(); + }, [takeSnapshot]); + const onEdgesDelete: OnEdgesDelete = useCallback(() => { + // 👇 make deleting edges undoable + takeSnapshot(); + }, [takeSnapshot]); - const onEdgesDelete: OnEdgesDelete = useCallback(() => { - // 👇 make deleting edges undoable - takeSnapshot(); - }, [takeSnapshot]); + const onDragOver = useCallback((event: React.DragEvent) => { + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + }, []); - const onDragOver = useCallback((event: React.DragEvent) => { - event.preventDefault(); - event.dataTransfer.dropEffect = "move"; - }, []); + const onDrop = useCallback( + (event: React.DragEvent) => { + event.preventDefault(); + takeSnapshot(); - const onDrop = useCallback( - (event: React.DragEvent) => { - event.preventDefault(); - takeSnapshot(); + // Get the current bounds of the ReactFlow wrapper element + const reactflowBounds = reactFlowWrapper.current.getBoundingClientRect(); - // Get the current bounds of the ReactFlow wrapper element - const reactflowBounds = reactFlowWrapper.current.getBoundingClientRect(); + // Extract the data from the drag event and parse it as a JSON object + 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 - let data: { type: string; node?: APIClassType } = JSON.parse( - event.dataTransfer.getData("json") - ); + // If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node + // Calculate the position where the node should be created + 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 - if ( - data.type !== "chatInput" || - (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 + let newId = getNodeId(); + let newNode: NodeType; - // Generate a unique node ID - let newId = getId(); + if (data.type !== "groupNode") { + // 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 - const newNode: NodeType = { - id: newId, - type: "genericNode", - position, - data: { - ...data, - id: newId, - value: null, - }, - }; + // Add the new node to the list of nodes in state + } + setNodes((nds) => nds.concat(newNode)); + }, + // Specify dependencies for useCallback + [getNodeId, reactFlowInstance, setErrorData, setNodes, takeSnapshot] + ); - // Add the new node to the list of nodes in state - setNodes((nds) => nds.concat(newNode)); - } else { - // If a chat input node already exists, set an error message - setErrorData({ - title: "Error creating node", - list: ["There can't be more than one chat input."], - }); - } - }, - // Specify dependencies for useCallback - [incrementNodeId, reactFlowInstance, setErrorData, setNodes, takeSnapshot] - ); + const onDelete = useCallback( + (mynodes) => { + takeSnapshot(); + setEdges( + edges.filter( + (ns) => !mynodes.some((n) => ns.source === n.id || ns.target === n.id) + ) + ); + }, + [takeSnapshot, edges, setEdges] + ); - const onDelete = useCallback((mynodes) => { - takeSnapshot(); - setEdges( - edges.filter( - (ns) => !mynodes.some((n) => ns.source === n.id || ns.target === n.id) - ) - ); - }, [takeSnapshot, edges, setEdges]); + const onEdgeUpdateStart = useCallback(() => { + edgeUpdateSuccessful.current = false; + }, []); - const onEdgeUpdateStart = useCallback(() => { - edgeUpdateSuccessful.current = false; - }, []); + const onEdgeUpdate = useCallback( + (oldEdge: Edge, newConnection: Connection) => { + if (isValidConnection(newConnection, reactFlowInstance)) { + edgeUpdateSuccessful.current = true; + setEdges((els) => updateEdge(oldEdge, newConnection, els)); + } + }, + [] + ); - const onEdgeUpdate = useCallback( - (oldEdge: Edge, newConnection: Connection) => { - if (isValidConnection(newConnection, reactFlowInstance)) { - edgeUpdateSuccessful.current = true; - setEdges((els) => updateEdge(oldEdge, newConnection, els)); - } - }, - [] - ); + const onEdgeUpdateEnd = useCallback((_, edge) => { + if (!edgeUpdateSuccessful.current) { + setEdges((eds) => eds.filter((e) => e.id !== edge.id)); + } - const onEdgeUpdateEnd = useCallback((_, edge) => { - if (!edgeUpdateSuccessful.current) { - setEdges((eds) => eds.filter((e) => e.id !== edge.id)); - } + edgeUpdateSuccessful.current = true; + }, []); - edgeUpdateSuccessful.current = true; - }, []); + const [selectionEnded, setSelectionEnded] = useState(false); - return ( -
- {Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? ( - <> - - updateFlow({ ...flow, data: reactFlowInstance.toObject() }) - } - edges={edges} - onNodesChange={onNodesChange} - onEdgesChange={onEdgesChangeMod} - onKeyDown={(e) => onKeyDown(e)} - onConnect={onConnect} - onLoad={setReactFlowInstance} - onInit={setReactFlowInstance} - nodeTypes={nodeTypes} - onEdgeUpdate={onEdgeUpdate} - onEdgeUpdateStart={onEdgeUpdateStart} - onEdgeUpdateEnd={onEdgeUpdateEnd} - onNodeDragStart={onNodeDragStart} - onSelectionDragStart={onSelectionDragStart} - onEdgesDelete={onEdgesDelete} - connectionLineComponent={ConnectionLineComponent} - onDragOver={onDragOver} - onDrop={onDrop} - onNodesDelete={onDelete} - selectNodesOnDrag={false} - > - - - - - - - ) : ( - <> - )} -
- ); -} + const onSelectionEnd = useCallback(() => { + setSelectionEnded(true); + }, []); + const onSelectionStart = useCallback(() => { + setSelectionEnded(false); + }, []); + + // Workaround to show the menu only after the selection has ended. + useEffect(() => { + if (selectionEnded && lastSelection && lastSelection.nodes.length > 1) { + setSelectionMenuVisible(true); + } else { + setSelectionMenuVisible(false); + } + }, [selectionEnded, lastSelection]); + + const onSelectionChange = useCallback((flow) => { + setLastSelection(flow); + }, []); + + return ( +
+ {Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? ( + <> + { + updateFlow({ ...flow, data: reactFlowInstance.toObject() }); + }} + edges={edges} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChangeMod} + onConnect={onConnect} + onLoad={setReactFlowInstance} + onInit={setReactFlowInstance} + 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} + > + + + + + + ) : ( + <> + )} +
+ ); +} \ No newline at end of file diff --git a/src/frontend/src/types/tabs/index.ts b/src/frontend/src/types/tabs/index.ts index 48a319a73..bb1b4cee2 100644 --- a/src/frontend/src/types/tabs/index.ts +++ b/src/frontend/src/types/tabs/index.ts @@ -6,15 +6,17 @@ export type TabsContextType = { setTabIndex: (index: number) => void; flows: Array; removeFlow: (id: string) => void; - addFlow: (flowData?: FlowType) => void; + addFlow: (flowData?: FlowType,newFlow?:boolean) => void; updateFlow: (newFlow: FlowType) => void; - incrementNodeId: () => number; + incrementNodeId: () => string; downloadFlow: (flow: FlowType) => void; - uploadFlow: () => void; + uploadFlow: (newFlow?:boolean) => void; hardReset: () => void; //disable CopyPaste disableCP: boolean; setDisableCP: (value: boolean) => void; + getNodeId: () => string; + paste: (selection: {nodes: any, edges: any}, position: {x: number, y: number}) => void; }; export type LangFlowState = { @@ -22,4 +24,4 @@ export type LangFlowState = { flows: FlowType[]; id: string; nodeId: number; -}; +}; \ No newline at end of file