Initial push

This commit is contained in:
Henry
2023-04-06 22:17:34 +01:00
commit 05c86ff9c5
162 changed files with 9112 additions and 0 deletions
+296
View File
@@ -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
+136
View File
@@ -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' }}>&nbsp;*</span>}
</Typography>
</Box>
</>
)}
{inputParam && (
<>
<Box sx={{ p: 2 }}>
<Typography>
{inputParam.label}
{!inputParam.optional && <span style={{ color: 'red' }}>&nbsp;*</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
+37
View File
@@ -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%;
}
+515
View File
@@ -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
+113
View File
@@ -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 }
+104
View File
@@ -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