mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 19:00:59 +03:00
Initial push
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Box,
|
||||
ClickAwayListener,
|
||||
Divider,
|
||||
InputAdornment,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
OutlinedInput,
|
||||
Paper,
|
||||
Popper,
|
||||
Stack,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
|
||||
// third-party
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard'
|
||||
import Transitions from 'ui-component/extended/Transitions'
|
||||
import { StyledFab } from 'ui-component/button/StyledFab'
|
||||
|
||||
// icons
|
||||
import { IconPlus, IconSearch, IconMinus } from '@tabler/icons'
|
||||
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
// ==============================|| ADD NODES||============================== //
|
||||
|
||||
const AddNodes = ({ nodesData, node }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [nodes, setNodes] = useState({})
|
||||
const [open, setOpen] = useState(false)
|
||||
const [categoryExpanded, setCategoryExpanded] = useState({})
|
||||
|
||||
const anchorRef = useRef(null)
|
||||
const prevOpen = useRef(open)
|
||||
const ps = useRef()
|
||||
|
||||
const scrollTop = () => {
|
||||
const curr = ps.current
|
||||
if (curr) {
|
||||
curr.scrollTop = 0
|
||||
}
|
||||
}
|
||||
|
||||
const filterSearch = (value) => {
|
||||
setSearchValue(value)
|
||||
setTimeout(() => {
|
||||
if (value) {
|
||||
const returnData = nodesData.filter((nd) => nd.name.toLowerCase().includes(value.toLowerCase()))
|
||||
groupByCategory(returnData, true)
|
||||
scrollTop()
|
||||
} else if (value === '') {
|
||||
groupByCategory(nodesData)
|
||||
scrollTop()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const groupByCategory = (nodes, isFilter) => {
|
||||
const accordianCategories = {}
|
||||
const result = nodes.reduce(function (r, a) {
|
||||
r[a.category] = r[a.category] || []
|
||||
r[a.category].push(a)
|
||||
accordianCategories[a.category] = isFilter ? true : false
|
||||
return r
|
||||
}, Object.create(null))
|
||||
setNodes(result)
|
||||
setCategoryExpanded(accordianCategories)
|
||||
}
|
||||
|
||||
const handleAccordionChange = (category) => (event, isExpanded) => {
|
||||
const accordianCategories = { ...categoryExpanded }
|
||||
accordianCategories[category] = isExpanded
|
||||
setCategoryExpanded(accordianCategories)
|
||||
}
|
||||
|
||||
const handleClose = (event) => {
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||
return
|
||||
}
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen((prevOpen) => !prevOpen)
|
||||
}
|
||||
|
||||
const onDragStart = (event, node) => {
|
||||
event.dataTransfer.setData('application/reactflow', JSON.stringify(node))
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
anchorRef.current.focus()
|
||||
}
|
||||
|
||||
prevOpen.current = open
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
if (node) setOpen(false)
|
||||
}, [node])
|
||||
|
||||
useEffect(() => {
|
||||
if (nodesData) groupByCategory(nodesData)
|
||||
}, [nodesData])
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledFab
|
||||
sx={{ left: 20, top: 20 }}
|
||||
ref={anchorRef}
|
||||
size='small'
|
||||
color='primary'
|
||||
aria-label='add'
|
||||
title='Add Node'
|
||||
onClick={handleToggle}
|
||||
>
|
||||
{open ? <IconMinus /> : <IconPlus />}
|
||||
</StyledFab>
|
||||
<Popper
|
||||
placement='bottom-end'
|
||||
open={open}
|
||||
anchorEl={anchorRef.current}
|
||||
role={undefined}
|
||||
transition
|
||||
disablePortal
|
||||
popperOptions={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [-40, 14]
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
sx={{ zIndex: 1000 }}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Transitions in={open} {...TransitionProps}>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Stack>
|
||||
<Typography variant='h4'>Add Nodes</Typography>
|
||||
</Stack>
|
||||
<OutlinedInput
|
||||
sx={{ width: '100%', pr: 1, pl: 2, my: 2 }}
|
||||
id='input-search-node'
|
||||
value={searchValue}
|
||||
onChange={(e) => filterSearch(e.target.value)}
|
||||
placeholder='Search nodes'
|
||||
startAdornment={
|
||||
<InputAdornment position='start'>
|
||||
<IconSearch stroke={1.5} size='1rem' color={theme.palette.grey[500]} />
|
||||
</InputAdornment>
|
||||
}
|
||||
aria-describedby='search-helper-text'
|
||||
inputProps={{
|
||||
'aria-label': 'weight'
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
</Box>
|
||||
<PerfectScrollbar
|
||||
containerRef={(el) => {
|
||||
ps.current = el
|
||||
}}
|
||||
style={{ height: '100%', maxHeight: 'calc(100vh - 320px)', overflowX: 'hidden' }}
|
||||
>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<List
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: 370,
|
||||
py: 0,
|
||||
borderRadius: '10px',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
maxWidth: 370
|
||||
},
|
||||
'& .MuiListItemSecondaryAction-root': {
|
||||
top: 22
|
||||
},
|
||||
'& .MuiDivider-root': {
|
||||
my: 0
|
||||
},
|
||||
'& .list-container': {
|
||||
pl: 7
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Object.keys(nodes)
|
||||
.sort()
|
||||
.map((category) => (
|
||||
<Accordion
|
||||
expanded={categoryExpanded[category] || false}
|
||||
onChange={handleAccordionChange(category)}
|
||||
key={category}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`nodes-accordian-${category}`}
|
||||
id={`nodes-accordian-header-${category}`}
|
||||
>
|
||||
<Typography variant='h5'>{category}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{nodes[category].map((node, index) => (
|
||||
<div
|
||||
key={node.name}
|
||||
onDragStart={(event) => onDragStart(event, node)}
|
||||
draggable
|
||||
>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
p: 0,
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
cursor: 'move'
|
||||
}}
|
||||
>
|
||||
<ListItem alignItems='center'>
|
||||
<ListItemAvatar>
|
||||
<div
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 10,
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
alt={node.name}
|
||||
src={`${baseURL}/api/v1/node-icon/${node.name}`}
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
sx={{ ml: 1 }}
|
||||
primary={node.label}
|
||||
secondary={node.description}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
{index === nodes[category].length - 1 ? null : <Divider />}
|
||||
</div>
|
||||
))}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</PerfectScrollbar>
|
||||
</MainCard>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Transitions>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
AddNodes.propTypes = {
|
||||
nodesData: PropTypes.array,
|
||||
node: PropTypes.object
|
||||
}
|
||||
|
||||
export default AddNodes
|
||||
@@ -0,0 +1,72 @@
|
||||
import { getBezierPath, EdgeText } from 'reactflow'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { REMOVE_EDGE } from 'store/actions'
|
||||
|
||||
import './index.css'
|
||||
|
||||
const foreignObjectSize = 40
|
||||
|
||||
const ButtonEdge = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, data, markerEnd }) => {
|
||||
const [edgePath, edgeCenterX, edgeCenterY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition
|
||||
})
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const onEdgeClick = (evt, id) => {
|
||||
evt.stopPropagation()
|
||||
dispatch({ type: REMOVE_EDGE, edgeId: `${id}:${Date.now()}` })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<path id={id} style={style} className='react-flow__edge-path' d={edgePath} markerEnd={markerEnd} />
|
||||
{data && data.label && (
|
||||
<EdgeText
|
||||
x={sourceX + 10}
|
||||
y={sourceY + 10}
|
||||
label={data.label}
|
||||
labelStyle={{ fill: 'black' }}
|
||||
labelBgStyle={{ fill: 'transparent' }}
|
||||
labelBgPadding={[2, 4]}
|
||||
labelBgBorderRadius={2}
|
||||
/>
|
||||
)}
|
||||
<foreignObject
|
||||
width={foreignObjectSize}
|
||||
height={foreignObjectSize}
|
||||
x={edgeCenterX - foreignObjectSize / 2}
|
||||
y={edgeCenterY - foreignObjectSize / 2}
|
||||
className='edgebutton-foreignobject'
|
||||
requiredExtensions='http://www.w3.org/1999/xhtml'
|
||||
>
|
||||
<div>
|
||||
<button className='edgebutton' onClick={(event) => onEdgeClick(event, id)}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ButtonEdge.propTypes = {
|
||||
id: PropTypes.string,
|
||||
sourceX: PropTypes.number,
|
||||
sourceY: PropTypes.number,
|
||||
targetX: PropTypes.number,
|
||||
targetY: PropTypes.number,
|
||||
sourcePosition: PropTypes.any,
|
||||
targetPosition: PropTypes.any,
|
||||
style: PropTypes.object,
|
||||
data: PropTypes.object,
|
||||
markerEnd: PropTypes.any
|
||||
}
|
||||
|
||||
export default ButtonEdge
|
||||
@@ -0,0 +1,291 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Avatar, Box, ButtonBase, Typography, Stack, TextField } from '@mui/material'
|
||||
|
||||
// icons
|
||||
import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck, IconX } from '@tabler/icons'
|
||||
|
||||
// project imports
|
||||
import Settings from 'views/settings'
|
||||
import SaveChatflowDialog from 'ui-component/dialog/SaveChatflowDialog'
|
||||
|
||||
// API
|
||||
import chatflowsApi from 'api/chatflows'
|
||||
|
||||
// Hooks
|
||||
import useApi from 'hooks/useApi'
|
||||
|
||||
// utils
|
||||
import { generateExportFlowData } from 'utils/genericHelper'
|
||||
|
||||
// ==============================|| CANVAS HEADER ||============================== //
|
||||
|
||||
const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFlow }) => {
|
||||
const theme = useTheme()
|
||||
const navigate = useNavigate()
|
||||
const flowNameRef = useRef()
|
||||
const settingsRef = useRef()
|
||||
|
||||
const [isEditingFlowName, setEditingFlowName] = useState(null)
|
||||
const [flowName, setFlowName] = useState('')
|
||||
const [isSettingsOpen, setSettingsOpen] = useState(false)
|
||||
const [flowDialogOpen, setFlowDialogOpen] = useState(false)
|
||||
|
||||
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
|
||||
const canvas = useSelector((state) => state.canvas)
|
||||
|
||||
const onSettingsItemClick = (setting) => {
|
||||
setSettingsOpen(false)
|
||||
|
||||
if (setting === 'deleteChatflow') {
|
||||
handleDeleteFlow()
|
||||
} else if (setting === 'exportChatflow') {
|
||||
try {
|
||||
const flowData = JSON.parse(chatflow.flowData)
|
||||
let dataStr = JSON.stringify(generateExportFlowData(flowData))
|
||||
let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
|
||||
|
||||
let exportFileDefaultName = `${chatflow.name} Chatflow.json`
|
||||
|
||||
let linkElement = document.createElement('a')
|
||||
linkElement.setAttribute('href', dataUri)
|
||||
linkElement.setAttribute('download', exportFileDefaultName)
|
||||
linkElement.click()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onUploadFile = (file) => {
|
||||
setSettingsOpen(false)
|
||||
handleLoadFlow(file)
|
||||
}
|
||||
|
||||
const submitFlowName = () => {
|
||||
if (chatflow.id) {
|
||||
const updateBody = {
|
||||
name: flowNameRef.current.value
|
||||
}
|
||||
updateChatflowApi.request(chatflow.id, updateBody)
|
||||
}
|
||||
}
|
||||
|
||||
const onSaveChatflowClick = () => {
|
||||
if (chatflow.id) handleSaveFlow(chatflow.name)
|
||||
else setFlowDialogOpen(true)
|
||||
}
|
||||
|
||||
const onConfirmSaveName = (flowName) => {
|
||||
setFlowDialogOpen(false)
|
||||
handleSaveFlow(flowName)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateChatflowApi.data) {
|
||||
setFlowName(updateChatflowApi.data.name)
|
||||
}
|
||||
setEditingFlowName(false)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updateChatflowApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (chatflow) {
|
||||
setFlowName(chatflow.name)
|
||||
}
|
||||
}, [chatflow])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<ButtonBase title='Back' sx={{ borderRadius: '50%' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.secondary.light,
|
||||
color: theme.palette.secondary.dark,
|
||||
'&:hover': {
|
||||
background: theme.palette.secondary.dark,
|
||||
color: theme.palette.secondary.light
|
||||
}
|
||||
}}
|
||||
color='inherit'
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<IconChevronLeft stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
{!isEditingFlowName && (
|
||||
<Stack flexDirection='row'>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 600,
|
||||
ml: 2
|
||||
}}
|
||||
>
|
||||
{canvas.isDirty && <strong style={{ color: theme.palette.orange.main }}>*</strong>} {flowName}
|
||||
</Typography>
|
||||
{chatflow?.id && (
|
||||
<ButtonBase title='Edit Name' sx={{ borderRadius: '50%' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
ml: 1,
|
||||
background: theme.palette.secondary.light,
|
||||
color: theme.palette.secondary.dark,
|
||||
'&:hover': {
|
||||
background: theme.palette.secondary.dark,
|
||||
color: theme.palette.secondary.light
|
||||
}
|
||||
}}
|
||||
color='inherit'
|
||||
onClick={() => setEditingFlowName(true)}
|
||||
>
|
||||
<IconPencil stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{isEditingFlowName && (
|
||||
<Stack flexDirection='row'>
|
||||
<TextField
|
||||
size='small'
|
||||
inputRef={flowNameRef}
|
||||
sx={{
|
||||
width: '50%',
|
||||
ml: 2
|
||||
}}
|
||||
defaultValue={flowName}
|
||||
/>
|
||||
<ButtonBase title='Save Name' sx={{ borderRadius: '50%' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.success.light,
|
||||
color: theme.palette.success.dark,
|
||||
ml: 1,
|
||||
'&:hover': {
|
||||
background: theme.palette.success.dark,
|
||||
color: theme.palette.success.light
|
||||
}
|
||||
}}
|
||||
color='inherit'
|
||||
onClick={submitFlowName}
|
||||
>
|
||||
<IconCheck stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
<ButtonBase title='Cancel' sx={{ borderRadius: '50%' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.error.light,
|
||||
color: theme.palette.error.dark,
|
||||
ml: 1,
|
||||
'&:hover': {
|
||||
background: theme.palette.error.dark,
|
||||
color: theme.palette.error.light
|
||||
}
|
||||
}}
|
||||
color='inherit'
|
||||
onClick={() => setEditingFlowName(false)}
|
||||
>
|
||||
<IconX stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<ButtonBase title='Save Chatflow' sx={{ borderRadius: '50%', mr: 2 }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.canvasHeader.saveLight,
|
||||
color: theme.palette.canvasHeader.saveDark,
|
||||
'&:hover': {
|
||||
background: theme.palette.canvasHeader.saveDark,
|
||||
color: theme.palette.canvasHeader.saveLight
|
||||
}
|
||||
}}
|
||||
color='inherit'
|
||||
onClick={onSaveChatflowClick}
|
||||
>
|
||||
<IconDeviceFloppy stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
<ButtonBase ref={settingsRef} title='Settings' sx={{ borderRadius: '50%' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.canvasHeader.settingsLight,
|
||||
color: theme.palette.canvasHeader.settingsDark,
|
||||
'&:hover': {
|
||||
background: theme.palette.canvasHeader.settingsDark,
|
||||
color: theme.palette.canvasHeader.settingsLight
|
||||
}
|
||||
}}
|
||||
onClick={() => setSettingsOpen(!isSettingsOpen)}
|
||||
>
|
||||
<IconSettings stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
<Settings
|
||||
chatflow={chatflow}
|
||||
isSettingsOpen={isSettingsOpen}
|
||||
anchorEl={settingsRef.current}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
onSettingsItemClick={onSettingsItemClick}
|
||||
onUploadFile={onUploadFile}
|
||||
/>
|
||||
<SaveChatflowDialog
|
||||
show={flowDialogOpen}
|
||||
dialogProps={{
|
||||
title: `Save New Chatflow`,
|
||||
confirmButtonName: 'Save',
|
||||
cancelButtonName: 'Cancel'
|
||||
}}
|
||||
onCancel={() => setFlowDialogOpen(false)}
|
||||
onConfirm={onConfirmSaveName}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
CanvasHeader.propTypes = {
|
||||
chatflow: PropTypes.object,
|
||||
handleSaveFlow: PropTypes.func,
|
||||
handleDeleteFlow: PropTypes.func,
|
||||
handleLoadFlow: PropTypes.func
|
||||
}
|
||||
|
||||
export default CanvasHeader
|
||||
@@ -0,0 +1,136 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useContext } from 'react'
|
||||
|
||||
// material-ui
|
||||
import { styled, useTheme } from '@mui/material/styles'
|
||||
import { IconButton, Box, Typography, Divider } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard'
|
||||
import NodeInputHandler from './NodeInputHandler'
|
||||
import NodeOutputHandler from './NodeOutputHandler'
|
||||
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
import { IconTrash } from '@tabler/icons'
|
||||
import { flowContext } from 'store/context/ReactFlowContext'
|
||||
|
||||
const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
background: theme.palette.card.main,
|
||||
color: theme.darkTextPrimary,
|
||||
border: 'solid 1px',
|
||||
borderColor: theme.palette.primary[200] + 75,
|
||||
width: '300px',
|
||||
height: 'auto',
|
||||
padding: '10px',
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.primary.main
|
||||
}
|
||||
}))
|
||||
|
||||
// ===========================|| CANVAS NODE ||=========================== //
|
||||
|
||||
const CanvasNode = ({ data }) => {
|
||||
const theme = useTheme()
|
||||
const { deleteNode } = useContext(flowContext)
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardWrapper
|
||||
content={false}
|
||||
sx={{
|
||||
padding: 0,
|
||||
borderColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary
|
||||
}}
|
||||
border={false}
|
||||
>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Box item style={{ width: 50, marginRight: 10, padding: 5 }}>
|
||||
<div
|
||||
style={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.largeAvatar,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'grab'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }}
|
||||
src={`${baseURL}/api/v1/node-icon/${data.name}`}
|
||||
alt='Notification'
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '1rem',
|
||||
fontWeight: 500
|
||||
}}
|
||||
>
|
||||
{data.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
deleteNode(data.id)
|
||||
}}
|
||||
sx={{ height: 35, width: 35, mr: 1 }}
|
||||
>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</div>
|
||||
{(data.inputAnchors.length > 0 || data.inputParams.length > 0) && (
|
||||
<>
|
||||
<Divider />
|
||||
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
Inputs
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
{data.inputAnchors.map((inputAnchor, index) => (
|
||||
<NodeInputHandler key={index} inputAnchor={inputAnchor} data={data} />
|
||||
))}
|
||||
{data.inputParams.map((inputParam, index) => (
|
||||
<NodeInputHandler key={index} inputParam={inputParam} data={data} />
|
||||
))}
|
||||
|
||||
<Divider />
|
||||
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
Output
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider />
|
||||
|
||||
{data.outputAnchors.map((outputAnchor, index) => (
|
||||
<NodeOutputHandler key={index} outputAnchor={outputAnchor} data={data} />
|
||||
))}
|
||||
</Box>
|
||||
</CardWrapper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
CanvasNode.propTypes = {
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
export default CanvasNode
|
||||
@@ -0,0 +1,104 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { Handle, Position, useUpdateNodeInternals } from 'reactflow'
|
||||
import { useEffect, useRef, useState, useContext } from 'react'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Box, Typography, Tooltip } from '@mui/material'
|
||||
|
||||
import { Dropdown } from 'ui-component/dropdown/Dropdown'
|
||||
import { Input } from 'ui-component/input/Input'
|
||||
import { flowContext } from 'store/context/ReactFlowContext'
|
||||
import { isValidConnection } from 'utils/genericHelper'
|
||||
|
||||
// ===========================|| NodeInputHandler ||=========================== //
|
||||
|
||||
const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
|
||||
const theme = useTheme()
|
||||
const ref = useRef(null)
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
const [position, setPosition] = useState(0)
|
||||
const { reactFlowInstance } = useContext(flowContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
||||
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2)
|
||||
updateNodeInternals(data.id)
|
||||
}
|
||||
}, [data.id, ref, updateNodeInternals])
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeInternals(data.id)
|
||||
}, [data.id, position, updateNodeInternals])
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{inputAnchor && (
|
||||
<>
|
||||
<Tooltip
|
||||
placement='left'
|
||||
title={
|
||||
<Typography sx={{ color: 'white', p: 1 }} variant='h5'>
|
||||
{'Type: ' + inputAnchor.type}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Handle
|
||||
type='target'
|
||||
position={Position.Left}
|
||||
key={inputAnchor.id}
|
||||
id={inputAnchor.id}
|
||||
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
|
||||
style={{
|
||||
height: 10,
|
||||
width: 10,
|
||||
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
top: position
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography>
|
||||
{inputAnchor.label}
|
||||
{!inputAnchor.optional && <span style={{ color: 'red' }}> *</span>}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{inputParam && (
|
||||
<>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography>
|
||||
{inputParam.label}
|
||||
{!inputParam.optional && <span style={{ color: 'red' }}> *</span>}
|
||||
</Typography>
|
||||
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
|
||||
<Input
|
||||
inputParam={inputParam}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
|
||||
/>
|
||||
)}
|
||||
{inputParam.type === 'options' && (
|
||||
<Dropdown
|
||||
name={inputParam.name}
|
||||
options={inputParam.options}
|
||||
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'chose an option'}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
NodeInputHandler.propTypes = {
|
||||
inputAnchor: PropTypes.object,
|
||||
inputParam: PropTypes.object,
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
export default NodeInputHandler
|
||||
@@ -0,0 +1,71 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { Handle, Position, useUpdateNodeInternals } from 'reactflow'
|
||||
import { useEffect, useRef, useState, useContext } from 'react'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Box, Typography, Tooltip } from '@mui/material'
|
||||
import { flowContext } from 'store/context/ReactFlowContext'
|
||||
import { isValidConnection } from 'utils/genericHelper'
|
||||
|
||||
// ===========================|| NodeOutputHandler ||=========================== //
|
||||
|
||||
const NodeOutputHandler = ({ outputAnchor, data }) => {
|
||||
const theme = useTheme()
|
||||
const ref = useRef(null)
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
const [position, setPosition] = useState(0)
|
||||
const { reactFlowInstance } = useContext(flowContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current?.offsetTop && ref.current?.clientHeight) {
|
||||
setTimeout(() => {
|
||||
setPosition(ref.current?.offsetTop + ref.current?.clientHeight / 2)
|
||||
updateNodeInternals(data.id)
|
||||
}, 0)
|
||||
}
|
||||
}, [data.id, ref, updateNodeInternals])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
updateNodeInternals(data.id)
|
||||
}, 0)
|
||||
}, [data.id, position, updateNodeInternals])
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Tooltip
|
||||
placement='right'
|
||||
title={
|
||||
<Typography sx={{ color: 'white', p: 1 }} variant='h5'>
|
||||
{'Type: ' + outputAnchor.type}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Handle
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
key={outputAnchor.id}
|
||||
id={outputAnchor.id}
|
||||
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
|
||||
style={{
|
||||
height: 10,
|
||||
width: 10,
|
||||
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
top: position
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Box sx={{ p: 2, textAlign: 'end' }}>
|
||||
<Typography>{outputAnchor.label}</Typography>
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
NodeOutputHandler.propTypes = {
|
||||
outputAnchor: PropTypes.object,
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
export default NodeOutputHandler
|
||||
@@ -0,0 +1,37 @@
|
||||
.edgebutton {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #eee;
|
||||
border: 1px solid #fff;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.edgebutton:hover {
|
||||
background: #5e35b1;
|
||||
color: #eee;
|
||||
box-shadow: 0 0 6px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.edgebutton-foreignobject div {
|
||||
background: transparent;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.reactflow-parent-wrapper {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.reactflow-parent-wrapper .reactflow-wrapper {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
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 } 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 { ChatMessage } from 'views/chatmessage/ChatMessage'
|
||||
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 } 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 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)
|
||||
if (inputAnchor && inputAnchor.list) {
|
||||
const newValues = node.data.inputs[targetInput] || []
|
||||
newValues.push(`{{${sourceNodeId}.data.instance}}`)
|
||||
value = newValues
|
||||
} else {
|
||||
value = `{{${sourceNodeId}.data.instance}}`
|
||||
}
|
||||
node.data = {
|
||||
...node.data,
|
||||
inputs: {
|
||||
...node.data.inputs,
|
||||
[targetInput]: value
|
||||
}
|
||||
}
|
||||
}
|
||||
return node
|
||||
})
|
||||
)
|
||||
|
||||
setEdges((eds) => addEdge(newEdge, eds))
|
||||
setDirty()
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
// Listen to edge button click remove redux event
|
||||
useEffect(() => {
|
||||
if (reactFlowInstance) {
|
||||
const edges = reactFlowInstance.getEdges()
|
||||
const toRemoveEdgeId = canvasDataStore.removeEdgeId.split(':')[0]
|
||||
setEdges(edges.filter((edge) => edge.id !== toRemoveEdgeId))
|
||||
setDirty()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [canvasDataStore.removeEdgeId])
|
||||
|
||||
useEffect(() => setChatflow(canvasDataStore.chatflow), [canvasDataStore.chatflow])
|
||||
|
||||
// Initialization
|
||||
useEffect(() => {
|
||||
if (chatflowId) {
|
||||
getSpecificChatflowApi.request(chatflowId)
|
||||
} 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
|
||||
}, [])
|
||||
|
||||
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
|
||||
>
|
||||
<Controls
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
/>
|
||||
<Background color='#aaa' gap={16} />
|
||||
<AddNodes nodesData={getNodesApi.data} node={selectedNode} />
|
||||
<ChatMessage chatflowid={chatflowId} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
<ConfirmDialog />
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Canvas
|
||||
@@ -0,0 +1,113 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import { Grid, Box, Stack } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard'
|
||||
import ItemCard from 'ui-component/cards/ItemCard'
|
||||
import { gridSpacing } from 'store/constant'
|
||||
import WorkflowEmptySVG from 'assets/images/workflow_empty.svg'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
|
||||
// API
|
||||
import chatflowsApi from 'api/chatflows'
|
||||
|
||||
// Hooks
|
||||
import useApi from 'hooks/useApi'
|
||||
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
// ==============================|| CHATFLOWS ||============================== //
|
||||
|
||||
const Chatflows = () => {
|
||||
const navigate = useNavigate()
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [images, setImages] = useState({})
|
||||
|
||||
const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows)
|
||||
|
||||
const addNew = () => {
|
||||
navigate('/canvas')
|
||||
}
|
||||
|
||||
const goToCanvas = (selectedChatflow) => {
|
||||
navigate(`/canvas/${selectedChatflow.id}`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAllChatflowsApi.request()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(getAllChatflowsApi.loading)
|
||||
}, [getAllChatflowsApi.loading])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllChatflowsApi.data) {
|
||||
try {
|
||||
const chatflows = getAllChatflowsApi.data
|
||||
const images = {}
|
||||
for (let i = 0; i < chatflows.length; i += 1) {
|
||||
const flowDataStr = chatflows[i].flowData
|
||||
const flowData = JSON.parse(flowDataStr)
|
||||
const nodes = flowData.nodes || []
|
||||
images[chatflows[i].id] = []
|
||||
for (let j = 0; j < nodes.length; j += 1) {
|
||||
const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}`
|
||||
if (!images[chatflows[i].id].includes(imageSrc)) {
|
||||
images[chatflows[i].id].push(imageSrc)
|
||||
}
|
||||
}
|
||||
}
|
||||
setImages(images)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}, [getAllChatflowsApi.data])
|
||||
|
||||
return (
|
||||
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
||||
<Stack flexDirection='row'>
|
||||
<h1>Chatflows</h1>
|
||||
<Grid sx={{ mb: 1.25 }} container direction='row'>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Grid item>
|
||||
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew}>
|
||||
Add New
|
||||
</StyledButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
<Grid container spacing={gridSpacing}>
|
||||
{!isLoading &&
|
||||
getAllChatflowsApi.data &&
|
||||
getAllChatflowsApi.data.map((data, index) => (
|
||||
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
|
||||
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
{!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
||||
<Box sx={{ p: 2, height: 'auto' }}>
|
||||
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={WorkflowEmptySVG} alt='WorkflowEmptySVG' />
|
||||
</Box>
|
||||
<div>No Chatflows Yet</div>
|
||||
</Stack>
|
||||
)}
|
||||
</MainCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default Chatflows
|
||||
@@ -0,0 +1,127 @@
|
||||
.cloudform {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.messagelist {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.messagelistloading {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.usermessage {
|
||||
padding: 1rem 1.5rem 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.usermessagewaiting-light {
|
||||
padding: 1rem 1.5rem 1rem 1.5rem;
|
||||
background: linear-gradient(to left, #ede7f6, #e3f2fd, #ede7f6);
|
||||
background-size: 200% 200%;
|
||||
background-position: -100% 0;
|
||||
animation: loading-gradient 2s ease-in-out infinite;
|
||||
animation-direction: alternate;
|
||||
animation-name: loading-gradient;
|
||||
}
|
||||
|
||||
.usermessagewaiting-dark {
|
||||
padding: 1rem 1.5rem 1rem 1.5rem;
|
||||
color: #ececf1;
|
||||
background: linear-gradient(to left, #2e2352, #1d3d60, #2e2352);
|
||||
background-size: 200% 200%;
|
||||
background-position: -100% 0;
|
||||
animation: loading-gradient 2s ease-in-out infinite;
|
||||
animation-direction: alternate;
|
||||
animation-name: loading-gradient;
|
||||
}
|
||||
|
||||
@keyframes loading-gradient {
|
||||
0% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.apimessage {
|
||||
padding: 1rem 1.5rem 1rem 1.5rem;
|
||||
animation: fadein 0.5s;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.apimessage,
|
||||
.usermessage,
|
||||
.usermessagewaiting {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.markdownanswer {
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.markdownanswer a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.markdownanswer a {
|
||||
color: #16bed7;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.markdownanswer code {
|
||||
color: #15cb19;
|
||||
font-weight: 500;
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
.markdownanswer ol,
|
||||
.markdownanswer ul {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.boticon,
|
||||
.usericon {
|
||||
margin-right: 1rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.markdownanswer h1,
|
||||
.markdownanswer h2,
|
||||
.markdownanswer h3 {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.cloud {
|
||||
width: '100%';
|
||||
max-width: 500px;
|
||||
height: 73vh;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import PropTypes from 'prop-types'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||
|
||||
import {
|
||||
ClickAwayListener,
|
||||
Paper,
|
||||
Popper,
|
||||
CircularProgress,
|
||||
OutlinedInput,
|
||||
Divider,
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
Box,
|
||||
Button
|
||||
} from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { IconMessage, IconX, IconSend, IconEraser } from '@tabler/icons'
|
||||
|
||||
// project import
|
||||
import { StyledFab } from 'ui-component/button/StyledFab'
|
||||
import MainCard from 'ui-component/cards/MainCard'
|
||||
import Transitions from 'ui-component/extended/Transitions'
|
||||
import './ChatMessage.css'
|
||||
|
||||
// api
|
||||
import chatmessageApi from 'api/chatmessage'
|
||||
import predictionApi from 'api/prediction'
|
||||
|
||||
// Hooks
|
||||
import useApi from 'hooks/useApi'
|
||||
import useConfirm from 'hooks/useConfirm'
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
|
||||
export const ChatMessage = ({ chatflowid }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const { confirm } = useConfirm()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useNotifier()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [userInput, setUserInput] = useState('')
|
||||
const [history, setHistory] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [messages, setMessages] = useState([
|
||||
{
|
||||
message: 'Hi there! How can I help?',
|
||||
type: 'apiMessage'
|
||||
}
|
||||
])
|
||||
|
||||
const messagesEndRef = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
const anchorRef = useRef(null)
|
||||
const prevOpen = useRef(open)
|
||||
const getChatmessageApi = useApi(chatmessageApi.getChatmessageFromChatflow)
|
||||
|
||||
const handleClose = (event) => {
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||
return
|
||||
}
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen((prevOpen) => !prevOpen)
|
||||
}
|
||||
|
||||
const clearChat = async () => {
|
||||
const confirmPayload = {
|
||||
title: `Clear Chat History`,
|
||||
description: `Are you sure you want to clear all chat history?`,
|
||||
confirmButtonName: 'Clear',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
const isConfirmed = await confirm(confirmPayload)
|
||||
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
await chatmessageApi.deleteChatmessage(chatflowid)
|
||||
enqueueSnackbar({
|
||||
message: 'Succesfully cleared all chat history',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
} 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 scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const addChatMessage = async (message, type) => {
|
||||
try {
|
||||
const newChatMessageBody = {
|
||||
role: type,
|
||||
content: message,
|
||||
chatflowid: chatflowid
|
||||
}
|
||||
await chatmessageApi.createNewChatmessage(chatflowid, newChatMessageBody)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
const handleError = (message = 'Oops! There seems to be an error. Please try again.') => {
|
||||
setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }])
|
||||
addChatMessage(message, 'apiMessage')
|
||||
setLoading(false)
|
||||
setUserInput('')
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (userInput.trim() === '') {
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage' }])
|
||||
addChatMessage(userInput, 'userMessage')
|
||||
|
||||
// Send user question and history to API
|
||||
try {
|
||||
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, { question: userInput, history: history })
|
||||
if (response.data) {
|
||||
const data = response.data
|
||||
setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }])
|
||||
addChatMessage(data, 'apiMessage')
|
||||
setLoading(false)
|
||||
setUserInput('')
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus()
|
||||
scrollToBottom()
|
||||
}, 100)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
handleError(errorData)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent blank submissions and allow for multiline input
|
||||
const handleEnter = (e) => {
|
||||
if (e.key === 'Enter' && userInput) {
|
||||
if (!e.shiftKey && userInput) {
|
||||
handleSubmit(e)
|
||||
}
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
// Get chatmessages successful
|
||||
useEffect(() => {
|
||||
if (getChatmessageApi.data) {
|
||||
const loadedMessages = []
|
||||
for (const message of getChatmessageApi.data) {
|
||||
loadedMessages.push({
|
||||
message: message.content,
|
||||
type: message.role
|
||||
})
|
||||
}
|
||||
setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getChatmessageApi.data])
|
||||
|
||||
// Keep history in sync with messages
|
||||
useEffect(() => {
|
||||
if (messages.length >= 3) {
|
||||
setHistory([[messages[messages.length - 2].message, messages[messages.length - 1].message]])
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
// Auto scroll chat to bottom
|
||||
useEffect(() => {
|
||||
scrollToBottom()
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
anchorRef.current.focus()
|
||||
}
|
||||
|
||||
if (open && chatflowid) {
|
||||
getChatmessageApi.request(chatflowid)
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
prevOpen.current = open
|
||||
|
||||
return () => {
|
||||
setUserInput('')
|
||||
setHistory([])
|
||||
setLoading(false)
|
||||
setMessages([
|
||||
{
|
||||
message: 'Hi there! How can I help?',
|
||||
type: 'apiMessage'
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, chatflowid])
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledFab
|
||||
sx={{ position: 'absolute', right: 20, top: 20 }}
|
||||
ref={anchorRef}
|
||||
size='small'
|
||||
color='secondary'
|
||||
aria-label='chat'
|
||||
title='Chat'
|
||||
onClick={handleToggle}
|
||||
>
|
||||
{open ? <IconX /> : <IconMessage />}
|
||||
</StyledFab>
|
||||
{open && (
|
||||
<StyledFab
|
||||
sx={{ position: 'absolute', right: 80, top: 20 }}
|
||||
onClick={clearChat}
|
||||
size='small'
|
||||
color='error'
|
||||
aria-label='clear'
|
||||
title='Clear Chat History'
|
||||
>
|
||||
<IconEraser />
|
||||
</StyledFab>
|
||||
)}
|
||||
<Popper
|
||||
placement='bottom-end'
|
||||
open={open}
|
||||
anchorEl={anchorRef.current}
|
||||
role={undefined}
|
||||
transition
|
||||
disablePortal
|
||||
popperOptions={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [40, 14]
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
sx={{ zIndex: 1000 }}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Transitions in={open} {...TransitionProps}>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
|
||||
<div className='cloud'>
|
||||
<div className='messagelist'>
|
||||
{messages.map((message, index) => {
|
||||
return (
|
||||
// The latest message sent by the user will be animated while waiting for a response
|
||||
<Box
|
||||
sx={{
|
||||
background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : ''
|
||||
}}
|
||||
key={index}
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
className={
|
||||
message.type === 'userMessage' && loading && index === messages.length - 1
|
||||
? customization.isDarkMode
|
||||
? 'usermessagewaiting-dark'
|
||||
: 'usermessagewaiting-light'
|
||||
: message.type === 'usermessagewaiting'
|
||||
? 'apimessage'
|
||||
: 'usermessage'
|
||||
}
|
||||
>
|
||||
{/* Display the correct icon depending on the message type */}
|
||||
{message.type === 'apiMessage' ? (
|
||||
<img
|
||||
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png'
|
||||
alt='AI'
|
||||
width='30'
|
||||
height='30'
|
||||
className='boticon'
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png'
|
||||
alt='Me'
|
||||
width='30'
|
||||
height='30'
|
||||
className='usericon'
|
||||
/>
|
||||
)}
|
||||
<div className='markdownanswer'>
|
||||
{/* Messages are being rendered in Markdown format */}
|
||||
<ReactMarkdown linkTarget={'_blank'}>{message.message}</ReactMarkdown>
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className='center'>
|
||||
<div className='cloudform'>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<OutlinedInput
|
||||
inputRef={inputRef}
|
||||
// eslint-disable-next-line
|
||||
autoFocus
|
||||
sx={{ width: '50vh' }}
|
||||
disabled={loading || !chatflowid}
|
||||
onKeyDown={handleEnter}
|
||||
id='userInput'
|
||||
name='userInput'
|
||||
placeholder={loading ? 'Waiting for response...' : 'Type your question...'}
|
||||
value={userInput}
|
||||
onChange={(e) => setUserInput(e.target.value)}
|
||||
endAdornment={
|
||||
<InputAdornment position='end'>
|
||||
<IconButton type='submit' disabled={loading || !chatflowid} edge='end'>
|
||||
{loading ? (
|
||||
<div>
|
||||
<CircularProgress color='inherit' size={20} />
|
||||
</div>
|
||||
) : (
|
||||
// Send icon SVG in input field
|
||||
<IconSend
|
||||
color={
|
||||
loading || !chatflowid
|
||||
? '#9e9e9e'
|
||||
: customization.isDarkMode
|
||||
? 'white'
|
||||
: '#1e88e5'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</MainCard>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Transitions>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ChatMessage.propTypes = { chatflowid: PropTypes.string }
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Box, List, Paper, Popper, ClickAwayListener } from '@mui/material'
|
||||
|
||||
// third-party
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard'
|
||||
import Transitions from 'ui-component/extended/Transitions'
|
||||
import NavItem from 'layout/MainLayout/Sidebar/MenuList/NavItem'
|
||||
|
||||
import settings from 'menu-items/settings'
|
||||
|
||||
// ==============================|| SETTINGS ||============================== //
|
||||
|
||||
const Settings = ({ chatflow, isSettingsOpen, anchorEl, onSettingsItemClick, onUploadFile, onClose }) => {
|
||||
const theme = useTheme()
|
||||
const [settingsMenu, setSettingsMenu] = useState([])
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (chatflow && !chatflow.id) {
|
||||
const settingsMenu = settings.children.filter((menu) => menu.id === 'loadChatflow')
|
||||
setSettingsMenu(settingsMenu)
|
||||
} else if (chatflow && chatflow.id) {
|
||||
const settingsMenu = settings.children
|
||||
setSettingsMenu(settingsMenu)
|
||||
}
|
||||
}, [chatflow])
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(isSettingsOpen)
|
||||
}, [isSettingsOpen])
|
||||
|
||||
// settings list items
|
||||
const items = settingsMenu.map((menu) => {
|
||||
return (
|
||||
<NavItem
|
||||
key={menu.id}
|
||||
item={menu}
|
||||
level={1}
|
||||
navType='SETTINGS'
|
||||
onClick={(id) => onSettingsItemClick(id)}
|
||||
onUploadFile={onUploadFile}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popper
|
||||
placement='bottom-end'
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
role={undefined}
|
||||
transition
|
||||
disablePortal
|
||||
popperOptions={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [170, 20]
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
sx={{ zIndex: 1000 }}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Transitions in={open} {...TransitionProps}>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={onClose}>
|
||||
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
|
||||
<PerfectScrollbar style={{ height: '100%', maxHeight: 'calc(100vh - 250px)', overflowX: 'hidden' }}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<List>{items}</List>
|
||||
</Box>
|
||||
</PerfectScrollbar>
|
||||
</MainCard>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Transitions>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Settings.propTypes = {
|
||||
chatflow: PropTypes.object,
|
||||
isSettingsOpen: PropTypes.bool,
|
||||
anchorEl: PropTypes.any,
|
||||
onSettingsItemClick: PropTypes.func,
|
||||
onUploadFile: PropTypes.func,
|
||||
onClose: PropTypes.func
|
||||
}
|
||||
|
||||
export default Settings
|
||||
Reference in New Issue
Block a user