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