import { useEffect, useRef, useState, useCallback, useContext } from 'react' import ReactFlow, { addEdge, Controls, Background, useNodesState, useEdgesState } from 'reactflow' import 'reactflow/dist/style.css' import { useDispatch, useSelector } from 'react-redux' import { useNavigate, useLocation } from 'react-router-dom' import { usePrompt } from '../../utils/usePrompt' import { REMOVE_DIRTY, SET_DIRTY, SET_CHATFLOW, enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' // material-ui import { Toolbar, Box, AppBar, Button } from '@mui/material' import { useTheme } from '@mui/material/styles' // project imports import CanvasNode from './CanvasNode' import ButtonEdge from './ButtonEdge' import CanvasHeader from './CanvasHeader' import AddNodes from './AddNodes' import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' import { ChatPopUp } from 'views/chatmessage/ChatPopUp' import { flowContext } from 'store/context/ReactFlowContext' // API import nodesApi from 'api/nodes' import chatflowsApi from 'api/chatflows' // Hooks import useApi from 'hooks/useApi' import useConfirm from 'hooks/useConfirm' // icons import { IconX } from '@tabler/icons' // utils import { getUniqueNodeId, initNode, getEdgeLabelName, rearrangeToolsOrdering } from 'utils/genericHelper' import useNotifier from 'utils/useNotifier' const nodeTypes = { customNode: CanvasNode } const edgeTypes = { buttonedge: ButtonEdge } // ==============================|| CANVAS ||============================== // const Canvas = () => { const theme = useTheme() const navigate = useNavigate() const { state } = useLocation() const templateFlowData = state ? state.templateFlowData : '' const URLpath = document.location.pathname.toString().split('/') const chatflowId = URLpath[URLpath.length - 1] === 'canvas' ? '' : URLpath[URLpath.length - 1] const { confirm } = useConfirm() const dispatch = useDispatch() const canvas = useSelector((state) => state.canvas) const [canvasDataStore, setCanvasDataStore] = useState(canvas) const [chatflow, setChatflow] = useState(null) const { reactFlowInstance, setReactFlowInstance } = useContext(flowContext) // ==============================|| Snackbar ||============================== // useNotifier() const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) // ==============================|| ReactFlow ||============================== // const [nodes, setNodes, onNodesChange] = useNodesState() const [edges, setEdges, onEdgesChange] = useEdgesState() const [selectedNode, setSelectedNode] = useState(null) const reactFlowWrapper = useRef(null) // ==============================|| Chatflow API ||============================== // const getNodesApi = useApi(nodesApi.getAllNodes) const createNewChatflowApi = useApi(chatflowsApi.createNewChatflow) const testChatflowApi = useApi(chatflowsApi.testChatflow) const updateChatflowApi = useApi(chatflowsApi.updateChatflow) const getSpecificChatflowApi = useApi(chatflowsApi.getSpecificChatflow) // ==============================|| Events & Actions ||============================== // const onConnect = (params) => { const newEdge = { ...params, type: 'buttonedge', id: `${params.source}-${params.sourceHandle}-${params.target}-${params.targetHandle}`, data: { label: getEdgeLabelName(params.sourceHandle) } } const targetNodeId = params.targetHandle.split('-')[0] const sourceNodeId = params.sourceHandle.split('-')[0] const targetInput = params.targetHandle.split('-')[2] setNodes((nds) => nds.map((node) => { if (node.id === targetNodeId) { setTimeout(() => setDirty(), 0) let value const inputAnchor = node.data.inputAnchors.find((ancr) => ancr.name === targetInput) const inputParam = node.data.inputParams.find((param) => param.name === targetInput) if (inputAnchor && inputAnchor.list) { const newValues = node.data.inputs[targetInput] || [] if (targetInput === 'tools') { rearrangeToolsOrdering(newValues, sourceNodeId) } else { newValues.push(`{{${sourceNodeId}.data.instance}}`) } value = newValues } else if (inputParam && inputParam.acceptVariable) { value = node.data.inputs[targetInput] || '' } else { value = `{{${sourceNodeId}.data.instance}}` } node.data = { ...node.data, inputs: { ...node.data.inputs, [targetInput]: value } } } return node }) ) setEdges((eds) => addEdge(newEdge, eds)) } const handleLoadFlow = (file) => { try { const flowData = JSON.parse(file) const nodes = flowData.nodes || [] setNodes(nodes) setEdges(flowData.edges || []) setDirty() } catch (e) { console.error(e) } } const handleDeleteFlow = async () => { const confirmPayload = { title: `Delete`, description: `Delete chatflow ${chatflow.name}?`, confirmButtonName: 'Delete', cancelButtonName: 'Cancel' } const isConfirmed = await confirm(confirmPayload) if (isConfirmed) { try { await chatflowsApi.deleteChatflow(chatflow.id) navigate(-1) } catch (error) { const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ message: errorData, options: { key: new Date().getTime() + Math.random(), variant: 'error', persist: true, action: (key) => ( ) } }) } } } const handleSaveFlow = (chatflowName) => { if (reactFlowInstance) { setNodes((nds) => nds.map((node) => { node.data = { ...node.data, selected: false } return node }) ) const rfInstanceObject = reactFlowInstance.toObject() const flowData = JSON.stringify(rfInstanceObject) if (!chatflow.id) { const newChatflowBody = { name: chatflowName, deployed: false, flowData } createNewChatflowApi.request(newChatflowBody) } else { const updateBody = { name: chatflowName, flowData } updateChatflowApi.request(chatflow.id, updateBody) } } } // eslint-disable-next-line const onNodeClick = useCallback((event, clickedNode) => { setSelectedNode(clickedNode) setNodes((nds) => nds.map((node) => { if (node.id === clickedNode.id) { node.data = { ...node.data, selected: true } } else { node.data = { ...node.data, selected: false } } return node }) ) }) const onDragOver = useCallback((event) => { event.preventDefault() event.dataTransfer.dropEffect = 'move' }, []) const onDrop = useCallback( (event) => { event.preventDefault() const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect() let nodeData = event.dataTransfer.getData('application/reactflow') // check if the dropped element is valid if (typeof nodeData === 'undefined' || !nodeData) { return } nodeData = JSON.parse(nodeData) const position = reactFlowInstance.project({ x: event.clientX - reactFlowBounds.left - 100, y: event.clientY - reactFlowBounds.top - 50 }) const newNodeId = getUniqueNodeId(nodeData, reactFlowInstance.getNodes()) const newNode = { id: newNodeId, position, type: 'customNode', data: initNode(nodeData, newNodeId) } setSelectedNode(newNode) setNodes((nds) => nds.concat(newNode).map((node) => { if (node.id === newNode.id) { node.data = { ...node.data, selected: true } } else { node.data = { ...node.data, selected: false } } return node }) ) setTimeout(() => setDirty(), 0) }, // eslint-disable-next-line [reactFlowInstance] ) const saveChatflowSuccess = () => { dispatch({ type: REMOVE_DIRTY }) enqueueSnackbar({ message: 'Chatflow saved', options: { key: new Date().getTime() + Math.random(), variant: 'success', action: (key) => ( ) } }) } const errorFailed = (message) => { enqueueSnackbar({ message, options: { key: new Date().getTime() + Math.random(), variant: 'error', persist: true, action: (key) => ( ) } }) } const setDirty = () => { dispatch({ type: SET_DIRTY }) } // ==============================|| useEffect ||============================== // // Get specific chatflow successful useEffect(() => { if (getSpecificChatflowApi.data) { const chatflow = getSpecificChatflowApi.data const initialFlow = chatflow.flowData ? JSON.parse(chatflow.flowData) : [] setNodes(initialFlow.nodes || []) setEdges(initialFlow.edges || []) dispatch({ type: SET_CHATFLOW, chatflow }) } else if (getSpecificChatflowApi.error) { const error = getSpecificChatflowApi.error const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` errorFailed(`Failed to retrieve chatflow: ${errorData}`) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [getSpecificChatflowApi.data, getSpecificChatflowApi.error]) // Create new chatflow successful useEffect(() => { if (createNewChatflowApi.data) { const chatflow = createNewChatflowApi.data dispatch({ type: SET_CHATFLOW, chatflow }) saveChatflowSuccess() window.history.replaceState(null, null, `/canvas/${chatflow.id}`) } else if (createNewChatflowApi.error) { const error = createNewChatflowApi.error const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` errorFailed(`Failed to save chatflow: ${errorData}`) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [createNewChatflowApi.data, createNewChatflowApi.error]) // Update chatflow successful useEffect(() => { if (updateChatflowApi.data) { dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data }) saveChatflowSuccess() } else if (updateChatflowApi.error) { const error = updateChatflowApi.error const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` errorFailed(`Failed to save chatflow: ${errorData}`) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [updateChatflowApi.data, updateChatflowApi.error]) // Test chatflow failed useEffect(() => { if (testChatflowApi.error) { enqueueSnackbar({ message: 'Test chatflow failed', options: { key: new Date().getTime() + Math.random(), variant: 'error', persist: true, action: (key) => ( ) } }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [testChatflowApi.error]) useEffect(() => setChatflow(canvasDataStore.chatflow), [canvasDataStore.chatflow]) // Initialization useEffect(() => { if (chatflowId) { getSpecificChatflowApi.request(chatflowId) } else { if (localStorage.getItem('duplicatedFlowData')) { handleLoadFlow(localStorage.getItem('duplicatedFlowData')) setTimeout(() => localStorage.removeItem('duplicatedFlowData'), 0) } else { setNodes([]) setEdges([]) } dispatch({ type: SET_CHATFLOW, chatflow: { name: 'Untitled chatflow' } }) } getNodesApi.request() // Clear dirty state before leaving and remove any ongoing test triggers and webhooks return () => { setTimeout(() => dispatch({ type: REMOVE_DIRTY }), 0) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { setCanvasDataStore(canvas) }, [canvas]) useEffect(() => { function handlePaste(e) { const pasteData = e.clipboardData.getData('text') //TODO: prevent paste event when input focused, temporary fix: catch chatflow syntax if (pasteData.includes('{"nodes":[') && pasteData.includes('],"edges":[')) { handleLoadFlow(pasteData) } } window.addEventListener('paste', handlePaste) return () => { window.removeEventListener('paste', handlePaste) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { if (templateFlowData && templateFlowData.includes('"nodes":[') && templateFlowData.includes('],"edges":[')) { handleLoadFlow(templateFlowData) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [templateFlowData]) usePrompt('You have unsaved changes! Do you want to navigate away?', canvasDataStore.isDirty) return ( <>
) } export default Canvas