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
+32
View File
@@ -0,0 +1,32 @@
import { useSelector } from 'react-redux'
import { ThemeProvider } from '@mui/material/styles'
import { CssBaseline, StyledEngineProvider } from '@mui/material'
// routing
import Routes from 'routes'
// defaultTheme
import themes from 'themes'
// project imports
import NavigationScroll from 'layout/NavigationScroll'
// ==============================|| APP ||============================== //
const App = () => {
const customization = useSelector((state) => state.customization)
return (
<StyledEngineProvider injectFirst>
<ThemeProvider theme={themes(customization)}>
<CssBaseline />
<NavigationScroll>
<Routes />
</NavigationScroll>
</ThemeProvider>
</StyledEngineProvider>
)
}
export default App
+19
View File
@@ -0,0 +1,19 @@
import client from './client'
const getAllChatflows = () => client.get('/chatflows')
const getSpecificChatflow = (id) => client.get(`/chatflows/${id}`)
const createNewChatflow = (body) => client.post(`/chatflows`, body)
const updateChatflow = (id, body) => client.put(`/chatflows/${id}`, body)
const deleteChatflow = (id) => client.delete(`/chatflows/${id}`)
export default {
getAllChatflows,
getSpecificChatflow,
createNewChatflow,
updateChatflow,
deleteChatflow
}
+13
View File
@@ -0,0 +1,13 @@
import client from './client'
const getChatmessageFromChatflow = (id) => client.get(`/chatmessage/${id}`)
const createNewChatmessage = (id, body) => client.post(`/chatmessage/${id}`, body)
const deleteChatmessage = (id) => client.delete(`/chatmessage/${id}`)
export default {
getChatmessageFromChatflow,
createNewChatmessage,
deleteChatmessage
}
+11
View File
@@ -0,0 +1,11 @@
import axios from 'axios'
import { baseURL } from 'store/constant'
const apiClient = axios.create({
baseURL: `${baseURL}/api/v1`,
headers: {
'Content-type': 'application/json'
}
})
export default apiClient
+10
View File
@@ -0,0 +1,10 @@
import client from './client'
const getAllNodes = () => client.get('/nodes')
const getSpecificNode = (name) => client.get(`/nodes/${name}`)
export default {
getAllNodes,
getSpecificNode
}
+7
View File
@@ -0,0 +1,7 @@
import client from './client'
const sendMessageAndGetPrediction = (id, input) => client.post(`/prediction/${id}`, input)
export default {
sendMessageAndGetPrediction
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,157 @@
// paper & background
$paper: #ffffff;
// primary
$primaryLight: #e3f2fd;
$primaryMain: #2196f3;
$primaryDark: #1e88e5;
$primary200: #90caf9;
$primary800: #1565c0;
// secondary
$secondaryLight: #ede7f6;
$secondaryMain: #673ab7;
$secondaryDark: #5e35b1;
$secondary200: #b39ddb;
$secondary800: #4527a0;
// success Colors
$successLight: #cdf5d8;
$success200: #69f0ae;
$successMain: #00e676;
$successDark: #00c853;
// error
$errorLight: #f3d2d2;
$errorMain: #f44336;
$errorDark: #c62828;
// orange
$orangeLight: #fbe9e7;
$orangeMain: #ffab91;
$orangeDark: #d84315;
// warning
$warningLight: #fff8e1;
$warningMain: #ffe57f;
$warningDark: #ffc107;
// grey
$grey50: #fafafa;
$grey100: #f5f5f5;
$grey200: #eeeeee;
$grey300: #e0e0e0;
$grey500: #9e9e9e;
$grey600: #757575;
$grey700: #616161;
$grey900: #212121;
// ==============================|| DARK THEME VARIANTS ||============================== //
// paper & background
$darkBackground: #191b1f;
$darkPaper: #191b1f;
// dark 800 & 900
$darkLevel1: #252525; // level 1
$darkLevel2: #242424; // level 2
// primary dark
$darkPrimaryLight: #23262c;
$darkPrimaryMain: #23262c;
$darkPrimaryDark: #191b1f;
$darkPrimary200: #c9d4e9;
$darkPrimary800: #32353b;
// secondary dark
$darkSecondaryLight: #454c59;
$darkSecondaryMain: #7c4dff;
$darkSecondaryDark: #ffffff;
$darkSecondary200: #32353b;
$darkSecondary800: #6200ea;
// text variants
$darkTextTitle: #d7dcec;
$darkTextPrimary: #bdc8f0;
$darkTextSecondary: #8492c4;
// ==============================|| JAVASCRIPT ||============================== //
:export {
// paper & background
paper: $paper;
// primary
primaryLight: $primaryLight;
primary200: $primary200;
primaryMain: $primaryMain;
primaryDark: $primaryDark;
primary800: $primary800;
// secondary
secondaryLight: $secondaryLight;
secondary200: $secondary200;
secondaryMain: $secondaryMain;
secondaryDark: $secondaryDark;
secondary800: $secondary800;
// success
successLight: $successLight;
success200: $success200;
successMain: $successMain;
successDark: $successDark;
// error
errorLight: $errorLight;
errorMain: $errorMain;
errorDark: $errorDark;
// orange
orangeLight: $orangeLight;
orangeMain: $orangeMain;
orangeDark: $orangeDark;
// warning
warningLight: $warningLight;
warningMain: $warningMain;
warningDark: $warningDark;
// grey
grey50: $grey50;
grey100: $grey100;
grey200: $grey200;
grey300: $grey300;
grey500: $grey500;
grey600: $grey600;
grey700: $grey700;
grey900: $grey900;
// ==============================|| DARK THEME VARIANTS ||============================== //
// paper & background
darkPaper: $darkPaper;
darkBackground: $darkBackground;
// dark 800 & 900
darkLevel1: $darkLevel1;
darkLevel2: $darkLevel2;
// text variants
darkTextTitle: $darkTextTitle;
darkTextPrimary: $darkTextPrimary;
darkTextSecondary: $darkTextSecondary;
// primary dark
darkPrimaryLight: $darkPrimaryLight;
darkPrimaryMain: $darkPrimaryMain;
darkPrimaryDark: $darkPrimaryDark;
darkPrimary200: $darkPrimary200;
darkPrimary800: $darkPrimary800;
// secondary dark
darkSecondaryLight: $darkSecondaryLight;
darkSecondaryMain: $darkSecondaryMain;
darkSecondaryDark: $darkSecondaryDark;
darkSecondary200: $darkSecondary200;
darkSecondary800: $darkSecondary800;
}
+122
View File
@@ -0,0 +1,122 @@
// color variants
@import 'themes-vars.module.scss';
// third-party
@import '~react-perfect-scrollbar/dist/css/styles.css';
// ==============================|| LIGHT BOX ||============================== //
.fullscreen .react-images__blanket {
z-index: 1200;
}
// ==============================|| PERFECT SCROLLBAR ||============================== //
.scrollbar-container {
.ps__rail-y {
&:hover > .ps__thumb-y,
&:focus > .ps__thumb-y,
&.ps--clicking .ps__thumb-y {
background-color: $grey500;
width: 5px;
}
}
.ps__thumb-y {
background-color: $grey500;
border-radius: 6px;
width: 5px;
right: 0;
}
}
.scrollbar-container.ps,
.scrollbar-container > .ps {
&.ps--active-y > .ps__rail-y {
width: 5px;
background-color: transparent !important;
z-index: 999;
&:hover,
&.ps--clicking {
width: 5px;
background-color: transparent;
}
}
&.ps--scrolling-y > .ps__rail-y,
&.ps--scrolling-x > .ps__rail-x {
opacity: 0.4;
background-color: transparent;
}
}
// ==============================|| ANIMATION KEYFRAMES ||============================== //
@keyframes wings {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0px);
}
}
@keyframes blink {
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes bounce {
0%,
20%,
53%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
transform: translateZ(0);
}
40%,
43% {
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
transform: translate3d(0, -5px, 0);
}
70% {
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
transform: translate3d(0, -7px, 0);
}
80% {
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
transform: translateZ(0);
}
90% {
transform: translate3d(0, -2px, 0);
}
}
@keyframes slideY {
0%,
50%,
100% {
transform: translateY(0px);
}
25% {
transform: translateY(-10px);
}
75% {
transform: translateY(10px);
}
}
@keyframes slideX {
0%,
50%,
100% {
transform: translateX(0px);
}
25% {
transform: translateX(-10px);
}
75% {
transform: translateX(10px);
}
}
+9
View File
@@ -0,0 +1,9 @@
const config = {
// basename: only at build time to set, and Don't add '/' at end off BASENAME for breadcrumbs, also Don't put only '/' use blank('') instead,
basename: '',
defaultPath: '/chatflows',
fontFamily: `'Roboto', sans-serif`,
borderRadius: 12
}
export default config
+26
View File
@@ -0,0 +1,26 @@
import { useState } from 'react'
export default (apiFunc) => {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(false)
const request = async (...args) => {
setLoading(true)
try {
const result = await apiFunc(...args)
setData(result.data)
} catch (err) {
setError(err || 'Unexpected Error!')
} finally {
setLoading(false)
}
}
return {
data,
error,
loading,
request
}
}
+37
View File
@@ -0,0 +1,37 @@
import { useContext } from 'react'
import ConfirmContext from 'store/context/ConfirmContext'
import { HIDE_CONFIRM, SHOW_CONFIRM } from 'store/actions'
let resolveCallback
const useConfirm = () => {
const [confirmState, dispatch] = useContext(ConfirmContext)
const closeConfirm = () => {
dispatch({
type: HIDE_CONFIRM
})
}
const onConfirm = () => {
closeConfirm()
resolveCallback(true)
}
const onCancel = () => {
closeConfirm()
resolveCallback(false)
}
const confirm = (confirmPayload) => {
dispatch({
type: SHOW_CONFIRM,
payload: confirmPayload
})
return new Promise((res) => {
resolveCallback = res
})
}
return { confirm, onConfirm, onCancel, confirmState }
}
export default useConfirm
+18
View File
@@ -0,0 +1,18 @@
import { useEffect, useRef } from 'react'
// ==============================|| ELEMENT REFERENCE HOOKS ||============================== //
const useScriptRef = () => {
const scripted = useRef(true)
useEffect(
() => () => {
scripted.current = false
},
[]
)
return scripted
}
export default useScriptRef
+33
View File
@@ -0,0 +1,33 @@
import React from 'react'
import App from './App'
import { store } from 'store'
import { createRoot } from 'react-dom/client'
// style + assets
import 'assets/scss/style.scss'
// third party
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { SnackbarProvider } from 'notistack'
import ConfirmContextProvider from 'store/context/ConfirmContextProvider'
import { ReactFlowContext } from 'store/context/ReactFlowContext'
const container = document.getElementById('root')
const root = createRoot(container)
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<SnackbarProvider>
<ConfirmContextProvider>
<ReactFlowContext>
<App />
</ReactFlowContext>
</ConfirmContextProvider>
</SnackbarProvider>
</BrowserRouter>
</Provider>
</React.StrictMode>
)
@@ -0,0 +1,127 @@
import PropTypes from 'prop-types'
import { useSelector, useDispatch } from 'react-redux'
import { useState } from 'react'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Avatar, Box, ButtonBase, Switch } from '@mui/material'
import { styled } from '@mui/material/styles'
// project imports
import LogoSection from '../LogoSection'
// assets
import { IconMenu2 } from '@tabler/icons'
// store
import { SET_DARKMODE } from 'store/actions'
// ==============================|| MAIN NAVBAR / HEADER ||============================== //
const MaterialUISwitch = styled(Switch)(({ theme }) => ({
width: 62,
height: 34,
padding: 7,
'& .MuiSwitch-switchBase': {
margin: 1,
padding: 0,
transform: 'translateX(6px)',
'&.Mui-checked': {
color: '#fff',
transform: 'translateX(22px)',
'& .MuiSwitch-thumb:before': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
'#fff'
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`
},
'& + .MuiSwitch-track': {
opacity: 1,
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be'
}
}
},
'& .MuiSwitch-thumb': {
backgroundColor: theme.palette.mode === 'dark' ? '#003892' : '#001e3c',
width: 32,
height: 32,
'&:before': {
content: "''",
position: 'absolute',
width: '100%',
height: '100%',
left: 0,
top: 0,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
'#fff'
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`
}
},
'& .MuiSwitch-track': {
opacity: 1,
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
borderRadius: 20 / 2
}
}))
const Header = ({ handleLeftDrawerToggle }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [isDark, setIsDark] = useState(customization.isDarkMode)
const dispatch = useDispatch()
const changeDarkMode = () => {
dispatch({ type: SET_DARKMODE, isDarkMode: !isDark })
setIsDark((isDark) => !isDark)
localStorage.setItem('isDarkMode', !isDark)
}
return (
<>
{/* logo & toggler button */}
<Box
sx={{
width: 228,
display: 'flex',
[theme.breakpoints.down('md')]: {
width: 'auto'
}
}}
>
<Box component='span' sx={{ display: { xs: 'none', md: 'block' }, flexGrow: 1 }}>
<LogoSection />
</Box>
<ButtonBase sx={{ borderRadius: '12px', overflow: 'hidden' }}>
<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
}
}}
onClick={handleLeftDrawerToggle}
color='inherit'
>
<IconMenu2 stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
</Box>
<Box sx={{ flexGrow: 1 }} />
<MaterialUISwitch checked={isDark} onChange={changeDarkMode} />
</>
)
}
Header.propTypes = {
handleLeftDrawerToggle: PropTypes.func
}
export default Header
@@ -0,0 +1,18 @@
import { Link } from 'react-router-dom'
// material-ui
import { ButtonBase } from '@mui/material'
// project imports
import config from 'config'
import Logo from 'ui-component/extended/Logo'
// ==============================|| MAIN LOGO ||============================== //
const LogoSection = () => (
<ButtonBase disableRipple component={Link} to={config.defaultPath}>
<Logo />
</ButtonBase>
)
export default LogoSection
@@ -0,0 +1,124 @@
import PropTypes from 'prop-types'
import { useState } from 'react'
import { useSelector } from 'react-redux'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Collapse, List, ListItemButton, ListItemIcon, ListItemText, Typography } from '@mui/material'
// project imports
import NavItem from '../NavItem'
// assets
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'
import { IconChevronDown, IconChevronUp } from '@tabler/icons'
// ==============================|| SIDEBAR MENU LIST COLLAPSE ITEMS ||============================== //
const NavCollapse = ({ menu, level }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [open, setOpen] = useState(false)
const [selected, setSelected] = useState(null)
const handleClick = () => {
setOpen(!open)
setSelected(!selected ? menu.id : null)
}
// menu collapse & item
const menus = menu.children?.map((item) => {
switch (item.type) {
case 'collapse':
return <NavCollapse key={item.id} menu={item} level={level + 1} />
case 'item':
return <NavItem key={item.id} item={item} level={level + 1} />
default:
return (
<Typography key={item.id} variant='h6' color='error' align='center'>
Menu Items Error
</Typography>
)
}
})
const Icon = menu.icon
const menuIcon = menu.icon ? (
<Icon strokeWidth={1.5} size='1.3rem' style={{ marginTop: 'auto', marginBottom: 'auto' }} />
) : (
<FiberManualRecordIcon
sx={{
width: selected === menu.id ? 8 : 6,
height: selected === menu.id ? 8 : 6
}}
fontSize={level > 0 ? 'inherit' : 'medium'}
/>
)
return (
<>
<ListItemButton
sx={{
borderRadius: `${customization.borderRadius}px`,
mb: 0.5,
alignItems: 'flex-start',
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
py: level > 1 ? 1 : 1.25,
pl: `${level * 24}px`
}}
selected={selected === menu.id}
onClick={handleClick}
>
<ListItemIcon sx={{ my: 'auto', minWidth: !menu.icon ? 18 : 36 }}>{menuIcon}</ListItemIcon>
<ListItemText
primary={
<Typography variant={selected === menu.id ? 'h5' : 'body1'} color='inherit' sx={{ my: 'auto' }}>
{menu.title}
</Typography>
}
secondary={
menu.caption && (
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
{menu.caption}
</Typography>
)
}
/>
{open ? (
<IconChevronUp stroke={1.5} size='1rem' style={{ marginTop: 'auto', marginBottom: 'auto' }} />
) : (
<IconChevronDown stroke={1.5} size='1rem' style={{ marginTop: 'auto', marginBottom: 'auto' }} />
)}
</ListItemButton>
<Collapse in={open} timeout='auto' unmountOnExit>
<List
component='div'
disablePadding
sx={{
position: 'relative',
'&:after': {
content: "''",
position: 'absolute',
left: '32px',
top: 0,
height: '100%',
width: '1px',
opacity: 1,
background: theme.palette.primary.light
}
}}
>
{menus}
</List>
</Collapse>
</>
)
}
NavCollapse.propTypes = {
menu: PropTypes.object,
level: PropTypes.number
}
export default NavCollapse
@@ -0,0 +1,61 @@
import PropTypes from 'prop-types'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Divider, List, Typography } from '@mui/material'
// project imports
import NavItem from '../NavItem'
import NavCollapse from '../NavCollapse'
// ==============================|| SIDEBAR MENU LIST GROUP ||============================== //
const NavGroup = ({ item }) => {
const theme = useTheme()
// menu list collapse & items
const items = item.children?.map((menu) => {
switch (menu.type) {
case 'collapse':
return <NavCollapse key={menu.id} menu={menu} level={1} />
case 'item':
return <NavItem key={menu.id} item={menu} level={1} navType='MENU' />
default:
return (
<Typography key={menu.id} variant='h6' color='error' align='center'>
Menu Items Error
</Typography>
)
}
})
return (
<>
<List
subheader={
item.title && (
<Typography variant='caption' sx={{ ...theme.typography.menuCaption }} display='block' gutterBottom>
{item.title}
{item.caption && (
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
{item.caption}
</Typography>
)}
</Typography>
)
}
>
{items}
</List>
{/* group divider */}
<Divider sx={{ mt: 0.25, mb: 1.25 }} />
</>
)
}
NavGroup.propTypes = {
item: PropTypes.object
}
export default NavGroup
@@ -0,0 +1,150 @@
import PropTypes from 'prop-types'
import { forwardRef, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography, useMediaQuery } from '@mui/material'
// project imports
import { MENU_OPEN, SET_MENU } from 'store/actions'
import config from 'config'
// assets
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'
// ==============================|| SIDEBAR MENU LIST ITEMS ||============================== //
const NavItem = ({ item, level, navType, onClick, onUploadFile }) => {
const theme = useTheme()
const dispatch = useDispatch()
const customization = useSelector((state) => state.customization)
const matchesSM = useMediaQuery(theme.breakpoints.down('lg'))
const Icon = item.icon
const itemIcon = item?.icon ? (
<Icon stroke={1.5} size='1.3rem' />
) : (
<FiberManualRecordIcon
sx={{
width: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6,
height: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6
}}
fontSize={level > 0 ? 'inherit' : 'medium'}
/>
)
let itemTarget = '_self'
if (item.target) {
itemTarget = '_blank'
}
let listItemProps = {
component: forwardRef(function ListItemPropsComponent(props, ref) {
return <Link ref={ref} {...props} to={`${config.basename}${item.url}`} target={itemTarget} />
})
}
if (item?.external) {
listItemProps = { component: 'a', href: item.url, target: itemTarget }
}
if (item?.id === 'loadChatflow') {
listItemProps.component = 'label'
}
const handleFileUpload = (e) => {
if (!e.target.files) return
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = (evt) => {
if (!evt?.target?.result) {
return
}
const { result } = evt.target
onUploadFile(result)
}
reader.readAsText(file)
}
const itemHandler = (id) => {
if (navType === 'SETTINGS' && id !== 'loadChatflow') {
onClick(id)
} else {
dispatch({ type: MENU_OPEN, id })
if (matchesSM) dispatch({ type: SET_MENU, opened: false })
}
}
// active menu item on page load
useEffect(() => {
if (navType === 'MENU') {
const currentIndex = document.location.pathname
.toString()
.split('/')
.findIndex((id) => id === item.id)
if (currentIndex > -1) {
dispatch({ type: MENU_OPEN, id: item.id })
}
if (!document.location.pathname.toString().split('/')[1]) {
itemHandler('chatflows')
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navType])
return (
<ListItemButton
{...listItemProps}
disabled={item.disabled}
sx={{
borderRadius: `${customization.borderRadius}px`,
mb: 0.5,
alignItems: 'flex-start',
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
py: level > 1 ? 1 : 1.25,
pl: `${level * 24}px`
}}
selected={customization.isOpen.findIndex((id) => id === item.id) > -1}
onClick={() => itemHandler(item.id)}
>
{item.id === 'loadChatflow' && <input type='file' hidden accept='.json' onChange={(e) => handleFileUpload(e)} />}
<ListItemIcon sx={{ my: 'auto', minWidth: !item?.icon ? 18 : 36 }}>{itemIcon}</ListItemIcon>
<ListItemText
primary={
<Typography variant={customization.isOpen.findIndex((id) => id === item.id) > -1 ? 'h5' : 'body1'} color='inherit'>
{item.title}
</Typography>
}
secondary={
item.caption && (
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
{item.caption}
</Typography>
)
}
/>
{item.chip && (
<Chip
color={item.chip.color}
variant={item.chip.variant}
size={item.chip.size}
label={item.chip.label}
avatar={item.chip.avatar && <Avatar>{item.chip.avatar}</Avatar>}
/>
)}
</ListItemButton>
)
}
NavItem.propTypes = {
item: PropTypes.object,
level: PropTypes.number,
navType: PropTypes.string,
onClick: PropTypes.func,
onUploadFile: PropTypes.func
}
export default NavItem
@@ -0,0 +1,27 @@
// material-ui
import { Typography } from '@mui/material'
// project imports
import NavGroup from './NavGroup'
import menuItem from 'menu-items'
// ==============================|| SIDEBAR MENU LIST ||============================== //
const MenuList = () => {
const navItems = menuItem.items.map((item) => {
switch (item.type) {
case 'group':
return <NavGroup key={item.id} item={item} />
default:
return (
<Typography key={item.id} variant='h6' color='error' align='center'>
Menu Items Error
</Typography>
)
}
})
return <>{navItems}</>
}
export default MenuList
@@ -0,0 +1,85 @@
import PropTypes from 'prop-types'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Box, Drawer, useMediaQuery } from '@mui/material'
// third-party
import PerfectScrollbar from 'react-perfect-scrollbar'
import { BrowserView, MobileView } from 'react-device-detect'
// project imports
import MenuList from './MenuList'
import LogoSection from '../LogoSection'
import { drawerWidth } from 'store/constant'
// ==============================|| SIDEBAR DRAWER ||============================== //
const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
const theme = useTheme()
const matchUpMd = useMediaQuery(theme.breakpoints.up('md'))
const drawer = (
<>
<Box sx={{ display: { xs: 'block', md: 'none' } }}>
<Box sx={{ display: 'flex', p: 2, mx: 'auto' }}>
<LogoSection />
</Box>
</Box>
<BrowserView>
<PerfectScrollbar
component='div'
style={{
height: !matchUpMd ? 'calc(100vh - 56px)' : 'calc(100vh - 88px)',
paddingLeft: '16px',
paddingRight: '16px'
}}
>
<MenuList />
</PerfectScrollbar>
</BrowserView>
<MobileView>
<Box sx={{ px: 2 }}>
<MenuList />
</Box>
</MobileView>
</>
)
const container = window !== undefined ? () => window.document.body : undefined
return (
<Box component='nav' sx={{ flexShrink: { md: 0 }, width: matchUpMd ? drawerWidth : 'auto' }} aria-label='mailbox folders'>
<Drawer
container={container}
variant={matchUpMd ? 'persistent' : 'temporary'}
anchor='left'
open={drawerOpen}
onClose={drawerToggle}
sx={{
'& .MuiDrawer-paper': {
width: drawerWidth,
background: theme.palette.background.default,
color: theme.palette.text.primary,
borderRight: 'none',
[theme.breakpoints.up('md')]: {
top: '66px'
}
}
}}
ModalProps={{ keepMounted: true }}
color='inherit'
>
{drawer}
</Drawer>
</Box>
)
}
Sidebar.propTypes = {
drawerOpen: PropTypes.bool,
drawerToggle: PropTypes.func,
window: PropTypes.object
}
export default Sidebar
+107
View File
@@ -0,0 +1,107 @@
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Outlet } from 'react-router-dom'
// material-ui
import { styled, useTheme } from '@mui/material/styles'
import { AppBar, Box, CssBaseline, Toolbar, useMediaQuery } from '@mui/material'
// project imports
import Header from './Header'
import Sidebar from './Sidebar'
import { drawerWidth } from 'store/constant'
import { SET_MENU } from 'store/actions'
// styles
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
...theme.typography.mainContent,
...(!open && {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
[theme.breakpoints.up('md')]: {
marginLeft: -(drawerWidth - 20),
width: `calc(100% - ${drawerWidth}px)`
},
[theme.breakpoints.down('md')]: {
marginLeft: '20px',
width: `calc(100% - ${drawerWidth}px)`,
padding: '16px'
},
[theme.breakpoints.down('sm')]: {
marginLeft: '10px',
width: `calc(100% - ${drawerWidth}px)`,
padding: '16px',
marginRight: '10px'
}
}),
...(open && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen
}),
marginLeft: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
width: `calc(100% - ${drawerWidth}px)`,
[theme.breakpoints.down('md')]: {
marginLeft: '20px'
},
[theme.breakpoints.down('sm')]: {
marginLeft: '10px'
}
})
}))
// ==============================|| MAIN LAYOUT ||============================== //
const MainLayout = () => {
const theme = useTheme()
const matchDownMd = useMediaQuery(theme.breakpoints.down('lg'))
// Handle left drawer
const leftDrawerOpened = useSelector((state) => state.customization.opened)
const dispatch = useDispatch()
const handleLeftDrawerToggle = () => {
dispatch({ type: SET_MENU, opened: !leftDrawerOpened })
}
useEffect(() => {
dispatch({ type: SET_MENU, opened: !matchDownMd })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [matchDownMd])
return (
<Box sx={{ display: 'flex' }}>
<CssBaseline />
{/* header */}
<AppBar
enableColorOnDark
position='fixed'
color='inherit'
elevation={0}
sx={{
bgcolor: theme.palette.background.default,
transition: leftDrawerOpened ? theme.transitions.create('width') : 'none'
}}
>
<Toolbar>
<Header handleLeftDrawerToggle={handleLeftDrawerToggle} />
</Toolbar>
</AppBar>
{/* drawer */}
<Sidebar drawerOpen={leftDrawerOpened} drawerToggle={handleLeftDrawerToggle} />
{/* main content */}
<Main theme={theme} open={leftDrawerOpened}>
<Outlet />
</Main>
</Box>
)
}
export default MainLayout
@@ -0,0 +1,11 @@
import { Outlet } from 'react-router-dom'
// ==============================|| MINIMAL LAYOUT ||============================== //
const MinimalLayout = () => (
<>
<Outlet />
</>
)
export default MinimalLayout
+39
View File
@@ -0,0 +1,39 @@
import PropTypes from 'prop-types'
import { motion } from 'framer-motion'
// ==============================|| ANIMATION FOR CONTENT ||============================== //
const NavMotion = ({ children }) => {
const motionVariants = {
initial: {
opacity: 0,
scale: 0.99
},
in: {
opacity: 1,
scale: 1
},
out: {
opacity: 0,
scale: 1.01
}
}
const motionTransition = {
type: 'tween',
ease: 'anticipate',
duration: 0.4
}
return (
<motion.div initial='initial' animate='in' exit='out' variants={motionVariants} transition={motionTransition}>
{children}
</motion.div>
)
}
NavMotion.propTypes = {
children: PropTypes.node
}
export default NavMotion
@@ -0,0 +1,26 @@
import PropTypes from 'prop-types'
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
// ==============================|| NAVIGATION SCROLL TO TOP ||============================== //
const NavigationScroll = ({ children }) => {
const location = useLocation()
const { pathname } = location
useEffect(() => {
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
})
}, [pathname])
return children || null
}
NavigationScroll.propTypes = {
children: PropTypes.node
}
export default NavigationScroll
+25
View File
@@ -0,0 +1,25 @@
// assets
import { IconHierarchy, IconKey, IconBook, IconListCheck } from '@tabler/icons'
// constant
const icons = { IconHierarchy, IconKey, IconBook, IconListCheck }
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
const dashboard = {
id: 'dashboard',
title: '',
type: 'group',
children: [
{
id: 'chatflows',
title: 'Chatflows',
type: 'item',
url: '/chatflows',
icon: icons.IconHierarchy,
breadcrumbs: true
}
]
}
export default dashboard
+9
View File
@@ -0,0 +1,9 @@
import dashboard from './dashboard'
// ==============================|| MENU ITEMS ||============================== //
const menuItems = {
items: [dashboard]
}
export default menuItems
+38
View File
@@ -0,0 +1,38 @@
// assets
import { IconTrash, IconFileUpload, IconFileExport } from '@tabler/icons'
// constant
const icons = { IconTrash, IconFileUpload, IconFileExport }
// ==============================|| SETTINGS MENU ITEMS ||============================== //
const settings = {
id: 'settings',
title: '',
type: 'group',
children: [
{
id: 'loadChatflow',
title: 'Load Chatflow',
type: 'item',
url: '',
icon: icons.IconFileUpload
},
{
id: 'exportChatflow',
title: 'Export Chatflow',
type: 'item',
url: '',
icon: icons.IconFileExport
},
{
id: 'deleteChatflow',
title: 'Delete Chatflow',
type: 'item',
url: '',
icon: icons.IconTrash
}
]
}
export default settings
+27
View File
@@ -0,0 +1,27 @@
import { lazy } from 'react'
// project imports
import Loadable from 'ui-component/loading/Loadable'
import MinimalLayout from 'layout/MinimalLayout'
// canvas routing
const Canvas = Loadable(lazy(() => import('views/canvas')))
// ==============================|| CANVAS ROUTING ||============================== //
const CanvasRoutes = {
path: '/',
element: <MinimalLayout />,
children: [
{
path: '/canvas',
element: <Canvas />
},
{
path: '/canvas/:id',
element: <Canvas />
}
]
}
export default CanvasRoutes
+27
View File
@@ -0,0 +1,27 @@
import { lazy } from 'react'
// project imports
import MainLayout from 'layout/MainLayout'
import Loadable from 'ui-component/loading/Loadable'
// chatflows routing
const Chatflows = Loadable(lazy(() => import('views/chatflows')))
// ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = {
path: '/',
element: <MainLayout />,
children: [
{
path: '/',
element: <Chatflows />
},
{
path: '/chatflows',
element: <Chatflows />
}
]
}
export default MainRoutes
+12
View File
@@ -0,0 +1,12 @@
import { useRoutes } from 'react-router-dom'
// routes
import MainRoutes from './MainRoutes'
import CanvasRoutes from './CanvasRoutes'
import config from 'config'
// ==============================|| ROUTING RENDER ||============================== //
export default function ThemeRoutes() {
return useRoutes([MainRoutes, CanvasRoutes], config.basename)
}
+132
View File
@@ -0,0 +1,132 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
)
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing
if (installingWorker == null) {
return
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.info(
'New content is available and will be used when all tabs for this page are closed. See https://bit.ly/CRA-PWA.'
)
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration)
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.info('Content is cached for offline use.')
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration)
}
}
}
}
}
})
.catch((error) => {
console.error('Error during service worker registration:', error)
})
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type')
if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload()
})
})
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config)
}
})
.catch(() => {
console.info('No internet connection found. App is running in offline mode.')
})
}
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.info(
'This web app is being served cache-first by a service worker. To learn more, visit https://bit.ly/CRA-PWA'
)
})
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config)
}
})
}
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
registration.unregister()
})
.catch((error) => {
console.error(error.message)
})
}
}
+46
View File
@@ -0,0 +1,46 @@
// action - customization reducer
export const SET_MENU = '@customization/SET_MENU'
export const MENU_TOGGLE = '@customization/MENU_TOGGLE'
export const MENU_OPEN = '@customization/MENU_OPEN'
export const SET_FONT_FAMILY = '@customization/SET_FONT_FAMILY'
export const SET_BORDER_RADIUS = '@customization/SET_BORDER_RADIUS'
export const SET_LAYOUT = '@customization/SET_LAYOUT '
export const SET_DARKMODE = '@customization/SET_DARKMODE'
// action - canvas reducer
export const REMOVE_EDGE = '@canvas/REMOVE_EDGE'
export const SET_DIRTY = '@canvas/SET_DIRTY'
export const REMOVE_DIRTY = '@canvas/REMOVE_DIRTY'
export const SET_CHATFLOW = '@canvas/SET_CHATFLOW'
// action - notifier reducer
export const ENQUEUE_SNACKBAR = 'ENQUEUE_SNACKBAR'
export const CLOSE_SNACKBAR = 'CLOSE_SNACKBAR'
export const REMOVE_SNACKBAR = 'REMOVE_SNACKBAR'
// action - dialog reducer
export const SHOW_CONFIRM = 'SHOW_CONFIRM'
export const HIDE_CONFIRM = 'HIDE_CONFIRM'
export const enqueueSnackbar = (notification) => {
const key = notification.options && notification.options.key
return {
type: ENQUEUE_SNACKBAR,
notification: {
...notification,
key: key || new Date().getTime() + Math.random()
}
}
}
export const closeSnackbar = (key) => ({
type: CLOSE_SNACKBAR,
dismissAll: !key, // dismiss all if no key has been defined
key
})
export const removeSnackbar = (key) => ({
type: REMOVE_SNACKBAR,
key
})
+5
View File
@@ -0,0 +1,5 @@
// constant
export const gridSpacing = 3
export const drawerWidth = 260
export const appDrawerWidth = 320
export const baseURL = process.env.NODE_ENV === 'production' ? window.location.origin : window.location.origin.replace(':8080', ':3000')
@@ -0,0 +1,5 @@
import React from 'react'
const ConfirmContext = React.createContext()
export default ConfirmContext
@@ -0,0 +1,16 @@
import { useReducer } from 'react'
import PropTypes from 'prop-types'
import alertReducer, { initialState } from '../reducers/dialogReducer'
import ConfirmContext from './ConfirmContext'
const ConfirmContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(alertReducer, initialState)
return <ConfirmContext.Provider value={[state, dispatch]}>{children}</ConfirmContext.Provider>
}
ConfirmContextProvider.propTypes = {
children: PropTypes.any
}
export default ConfirmContextProvider
@@ -0,0 +1,35 @@
import { createContext, useState } from 'react'
import PropTypes from 'prop-types'
const initialValue = {
reactFlowInstance: null,
setReactFlowInstance: () => {},
deleteNode: () => {}
}
export const flowContext = createContext(initialValue)
export const ReactFlowContext = ({ children }) => {
const [reactFlowInstance, setReactFlowInstance] = useState(null)
const deleteNode = (id) => {
reactFlowInstance.setNodes(reactFlowInstance.getNodes().filter((n) => n.id !== id))
reactFlowInstance.setEdges(reactFlowInstance.getEdges().filter((ns) => ns.source !== id && ns.target !== id))
}
return (
<flowContext.Provider
value={{
reactFlowInstance,
setReactFlowInstance,
deleteNode
}}
>
{children}
</flowContext.Provider>
)
}
ReactFlowContext.propTypes = {
children: PropTypes.any
}
+9
View File
@@ -0,0 +1,9 @@
import { createStore } from 'redux'
import reducer from './reducer'
// ==============================|| REDUX - MAIN STORE ||============================== //
const store = createStore(reducer)
const persister = 'Free'
export { store, persister }
+18
View File
@@ -0,0 +1,18 @@
import { combineReducers } from 'redux'
// reducer import
import customizationReducer from './reducers/customizationReducer'
import canvasReducer from './reducers/canvasReducer'
import notifierReducer from './reducers/notifierReducer'
import dialogReducer from './reducers/dialogReducer'
// ==============================|| COMBINE REDUCER ||============================== //
const reducer = combineReducers({
customization: customizationReducer,
canvas: canvasReducer,
notifier: notifierReducer,
dialog: dialogReducer
})
export default reducer
@@ -0,0 +1,39 @@
// action - state management
import * as actionTypes from '../actions'
export const initialState = {
removeEdgeId: '',
isDirty: false,
chatflow: null
}
// ==============================|| CANVAS REDUCER ||============================== //
const canvasReducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.REMOVE_EDGE:
return {
...state,
removeEdgeId: action.edgeId
}
case actionTypes.SET_DIRTY:
return {
...state,
isDirty: true
}
case actionTypes.REMOVE_DIRTY:
return {
...state,
isDirty: false
}
case actionTypes.SET_CHATFLOW:
return {
...state,
chatflow: action.chatflow
}
default:
return state
}
}
export default canvasReducer
@@ -0,0 +1,57 @@
// project imports
import config from 'config'
// action - state management
import * as actionTypes from '../actions'
export const initialState = {
isOpen: [], // for active default menu
fontFamily: config.fontFamily,
borderRadius: config.borderRadius,
opened: true,
isHorizontal: localStorage.getItem('isHorizontal') === 'true' ? true : false,
isDarkMode: localStorage.getItem('isDarkMode') === 'true' ? true : false
}
// ==============================|| CUSTOMIZATION REDUCER ||============================== //
const customizationReducer = (state = initialState, action) => {
let id
switch (action.type) {
case actionTypes.MENU_OPEN:
id = action.id
return {
...state,
isOpen: [id]
}
case actionTypes.SET_MENU:
return {
...state,
opened: action.opened
}
case actionTypes.SET_FONT_FAMILY:
return {
...state,
fontFamily: action.fontFamily
}
case actionTypes.SET_BORDER_RADIUS:
return {
...state,
borderRadius: action.borderRadius
}
case actionTypes.SET_LAYOUT:
return {
...state,
isHorizontal: action.isHorizontal
}
case actionTypes.SET_DARKMODE:
return {
...state,
isDarkMode: action.isDarkMode
}
default:
return state
}
}
export default customizationReducer
@@ -0,0 +1,28 @@
import { SHOW_CONFIRM, HIDE_CONFIRM } from '../actions'
export const initialState = {
show: false,
title: '',
description: '',
confirmButtonName: 'OK',
cancelButtonName: 'Cancel'
}
const alertReducer = (state = initialState, action) => {
switch (action.type) {
case SHOW_CONFIRM:
return {
show: true,
title: action.payload.title,
description: action.payload.description,
confirmButtonName: action.payload.confirmButtonName,
cancelButtonName: action.payload.cancelButtonName
}
case HIDE_CONFIRM:
return initialState
default:
return state
}
}
export default alertReducer
@@ -0,0 +1,40 @@
import { ENQUEUE_SNACKBAR, CLOSE_SNACKBAR, REMOVE_SNACKBAR } from '../actions'
export const initialState = {
notifications: []
}
const notifierReducer = (state = initialState, action) => {
switch (action.type) {
case ENQUEUE_SNACKBAR:
return {
...state,
notifications: [
...state.notifications,
{
key: action.key,
...action.notification
}
]
}
case CLOSE_SNACKBAR:
return {
...state,
notifications: state.notifications.map((notification) =>
action.dismissAll || notification.key === action.key ? { ...notification, dismissed: true } : { ...notification }
)
}
case REMOVE_SNACKBAR:
return {
...state,
notifications: state.notifications.filter((notification) => notification.key !== action.key)
}
default:
return state
}
}
export default notifierReducer
+204
View File
@@ -0,0 +1,204 @@
export default function componentStyleOverrides(theme) {
const bgColor = theme.colors?.grey50
return {
MuiButton: {
styleOverrides: {
root: {
fontWeight: 500,
borderRadius: '4px'
}
}
},
MuiSvgIcon: {
styleOverrides: {
root: {
color: theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit',
background: theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryLight : 'inherit'
}
}
},
MuiPaper: {
defaultProps: {
elevation: 0
},
styleOverrides: {
root: {
backgroundImage: 'none'
},
rounded: {
borderRadius: `${theme?.customization?.borderRadius}px`
}
}
},
MuiCardHeader: {
styleOverrides: {
root: {
color: theme.colors?.textDark,
padding: '24px'
},
title: {
fontSize: '1.125rem'
}
}
},
MuiCardContent: {
styleOverrides: {
root: {
padding: '24px'
}
}
},
MuiCardActions: {
styleOverrides: {
root: {
padding: '24px'
}
}
},
MuiListItemButton: {
styleOverrides: {
root: {
color: theme.darkTextPrimary,
paddingTop: '10px',
paddingBottom: '10px',
'&.Mui-selected': {
color: theme.menuSelected,
backgroundColor: theme.menuSelectedBack,
'&:hover': {
backgroundColor: theme.menuSelectedBack
},
'& .MuiListItemIcon-root': {
color: theme.menuSelected
}
},
'&:hover': {
backgroundColor: theme.menuSelectedBack,
color: theme.menuSelected,
'& .MuiListItemIcon-root': {
color: theme.menuSelected
}
}
}
}
},
MuiListItemIcon: {
styleOverrides: {
root: {
color: theme.darkTextPrimary,
minWidth: '36px'
}
}
},
MuiListItemText: {
styleOverrides: {
primary: {
color: theme.textDark
}
}
},
MuiInputBase: {
styleOverrides: {
input: {
color: theme.textDark,
'&::placeholder': {
color: theme.darkTextSecondary,
fontSize: '0.875rem'
}
}
}
},
MuiOutlinedInput: {
styleOverrides: {
root: {
background: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary800 : bgColor,
borderRadius: `${theme?.customization?.borderRadius}px`,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: theme.colors?.grey400
},
'&:hover $notchedOutline': {
borderColor: theme.colors?.primaryLight
},
'&.MuiInputBase-multiline': {
padding: 1
}
},
input: {
fontWeight: 500,
background: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary800 : bgColor,
padding: '15.5px 14px',
borderRadius: `${theme?.customization?.borderRadius}px`,
'&.MuiInputBase-inputSizeSmall': {
padding: '10px 14px',
'&.MuiInputBase-inputAdornedStart': {
paddingLeft: 0
}
}
},
inputAdornedStart: {
paddingLeft: 4
},
notchedOutline: {
borderRadius: `${theme?.customization?.borderRadius}px`
}
}
},
MuiSlider: {
styleOverrides: {
root: {
'&.Mui-disabled': {
color: theme.colors?.grey300
}
},
mark: {
backgroundColor: theme.paper,
width: '4px'
},
valueLabel: {
color: theme?.colors?.primaryLight
}
}
},
MuiDivider: {
styleOverrides: {
root: {
borderColor: theme.divider,
opacity: 1
}
}
},
MuiAvatar: {
styleOverrides: {
root: {
color: theme.colors?.primaryDark,
background: theme.colors?.primary200
}
}
},
MuiChip: {
styleOverrides: {
root: {
'&.MuiChip-deletable .MuiChip-deleteIcon': {
color: 'inherit'
}
}
}
},
MuiTooltip: {
styleOverrides: {
tooltip: {
color: theme?.customization?.isDarkMode ? theme.colors?.paper : theme.paper,
background: theme.colors?.grey700
}
}
},
MuiAutocomplete: {
styleOverrides: {
option: {
'&:hover': {
background: theme?.customization?.isDarkMode ? '#233345 !important' : ''
}
}
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
import { createTheme } from '@mui/material/styles'
// assets
import colors from 'assets/scss/_themes-vars.module.scss'
// project imports
import componentStyleOverrides from './compStyleOverride'
import themePalette from './palette'
import themeTypography from './typography'
/**
* Represent theme style and structure as per Material-UI
* @param {JsonObject} customization customization parameter object
*/
export const theme = (customization) => {
const color = colors
const themeOption = customization.isDarkMode
? {
colors: color,
heading: color.paper,
paper: color.darkPrimaryLight,
backgroundDefault: color.darkPaper,
background: color.darkPrimaryLight,
darkTextPrimary: color.paper,
darkTextSecondary: color.paper,
textDark: color.paper,
menuSelected: color.darkSecondaryDark,
menuSelectedBack: color.darkSecondaryLight,
divider: color.darkPaper,
customization
}
: {
colors: color,
heading: color.grey900,
paper: color.paper,
backgroundDefault: color.paper,
background: color.primaryLight,
darkTextPrimary: color.grey700,
darkTextSecondary: color.grey500,
textDark: color.grey900,
menuSelected: color.secondaryDark,
menuSelectedBack: color.secondaryLight,
divider: color.grey200,
customization
}
const themeOptions = {
direction: 'ltr',
palette: themePalette(themeOption),
mixins: {
toolbar: {
minHeight: '48px',
padding: '16px',
'@media (min-width: 600px)': {
minHeight: '48px'
}
}
},
typography: themeTypography(themeOption)
}
const themes = createTheme(themeOptions)
themes.components = componentStyleOverrides(themeOption)
return themes
}
export default theme
+96
View File
@@ -0,0 +1,96 @@
/**
* Color intention that you want to used in your theme
* @param {JsonObject} theme Theme customization object
*/
export default function themePalette(theme) {
return {
mode: theme?.customization?.navType,
common: {
black: theme.colors?.darkPaper
},
primary: {
light: theme.customization.isDarkMode ? theme.colors?.darkPrimaryLight : theme.colors?.primaryLight,
main: theme.colors?.primaryMain,
dark: theme.customization.isDarkMode ? theme.colors?.darkPrimaryDark : theme.colors?.primaryDark,
200: theme.customization.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.primary200,
800: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.primary800
},
secondary: {
light: theme.customization.isDarkMode ? theme.colors?.darkSecondaryLight : theme.colors?.secondaryLight,
main: theme.customization.isDarkMode ? theme.colors?.darkSecondaryMain : theme.colors?.secondaryMain,
dark: theme.customization.isDarkMode ? theme.colors?.darkSecondaryDark : theme.colors?.secondaryDark,
200: theme.colors?.secondary200,
800: theme.colors?.secondary800
},
error: {
light: theme.colors?.errorLight,
main: theme.colors?.errorMain,
dark: theme.colors?.errorDark
},
orange: {
light: theme.colors?.orangeLight,
main: theme.colors?.orangeMain,
dark: theme.colors?.orangeDark
},
warning: {
light: theme.colors?.warningLight,
main: theme.colors?.warningMain,
dark: theme.colors?.warningDark
},
success: {
light: theme.colors?.successLight,
200: theme.colors?.success200,
main: theme.colors?.successMain,
dark: theme.colors?.successDark
},
grey: {
50: theme.colors?.grey50,
100: theme.colors?.grey100,
200: theme.colors?.grey200,
300: theme.colors?.grey300,
500: theme.darkTextSecondary,
600: theme.heading,
700: theme.darkTextPrimary,
900: theme.textDark
},
dark: {
light: theme.colors?.darkTextPrimary,
main: theme.colors?.darkLevel1,
dark: theme.colors?.darkLevel2,
800: theme.colors?.darkBackground,
900: theme.colors?.darkPaper
},
text: {
primary: theme.darkTextPrimary,
secondary: theme.darkTextSecondary,
dark: theme.textDark,
hint: theme.colors?.grey100
},
background: {
paper: theme.paper,
default: theme.backgroundDefault
},
card: {
main: theme.customization.isDarkMode ? theme.colors?.darkPrimaryMain : theme.colors?.paper,
light: theme.customization.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.paper,
hover: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.paper
},
asyncSelect: {
main: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.grey50
},
canvasHeader: {
executionLight: theme.colors?.successLight,
executionDark: theme.colors?.successDark,
deployLight: theme.colors?.primaryLight,
deployDark: theme.colors?.primaryDark,
saveLight: theme.colors?.secondaryLight,
saveDark: theme.colors?.secondaryDark,
settingsLight: theme.colors?.grey300,
settingsDark: theme.colors?.grey700
},
codeEditor: {
main: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.primaryLight
}
}
}
+133
View File
@@ -0,0 +1,133 @@
/**
* Typography used in theme
* @param {JsonObject} theme theme customization object
*/
export default function themeTypography(theme) {
return {
fontFamily: theme?.customization?.fontFamily,
h6: {
fontWeight: 500,
color: theme.heading,
fontSize: '0.75rem'
},
h5: {
fontSize: '0.875rem',
color: theme.heading,
fontWeight: 500
},
h4: {
fontSize: '1rem',
color: theme.heading,
fontWeight: 600
},
h3: {
fontSize: '1.25rem',
color: theme.heading,
fontWeight: 600
},
h2: {
fontSize: '1.5rem',
color: theme.heading,
fontWeight: 700
},
h1: {
fontSize: '2.125rem',
color: theme.heading,
fontWeight: 700
},
subtitle1: {
fontSize: '0.875rem',
fontWeight: 500,
color: theme.textDark
},
subtitle2: {
fontSize: '0.75rem',
fontWeight: 400,
color: theme.darkTextSecondary
},
caption: {
fontSize: '0.75rem',
color: theme.darkTextSecondary,
fontWeight: 400
},
body1: {
fontSize: '0.875rem',
fontWeight: 400,
lineHeight: '1.334em'
},
body2: {
letterSpacing: '0em',
fontWeight: 400,
lineHeight: '1.5em',
color: theme.darkTextPrimary
},
button: {
textTransform: 'capitalize'
},
customInput: {
marginTop: 1,
marginBottom: 1,
'& > label': {
top: 23,
left: 0,
color: theme.grey500,
'&[data-shrink="false"]': {
top: 5
}
},
'& > div > input': {
padding: '30.5px 14px 11.5px !important'
},
'& legend': {
display: 'none'
},
'& fieldset': {
top: 0
}
},
mainContent: {
backgroundColor: theme.background,
width: '100%',
minHeight: 'calc(100vh - 75px)',
flexGrow: 1,
padding: '20px',
marginTop: '75px',
marginRight: '20px',
borderRadius: `${theme?.customization?.borderRadius}px`
},
menuCaption: {
fontSize: '0.875rem',
fontWeight: 500,
color: theme.heading,
padding: '6px',
textTransform: 'capitalize',
marginTop: '10px'
},
subMenuCaption: {
fontSize: '0.6875rem',
fontWeight: 500,
color: theme.darkTextSecondary,
textTransform: 'capitalize'
},
commonAvatar: {
cursor: 'pointer',
borderRadius: '8px'
},
smallAvatar: {
width: '22px',
height: '22px',
fontSize: '1rem'
},
mediumAvatar: {
width: '34px',
height: '34px',
fontSize: '1.2rem'
},
largeAvatar: {
width: '44px',
height: '44px',
fontSize: '1.5rem'
}
}
}
@@ -0,0 +1,97 @@
import PropTypes from 'prop-types'
import { forwardRef } from 'react'
// third-party
import { motion, useCycle } from 'framer-motion'
// ==============================|| ANIMATION BUTTON ||============================== //
const AnimateButton = forwardRef(function AnimateButton({ children, type, direction, offset, scale }, ref) {
let offset1
let offset2
switch (direction) {
case 'up':
case 'left':
offset1 = offset
offset2 = 0
break
case 'right':
case 'down':
default:
offset1 = 0
offset2 = offset
break
}
const [x, cycleX] = useCycle(offset1, offset2)
const [y, cycleY] = useCycle(offset1, offset2)
switch (type) {
case 'rotate':
return (
<motion.div
ref={ref}
animate={{ rotate: 360 }}
transition={{
repeat: Infinity,
repeatType: 'loop',
duration: 2,
repeatDelay: 0
}}
>
{children}
</motion.div>
)
case 'slide':
if (direction === 'up' || direction === 'down') {
return (
<motion.div
ref={ref}
animate={{ y: y !== undefined ? y : '' }}
onHoverEnd={() => cycleY()}
onHoverStart={() => cycleY()}
>
{children}
</motion.div>
)
}
return (
<motion.div ref={ref} animate={{ x: x !== undefined ? x : '' }} onHoverEnd={() => cycleX()} onHoverStart={() => cycleX()}>
{children}
</motion.div>
)
case 'scale':
default:
if (typeof scale === 'number') {
scale = {
hover: scale,
tap: scale
}
}
return (
<motion.div ref={ref} whileHover={{ scale: scale?.hover }} whileTap={{ scale: scale?.tap }}>
{children}
</motion.div>
)
}
})
AnimateButton.propTypes = {
children: PropTypes.node,
offset: PropTypes.number,
type: PropTypes.oneOf(['slide', 'scale', 'rotate']),
direction: PropTypes.oneOf(['up', 'down', 'left', 'right']),
scale: PropTypes.oneOfType([PropTypes.number, PropTypes.object])
}
AnimateButton.defaultProps = {
type: 'scale',
offset: 10,
direction: 'right',
scale: {
hover: 1,
tap: 0.9
}
}
export default AnimateButton
@@ -0,0 +1,11 @@
import { styled } from '@mui/material/styles'
import { Button } from '@mui/material'
export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({
color: 'white',
backgroundColor: theme.palette[color].main,
'&:hover': {
backgroundColor: theme.palette[color].main,
backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)`
}
}))
@@ -0,0 +1,11 @@
import { styled } from '@mui/material/styles'
import { Fab } from '@mui/material'
export const StyledFab = styled(Fab)(({ theme, color = 'primary' }) => ({
color: 'white',
backgroundColor: theme.palette[color].main,
'&:hover': {
backgroundColor: theme.palette[color].main,
backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)`
}
}))
@@ -0,0 +1,95 @@
import PropTypes from 'prop-types'
// material-ui
import { styled, useTheme } from '@mui/material/styles'
import { Box, Grid, Chip, Typography } from '@mui/material'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import SkeletonChatflowCard from 'ui-component/cards/Skeleton/ChatflowCard'
const CardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.main,
color: theme.darkTextPrimary,
overflow: 'hidden',
position: 'relative',
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
cursor: 'pointer',
'&:hover': {
background: theme.palette.card.hover,
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 20%)'
}
}))
// ===========================|| CONTRACT CARD ||=========================== //
const ItemCard = ({ isLoading, data, images, onClick }) => {
const theme = useTheme()
const chipSX = {
height: 24,
padding: '0 6px'
}
const activeChatflowSX = {
...chipSX,
color: 'white',
backgroundColor: theme.palette.success.dark
}
return (
<>
{isLoading ? (
<SkeletonChatflowCard />
) : (
<CardWrapper border={false} content={false} onClick={onClick}>
<Box sx={{ p: 2.25 }}>
<Grid container direction='column'>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Typography sx={{ fontSize: '1.5rem', fontWeight: 500 }}>{data.name}</Typography>
</div>
<Grid sx={{ mt: 1, mb: 1 }} container direction='row'>
{data.deployed && (
<Grid item>
<Chip label='Deployed' sx={activeChatflowSX} />
</Grid>
)}
</Grid>
{images && (
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
{images.map((img) => (
<div
key={img}
style={{
width: 40,
height: 40,
marginRight: 5,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }}
alt=''
src={img}
/>
</div>
))}
</div>
)}
</Grid>
</Box>
</CardWrapper>
)}
</>
)
}
ItemCard.propTypes = {
isLoading: PropTypes.bool,
data: PropTypes.object,
images: PropTypes.array,
onClick: PropTypes.func
}
export default ItemCard
@@ -0,0 +1,79 @@
import PropTypes from 'prop-types'
import { forwardRef } from 'react'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Card, CardContent, CardHeader, Divider, Typography } from '@mui/material'
// constant
const headerSX = {
'& .MuiCardHeader-action': { mr: 0 }
}
// ==============================|| CUSTOM MAIN CARD ||============================== //
const MainCard = forwardRef(function MainCard(
{
border = true,
boxShadow,
children,
content = true,
contentClass = '',
contentSX = {},
darkTitle,
secondary,
shadow,
sx = {},
title,
...others
},
ref
) {
const theme = useTheme()
return (
<Card
ref={ref}
{...others}
sx={{
border: border ? '1px solid' : 'none',
borderColor: theme.palette.primary[200] + 75,
':hover': {
boxShadow: boxShadow ? shadow || '0 2px 14px 0 rgb(32 40 45 / 8%)' : 'inherit'
},
...sx
}}
>
{/* card header and action */}
{!darkTitle && title && <CardHeader sx={headerSX} title={title} action={secondary} />}
{darkTitle && title && <CardHeader sx={headerSX} title={<Typography variant='h3'>{title}</Typography>} action={secondary} />}
{/* content & header divider */}
{title && <Divider />}
{/* card content */}
{content && (
<CardContent sx={contentSX} className={contentClass}>
{children}
</CardContent>
)}
{!content && children}
</Card>
)
})
MainCard.propTypes = {
border: PropTypes.bool,
boxShadow: PropTypes.bool,
children: PropTypes.node,
content: PropTypes.bool,
contentClass: PropTypes.string,
contentSX: PropTypes.object,
darkTitle: PropTypes.bool,
secondary: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
shadow: PropTypes.string,
sx: PropTypes.object,
title: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object])
}
export default MainCard
@@ -0,0 +1,32 @@
// material-ui
import { Card, CardContent, Grid } from '@mui/material'
import Skeleton from '@mui/material/Skeleton'
// ==============================|| SKELETON - BRIDGE CARD ||============================== //
const ChatflowCard = () => (
<Card>
<CardContent>
<Grid container direction='column'>
<Grid item>
<Grid container justifyContent='space-between'>
<Grid item>
<Skeleton variant='rectangular' width={44} height={44} />
</Grid>
<Grid item>
<Skeleton variant='rectangular' width={34} height={34} />
</Grid>
</Grid>
</Grid>
<Grid item>
<Skeleton variant='rectangular' sx={{ my: 2 }} height={40} />
</Grid>
<Grid item>
<Skeleton variant='rectangular' height={30} />
</Grid>
</Grid>
</CardContent>
</Card>
)
export default ChatflowCard
@@ -0,0 +1,39 @@
import { createPortal } from 'react-dom'
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'
import useConfirm from 'hooks/useConfirm'
import { StyledButton } from 'ui-component/button/StyledButton'
const ConfirmDialog = () => {
const { onConfirm, onCancel, confirmState } = useConfirm()
const portalElement = document.getElementById('portal')
const component = confirmState.show ? (
<Dialog
fullWidth
maxWidth='xs'
open={confirmState.show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{confirmState.title}
</DialogTitle>
<DialogContent>
<DialogContentText sx={{ color: 'black' }} id='alert-dialog-description'>
{confirmState.description}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>{confirmState.cancelButtonName}</Button>
<StyledButton variant='contained' onClick={onConfirm}>
{confirmState.confirmButtonName}
</StyledButton>
</DialogActions>
</Dialog>
) : null
return createPortal(component, portalElement)
}
export default ConfirmDialog
@@ -0,0 +1,61 @@
import { createPortal } from 'react-dom'
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { Button, Dialog, DialogActions, DialogContent, OutlinedInput, DialogTitle } from '@mui/material'
import { StyledButton } from 'ui-component/button/StyledButton'
const SaveChatflowDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const [chatflowName, setChatflowName] = useState('')
const [isReadyToSave, setIsReadyToSave] = useState(false)
useEffect(() => {
if (chatflowName) setIsReadyToSave(true)
else setIsReadyToSave(false)
}, [chatflowName])
const component = show ? (
<Dialog
open={show}
fullWidth
maxWidth='xs'
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent>
<OutlinedInput
sx={{ mt: 1 }}
id='chatflow-name'
type='text'
fullWidth
placeholder='My New Chatflow'
value={chatflowName}
onChange={(e) => setChatflowName(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
<StyledButton disabled={!isReadyToSave} variant='contained' onClick={() => onConfirm(chatflowName)}>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
</Dialog>
) : null
return createPortal(component, portalElement)
}
SaveChatflowDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default SaveChatflowDialog
@@ -0,0 +1,61 @@
import { useState } from 'react'
import { useSelector } from 'react-redux'
import { Popper, FormControl, TextField, Box, Typography } from '@mui/material'
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
import { styled } from '@mui/material/styles'
import PropTypes from 'prop-types'
const StyledPopper = styled(Popper)({
boxShadow: '0px 8px 10px -5px rgb(0 0 0 / 20%), 0px 16px 24px 2px rgb(0 0 0 / 14%), 0px 6px 30px 5px rgb(0 0 0 / 12%)',
borderRadius: '10px',
[`& .${autocompleteClasses.listbox}`]: {
boxSizing: 'border-box',
'& ul': {
padding: 10,
margin: 10
}
}
})
export const Dropdown = ({ name, value, options, onSelect }) => {
const customization = useSelector((state) => state.customization)
const findMatchingOptions = (options = [], value) => options.find((option) => option.name === value)
const getDefaultOptionValue = () => ''
let [internalValue, setInternalValue] = useState(value ?? 'choose an option')
return (
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
<Autocomplete
id={name}
size='small'
options={options || []}
value={findMatchingOptions(options, internalValue) || getDefaultOptionValue()}
onChange={(e, selection) => {
const value = selection ? selection.name : ''
setInternalValue(value)
onSelect(value)
}}
PopperComponent={StyledPopper}
renderInput={(params) => <TextField {...params} value={internalValue} />}
renderOption={(props, option) => (
<Box component='li' {...props}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant='h5'>{option.label}</Typography>
{option.description && (
<Typography sx={{ color: customization.isDarkMode ? '#9e9e9e' : '' }}>{option.description}</Typography>
)}
</div>
</Box>
)}
/>
</FormControl>
)
}
Dropdown.propTypes = {
name: PropTypes.string,
value: PropTypes.string,
options: PropTypes.array,
onSelect: PropTypes.func
}
@@ -0,0 +1,40 @@
import Editor from 'react-simple-code-editor'
import { highlight, languages } from 'prismjs/components/prism-core'
import 'prismjs/components/prism-clike'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-json'
import 'prismjs/components/prism-markup'
import './prism-dark.css'
import PropTypes from 'prop-types'
import { useTheme } from '@mui/material/styles'
export const DarkCodeEditor = ({ value, placeholder, type, style, onValueChange, onMouseUp, onBlur }) => {
const theme = useTheme()
return (
<Editor
value={value}
placeholder={placeholder}
highlight={(code) => highlight(code, type === 'json' ? languages.json : languages.js)}
padding={10}
onValueChange={onValueChange}
onMouseUp={onMouseUp}
onBlur={onBlur}
style={{
...style,
background: theme.palette.codeEditor.main
}}
textareaClassName='editor__textarea'
/>
)
}
DarkCodeEditor.propTypes = {
value: PropTypes.string,
placeholder: PropTypes.string,
type: PropTypes.string,
style: PropTypes.object,
onValueChange: PropTypes.func,
onMouseUp: PropTypes.func,
onBlur: PropTypes.func
}
@@ -0,0 +1,40 @@
import Editor from 'react-simple-code-editor'
import { highlight, languages } from 'prismjs/components/prism-core'
import 'prismjs/components/prism-clike'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-json'
import 'prismjs/components/prism-markup'
import './prism-light.css'
import PropTypes from 'prop-types'
import { useTheme } from '@mui/material/styles'
export const LightCodeEditor = ({ value, placeholder, type, style, onValueChange, onMouseUp, onBlur }) => {
const theme = useTheme()
return (
<Editor
value={value}
placeholder={placeholder}
highlight={(code) => highlight(code, type === 'json' ? languages.json : languages.js)}
padding={10}
onValueChange={onValueChange}
onMouseUp={onMouseUp}
onBlur={onBlur}
style={{
...style,
background: theme.palette.card.main
}}
textareaClassName='editor__textarea'
/>
)
}
LightCodeEditor.propTypes = {
value: PropTypes.string,
placeholder: PropTypes.string,
type: PropTypes.string,
style: PropTypes.object,
onValueChange: PropTypes.func,
onMouseUp: PropTypes.func,
onBlur: PropTypes.func
}
@@ -0,0 +1,275 @@
pre[class*='language-'],
code[class*='language-'] {
color: #d4d4d4;
font-size: 13px;
text-shadow: none;
font-family: Menlo, Monaco, Consolas, 'Andale Mono', 'Ubuntu Mono', 'Courier New', monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*='language-']::selection,
code[class*='language-']::selection,
pre[class*='language-'] *::selection,
code[class*='language-'] *::selection {
text-shadow: none;
background: #264f78;
}
@media print {
pre[class*='language-'],
code[class*='language-'] {
text-shadow: none;
}
}
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
background: #1e1e1e;
}
:not(pre) > code[class*='language-'] {
padding: 0.1em 0.3em;
border-radius: 0.3em;
color: #db4c69;
background: #1e1e1e;
}
/*********************************************************
* Tokens
*/
.namespace {
opacity: 0.7;
}
.token.doctype .token.doctype-tag {
color: #569cd6;
}
.token.doctype .token.name {
color: #9cdcfe;
}
.token.comment,
.token.prolog {
color: #6a9955;
}
.token.punctuation,
.language-html .language-css .token.punctuation,
.language-html .language-javascript .token.punctuation {
color: #d4d4d4;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.inserted,
.token.unit {
color: #b5cea8;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.deleted {
color: #ce9178;
}
.language-css .token.string.url {
text-decoration: underline;
}
.token.operator,
.token.entity {
color: #d4d4d4;
}
.token.operator.arrow {
color: #569cd6;
}
.token.atrule {
color: #ce9178;
}
.token.atrule .token.rule {
color: #c586c0;
}
.token.atrule .token.url {
color: #9cdcfe;
}
.token.atrule .token.url .token.function {
color: #dcdcaa;
}
.token.atrule .token.url .token.punctuation {
color: #d4d4d4;
}
.token.keyword {
color: #569cd6;
}
.token.keyword.module,
.token.keyword.control-flow {
color: #c586c0;
}
.token.function,
.token.function .token.maybe-class-name {
color: #dcdcaa;
}
.token.regex {
color: #d16969;
}
.token.important {
color: #569cd6;
}
.token.italic {
font-style: italic;
}
.token.constant {
color: #9cdcfe;
}
.token.class-name,
.token.maybe-class-name {
color: #4ec9b0;
}
.token.console {
color: #9cdcfe;
}
.token.parameter {
color: #9cdcfe;
}
.token.interpolation {
color: #9cdcfe;
}
.token.punctuation.interpolation-punctuation {
color: #569cd6;
}
.token.boolean {
color: #569cd6;
}
.token.property,
.token.variable,
.token.imports .token.maybe-class-name,
.token.exports .token.maybe-class-name {
color: #9cdcfe;
}
.token.selector {
color: #d7ba7d;
}
.token.escape {
color: #d7ba7d;
}
.token.tag {
color: #569cd6;
}
.token.tag .token.punctuation {
color: #808080;
}
.token.cdata {
color: #808080;
}
.token.attr-name {
color: #9cdcfe;
}
.token.attr-value,
.token.attr-value .token.punctuation {
color: #ce9178;
}
.token.attr-value .token.punctuation.attr-equals {
color: #d4d4d4;
}
.token.entity {
color: #569cd6;
}
.token.namespace {
color: #4ec9b0;
}
/*********************************************************
* Language Specific
*/
pre[class*='language-javascript'],
code[class*='language-javascript'],
pre[class*='language-jsx'],
code[class*='language-jsx'],
pre[class*='language-typescript'],
code[class*='language-typescript'],
pre[class*='language-tsx'],
code[class*='language-tsx'] {
color: #9cdcfe;
}
pre[class*='language-css'],
code[class*='language-css'] {
color: #ce9178;
}
pre[class*='language-html'],
code[class*='language-html'] {
color: #d4d4d4;
}
.language-regex .token.anchor {
color: #dcdcaa;
}
.language-html .token.punctuation {
color: #808080;
}
/*********************************************************
* Line highlighting
*/
pre[class*='language-'] > code[class*='language-'] {
position: relative;
z-index: 1;
}
.line-highlight.line-highlight {
background: #f7ebc6;
box-shadow: inset 5px 0 0 #f7d87c;
z-index: 0;
}
@@ -0,0 +1,207 @@
code[class*='language-'],
pre[class*='language-'] {
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
color: #90a4ae;
background: #fafafa;
font-family: Roboto Mono, monospace;
font-size: 1em;
line-height: 1.5em;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
code[class*='language-']::-moz-selection,
pre[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection,
pre[class*='language-'] ::-moz-selection {
background: #cceae7;
color: #263238;
}
code[class*='language-']::selection,
pre[class*='language-']::selection,
code[class*='language-'] ::selection,
pre[class*='language-'] ::selection {
background: #cceae7;
color: #263238;
}
:not(pre) > code[class*='language-'] {
white-space: normal;
border-radius: 0.2em;
padding: 0.1em;
}
pre[class*='language-'] {
overflow: auto;
position: relative;
margin: 0.5em 0;
padding: 1.25em 1em;
}
.language-css > code,
.language-sass > code,
.language-scss > code {
color: #f76d47;
}
[class*='language-'] .namespace {
opacity: 0.7;
}
.token.atrule {
color: #7c4dff;
}
.token.attr-name {
color: #39adb5;
}
.token.attr-value {
color: #f6a434;
}
.token.attribute {
color: #f6a434;
}
.token.boolean {
color: #7c4dff;
}
.token.builtin {
color: #39adb5;
}
.token.cdata {
color: #39adb5;
}
.token.char {
color: #39adb5;
}
.token.class {
color: #39adb5;
}
.token.class-name {
color: #6182b8;
}
.token.comment {
color: #aabfc9;
}
.token.constant {
color: #7c4dff;
}
.token.deleted {
color: #e53935;
}
.token.doctype {
color: #aabfc9;
}
.token.entity {
color: #e53935;
}
.token.function {
color: #7c4dff;
}
.token.hexcode {
color: #f76d47;
}
.token.id {
color: #7c4dff;
font-weight: bold;
}
.token.important {
color: #7c4dff;
font-weight: bold;
}
.token.inserted {
color: #39adb5;
}
.token.keyword {
color: #7c4dff;
}
.token.number {
color: #f76d47;
}
.token.operator {
color: #39adb5;
}
.token.prolog {
color: #aabfc9;
}
.token.property {
color: #39adb5;
}
.token.pseudo-class {
color: #f6a434;
}
.token.pseudo-element {
color: #f6a434;
}
.token.punctuation {
color: #39adb5;
}
.token.regex {
color: #6182b8;
}
.token.selector {
color: #e53935;
}
.token.string {
color: #f6a434;
}
.token.symbol {
color: #7c4dff;
}
.token.tag {
color: #e53935;
}
.token.unit {
color: #f76d47;
}
.token.url {
color: #e53935;
}
.token.variable {
color: #e53935;
}
@@ -0,0 +1,72 @@
import PropTypes from 'prop-types'
// material-ui
import { useTheme } from '@mui/material/styles'
import MuiAvatar from '@mui/material/Avatar'
// ==============================|| AVATAR ||============================== //
const Avatar = ({ color, outline, size, sx, ...others }) => {
const theme = useTheme()
const colorSX = color && !outline && { color: theme.palette.background.paper, bgcolor: `${color}.main` }
const outlineSX = outline && {
color: color ? `${color}.main` : `primary.main`,
bgcolor: theme.palette.background.paper,
border: '2px solid',
borderColor: color ? `${color}.main` : `primary.main`
}
let sizeSX = {}
switch (size) {
case 'badge':
sizeSX = {
width: theme.spacing(3.5),
height: theme.spacing(3.5)
}
break
case 'xs':
sizeSX = {
width: theme.spacing(4.25),
height: theme.spacing(4.25)
}
break
case 'sm':
sizeSX = {
width: theme.spacing(5),
height: theme.spacing(5)
}
break
case 'lg':
sizeSX = {
width: theme.spacing(9),
height: theme.spacing(9)
}
break
case 'xl':
sizeSX = {
width: theme.spacing(10.25),
height: theme.spacing(10.25)
}
break
case 'md':
sizeSX = {
width: theme.spacing(7.5),
height: theme.spacing(7.5)
}
break
default:
sizeSX = {}
}
return <MuiAvatar sx={{ ...colorSX, ...outlineSX, ...sizeSX, ...sx }} {...others} />
}
Avatar.propTypes = {
className: PropTypes.string,
color: PropTypes.string,
outline: PropTypes.bool,
size: PropTypes.string,
sx: PropTypes.object
}
export default Avatar
@@ -0,0 +1,184 @@
import PropTypes from 'prop-types'
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Box, Card, Divider, Grid, Typography } from '@mui/material'
import MuiBreadcrumbs from '@mui/material/Breadcrumbs'
// project imports
import config from 'config'
import { gridSpacing } from 'store/constant'
// assets
import { IconTallymark1 } from '@tabler/icons'
import AccountTreeTwoToneIcon from '@mui/icons-material/AccountTreeTwoTone'
import HomeIcon from '@mui/icons-material/Home'
import HomeTwoToneIcon from '@mui/icons-material/HomeTwoTone'
const linkSX = {
display: 'flex',
color: 'grey.900',
textDecoration: 'none',
alignContent: 'center',
alignItems: 'center'
}
// ==============================|| BREADCRUMBS ||============================== //
const Breadcrumbs = ({ card, divider, icon, icons, maxItems, navigation, rightAlign, separator, title, titleBottom, ...others }) => {
const theme = useTheme()
const iconStyle = {
marginRight: theme.spacing(0.75),
marginTop: `-${theme.spacing(0.25)}`,
width: '1rem',
height: '1rem',
color: theme.palette.secondary.main
}
const [main, setMain] = useState()
const [item, setItem] = useState()
// set active item state
const getCollapse = (menu) => {
if (menu.children) {
menu.children.filter((collapse) => {
if (collapse.type && collapse.type === 'collapse') {
getCollapse(collapse)
} else if (collapse.type && collapse.type === 'item') {
if (document.location.pathname === config.basename + collapse.url) {
setMain(menu)
setItem(collapse)
}
}
return false
})
}
}
useEffect(() => {
navigation?.items?.map((menu) => {
if (menu.type && menu.type === 'group') {
getCollapse(menu)
}
return false
})
})
// item separator
const SeparatorIcon = separator
const separatorIcon = separator ? <SeparatorIcon stroke={1.5} size='1rem' /> : <IconTallymark1 stroke={1.5} size='1rem' />
let mainContent
let itemContent
let breadcrumbContent = <Typography />
let itemTitle = ''
let CollapseIcon
let ItemIcon
// collapse item
if (main && main.type === 'collapse') {
CollapseIcon = main.icon ? main.icon : AccountTreeTwoToneIcon
mainContent = (
<Typography component={Link} to='#' variant='subtitle1' sx={linkSX}>
{icons && <CollapseIcon style={iconStyle} />}
{main.title}
</Typography>
)
}
// items
if (item && item.type === 'item') {
itemTitle = item.title
ItemIcon = item.icon ? item.icon : AccountTreeTwoToneIcon
itemContent = (
<Typography
variant='subtitle1'
sx={{
display: 'flex',
textDecoration: 'none',
alignContent: 'center',
alignItems: 'center',
color: 'grey.500'
}}
>
{icons && <ItemIcon style={iconStyle} />}
{itemTitle}
</Typography>
)
// main
if (item.breadcrumbs !== false) {
breadcrumbContent = (
<Card
sx={{
border: 'none'
}}
{...others}
>
<Box sx={{ p: 2, pl: card === false ? 0 : 2 }}>
<Grid
container
direction={rightAlign ? 'row' : 'column'}
justifyContent={rightAlign ? 'space-between' : 'flex-start'}
alignItems={rightAlign ? 'center' : 'flex-start'}
spacing={1}
>
{title && !titleBottom && (
<Grid item>
<Typography variant='h3' sx={{ fontWeight: 500 }}>
{item.title}
</Typography>
</Grid>
)}
<Grid item>
<MuiBreadcrumbs
sx={{ '& .MuiBreadcrumbs-separator': { width: 16, ml: 1.25, mr: 1.25 } }}
aria-label='breadcrumb'
maxItems={maxItems || 8}
separator={separatorIcon}
>
<Typography component={Link} to='/' color='inherit' variant='subtitle1' sx={linkSX}>
{icons && <HomeTwoToneIcon sx={iconStyle} />}
{icon && <HomeIcon sx={{ ...iconStyle, mr: 0 }} />}
{!icon && 'Dashboard'}
</Typography>
{mainContent}
{itemContent}
</MuiBreadcrumbs>
</Grid>
{title && titleBottom && (
<Grid item>
<Typography variant='h3' sx={{ fontWeight: 500 }}>
{item.title}
</Typography>
</Grid>
)}
</Grid>
</Box>
{card === false && divider !== false && <Divider sx={{ borderColor: theme.palette.primary.main, mb: gridSpacing }} />}
</Card>
)
}
}
return breadcrumbContent
}
Breadcrumbs.propTypes = {
card: PropTypes.bool,
divider: PropTypes.bool,
icon: PropTypes.bool,
icons: PropTypes.bool,
maxItems: PropTypes.number,
navigation: PropTypes.object,
rightAlign: PropTypes.bool,
separator: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
title: PropTypes.bool,
titleBottom: PropTypes.bool
}
export default Breadcrumbs
@@ -0,0 +1,22 @@
import logo from 'assets/images/flowise_logo.png'
import logoDark from 'assets/images/flowise_logo_dark.png'
import { useSelector } from 'react-redux'
// ==============================|| LOGO ||============================== //
const Logo = () => {
const customization = useSelector((state) => state.customization)
return (
<div style={{ alignItems: 'center', display: 'flex', flexDirection: 'row' }}>
<img
style={{ objectFit: 'contain', height: 'auto', width: 150 }}
src={customization.isDarkMode ? logoDark : logo}
alt='Flowise'
/>
</div>
)
}
export default Logo
@@ -0,0 +1,107 @@
import PropTypes from 'prop-types'
import { forwardRef } from 'react'
// material-ui
import { Collapse, Fade, Box, Grow, Slide, Zoom } from '@mui/material'
// ==============================|| TRANSITIONS ||============================== //
const Transitions = forwardRef(function Transitions({ children, position, type, direction, ...others }, ref) {
let positionSX = {
transformOrigin: '0 0 0'
}
switch (position) {
case 'top-right':
positionSX = {
transformOrigin: 'top right'
}
break
case 'top':
positionSX = {
transformOrigin: 'top'
}
break
case 'bottom-left':
positionSX = {
transformOrigin: 'bottom left'
}
break
case 'bottom-right':
positionSX = {
transformOrigin: 'bottom right'
}
break
case 'bottom':
positionSX = {
transformOrigin: 'bottom'
}
break
case 'top-left':
default:
positionSX = {
transformOrigin: '0 0 0'
}
break
}
return (
<Box ref={ref}>
{type === 'grow' && (
<Grow {...others}>
<Box sx={positionSX}>{children}</Box>
</Grow>
)}
{type === 'collapse' && (
<Collapse {...others} sx={positionSX}>
{children}
</Collapse>
)}
{type === 'fade' && (
<Fade
{...others}
timeout={{
appear: 500,
enter: 600,
exit: 400
}}
>
<Box sx={positionSX}>{children}</Box>
</Fade>
)}
{type === 'slide' && (
<Slide
{...others}
timeout={{
appear: 0,
enter: 400,
exit: 200
}}
direction={direction}
>
<Box sx={positionSX}>{children}</Box>
</Slide>
)}
{type === 'zoom' && (
<Zoom {...others}>
<Box sx={positionSX}>{children}</Box>
</Zoom>
)}
</Box>
)
})
Transitions.propTypes = {
children: PropTypes.node,
type: PropTypes.oneOf(['grow', 'fade', 'collapse', 'slide', 'zoom']),
position: PropTypes.oneOf(['top-left', 'top-right', 'top', 'bottom-left', 'bottom-right', 'bottom']),
direction: PropTypes.oneOf(['up', 'down', 'left', 'right'])
}
Transitions.defaultProps = {
type: 'grow',
position: 'top-left',
direction: 'up'
}
export default Transitions
@@ -0,0 +1,32 @@
import { useState } from 'react'
import PropTypes from 'prop-types'
import { FormControl, OutlinedInput } from '@mui/material'
export const Input = ({ inputParam, value, onChange }) => {
const [myValue, setMyValue] = useState(value ?? '')
return (
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
<OutlinedInput
id={inputParam.name}
size='small'
type={inputParam.type === 'string' ? 'text' : inputParam.type}
placeholder={inputParam.placeholder}
multiline={!!inputParam.rows}
maxRows={inputParam.rows || 0}
minRows={inputParam.rows || 0}
value={myValue}
name={inputParam.name}
onChange={(e) => {
setMyValue(e.target.value)
onChange(e.target.value)
}}
/>
</FormControl>
)
}
Input.propTypes = {
inputParam: PropTypes.object,
value: PropTypes.string,
onChange: PropTypes.func
}
@@ -0,0 +1,17 @@
import { Suspense } from 'react'
// project imports
import Loader from './Loader'
// ==============================|| LOADABLE - LAZY LOADING ||============================== //
const Loadable = (Component) =>
function WithLoader(props) {
return (
<Suspense fallback={<Loader />}>
<Component {...props} />
</Suspense>
)
}
export default Loadable
@@ -0,0 +1,21 @@
// material-ui
import LinearProgress from '@mui/material/LinearProgress'
import { styled } from '@mui/material/styles'
// styles
const LoaderWrapper = styled('div')({
position: 'fixed',
top: 0,
left: 0,
zIndex: 1301,
width: '100%'
})
// ==============================|| LOADER ||============================== //
const Loader = () => (
<LoaderWrapper>
<LinearProgress color='primary' />
</LoaderWrapper>
)
export default Loader
@@ -0,0 +1,25 @@
import { Info } from '@mui/icons-material'
import { IconButton, Tooltip } from '@mui/material'
import parser from 'html-react-parser'
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
export const TooltipWithParser = ({ title }) => {
const customization = useSelector((state) => state.customization)
return (
<Tooltip title={parser(title)} placement='right'>
<div style={{ display: 'flex', alignItems: 'center' }}>
<IconButton sx={{ height: 25, width: 25 }}>
<Info
style={{ background: 'transparent', color: customization.isDarkMode ? 'white' : 'inherit', height: 18, width: 18 }}
/>
</IconButton>
</div>
</Tooltip>
)
}
TooltipWithParser.propTypes = {
title: PropTypes.node
}
+233
View File
@@ -0,0 +1,233 @@
import moment from 'moment'
export const getUniqueNodeId = (nodeData, nodes) => {
// Get amount of same nodes
let totalSameNodes = 0
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i]
if (node.data.name === nodeData.name) {
totalSameNodes += 1
}
}
// Get unique id
let nodeId = `${nodeData.name}_${totalSameNodes}`
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i]
if (node.id === nodeId) {
totalSameNodes += 1
nodeId = `${nodeData.name}_${totalSameNodes}`
}
}
return nodeId
}
export const initializeNodeData = (nodeParams) => {
const initialValues = {}
for (let i = 0; i < nodeParams.length; i += 1) {
const input = nodeParams[i]
// Load from nodeParams default values
initialValues[input.name] = input.default || ''
// Special case for array, always initialize the item if default is not set
if (input.type === 'array' && !input.default) {
const newObj = {}
for (let j = 0; j < input.array.length; j += 1) {
newObj[input.array[j].name] = input.array[j].default || ''
}
initialValues[input.name] = [newObj]
}
}
return initialValues
}
export const initNode = (nodeData, newNodeId) => {
const inputAnchors = []
const incoming = nodeData.inputs ? nodeData.inputs.length : 0
const outgoing = 1
const whitelistTypes = ['asyncOptions', 'options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder']
for (let i = 0; i < incoming; i += 1) {
if (whitelistTypes.includes(nodeData.inputs[i].type)) continue
const newInput = {
...nodeData.inputs[i],
id: `${newNodeId}-input-${nodeData.inputs[i].name}-${nodeData.inputs[i].type}`
}
inputAnchors.push(newInput)
}
const outputAnchors = []
for (let i = 0; i < outgoing; i += 1) {
const newOutput = {
id: `${newNodeId}-output-${nodeData.name}-${nodeData.baseClasses.join('|')}`,
name: nodeData.name,
label: nodeData.type,
type: nodeData.baseClasses.join(' | ')
}
outputAnchors.push(newOutput)
}
nodeData.id = newNodeId
nodeData.inputAnchors = inputAnchors
nodeData.outputAnchors = outputAnchors
/*
Initial inputs = [
{
label: 'field_label',
name: 'field'
}
]
// Turn into inputs object with default values
Converted inputs = { 'field': 'defaultvalue' }
// Move remaining inputs that are not part of inputAnchors to inputParams
inputParams = [
{
label: 'field_label',
name: 'field'
}
]
*/
if (nodeData.inputs) {
nodeData.inputParams = nodeData.inputs.filter(({ name }) => !nodeData.inputAnchors.some((exclude) => exclude.name === name))
nodeData.inputs = initializeNodeData(nodeData.inputs)
} else {
nodeData.inputParams = []
nodeData.inputs = {}
}
return nodeData
}
export const getEdgeLabelName = (source) => {
const sourceSplit = source.split('-')
if (sourceSplit.length && sourceSplit[0].includes('ifElse')) {
const outputAnchorsIndex = sourceSplit[sourceSplit.length - 1]
return outputAnchorsIndex === '0' ? 'true' : 'false'
}
return ''
}
export const isValidConnection = (connection, reactFlowInstance) => {
const sourceHandle = connection.sourceHandle
const targetHandle = connection.targetHandle
const target = connection.target
//sourceHandle: "llmChain_0-output-llmChain-BaseChain"
//targetHandle: "mrlkAgentLLM_0-input-model-BaseLanguageModel"
const sourceTypes = sourceHandle.split('-')[sourceHandle.split('-').length - 1].split('|')
const targetTypes = targetHandle.split('-')[targetHandle.split('-').length - 1].split('|')
if (targetTypes.some((t) => sourceTypes.includes(t))) {
let targetNode = reactFlowInstance.getNode(target)
if (!targetNode) {
if (!reactFlowInstance.getEdges().find((e) => e.targetHandle === targetHandle)) {
return true
}
} else {
const targetNodeInputAnchor = targetNode.data.inputAnchors.find((ancr) => ancr.id === targetHandle)
if (
(targetNodeInputAnchor &&
!targetNodeInputAnchor?.list &&
!reactFlowInstance.getEdges().find((e) => e.targetHandle === targetHandle)) ||
targetNodeInputAnchor?.list
) {
return true
}
}
}
return false
}
export const convertDateStringToDateObject = (dateString) => {
if (dateString === undefined || !dateString) return undefined
const date = moment(dateString)
if (!date.isValid) return undefined
// Sat Sep 24 2022 07:30:14
return new Date(date.year(), date.month(), date.date(), date.hours(), date.minutes())
}
export const getFileName = (fileBase64) => {
const splitDataURI = fileBase64.split(',')
const filename = splitDataURI[splitDataURI.length - 1].split(':')[1]
return filename
}
export const getFolderName = (base64ArrayStr) => {
try {
const base64Array = JSON.parse(base64ArrayStr)
const filenames = []
for (let i = 0; i < base64Array.length; i += 1) {
const fileBase64 = base64Array[i]
const splitDataURI = fileBase64.split(',')
const filename = splitDataURI[splitDataURI.length - 1].split(':')[1]
filenames.push(filename)
}
return filenames.length ? filenames.join(',') : ''
} catch (e) {
return ''
}
}
export const generateExportFlowData = (flowData) => {
const nodes = flowData.nodes
const edges = flowData.edges
for (let i = 0; i < nodes.length; i += 1) {
nodes[i].selected = false
const node = nodes[i]
const newNodeData = {
id: node.data.id,
label: node.data.label,
name: node.data.name,
type: node.data.type,
baseClasses: node.data.baseClasses,
category: node.data.category,
description: node.data.description,
inputParams: node.data.inputParams,
inputAnchors: node.data.inputAnchors,
inputs: {},
outputAnchors: node.data.outputAnchors,
selected: false
}
// Remove password
if (node.data.inputs && Object.keys(node.data.inputs).length) {
const nodeDataInputs = {}
for (const input in node.data.inputs) {
const inputParam = node.data.inputParams.find((inp) => inp.name === input)
if (inputParam && inputParam.type === 'password') continue
nodeDataInputs[input] = node.data.inputs[input]
}
newNodeData.inputs = nodeDataInputs
}
nodes[i].data = newNodeData
}
const exportJson = {
nodes,
edges
}
return exportJson
}
export const copyToClipboard = (e) => {
const src = e.src
if (Array.isArray(src) || typeof src === 'object') {
navigator.clipboard.writeText(JSON.stringify(src, null, ' '))
} else {
navigator.clipboard.writeText(src)
}
}
+56
View File
@@ -0,0 +1,56 @@
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useSnackbar } from 'notistack'
import { removeSnackbar } from 'store/actions'
let displayed = []
const useNotifier = () => {
const dispatch = useDispatch()
const notifier = useSelector((state) => state.notifier)
const { notifications } = notifier
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const storeDisplayed = (id) => {
displayed = [...displayed, id]
}
const removeDisplayed = (id) => {
displayed = [...displayed.filter((key) => id !== key)]
}
React.useEffect(() => {
notifications.forEach(({ key, message, options = {}, dismissed = false }) => {
if (dismissed) {
// dismiss snackbar using notistack
closeSnackbar(key)
return
}
// do nothing if snackbar is already displayed
if (displayed.includes(key)) return
// display snackbar using notistack
enqueueSnackbar(message, {
key,
...options,
onClose: (event, reason, myKey) => {
if (options.onClose) {
options.onClose(event, reason, myKey)
}
},
onExited: (event, myKey) => {
// remove this snackbar from redux store
dispatch(removeSnackbar(myKey))
removeDisplayed(myKey)
}
})
// keep track of snackbars that we've displayed
storeDisplayed(key)
})
}, [notifications, closeSnackbar, enqueueSnackbar, dispatch])
}
export default useNotifier
+37
View File
@@ -0,0 +1,37 @@
import { useCallback, useContext, useEffect } from 'react'
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom'
// https://stackoverflow.com/questions/71572678/react-router-v-6-useprompt-typescript
export function useBlocker(blocker, when = true) {
const { navigator } = useContext(NavigationContext)
useEffect(() => {
if (!when) return
const unblock = navigator.block((tx) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock()
tx.retry()
}
}
blocker(autoUnblockingTx)
})
return unblock
}, [navigator, blocker, when])
}
export function usePrompt(message, when = true) {
const blocker = useCallback(
(tx) => {
if (window.confirm(message)) tx.retry()
},
[message]
)
useBlocker(blocker, when)
}
+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