rohan13's picture
Flowise Changes
4114d85
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
const errorFailed = (message) => {
enqueueSnackbar({
message,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
// 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 (
<>
<Box>
<AppBar
enableColorOnDark
position='fixed'
color='inherit'
elevation={1}
sx={{
bgcolor: theme.palette.background.default
}}
>
<Toolbar>
<CanvasHeader
chatflow={chatflow}
handleSaveFlow={handleSaveFlow}
handleDeleteFlow={handleDeleteFlow}
handleLoadFlow={handleLoadFlow}
/>
</Toolbar>
</AppBar>
<Box sx={{ pt: '70px', height: '100vh', width: '100%' }}>
<div className='reactflow-parent-wrapper'>
<div className='reactflow-wrapper' ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onNodeClick={onNodeClick}
onEdgesChange={onEdgesChange}
onDrop={onDrop}
onDragOver={onDragOver}
onNodeDragStop={setDirty}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onConnect={onConnect}
onInit={setReactFlowInstance}
fitView
minZoom={0.1}
>
<Controls
style={{
display: 'flex',
flexDirection: 'row',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
/>
<Background color='#aaa' gap={16} />
<AddNodes nodesData={getNodesApi.data} node={selectedNode} />
<ChatPopUp chatflowid={chatflowId} />
</ReactFlow>
</div>
</div>
</Box>
<ConfirmDialog />
</Box>
</>
)
}
export default Canvas