diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 6f9b6940..78d333fe 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/package.json b/frontend/package.json index e0f6e93b..8589c9a3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,6 +39,7 @@ "@uiw/codemirror-theme-andromeda": "^4.21.25", "@uiw/codemirror-theme-noctis-lilac": "^4.21.25", "@uiw/react-codemirror": "^4.21.25", + "@xyflow/react": "^12.2.0", "axios": "^1.6.7", "babel-jest": "^29.7.0", "classnames": "^2.5.1", @@ -60,7 +61,6 @@ "react-router-dom": "^6.22.2", "react-test-renderer": "^18.3.1", "react-xarrows": "^2.0.2", - "reactflow": "^11.10.4", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "uuid": "^9.0.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b8684ca6..ddae27bf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { NextUIProvider } from "@nextui-org/react" +import { ReactFlowProvider } from "@xyflow/react" import { RouterProvider, createBrowserRouter } from "react-router-dom" -import { ReactFlowProvider } from "reactflow" import { Preloader } from "./UI/Preloader/Preloader" import ContextWrapper from "./contexts" import { UndoRedoProvider } from "./contexts/undoRedoContext" diff --git a/frontend/src/UI/Preloader/preloader.css b/frontend/src/UI/Preloader/preloader.css index 8b63a157..ab7e5dd4 100644 --- a/frontend/src/UI/Preloader/preloader.css +++ b/frontend/src/UI/Preloader/preloader.css @@ -1,5 +1,5 @@ #preloader-wrapper { - background-color: hsl(var(--background)); + background-color: var(--background); position: absolute; top: 0; left: 0; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts old mode 100644 new mode 100755 index 93ee05bd..6b6e7f1a --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,9 +1,7 @@ import axios from "axios" import { DEV, VITE_BASE_API_URL } from "../env.consts" -const clearUrlFromQueries = (url: string) => {} -// const baseURL = VITE_BASE_API_URL ?? "http://localhost:8000/api/v1" const baseURL = DEV ? VITE_BASE_API_URL : window.location.protocol + "//" + window.location.host + "/api/v1" diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/components/ReactFlowCustom.tsx b/frontend/src/components/ReactFlowCustom.tsx new file mode 100644 index 00000000..36c1259c --- /dev/null +++ b/frontend/src/components/ReactFlowCustom.tsx @@ -0,0 +1,12 @@ +import { Edge, ReactFlow, ReactFlowProps } from '@xyflow/react' +import { AppNode } from '../types/NodeTypes' + +const ReactFlowCustom = ({ ...props }: ReactFlowProps) => { + return ( + + {props.children} + + ) +} + +export default ReactFlowCustom \ No newline at end of file diff --git a/frontend/src/components/chat/Chat.tsx b/frontend/src/components/chat/Chat.tsx index c25e53e4..f973ae4d 100644 --- a/frontend/src/components/chat/Chat.tsx +++ b/frontend/src/components/chat/Chat.tsx @@ -5,7 +5,7 @@ import { Paperclip, RefreshCcw, Send, Smile, X } from "lucide-react" import { memo, useContext, useEffect, useRef, useState } from "react" import { useSearchParams } from "react-router-dom" import { chatContext } from "../../contexts/chatContext" -import { notificationsContext } from "../../contexts/notificationsContext" +import { NotificationsContext } from "../../contexts/notificationsContext" import { runContext } from "../../contexts/runContext" import { workspaceContext } from "../../contexts/workspaceContext" import { DEV } from "../../env.consts" @@ -19,7 +19,7 @@ const Chat = memo(() => { const [searchParams, setSearchParams] = useSearchParams() const ws = useRef(null) const { setMouseOnPane } = useContext(workspaceContext) - const { notification: n } = useContext(notificationsContext) + const { notification: n } = useContext(NotificationsContext) const [isEmoji, setIsEmoji] = useState(false) @@ -122,7 +122,7 @@ const Chat = memo(() => { const socket = new WebSocket( `ws://${DEV ? "localhost:8000" : window.location.host}/api/v1/bot/run/connect?run_id=${run.id}` ) - socket.onopen = (e) => { + socket.onopen = () => { n.add({ message: "Chat was successfully connected!", title: "Success", type: "success" }) } socket.onmessage = (event: MessageEvent) => { diff --git a/frontend/src/components/edges/ButtonEdge/ButtonEdge.tsx b/frontend/src/components/edges/ButtonEdge/ButtonEdge.tsx index 4aa018b8..703326f4 100644 --- a/frontend/src/components/edges/ButtonEdge/ButtonEdge.tsx +++ b/frontend/src/components/edges/ButtonEdge/ButtonEdge.tsx @@ -1,4 +1,3 @@ -import { X } from "lucide-react" import { BaseEdge, Edge, @@ -6,7 +5,8 @@ import { EdgeProps, getBezierPath, useReactFlow, -} from "reactflow" +} from "@xyflow/react" +import { X } from "lucide-react" import "./buttonedge.css" export default function CustomEdge({ @@ -53,9 +53,9 @@ export default function CustomEdge({ }} className='nodrag nopan'> diff --git a/frontend/src/components/footbar/FootBar.tsx b/frontend/src/components/footbar/FootBar.tsx index 41f637ac..eb3f09fb 100644 --- a/frontend/src/components/footbar/FootBar.tsx +++ b/frontend/src/components/footbar/FootBar.tsx @@ -5,11 +5,11 @@ import { Key, memo, useCallback, useContext, useState } from "react" import { Link, useSearchParams } from "react-router-dom" import { buildContext } from "../../contexts/buildContext" import { MetaContext } from "../../contexts/metaContext" -import { notificationsContext } from "../../contexts/notificationsContext" +import { NotificationsContext } from "../../contexts/notificationsContext" import { workspaceContext } from "../../contexts/workspaceContext" -import { Logo } from "../../icons/Logo" import MonitorIcon from "../../icons/buildmenu/MonitorIcon" import LocalStorageIcon from "../../icons/footbar/LocalStorageIcon" +import { Logo } from "../../icons/Logo" import LocalStorage from "../../modals/LocalStorage/LocalStorage" import { parseSearchParams } from "../../utils" import { NotificationsWindow } from "../notifications/NotificationsWindow" @@ -26,7 +26,7 @@ const FootBar = memo(() => { const { logsPage, setLogsPage } = useContext(buildContext) const [searchParams, setSearchParams] = useSearchParams() const [isNotificationsOpen, setIsNotificationsOpen] = useState(false) - const { notifications } = useContext(notificationsContext) + const { notifications } = useContext(NotificationsContext) const onSelectionChange = useCallback( (key: Key) => { diff --git a/frontend/src/components/header/BuildMenu.tsx b/frontend/src/components/header/BuildMenu.tsx index f28b66d3..67452b26 100644 --- a/frontend/src/components/header/BuildMenu.tsx +++ b/frontend/src/components/header/BuildMenu.tsx @@ -1,6 +1,6 @@ import { Button, Spinner, Tooltip } from "@nextui-org/react" import classNames from "classnames" -import { useContext, useState } from "react" +import { useContext } from "react" import { useSearchParams } from "react-router-dom" import { buildContext } from "../../contexts/buildContext" import { chatContext } from "../../contexts/chatContext" @@ -11,10 +11,9 @@ import StopIcon from "../../icons/buildmenu/StopIcon" import { parseSearchParams } from "../../utils" const BuildMenu = () => { - const { buildStart, buildPending, buildStatus, setLogsPage, logsPage } = useContext(buildContext) + const { buildStart, buildPending } = useContext(buildContext) const { chat, setChat } = useContext(chatContext) const { runStart, runPending, runStatus, runStop, run } = useContext(runContext) - const [showBuildMenu, setShowBuildMenu] = useState(false) const [searchParams, setSearchParams] = useSearchParams() return ( @@ -30,7 +29,9 @@ const BuildMenu = () => { className={classNames("transition-all duration-300", showBuildMenu && "rotate-180")} /> */} - + */} - + */} + {location.pathname.includes("flow") && } + {location.pathname.includes("home") && ( + + + + + +

+ + Chatsky UI +

+
+

+ Version: {version} +

+ + +

GitHub

+
+ +

DeepPavlov.ai

+
+
+
+
+ )} ) diff --git a/frontend/src/components/header/components/NodeInstruments.tsx b/frontend/src/components/header/components/NodeInstruments.tsx index 66f402b5..f6fd6091 100644 --- a/frontend/src/components/header/components/NodeInstruments.tsx +++ b/frontend/src/components/header/components/NodeInstruments.tsx @@ -1,30 +1,29 @@ import { Button, Tooltip } from "@nextui-org/react" +import { Edge, useReactFlow } from "@xyflow/react" import classNames from "classnames" -import { useContext, useMemo } from "react" -import { Node, useReactFlow } from "reactflow" +import { useContext } from "react" import { flowContext } from "../../../contexts/flowContext" import { workspaceContext } from "../../../contexts/workspaceContext" import FallbackNodeIcon from "../../../icons/nodes/FallbackNodeIcon" import StartNodeIcon from "../../../icons/nodes/StartNodeIcon" import { FlowType } from "../../../types/FlowTypes" -import { NodeDataType } from "../../../types/NodeTypes" +import { AppNode } from "../../../types/NodeTypes" const NodeInstruments = ({ flow }: { flow: FlowType }) => { - const { setNodes } = useReactFlow() + const { setNodes } = useReactFlow() const { handleNodeFlags, selectedNode } = useContext(workspaceContext) const { deleteNode } = useContext(flowContext) - const selectedNodeData: Node | null = + const selectedNodeData: AppNode | null = flow?.data.nodes.find((node) => node.id === selectedNode) ?? null - const is_node_default = useMemo(() => selectedNodeData?.type === "default_node", [selectedNodeData]) - + // eslint-disable-next-line @typescript-eslint/no-unused-vars const deleteSelectedNodeHandler = () => { setNodes((nds) => nds.filter((node) => node.id !== selectedNode)) deleteNode(selectedNode) } - if (!is_node_default) return <> + if (selectedNodeData?.type !== 'default_node') return <> return (
diff --git a/frontend/src/components/nodes/DefaultNode.tsx b/frontend/src/components/nodes/DefaultNode.tsx index fffd94e7..5de21360 100644 --- a/frontend/src/components/nodes/DefaultNode.tsx +++ b/frontend/src/components/nodes/DefaultNode.tsx @@ -1,9 +1,9 @@ import { Button, useDisclosure } from "@nextui-org/react" +import { Handle, Position } from "@xyflow/react" +import "@xyflow/react/dist/style.css" import classNames from "classnames" import { PlusIcon } from "lucide-react" import { memo, useContext, useMemo, useState } from "react" -import { Handle, Position } from "reactflow" -import "reactflow/dist/style.css" import { workspaceContext } from "../../contexts/workspaceContext" import EditNodeIcon from "../../icons/nodes/EditNodeIcon" import FallbackNodeIcon from "../../icons/nodes/FallbackNodeIcon" @@ -14,11 +14,11 @@ import "../../index.css" import ConditionModal from "../../modals/ConditionModal/ConditionModal" import NodeModal from "../../modals/NodeModal/NodeModal" import ResponseModal from "../../modals/ResponseModal/ResponseModal" -import { NodeDataType } from "../../types/NodeTypes" +import { DefaultNodeDataType } from "../../types/NodeTypes" import Condition from "./conditions/Condition" import Response from "./responses/Response" -const DefaultNode = memo(({ data }: { data: NodeDataType }) => { +const DefaultNode = memo(({ data }: { data: DefaultNodeDataType }) => { const { onOpen: onConditionOpen, onClose: onConditionClose, @@ -27,7 +27,7 @@ const DefaultNode = memo(({ data }: { data: NodeDataType }) => { const { selectedNode } = useContext(workspaceContext) - const [nodeDataState, setNodeDataState] = useState(data) + const [nodeDataState, setNodeDataState] = useState(data) const { onOpen: onNodeOpen, onClose: onNodeClose, isOpen: isNodeOpen } = useDisclosure() const { @@ -75,7 +75,7 @@ const DefaultNode = memo(({ data }: { data: NodeDataType }) => { width: "0.7rem", height: "0.7rem", top: "1.875rem", - left: "-0.335rem", + left: "0rem", zIndex: 10, }} /> @@ -102,9 +102,9 @@ const DefaultNode = memo(({ data }: { data: NodeDataType }) => { />
-
+
diff --git a/frontend/src/components/nodes/LinkNode.tsx b/frontend/src/components/nodes/LinkNode.tsx old mode 100644 new mode 100755 index 21055407..3491faac --- a/frontend/src/components/nodes/LinkNode.tsx +++ b/frontend/src/components/nodes/LinkNode.tsx @@ -19,24 +19,25 @@ import { Tooltip, useDisclosure, } from "@nextui-org/react" +import { Handle, Position } from "@xyflow/react" +import "@xyflow/react/dist/style.css" import classNames from "classnames" import { AlertTriangle, Link2, Trash2 } from "lucide-react" import { memo, useContext, useEffect, useMemo, useState } from "react" -import { Handle, Node, Position } from "reactflow" -import "reactflow/dist/style.css" import { flowContext } from "../../contexts/flowContext" -import { notificationsContext } from "../../contexts/notificationsContext" +import { NotificationsContext } from "../../contexts/notificationsContext" import "../../index.css" import { FlowType } from "../../types/FlowTypes" -import { NodeDataType } from "../../types/NodeTypes" +import { AppNode, LinkNodeDataType } from "../../types/NodeTypes" -const LinkNode = memo(({ data }: { data: NodeDataType }) => { +const LinkNode = memo(({ data }: { data: LinkNodeDataType }) => { const { onOpen, onClose, isOpen } = useDisclosure() const { flows, deleteNode } = useContext(flowContext) const [toFlow, setToFlow] = useState() - const [toNode, setToNode] = useState>() + const [toNode, setToNode] = useState() const [error, setError] = useState(false) - const { notification: n } = useContext(notificationsContext) + const [r, setR] = useState(0) + const { notification: n } = useContext(NotificationsContext) // const { openPopUp } = useContext(PopUpContext) useEffect(() => { @@ -75,6 +76,9 @@ const LinkNode = memo(({ data }: { data: NodeDataType }) => { ) useEffect(() => { + if (r === 0) { + return setR((r) => r + 1) + } if (!TO_FLOW || !TO_NODE) { setError(true) n.add({ @@ -110,10 +114,7 @@ const LinkNode = memo(({ data }: { data: NodeDataType }) => { <>
+ className={classNames("default_node px-6 py-4", error && "border-error")}>
{ return (
-
+
{conditionTypeIcons[condition.type]} {condition.name} @@ -59,7 +59,7 @@ const Condition = ({ data, condition }: NodeComponentConditionType) => { borderStyle: "solid", width: "0.7rem", height: "0.7rem", - right: "-0.95rem", + right: "-0.7rem", zIndex: 10, }} /> diff --git a/frontend/src/components/nodes/responses/Response.tsx b/frontend/src/components/nodes/responses/Response.tsx index 2e4ea468..413bbcc8 100644 --- a/frontend/src/components/nodes/responses/Response.tsx +++ b/frontend/src/components/nodes/responses/Response.tsx @@ -5,7 +5,7 @@ const Response = ({ data }: NodeComponentType) => { return (
-

{data.response?.data[0]?.text ?? "No text response"}

+

{data.response.data[0]?.text ?? "No text response"}

) } diff --git a/frontend/src/components/notifications/NotificationsWindow.tsx b/frontend/src/components/notifications/NotificationsWindow.tsx index a947c854..31f1abb8 100644 --- a/frontend/src/components/notifications/NotificationsWindow.tsx +++ b/frontend/src/components/notifications/NotificationsWindow.tsx @@ -8,7 +8,7 @@ import { } from "@nextui-org/react" import { AlertOctagon, AlertTriangle, BugIcon, CheckCircle2, InfoIcon, Trash } from "lucide-react" import { useContext, useState } from "react" -import { notificationsContext } from "../../contexts/notificationsContext" +import { NotificationsContext } from "../../contexts/notificationsContext" import NotificationComponent from "./components/NotificationComponent" type NotificationsWindowProps = { @@ -30,7 +30,7 @@ type NotificationsWindowProps = { // } export const NotificationsWindow = ({ isOpen, setIsOpen }: NotificationsWindowProps) => { - const { notifications, notification } = useContext(notificationsContext) + const { notifications, notification } = useContext(NotificationsContext) const [notificationFilter, setNotificationFilter] = useState([]) const handleSelectionChange = (e: React.ChangeEvent) => { @@ -41,7 +41,6 @@ export const NotificationsWindow = ({ isOpen, setIsOpen }: NotificationsWindowPr } }; - console.log(notificationFilter) const renderNotifications = () => { const filtered_notifications = notifications.filter( @@ -54,13 +53,11 @@ export const NotificationsWindow = ({ isOpen, setIsOpen }: NotificationsWindowPr const stack = not.stack + 1 const next_not = time_sorted_notifications[idx + 1] if (next_not && not && next_not.message === not.message && not.stack !== 0) { - console.log(not, next_not) next_not.stack = stack not.stack = 0 } return not }) - console.log(stack_checked_notifications) const stack_filtered_notifications = stack_checked_notifications.filter((not) => not.stack > 0) if (stack_filtered_notifications.length === 0) { return ( diff --git a/frontend/src/components/notifications/components/NotificationComponent.tsx b/frontend/src/components/notifications/components/NotificationComponent.tsx index 53170a30..7377160b 100644 --- a/frontend/src/components/notifications/components/NotificationComponent.tsx +++ b/frontend/src/components/notifications/components/NotificationComponent.tsx @@ -2,7 +2,7 @@ import { Button } from "@nextui-org/react" import classNames from "classnames" import { AlertOctagon, AlertTriangle, Bug, CheckCircle2, Info, X } from "lucide-react" import { useContext, useMemo, useState } from "react" -import { notificationType, notificationsContext } from "../../../contexts/notificationsContext" +import { NotificationsContext, notificationType } from "../../../contexts/notificationsContext" type NotificationComponentType = { notification: notificationType & { @@ -11,7 +11,7 @@ type NotificationComponentType = { } const NotificationComponent = ({ notification }: NotificationComponentType) => { - const { notification: nt, notifications } = useContext(notificationsContext) + const { notification: nt, notifications } = useContext(NotificationsContext) const [isDelete, setIsDelete] = useState(false) const notificationTypeColor = (type: string) => { @@ -83,9 +83,7 @@ const NotificationComponent = ({ notification }: NotificationComponentType) => { if (notification.stack > 1) { const index = notifications.findIndex((n) => n.timestamp == notification.timestamp) for (let i = 0; i <= index; i++) { - console.log(notifications[i]) if (notifications[i].stack === 0 && notifications[i].message === notification.message) { - console.log("deletion") nt.delete(notifications[i].timestamp) } } diff --git a/frontend/src/components/sidebar/DragListItem.tsx b/frontend/src/components/sidebar/DragListItem.tsx index 864214ef..d49d2717 100644 --- a/frontend/src/components/sidebar/DragListItem.tsx +++ b/frontend/src/components/sidebar/DragListItem.tsx @@ -4,7 +4,7 @@ import React from "react"; const DragListItem = ({ item }: { item: { name: string; color: string; type: string } }) => { const onDragStart = (event: React.DragEvent, nodeType: string) => { // if (event.dataTransfer) { - event.dataTransfer.setData("application/reactflow", nodeType) + event.dataTransfer.setData("application/@xyflow/react", nodeType) event.dataTransfer.effectAllowed = "move" // } } diff --git a/frontend/src/consts.tsx b/frontend/src/consts.tsx index 0be0e0f0..6b9f5048 100644 --- a/frontend/src/consts.tsx +++ b/frontend/src/consts.tsx @@ -73,7 +73,7 @@ export const NODE_NAMES = [ "Science discussion", "Experience story", "Political discussion", - "Now plans", + "Now plans", "Sport talk", "Movie discussion", "Family story", @@ -108,7 +108,7 @@ export const NODE_NAMES = [ "Cooking discussion", "Technology story", "Music talk", - "Next year plans" + "Next year plans", ] export const START_FALLBACK_NODE_FLAGS = ["start", "fallback"] @@ -119,7 +119,7 @@ export const NODES = { default_node: { name: "Default Node", type: "default_node", - dragHandle: '.custom-drag-handle', + dragHandle: ".custom-drag-handle", conditions: [], global_conditions: [], local_conditions: [], @@ -148,15 +148,11 @@ export const NODES = { link_node: { name: "Link", type: "link_node", - dragHandle: '', - conditions: [], - global_conditions: [], - local_conditions: [], - response: { - name: "Link response", - type: "text", - data: [{ text: "Link response", priority: 1 }], - } + dragHandle: "", + transition: { + target_flow: "", + target_node: "", + }, }, } @@ -194,6 +190,5 @@ export const conditionTypeIcons = { export const responseTypeIcons = { python: , text: , - llm: + llm: , } - diff --git a/frontend/src/contexts/buildContext.tsx b/frontend/src/contexts/buildContext.tsx index a83433a8..07126512 100644 --- a/frontend/src/contexts/buildContext.tsx +++ b/frontend/src/contexts/buildContext.tsx @@ -11,7 +11,7 @@ import { get_builds, localBuildType, } from "../api/bot" -import { notificationsContext } from "./notificationsContext" +import { NotificationsContext } from "./notificationsContext" type BuildContextType = { build: boolean @@ -53,7 +53,7 @@ export const BuildProvider = ({ children }: { children: React.ReactNode }) => { const [searchParams, setSearchParams] = useSearchParams() const [logsPage, setLogsPage] = useState(searchParams.get("logs_page") === "opened") const [builds, setBuilds] = useState([]) - const { notification: n } = useContext(notificationsContext) + const { notification: n } = useContext(NotificationsContext) const setBuildsHandler = (builds: buildMinifyApiType[]) => { setBuilds(() => diff --git a/frontend/src/contexts/flowContext.tsx b/frontend/src/contexts/flowContext.tsx index 9ae0aa78..cb848f7a 100644 --- a/frontend/src/contexts/flowContext.tsx +++ b/frontend/src/contexts/flowContext.tsx @@ -1,14 +1,14 @@ /* eslint-disable react-refresh/only-export-components */ +import { Edge, OnBeforeDelete, ReactFlowInstance } from "@xyflow/react" import React, { createContext, useCallback, useContext, useEffect, useState } from "react" import { useParams } from "react-router-dom" -import { Edge, ReactFlowInstance } from "reactflow" import { v4 } from "uuid" import { get_flows, save_flows } from "../api/flows" import { FLOW_COLORS } from "../consts" import { FlowType } from "../types/FlowTypes" -import { NodeType } from "../types/NodeTypes" +import { AppNode } from "../types/NodeTypes" import { MetaContext } from "./metaContext" -import { notificationsContext } from "./notificationsContext" +import { NotificationsContext } from "./notificationsContext" // import { v4 } from "uuid" const globalFlow: FlowType = { @@ -22,12 +22,14 @@ const globalFlow: FlowType = { id: v4(), type: "default_node", data: { + id: "GLOBAL_NODE", flags: [], conditions: [], global_conditions: [], local_conditions: [], name: "Global node", response: { + id: "GLOBAL_NODE_RESPONSE", name: "global_response", type: "text", data: [{ text: "Global node response", priority: 1 }], @@ -48,9 +50,11 @@ const globalFlow: FlowType = { }, } +export type CustomReactFlowInstanceType = ReactFlowInstance + type TabContextType = { - reactFlowInstance: ReactFlowInstance | null - setReactFlowInstance: React.Dispatch> + reactFlowInstance: CustomReactFlowInstanceType | null + setReactFlowInstance: React.Dispatch> tab: string setTab: React.Dispatch> flows: FlowType[] @@ -64,6 +68,8 @@ type TabContextType = { deleteNode: (id: string) => void deleteEdge: (id: string) => void deleteObject: (id: string) => void + validateDeletion: OnBeforeDelete + validateNodeDeletion: (node: AppNode) => boolean } const initialValue: TabContextType = { @@ -84,20 +90,28 @@ const initialValue: TabContextType = { deleteNode: () => {}, deleteEdge: () => {}, deleteObject: () => {}, + validateDeletion: () => + new Promise(() => { + return false + }), + validateNodeDeletion: () => false, } export const flowContext = createContext(initialValue) export const FlowProvider = ({ children }: { children: React.ReactNode }) => { - - const [reactFlowInstance, setReactFlowInstance] = useState(null) + const [reactFlowInstance, setReactFlowInstance] = useState | null>(null) const [tab, setTab] = useState(initialValue.tab) const { flowId } = useParams() const [flows, setFlows] = useState([]) - const { notification: n } = useContext(notificationsContext) + const { notification: n } = useContext(NotificationsContext) const { screenLoading } = useContext(MetaContext) useEffect(() => { + setReactFlowInstance(null) setTab(flowId || "") }, [flowId]) @@ -123,10 +137,11 @@ export const FlowProvider = ({ children }: { children: React.ReactNode }) => { useEffect(() => { getFlows() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const saveFlows = async (flows: FlowType[]) => { - const res = await save_flows(flows) + await save_flows(flows) setFlows(flows) } @@ -160,23 +175,55 @@ export const FlowProvider = ({ children }: { children: React.ReactNode }) => { [flows] ) + const validateDeletion = ({ nodes, edges }: { nodes: AppNode[]; edges: Edge[] }) => { + const is_nodes_valid = nodes.every((node) => { + if (node.type === "default_node" && node?.data.flags?.includes("start")) { + n.add({ title: "Warning!", message: "Can't delete start node", type: "warning" }) + return false + } + if (node?.id?.includes("LOCAL")) { + n.add({ title: "Warning!", message: "Can't delete local node", type: "warning" }) + return false + } + if (node?.id?.includes("GLOBAL")) { + n.add({ title: "Warning!", message: "Can't delete global node", type: "warning" }) + return false + } + return true + }) + return new Promise((resolve) => { + resolve(is_nodes_valid) + }) + } + + const validateNodeDeletion = (node: AppNode) => { + if (node.type === "default_node" && node.data.flags.includes("start")) { + n.add({ title: "Warning!", message: "Can't delete start node", type: "warning" }) + return false + } + if (node.id.includes("LOCAL")) { + n.add({ title: "Warning!", message: "Can't delete local node", type: "warning" }) + return false + } + if (node.id.includes("GLOBAL")) { + n.add({ title: "Warning!", message: "Can't delete global node", type: "warning" }) + return false + } + return true + } + const deleteNode = useCallback( (id: string) => { const flow = flows.find((flow) => flow.data.nodes.some((node) => node.id === id)) if (!flow) return -1 - const deleted_node: NodeType = flow.data.nodes.find((node) => node.id === id) as NodeType - if (deleted_node?.data.flags?.includes("start")) + const deleted_node: AppNode = flow.data.nodes.find((node) => node.id === id) as AppNode + if (deleted_node.type === "default_node" && deleted_node?.data.flags?.includes("start")) return n.add({ title: "Warning!", message: "Can't delete start node", type: "warning" }) if (deleted_node?.id?.includes("LOCAL")) return n.add({ title: "Warning!", message: "Can't delete local node", type: "warning" }) if (deleted_node?.id?.includes("GLOBAL")) return n.add({ title: "Warning!", message: "Can't delete global node", type: "warning" }) - if (deleted_node?.data.flags?.includes("fallback")) { - console.log( - flow.data.nodes - .find((node) => node.id !== id && !node.data.id.includes("LOCAL")) - ?.data.flags.push("fallback") - ) + if (deleted_node.type === "default_node" && deleted_node?.data.flags?.includes("fallback")) { // any_node.data.flags?.push("fallback") } const newNodes = flow.data.nodes.filter((node) => node.id !== id) @@ -240,6 +287,8 @@ export const FlowProvider = ({ children }: { children: React.ReactNode }) => { deleteNode, deleteEdge, deleteObject, + validateDeletion, + validateNodeDeletion, }}> {children} diff --git a/frontend/src/contexts/metaContext.tsx b/frontend/src/contexts/metaContext.tsx index 07533a2d..6c9fe84d 100644 --- a/frontend/src/contexts/metaContext.tsx +++ b/frontend/src/contexts/metaContext.tsx @@ -55,6 +55,7 @@ const MetaProvider = ({ children }: MetaProviderProps) => { useEffect(() => { getVersion() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( diff --git a/frontend/src/contexts/notificationsContext.tsx b/frontend/src/contexts/notificationsContext.tsx index f02b9964..d2e3f259 100644 --- a/frontend/src/contexts/notificationsContext.tsx +++ b/frontend/src/contexts/notificationsContext.tsx @@ -34,7 +34,7 @@ type notificationsContextType = { } } -export const notificationsContext = createContext({ +export const NotificationsContext = createContext({ notifications: [], notification: { add: () => {}, @@ -153,13 +153,13 @@ const NotificationsProvider = ({ children }: { children: React.ReactNode }) => { } return ( - {children} - + ) } diff --git a/frontend/src/contexts/runContext.tsx b/frontend/src/contexts/runContext.tsx index fb49a0c0..96ccea4a 100644 --- a/frontend/src/contexts/runContext.tsx +++ b/frontend/src/contexts/runContext.tsx @@ -11,7 +11,7 @@ import { run_stop, } from "../api/bot" import { buildContext } from "./buildContext" -import { notificationsContext } from "./notificationsContext" +import { NotificationsContext } from "./notificationsContext" export type runApiType = { id: number @@ -58,7 +58,7 @@ export const RunProvider = ({ children }: { children: React.ReactNode }) => { const [runStatus, setRunStatus] = useState("stopped") const [runs, setRuns] = useState([]) const { setBuildsHandler, builds: context_builds } = useContext(buildContext) - const { notification: n } = useContext(notificationsContext) + const { notification: n } = useContext(NotificationsContext) const setRunsHandler = (runs: runMinifyApiType[]) => { setRuns(runs.map((run) => ({ ...run, type: "run" }))) diff --git a/frontend/src/contexts/undoRedoContext.tsx b/frontend/src/contexts/undoRedoContext.tsx index a8d40e5f..f6c78c21 100644 --- a/frontend/src/contexts/undoRedoContext.tsx +++ b/frontend/src/contexts/undoRedoContext.tsx @@ -1,10 +1,13 @@ +import { addEdge, Edge, Node, OnSelectionChangeParams, useReactFlow } from "@xyflow/react" import { cloneDeep } from "lodash" import { createContext, useCallback, useContext, useEffect, useState } from "react" -import { addEdge, Edge, Node, OnSelectionChangeParams, useReactFlow } from "reactflow" import { v4 } from "uuid" -import { NodeDataType } from "../types/NodeTypes" +import { AppNode } from "../types/NodeTypes" +import { OnSelectionChangeParamsCustom } from "../types/ReactFlowTypes" +import { generateNewNode } from "../utils" import { flowContext } from "./flowContext" -import { notificationsContext } from "./notificationsContext" +import { NotificationsContext } from "./notificationsContext" +import { workspaceContext } from "./workspaceContext" type undoRedoContextType = { undo: () => void @@ -16,6 +19,8 @@ type undoRedoContextType = { position: { x: number; y: number; paneX?: number; paneY?: number } ) => void copiedSelection: OnSelectionChangeParams | null + disableCopyPaste: boolean + setDisableCopyPaste: React.Dispatch> } type UseUndoRedoOptions = { @@ -43,6 +48,8 @@ const initialValue = { copy: () => {}, paste: () => {}, copiedSelection: null, + disableCopyPaste: false, + setDisableCopyPaste: () => {}, } const defaultOptions: UseUndoRedoOptions = { @@ -55,7 +62,8 @@ export const undoRedoContext = createContext(initialValue) export function UndoRedoProvider({ children }: { children: React.ReactNode }) { const { tab, flows } = useContext(flowContext) - const { notification: n } = useContext(notificationsContext) + const { notification: n } = useContext(NotificationsContext) + const { modalsOpened } = useContext(workspaceContext) const [past, setPast] = useState(flows.map(() => [])) const [future, setFuture] = useState(flows.map(() => [])) @@ -165,6 +173,15 @@ export function UndoRedoProvider({ children }: { children: React.ReactNode }) { const { reactFlowInstance } = useContext(flowContext) const [copiedSelection, setCopiedSelection] = useState(null) + const [disableCopyPaste, setDisableCopyPaste] = useState(false) + + useEffect(() => { + if (modalsOpened === 0) { + setDisableCopyPaste(false) + } else if (modalsOpened > 0) { + setDisableCopyPaste(true) + } + }, [modalsOpened]) const copy = (selection: OnSelectionChangeParams) => { if (selection && (selection.nodes.length || selection.edges.length)) { @@ -204,8 +221,9 @@ export function UndoRedoProvider({ children }: { children: React.ReactNode }) { type: "warning", }) } - const nodes: Node[] = reactFlowInstance.getNodes() - let edges: Edge[] = reactFlowInstance.getEdges() + const _selectionInstance = selectionInstance as OnSelectionChangeParamsCustom + const nodes = reactFlowInstance.getNodes() + let edges = reactFlowInstance.getEdges() let minimumX = Infinity let minimumY = Infinity const idsMap: { [id: string]: string } = {} @@ -223,30 +241,29 @@ export function UndoRedoProvider({ children }: { children: React.ReactNode }) { const insidePosition = position.paneX && position.paneY ? { x: position.paneX + position.x, y: position.paneY + position.y } - : reactFlowInstance.project({ x: position.x, y: position.y }) - - const resultNodes: Node[] = [] - - selectionInstance.nodes.forEach((n: Node) => { - // Generate a unique node ID - const newId = v4() - idsMap[n.id] = newId - const newConditions = n.data.conditions?.map((c) => { - const newCondId = v4() - sourceHandlesMap[c.id] = newCondId - return { ...c, id: newCondId } - }) - const newResponse = n.data.response - ? { - ...n.data.response, - id: v4(), - } - : undefined + : reactFlowInstance.screenToFlowPosition({ x: position.x, y: position.y }) + + const resultNodes: AppNode[] = [] + + _selectionInstance.nodes.forEach((n: AppNode) => { + let newConditions + let newResponse + if (n.type === "default_node") { + newConditions = n.data.conditions.map((c) => { + const newCondId = "condition_" + v4() + sourceHandlesMap[c.id] = newCondId + return { ...c, id: newCondId } + }) + newResponse = n.data.response + ? { + ...n.data.response, + id: "response_" + v4(), + } + : undefined + } // Create a new node object - const newNode: Node = { - id: newId, - type: n.type, + const newNode = generateNewNode(n.type, { position: { x: insidePosition.x + n.position.x - minimumX, y: insidePosition.y + n.position.y - minimumY, @@ -256,9 +273,25 @@ export function UndoRedoProvider({ children }: { children: React.ReactNode }) { conditions: newConditions, response: newResponse, flags: [], - id: newId, }, - } + }) + idsMap[n.id] = newNode.id + + // const newNode: AppNode = { + // id: newId, + // type: n.type, + // position: { + // x: insidePosition.x + n.position.x - minimumX, + // y: insidePosition.y + n.position.y - minimumY, + // }, + // data: { + // ...cloneDeep(n.data), + // conditions: newConditions, + // response: newResponse, + // flags: [], + // id: newId, + // }, + // } resultNodes.push({ ...newNode, selected: true }) }) @@ -267,26 +300,22 @@ export function UndoRedoProvider({ children }: { children: React.ReactNode }) { return } - const newNodes = [ - ...nodes.map((e: Node) => ({ ...e, selected: false })), - ...resultNodes, - ] + const newNodes = [...nodes.map((e: AppNode) => ({ ...e, selected: false })), ...resultNodes] - console.log(selectionInstance.edges) selectionInstance.edges.forEach((e) => { const source = idsMap[e.source] const target = idsMap[e.target] if (e.sourceHandle) { const sourceHandle = sourceHandlesMap[e.sourceHandle] - const id = v4() + const id = "reactflow__edge-" + v4() edges = addEdge( { source, target, sourceHandle, targetHandle: null, - id, + id: id, selected: false, }, edges.map((e) => ({ ...e, selected: false })) @@ -298,120 +327,6 @@ export function UndoRedoProvider({ children }: { children: React.ReactNode }) { reactFlowInstance.setEdges(edges) } - // function paste( - // selectionInstance, - // position: { x: number; y: number; paneX?: number; paneY?: number } - // ) { - // 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; - // } - // }); - - // const insidePosition = position.paneX - // ? { x: position.paneX + position.x, y: position.paneY + position.y } - // : reactFlowInstance.project({ x: position.x, y: position.y }); - - // const resultNodes: any[] = [] - - // selectionInstance.nodes.forEach((n: NodeType) => { - // // Generate a unique node ID - // let newId = getNodeId(n.data.type); - // idsMap[n.id] = newId; - - // const positionX = insidePosition.x + n.position.x - minimumX - // const positionY = insidePosition.y + n.position.y - minimumY - - // // 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: { - // ..._.cloneDeep(n.data), - // id: newId, - // }, - // }; - - // // FIXME: CHECK WORK >>>>>>> - // // check for intersections before paste - // if (nodes.some(({ position, id, width, height }) => { - // const xIntersect = ((positionX > position.x - width) && (positionX < (position.x + width))) - // const yIntersect = ((positionY > position.y - height) && (positionY < (position.y + height))) - // const result = xIntersect && yIntersect - // // console.log({id: id, xIntersect: xIntersect, yIntersect: yIntersect, result: result}) - // return result - // })) { - // return setErrorData({ title: "Invalid place! Nodes can't intersect!" }) - // } - // // FIXME: CHECK WORK >>>>>>>> - - // resultNodes.push({ ...newNode, selected: true }) - - // }); - - // if (resultNodes.length < selectionInstance.nodes.length) { - // return - // } - - // // Add the new node to the list of nodes in state - // nodes = nodes - // .map((e) => ({ ...e, selected: false })) - // .concat(resultNodes); - // reactFlowInstance.setNodes(nodes); - - // selectionInstance.edges.forEach((e) => { - // let source = idsMap[e.source]; - // let target = idsMap[e.target]; - // let sourceHandleSplitted = e.sourceHandle.split("|"); - // let sourceHandle = - // source + - // "|" + - // sourceHandleSplitted[1] + - // "|" + - // source - // 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, - // style: { stroke: "inherit" }, - // className: - // targetHandle.split("|")[0] === "Text" - // ? "stroke-foreground " - // : "stroke-foreground ", - // animated: targetHandle.split("|")[0] === "Text", - // selected: false, - // }, - // edges.map((e) => ({ ...e, selected: false })) - // ); - // }); - // reactFlowInstance.setEdges(edges); - // } - return ( {children} diff --git a/frontend/src/contexts/workspaceContext.tsx b/frontend/src/contexts/workspaceContext.tsx index 41381b6e..fd40bffa 100644 --- a/frontend/src/contexts/workspaceContext.tsx +++ b/frontend/src/contexts/workspaceContext.tsx @@ -1,11 +1,10 @@ /* eslint-disable react-refresh/only-export-components */ import { createContext, useCallback, useContext, useEffect, useState } from "react" import { useSearchParams } from "react-router-dom" -import { Node } from "reactflow" import { FlowType } from "../types/FlowTypes" -import { NodeDataType } from "../types/NodeTypes" +import { AppNode } from "../types/NodeTypes" import { flowContext } from "./flowContext" -import { notificationsContext } from "./notificationsContext" +import { NotificationsContext } from "./notificationsContext" type WorkspaceContextType = { workspaceMode: boolean @@ -20,7 +19,7 @@ type WorkspaceContextType = { setSelectedNode: React.Dispatch> handleNodeFlags: ( e: React.MouseEvent, - setNodes: React.Dispatch[]>> + setNodes: React.Dispatch> ) => void mouseOnPane: boolean setMouseOnPane: React.Dispatch> @@ -60,17 +59,13 @@ export const WorkspaceProvider = ({ children }: { children: React.ReactNode }) = const [workspaceMode, setWorkspaceMode] = useState(false) const [nodesLayoutMode, setNodesLayoutMode] = useState(false) const [managerMode, setManagerMode] = useState(false) - const [searchParams, setSearchParams] = useSearchParams() + const [searchParams] = useSearchParams() const [settingsPage, setSettingsPage] = useState(searchParams.get("settings") === "opened") const [selectedNode, setSelectedNode] = useState("") - const { updateFlow, flows, tab, quietSaveFlows, setFlows } = useContext(flowContext) + const { flows, quietSaveFlows, setFlows } = useContext(flowContext) const [mouseOnPane, setMouseOnPane] = useState(true) const [modalsOpened, setModalsOpened] = useState(0) - const { notification: n } = useContext(notificationsContext) - - useEffect(() => { - console.log(modalsOpened) - }, [modalsOpened]) + const { notification: n } = useContext(NotificationsContext) useEffect(() => { if (modalsOpened === 0) { @@ -83,10 +78,6 @@ export const WorkspaceProvider = ({ children }: { children: React.ReactNode }) = } }, [modalsOpened]) - useEffect(() => console.log(mouseOnPane), [mouseOnPane]) - - const flow = flows.find((flow) => flow.name === tab) - const toggleWorkspaceMode = useCallback(() => { setWorkspaceMode(() => !workspaceMode) n.add({ @@ -114,42 +105,44 @@ export const WorkspaceProvider = ({ children }: { children: React.ReactNode }) = }) }, [managerMode, n]) - const handleNodeFlags = useCallback((e: React.MouseEvent) => { - const nodes = flows.flatMap((flow) => flow.data.nodes) - console.log(nodes) - const new_nds = nodes.map((nd: Node) => { - if (nd.data.flags?.includes(e.currentTarget.name)) { - nd.data.flags = nd.data.flags.filter((flag) => flag !== e.currentTarget.name) - } - if (nd.id === selectedNode) { - if (nd.data.flags?.includes(e.currentTarget.name)) { + const handleNodeFlags = useCallback( + (e: React.MouseEvent) => { + const nodes = flows.flatMap((flow) => flow.data.nodes) + const new_nds = nodes.map((nd: AppNode) => { + if (nd.type === "default_node" && nd.data.flags?.includes(e.currentTarget.name)) { nd.data.flags = nd.data.flags.filter((flag) => flag !== e.currentTarget.name) - } else { - if (!nd.data.flags) nd.data.flags = [e.currentTarget.name] - else nd.data.flags = [...nd.data.flags, e.currentTarget.name] } - } - return nd - }) - const new_flows: FlowType[] = flows.map((flow) => { - return { - ...flow, - data: { - ...flow.data, - nodes: flow.data.nodes.map((nd: Node) => { - const new_nd = new_nds.find((n) => n.id === nd.id) - if (new_nd) return new_nd - else return nd - }), - }, - } - }) - setFlows(() => new_flows) - // if (flow) { - // updateFlow(flow) - // } - quietSaveFlows() - }, [flows, quietSaveFlows, selectedNode, setFlows]) + if (nd.type === "default_node" && nd.id === selectedNode) { + if (nd.data.flags?.includes(e.currentTarget.name)) { + nd.data.flags = nd.data.flags.filter((flag) => flag !== e.currentTarget.name) + } else { + if (!nd.data.flags) nd.data.flags = [e.currentTarget.name] + else nd.data.flags = [...nd.data.flags, e.currentTarget.name] + } + } + return nd + }) + const new_flows: FlowType[] = flows.map((flow) => { + return { + ...flow, + data: { + ...flow.data, + nodes: flow.data.nodes.map((nd: AppNode) => { + const new_nd = new_nds.find((n) => n.id === nd.id) + if (new_nd) return new_nd + else return nd + }), + }, + } + }) + setFlows(() => new_flows) + // if (flow) { + // updateFlow(flow) + // } + quietSaveFlows() + }, + [flows, quietSaveFlows, selectedNode, setFlows] + ) const onModalOpen = useCallback((onOpen: () => void) => { setMouseOnPane(false) diff --git a/frontend/src/icons/nodes/conditions/CodeConditionIcon.tsx b/frontend/src/icons/nodes/conditions/CodeConditionIcon.tsx index 933c473a..7518c176 100644 --- a/frontend/src/icons/nodes/conditions/CodeConditionIcon.tsx +++ b/frontend/src/icons/nodes/conditions/CodeConditionIcon.tsx @@ -10,25 +10,25 @@ const CodeConditionIcon = ({className, fill="var(--foreground)"}: React.SVGAttri fill='none' xmlns='http://www.w3.org/2000/svg'> ) diff --git a/frontend/src/icons/nodes/conditions/LLMConditionIcon.tsx b/frontend/src/icons/nodes/conditions/LLMConditionIcon.tsx index 7b981cc6..792646f0 100644 --- a/frontend/src/icons/nodes/conditions/LLMConditionIcon.tsx +++ b/frontend/src/icons/nodes/conditions/LLMConditionIcon.tsx @@ -10,11 +10,11 @@ const LLMConditionIcon = ({className, fill="var(--foreground)"}: React.SVGAttrib fill='none' xmlns='http://www.w3.org/2000/svg'> ) diff --git a/frontend/src/modals/ConditionModal/ConditionModal.tsx b/frontend/src/modals/ConditionModal/ConditionModal.tsx index 649e6204..cd03f148 100644 --- a/frontend/src/modals/ConditionModal/ConditionModal.tsx +++ b/frontend/src/modals/ConditionModal/ConditionModal.tsx @@ -9,23 +9,23 @@ import { Tab, Tabs, } from "@nextui-org/react" +import { Edge, useReactFlow } from "@xyflow/react" import classNames from "classnames" import { HelpCircle, TrashIcon } from "lucide-react" import { useContext, useEffect, useMemo, useState } from "react" import { useParams } from "react-router-dom" -import { useReactFlow } from "reactflow" import { lint_service } from "../../api/services" import ModalComponent from "../../components/ModalComponent" import { flowContext } from "../../contexts/flowContext" -import { notificationsContext } from "../../contexts/notificationsContext" +import { NotificationsContext } from "../../contexts/notificationsContext" import { conditionType, conditionTypeType } from "../../types/ConditionTypes" -import { NodeDataType, NodeType } from "../../types/NodeTypes" +import { AppNode, DefaultNodeDataType, DefaultNodeType } from "../../types/NodeTypes" import { generateNewConditionBase } from "../../utils" import PythonCondition from "./components/PythonCondition" import UsingLLMConditionSection from "./components/UsingLLMCondition" type ConditionModalProps = { - data: NodeDataType + data: DefaultNodeDataType condition?: conditionType is_create?: boolean size?: ModalProps["size"] @@ -61,8 +61,8 @@ const ConditionModal = ({ setSelected(key) } - const { getNode, setNodes, getNodes } = useReactFlow() - const { notification: n } = useContext(notificationsContext) + const { getNode, setNodes, getNodes } = useReactFlow() + const { notification: n } = useContext(NotificationsContext) const { updateFlow, flows, quietSaveFlows } = useContext(flowContext) const { flowId } = useParams() @@ -71,10 +71,11 @@ const ConditionModal = ({ ) const validateConditionName = (is_create: boolean) => { - const nodes = getNodes() as NodeType[] + const nodes = getNodes() as AppNode[] if (!is_create) { - const is_name_valid = !nodes.some((node: NodeType) => - node.data.conditions?.some( + const is_name_valid = !nodes.some((node: AppNode) => + node.type === "default_node" && + node.data.conditions.some( (c) => c.name === currentCondition.name && c.id !== currentCondition.id ) ) @@ -90,7 +91,8 @@ const ConditionModal = ({ } } } else { - const is_name_valid = !nodes.some((node: NodeType) => + const is_name_valid = !nodes.some((node: AppNode) => + node.type === "default_node" && node.data.conditions?.some((c) => c.name === currentCondition.name) ) if (!is_name_valid) { @@ -206,16 +208,12 @@ const ConditionModal = ({ [currentCondition] ) - // useEffect(() => { - // console.log(currentCondition) - // }, [currentCondition]) const lintCondition = async () => { setLintStatus(null) if (currentCondition.type === "python") { try { const res = await lint_service(currentCondition.data.python?.action ?? "") - console.log(res) setLintStatus(res) return res } catch (error) { @@ -231,7 +229,6 @@ const ConditionModal = ({ if (currentCondition.type === "python") { const lint = await lintCondition() const validate_action = validateConditionAction() - console.log(lint) if (lint && validate_action.status) { setTestConditionPending(() => false) return true @@ -261,14 +258,14 @@ const ConditionModal = ({ const currentFlow = flows.find((flow) => flow.name === flowId) const validate_name: ValidateErrorType = validateConditionName(is_create) if (validate_name.status) { - if (node && currentFlow) { - const new_node = { + if (node && node.type === 'default_node' && currentFlow) { + const new_node: DefaultNodeType = { ...node, data: { ...node.data, conditions: is_create ? [...node.data.conditions, currentCondition] - : data.conditions?.map((condition) => + : data.conditions.map((condition) => condition.id === currentCondition.id ? currentCondition : condition ), }, @@ -298,8 +295,8 @@ const ConditionModal = ({ const nodes = getNodes() const node = getNode(data.id) const currentFlow = flows.find((flow) => flow.name === flowId) - if (node && currentFlow) { - const new_node = { + if (node && node.type === 'default_node' && currentFlow) { + const new_node: DefaultNodeType = { ...node, data: { ...node.data, diff --git a/frontend/src/modals/FlowModal/CreateFlowModal.tsx b/frontend/src/modals/FlowModal/CreateFlowModal.tsx index d167bd76..b369c1fd 100644 --- a/frontend/src/modals/FlowModal/CreateFlowModal.tsx +++ b/frontend/src/modals/FlowModal/CreateFlowModal.tsx @@ -14,7 +14,7 @@ import { useContext, useState } from "react" import ModalComponent from "../../components/ModalComponent" import { FLOW_COLORS } from "../../consts" import { flowContext } from "../../contexts/flowContext" -import { notificationsContext } from "../../contexts/notificationsContext" +import { NotificationsContext } from "../../contexts/notificationsContext" import { ModalType } from "../../types/ModalTypes" import { generateNewFlow, validateFlowName } from "../../utils" @@ -29,7 +29,7 @@ export type CreateFlowType = { const CreateFlowModal = ({ isOpen, onClose, size = "3xl" }: CreateFlowModalProps) => { const { flows, setFlows, saveFlows } = useContext(flowContext) - const { notification: n } = useContext(notificationsContext) + const { notification: n } = useContext(NotificationsContext) const [flow, setFlow] = useState({ name: "", description: "", diff --git a/frontend/src/modals/FlowModal/ManageFlowsModal.tsx b/frontend/src/modals/FlowModal/ManageFlowsModal.tsx index 285fc04f..e56b6fd9 100644 --- a/frontend/src/modals/FlowModal/ManageFlowsModal.tsx +++ b/frontend/src/modals/FlowModal/ManageFlowsModal.tsx @@ -10,11 +10,11 @@ import { } from "@nextui-org/react" import { HelpCircle, TrashIcon } from "lucide-react" import { useContext, useEffect, useState } from "react" -import { useParams } from "react-router-dom" +import { useNavigate, useParams } from "react-router-dom" import ModalComponent from "../../components/ModalComponent" import { FLOW_COLORS } from "../../consts" import { flowContext } from "../../contexts/flowContext" -import { notificationsContext } from "../../contexts/notificationsContext" +import { NotificationsContext } from "../../contexts/notificationsContext" import { FlowType } from "../../types/FlowTypes" import { ModalType } from "../../types/ModalTypes" import { validateFlowName } from "../../utils" @@ -30,12 +30,13 @@ export type CreateFlowType = { const ManageFlowsModal = ({ isOpen, onClose, size = "3xl" }: CreateFlowModalProps) => { const { flows, setFlows, saveFlows } = useContext(flowContext) - const { notification: n } = useContext(notificationsContext) + const { notification: n } = useContext(NotificationsContext) const [newFlows, setNewFlows] = useState([...flows] ?? []) const { flowId } = useParams() const [flow, setFlow] = useState( newFlows.find((_flow) => _flow.name === flowId) ?? [][0] ) + const navigate = useNavigate() const [newFlow, setNewFlow] = useState(flow) // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -101,6 +102,22 @@ const ManageFlowsModal = ({ isOpen, onClose, size = "3xl" }: CreateFlowModalProp } } + const onFlowDelete = () => { + if (newFlow.name === "Global") { + return n.add({ + title: "Warning!", + message: "Global flow cannot be deleted.", + type: "warning", + }) + } else { + if (flowId === newFlow.name) { + navigate('/app/home') + } + setFlows([...newFlows.filter((_flow) => _flow.name !== newFlow.name)]) + saveFlows([...newFlows.filter((_flow) => _flow.name !== newFlow.name)]) + } + } + return (
- {node.data.conditions && + {node.type === 'default_node' && node.data.conditions && node.data.conditions.map((condition) => ( <> {condition.data.transition_type !== "manual" && ( diff --git a/frontend/src/types/FlowTypes.ts b/frontend/src/types/FlowTypes.ts index 9760bfd2..92e28910 100644 --- a/frontend/src/types/FlowTypes.ts +++ b/frontend/src/types/FlowTypes.ts @@ -1,4 +1,5 @@ -import { ReactFlowJsonObject } from "reactflow" +import { Edge, ReactFlowJsonObject } from "@xyflow/react" +import { AppNode } from "./NodeTypes" export type FlowType = { id: string @@ -6,5 +7,5 @@ export type FlowType = { description?: string color?: string subflow?: string - data: ReactFlowJsonObject + data: ReactFlowJsonObject } diff --git a/frontend/src/types/NodeTypes.ts b/frontend/src/types/NodeTypes.ts index a8e2dfd5..31c102dd 100644 --- a/frontend/src/types/NodeTypes.ts +++ b/frontend/src/types/NodeTypes.ts @@ -1,19 +1,39 @@ +import { Node } from "@xyflow/react" import { conditionType } from "./ConditionTypes" import { responseType } from "./ResponseTypes" export type NodesTypes = 'default_node' | 'link_node' -export type NodeType = { +export type DefaultNodeType = Node +export type LinkNodeType = Node +export type AppNode = DefaultNodeType | LinkNodeType +export type AllowAppNode = DefaultNodeType & LinkNodeType + + +export type DefaultNodeDataType = { + id: string + name: string + response: responseType + conditions: conditionType[] + global_conditions?: string[] + local_conditions?: string[] + flags: string[] +} + +export type LinkNodeDataType = { id: string - type: string - dragHandle?: string - data: NodeDataType - position: { - x: number - y: number + name: string + transition: { + target_flow: string + target_node: string } } +export type PartialDefaultNodeDataType = Partial +export type PartialLinkNodeDataType = Partial + +export type AppNodeDataType = DefaultNodeDataType | LinkNodeDataType + export type NodeDataType = { id: string name: string @@ -29,7 +49,7 @@ export type NodeDataType = { } export type NodeComponentType = { - data: NodeDataType + data: DefaultNodeDataType } export interface NodeComponentConditionType extends NodeComponentType { diff --git a/frontend/src/types/ReactFlowTypes.ts b/frontend/src/types/ReactFlowTypes.ts new file mode 100644 index 00000000..21fed9ff --- /dev/null +++ b/frontend/src/types/ReactFlowTypes.ts @@ -0,0 +1,8 @@ +import { Edge } from "@xyflow/react"; +import { AppNode } from "./NodeTypes"; + + +export type OnSelectionChangeParamsCustom = { + nodes: AppNode[] + edges: Edge[] +} \ No newline at end of file diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index 0bbf908b..bbafbfec 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -2,25 +2,22 @@ import { v4 } from "uuid" import { CreateFlowType } from "./modals/FlowModal/CreateFlowModal" import { conditionType } from "./types/ConditionTypes" import { FlowType } from "./types/FlowTypes" -import { NodeType } from "./types/NodeTypes" +import { + AppNode, + DefaultNodeDataType, + DefaultNodeType, + LinkNodeDataType, + LinkNodeType, + NodesTypes, +} from "./types/NodeTypes" export const generateNewFlow = (flow: CreateFlowType) => { - const node_id = v4() - const node_2_id = v4() - const condition_id = v4() const newFlow: FlowType = { ...flow, - id: v4(), + id: "flow_" + v4(), data: { nodes: [], - edges: [ - { - id: v4(), - source: node_id, - sourceHandle: condition_id, - target: node_2_id, - }, - ], + edges: [], viewport: { x: 0, y: 0, @@ -50,7 +47,7 @@ export const parseSearchParams = ( export const generateNewConditionBase = (): conditionType => { return { - id: v4(), + id: "condition_" + v4(), name: "new_cnd", type: "python", data: { @@ -60,14 +57,83 @@ export const generateNewConditionBase = (): conditionType => { } } -export const isNodeDeletionValid = (nodes: NodeType[], id: string) => { +export const isNodeDeletionValid = (nodes: AppNode[], id: string) => { const node = nodes.find((n) => n.id === id) if (!node) return false - return !node.data.flags?.includes("start") + if (node.type === "link_node") return true + if (node.type === "default_node") return !node.data.flags?.includes("start") } export function delay(ms: number) { return new Promise((resolve, reject) => { - setTimeout(resolve, ms); - }); + setTimeout(resolve, ms) + }) +} + +export const generateNewNode = ( + type: NodesTypes | undefined, + template?: Partial< + (Omit & { + data: Partial + }) & + (Omit & { data: Partial }) + > +) => { + const id = type + "_" + v4() + switch (type) { + case "default_node": + return { + id, + type, + position: template?.position ?? { x: 0, y: 0 }, + data: { + id, + name: template?.data?.name ?? "New node", + response: template?.data?.response ?? { + id: "response_" + v4(), + name: "response", + type: "text", + data: [{ text: "New node response", priority: 1 }], + }, + flags: template?.data?.flags ?? [], + conditions: template?.data?.conditions ?? [], + global_conditions: template?.data?.global_conditions ?? [], + local_conditions: template?.data?.local_conditions ?? [], + }, + } + case "link_node": + return { + id, + type, + position: template?.position ?? { x: 0, y: 0 }, + data: { + id, + name: template?.data?.name ?? "Link", + transition: template?.data?.transition ?? { + target_flow: template?.data?.transition?.target_flow ?? "", + target_node: template?.data?.transition?.target_flow ?? "", + }, + }, + } + } + return { + id, + type, + position: template?.position ?? { x: 0, y: 0 }, + data: { + id, + name: template?.data?.name ?? "New node", + response: template?.data?.response ?? { + id: "response_" + v4(), + name: "response", + type: "text", + data: [{ text: "New node response", priority: 1 }], + }, + flags: template?.data?.flags ?? [], + conditions: template?.data?.conditions ?? [], + global_conditions: template?.data?.global_conditions ?? [], + local_conditions: template?.data?.local_conditions ?? [], + }, + } } +