Initial push

This commit is contained in:
Henry
2023-04-06 22:17:34 +01:00
commit 05c86ff9c5
162 changed files with 9112 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
/tests
/src
/public
!build
yarn-debug.log*
yarn-error.log*
.eslintrc
.prettierignore
.prettierrc
jsconfig.json
+17
View File
@@ -0,0 +1,17 @@
<!-- markdownlint-disable MD030 -->
# Flowise UI
React frontend ui for Flowise.
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise.gif?raw=true)
Install:
```bash
npm i flowise-ui
```
## License
Source code in this repository is made available under the [MIT License](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md).
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"baseUrl": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
+76
View File
@@ -0,0 +1,76 @@
{
"name": "flowise-ui",
"version": "1.0.1",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://flowiseai.com",
"author": {
"name": "HenryHeng",
"email": "henryheng@flowiseai.com"
},
"dependencies": {
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.0.3",
"@mui/material": "^5.11.12",
"@tabler/icons": "^1.39.1",
"clsx": "^1.1.1",
"formik": "^2.2.6",
"framer-motion": "^4.1.13",
"history": "^5.0.0",
"html-react-parser": "^3.0.4",
"lodash": "^4.17.21",
"moment": "^2.29.3",
"notistack": "^2.0.4",
"prismjs": "^1.28.0",
"prop-types": "^15.7.2",
"react": "^18.2.0",
"react-datepicker": "^4.8.0",
"react-device-detect": "^1.17.0",
"react-dom": "^18.2.0",
"react-json-view": "^1.21.3",
"react-markdown": "^8.0.6",
"react-perfect-scrollbar": "^1.5.8",
"react-redux": "^8.0.5",
"react-router": "~6.3.0",
"react-router-dom": "~6.3.0",
"react-simple-code-editor": "^0.11.2",
"reactflow": "^11.5.6",
"redux": "^4.0.5",
"yup": "^0.32.9"
},
"scripts": {
"start": "react-scripts start",
"dev": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"babel": {
"presets": [
"@babel/preset-react"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/eslint-parser": "^7.15.8",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^12.8.3",
"pretty-quick": "^3.1.3",
"react-scripts": "^5.0.1",
"sass": "^1.42.1",
"typescript": "^4.8.4"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+62
View File
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Flowise - LangchainJS UI</title>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- Meta Tags-->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#2296f3" />
<meta name="title" content="Flowise - LangchainJS UI" />
<meta name="description" content="Flowise helps you to better integrate Web3 with existing Web2 applications" />
<meta name="keywords" content="react, material-ui, reactjs, reactjs, workflow automation, web3, web2, blockchain" />
<meta name="author" content="CodedThemes" />
<!-- Open Graph / Facebook -->
<meta property="og:locale" content="en_US" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://flowiseai.com/" />
<meta property="og:site_name" content="flowiseai.com" />
<meta property="article:publisher" content="https://www.facebook.com/codedthemes" />
<meta property="og:title" content="Flowise - LangchainJS UI" />
<meta property="og:description" content="Flowise helps you to better build LLM flows using Langchain in simple GUI" />
<meta property="og:image" content="https://flowiseai.com/og-image/og-facebook.png" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://flowiseai.com" />
<meta property="twitter:title" content="Flowise - LangchainJS UI" />
<meta property="twitter:description" content="Flowise helps you to better build LLM flows using Langchain in simple GUI" />
<meta property="twitter:image" content="https://flowiseai.com/og-image/og-twitter.png" />
<meta name="twitter:creator" content="@codedthemes" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="portal"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
import { useSelector } from 'react-redux'
import { ThemeProvider } from '@mui/material/styles'
import { CssBaseline, StyledEngineProvider } from '@mui/material'
// routing
import Routes from 'routes'
// defaultTheme
import themes from 'themes'
// project imports
import NavigationScroll from 'layout/NavigationScroll'
// ==============================|| APP ||============================== //
const App = () => {
const customization = useSelector((state) => state.customization)
return (
<StyledEngineProvider injectFirst>
<ThemeProvider theme={themes(customization)}>
<CssBaseline />
<NavigationScroll>
<Routes />
</NavigationScroll>
</ThemeProvider>
</StyledEngineProvider>
)
}
export default App
+19
View File
@@ -0,0 +1,19 @@
import client from './client'
const getAllChatflows = () => client.get('/chatflows')
const getSpecificChatflow = (id) => client.get(`/chatflows/${id}`)
const createNewChatflow = (body) => client.post(`/chatflows`, body)
const updateChatflow = (id, body) => client.put(`/chatflows/${id}`, body)
const deleteChatflow = (id) => client.delete(`/chatflows/${id}`)
export default {
getAllChatflows,
getSpecificChatflow,
createNewChatflow,
updateChatflow,
deleteChatflow
}
+13
View File
@@ -0,0 +1,13 @@
import client from './client'
const getChatmessageFromChatflow = (id) => client.get(`/chatmessage/${id}`)
const createNewChatmessage = (id, body) => client.post(`/chatmessage/${id}`, body)
const deleteChatmessage = (id) => client.delete(`/chatmessage/${id}`)
export default {
getChatmessageFromChatflow,
createNewChatmessage,
deleteChatmessage
}
+11
View File
@@ -0,0 +1,11 @@
import axios from 'axios'
import { baseURL } from 'store/constant'
const apiClient = axios.create({
baseURL: `${baseURL}/api/v1`,
headers: {
'Content-type': 'application/json'
}
})
export default apiClient
+10
View File
@@ -0,0 +1,10 @@
import client from './client'
const getAllNodes = () => client.get('/nodes')
const getSpecificNode = (name) => client.get(`/nodes/${name}`)
export default {
getAllNodes,
getSpecificNode
}
+7
View File
@@ -0,0 +1,7 @@
import client from './client'
const sendMessageAndGetPrediction = (id, input) => client.post(`/prediction/${id}`, input)
export default {
sendMessageAndGetPrediction
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,157 @@
// paper & background
$paper: #ffffff;
// primary
$primaryLight: #e3f2fd;
$primaryMain: #2196f3;
$primaryDark: #1e88e5;
$primary200: #90caf9;
$primary800: #1565c0;
// secondary
$secondaryLight: #ede7f6;
$secondaryMain: #673ab7;
$secondaryDark: #5e35b1;
$secondary200: #b39ddb;
$secondary800: #4527a0;
// success Colors
$successLight: #cdf5d8;
$success200: #69f0ae;
$successMain: #00e676;
$successDark: #00c853;
// error
$errorLight: #f3d2d2;
$errorMain: #f44336;
$errorDark: #c62828;
// orange
$orangeLight: #fbe9e7;
$orangeMain: #ffab91;
$orangeDark: #d84315;
// warning
$warningLight: #fff8e1;
$warningMain: #ffe57f;
$warningDark: #ffc107;
// grey
$grey50: #fafafa;
$grey100: #f5f5f5;
$grey200: #eeeeee;
$grey300: #e0e0e0;
$grey500: #9e9e9e;
$grey600: #757575;
$grey700: #616161;
$grey900: #212121;
// ==============================|| DARK THEME VARIANTS ||============================== //
// paper & background
$darkBackground: #191b1f;
$darkPaper: #191b1f;
// dark 800 & 900
$darkLevel1: #252525; // level 1
$darkLevel2: #242424; // level 2
// primary dark
$darkPrimaryLight: #23262c;
$darkPrimaryMain: #23262c;
$darkPrimaryDark: #191b1f;
$darkPrimary200: #c9d4e9;
$darkPrimary800: #32353b;
// secondary dark
$darkSecondaryLight: #454c59;
$darkSecondaryMain: #7c4dff;
$darkSecondaryDark: #ffffff;
$darkSecondary200: #32353b;
$darkSecondary800: #6200ea;
// text variants
$darkTextTitle: #d7dcec;
$darkTextPrimary: #bdc8f0;
$darkTextSecondary: #8492c4;
// ==============================|| JAVASCRIPT ||============================== //
:export {
// paper & background
paper: $paper;
// primary
primaryLight: $primaryLight;
primary200: $primary200;
primaryMain: $primaryMain;
primaryDark: $primaryDark;
primary800: $primary800;
// secondary
secondaryLight: $secondaryLight;
secondary200: $secondary200;
secondaryMain: $secondaryMain;
secondaryDark: $secondaryDark;
secondary800: $secondary800;
// success
successLight: $successLight;
success200: $success200;
successMain: $successMain;
successDark: $successDark;
// error
errorLight: $errorLight;
errorMain: $errorMain;
errorDark: $errorDark;
// orange
orangeLight: $orangeLight;
orangeMain: $orangeMain;
orangeDark: $orangeDark;
// warning
warningLight: $warningLight;
warningMain: $warningMain;
warningDark: $warningDark;
// grey
grey50: $grey50;
grey100: $grey100;
grey200: $grey200;
grey300: $grey300;
grey500: $grey500;
grey600: $grey600;
grey700: $grey700;
grey900: $grey900;
// ==============================|| DARK THEME VARIANTS ||============================== //
// paper & background
darkPaper: $darkPaper;
darkBackground: $darkBackground;
// dark 800 & 900
darkLevel1: $darkLevel1;
darkLevel2: $darkLevel2;
// text variants
darkTextTitle: $darkTextTitle;
darkTextPrimary: $darkTextPrimary;
darkTextSecondary: $darkTextSecondary;
// primary dark
darkPrimaryLight: $darkPrimaryLight;
darkPrimaryMain: $darkPrimaryMain;
darkPrimaryDark: $darkPrimaryDark;
darkPrimary200: $darkPrimary200;
darkPrimary800: $darkPrimary800;
// secondary dark
darkSecondaryLight: $darkSecondaryLight;
darkSecondaryMain: $darkSecondaryMain;
darkSecondaryDark: $darkSecondaryDark;
darkSecondary200: $darkSecondary200;
darkSecondary800: $darkSecondary800;
}
+122
View File
@@ -0,0 +1,122 @@
// color variants
@import 'themes-vars.module.scss';
// third-party
@import '~react-perfect-scrollbar/dist/css/styles.css';
// ==============================|| LIGHT BOX ||============================== //
.fullscreen .react-images__blanket {
z-index: 1200;
}
// ==============================|| PERFECT SCROLLBAR ||============================== //
.scrollbar-container {
.ps__rail-y {
&:hover > .ps__thumb-y,
&:focus > .ps__thumb-y,
&.ps--clicking .ps__thumb-y {
background-color: $grey500;
width: 5px;
}
}
.ps__thumb-y {
background-color: $grey500;
border-radius: 6px;
width: 5px;
right: 0;
}
}
.scrollbar-container.ps,
.scrollbar-container > .ps {
&.ps--active-y > .ps__rail-y {
width: 5px;
background-color: transparent !important;
z-index: 999;
&:hover,
&.ps--clicking {
width: 5px;
background-color: transparent;
}
}
&.ps--scrolling-y > .ps__rail-y,
&.ps--scrolling-x > .ps__rail-x {
opacity: 0.4;
background-color: transparent;
}
}
// ==============================|| ANIMATION KEYFRAMES ||============================== //
@keyframes wings {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0px);
}
}
@keyframes blink {
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes bounce {
0%,
20%,
53%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
transform: translateZ(0);
}
40%,
43% {
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
transform: translate3d(0, -5px, 0);
}
70% {
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
transform: translate3d(0, -7px, 0);
}
80% {
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
transform: translateZ(0);
}
90% {
transform: translate3d(0, -2px, 0);
}
}
@keyframes slideY {
0%,
50%,
100% {
transform: translateY(0px);
}
25% {
transform: translateY(-10px);
}
75% {
transform: translateY(10px);
}
}
@keyframes slideX {
0%,
50%,
100% {
transform: translateX(0px);
}
25% {
transform: translateX(-10px);
}
75% {
transform: translateX(10px);
}
}
+9
View File
@@ -0,0 +1,9 @@
const config = {
// basename: only at build time to set, and Don't add '/' at end off BASENAME for breadcrumbs, also Don't put only '/' use blank('') instead,
basename: '',
defaultPath: '/chatflows',
fontFamily: `'Roboto', sans-serif`,
borderRadius: 12
}
export default config
+26
View File
@@ -0,0 +1,26 @@
import { useState } from 'react'
export default (apiFunc) => {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(false)
const request = async (...args) => {
setLoading(true)
try {
const result = await apiFunc(...args)
setData(result.data)
} catch (err) {
setError(err || 'Unexpected Error!')
} finally {
setLoading(false)
}
}
return {
data,
error,
loading,
request
}
}
+37
View File
@@ -0,0 +1,37 @@
import { useContext } from 'react'
import ConfirmContext from 'store/context/ConfirmContext'
import { HIDE_CONFIRM, SHOW_CONFIRM } from 'store/actions'
let resolveCallback
const useConfirm = () => {
const [confirmState, dispatch] = useContext(ConfirmContext)
const closeConfirm = () => {
dispatch({
type: HIDE_CONFIRM
})
}
const onConfirm = () => {
closeConfirm()
resolveCallback(true)
}
const onCancel = () => {
closeConfirm()
resolveCallback(false)
}
const confirm = (confirmPayload) => {
dispatch({
type: SHOW_CONFIRM,
payload: confirmPayload
})
return new Promise((res) => {
resolveCallback = res
})
}
return { confirm, onConfirm, onCancel, confirmState }
}
export default useConfirm
+18
View File
@@ -0,0 +1,18 @@
import { useEffect, useRef } from 'react'
// ==============================|| ELEMENT REFERENCE HOOKS ||============================== //
const useScriptRef = () => {
const scripted = useRef(true)
useEffect(
() => () => {
scripted.current = false
},
[]
)
return scripted
}
export default useScriptRef
+33
View File
@@ -0,0 +1,33 @@
import React from 'react'
import App from './App'
import { store } from 'store'
import { createRoot } from 'react-dom/client'
// style + assets
import 'assets/scss/style.scss'
// third party
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { SnackbarProvider } from 'notistack'
import ConfirmContextProvider from 'store/context/ConfirmContextProvider'
import { ReactFlowContext } from 'store/context/ReactFlowContext'
const container = document.getElementById('root')
const root = createRoot(container)
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<SnackbarProvider>
<ConfirmContextProvider>
<ReactFlowContext>
<App />
</ReactFlowContext>
</ConfirmContextProvider>
</SnackbarProvider>
</BrowserRouter>
</Provider>
</React.StrictMode>
)
@@ -0,0 +1,127 @@
import PropTypes from 'prop-types'
import { useSelector, useDispatch } from 'react-redux'
import { useState } from 'react'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Avatar, Box, ButtonBase, Switch } from '@mui/material'
import { styled } from '@mui/material/styles'
// project imports
import LogoSection from '../LogoSection'
// assets
import { IconMenu2 } from '@tabler/icons'
// store
import { SET_DARKMODE } from 'store/actions'
// ==============================|| MAIN NAVBAR / HEADER ||============================== //
const MaterialUISwitch = styled(Switch)(({ theme }) => ({
width: 62,
height: 34,
padding: 7,
'& .MuiSwitch-switchBase': {
margin: 1,
padding: 0,
transform: 'translateX(6px)',
'&.Mui-checked': {
color: '#fff',
transform: 'translateX(22px)',
'& .MuiSwitch-thumb:before': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
'#fff'
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`
},
'& + .MuiSwitch-track': {
opacity: 1,
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be'
}
}
},
'& .MuiSwitch-thumb': {
backgroundColor: theme.palette.mode === 'dark' ? '#003892' : '#001e3c',
width: 32,
height: 32,
'&:before': {
content: "''",
position: 'absolute',
width: '100%',
height: '100%',
left: 0,
top: 0,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
'#fff'
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`
}
},
'& .MuiSwitch-track': {
opacity: 1,
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
borderRadius: 20 / 2
}
}))
const Header = ({ handleLeftDrawerToggle }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [isDark, setIsDark] = useState(customization.isDarkMode)
const dispatch = useDispatch()
const changeDarkMode = () => {
dispatch({ type: SET_DARKMODE, isDarkMode: !isDark })
setIsDark((isDark) => !isDark)
localStorage.setItem('isDarkMode', !isDark)
}
return (
<>
{/* logo & toggler button */}
<Box
sx={{
width: 228,
display: 'flex',
[theme.breakpoints.down('md')]: {
width: 'auto'
}
}}
>
<Box component='span' sx={{ display: { xs: 'none', md: 'block' }, flexGrow: 1 }}>
<LogoSection />
</Box>
<ButtonBase sx={{ borderRadius: '12px', overflow: 'hidden' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.secondary.light,
color: theme.palette.secondary.dark,
'&:hover': {
background: theme.palette.secondary.dark,
color: theme.palette.secondary.light
}
}}
onClick={handleLeftDrawerToggle}
color='inherit'
>
<IconMenu2 stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
</Box>
<Box sx={{ flexGrow: 1 }} />
<MaterialUISwitch checked={isDark} onChange={changeDarkMode} />
</>
)
}
Header.propTypes = {
handleLeftDrawerToggle: PropTypes.func
}
export default Header
@@ -0,0 +1,18 @@
import { Link } from 'react-router-dom'
// material-ui
import { ButtonBase } from '@mui/material'
// project imports
import config from 'config'
import Logo from 'ui-component/extended/Logo'
// ==============================|| MAIN LOGO ||============================== //
const LogoSection = () => (
<ButtonBase disableRipple component={Link} to={config.defaultPath}>
<Logo />
</ButtonBase>
)
export default LogoSection
@@ -0,0 +1,124 @@
import PropTypes from 'prop-types'
import { useState } from 'react'
import { useSelector } from 'react-redux'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Collapse, List, ListItemButton, ListItemIcon, ListItemText, Typography } from '@mui/material'
// project imports
import NavItem from '../NavItem'
// assets
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'
import { IconChevronDown, IconChevronUp } from '@tabler/icons'
// ==============================|| SIDEBAR MENU LIST COLLAPSE ITEMS ||============================== //
const NavCollapse = ({ menu, level }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [open, setOpen] = useState(false)
const [selected, setSelected] = useState(null)
const handleClick = () => {
setOpen(!open)
setSelected(!selected ? menu.id : null)
}
// menu collapse & item
const menus = menu.children?.map((item) => {
switch (item.type) {
case 'collapse':
return <NavCollapse key={item.id} menu={item} level={level + 1} />
case 'item':
return <NavItem key={item.id} item={item} level={level + 1} />
default:
return (
<Typography key={item.id} variant='h6' color='error' align='center'>
Menu Items Error
</Typography>
)
}
})
const Icon = menu.icon
const menuIcon = menu.icon ? (
<Icon strokeWidth={1.5} size='1.3rem' style={{ marginTop: 'auto', marginBottom: 'auto' }} />
) : (
<FiberManualRecordIcon
sx={{
width: selected === menu.id ? 8 : 6,
height: selected === menu.id ? 8 : 6
}}
fontSize={level > 0 ? 'inherit' : 'medium'}
/>
)
return (
<>
<ListItemButton
sx={{
borderRadius: `${customization.borderRadius}px`,
mb: 0.5,
alignItems: 'flex-start',
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
py: level > 1 ? 1 : 1.25,
pl: `${level * 24}px`
}}
selected={selected === menu.id}
onClick={handleClick}
>
<ListItemIcon sx={{ my: 'auto', minWidth: !menu.icon ? 18 : 36 }}>{menuIcon}</ListItemIcon>
<ListItemText
primary={
<Typography variant={selected === menu.id ? 'h5' : 'body1'} color='inherit' sx={{ my: 'auto' }}>
{menu.title}
</Typography>
}
secondary={
menu.caption && (
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
{menu.caption}
</Typography>
)
}
/>
{open ? (
<IconChevronUp stroke={1.5} size='1rem' style={{ marginTop: 'auto', marginBottom: 'auto' }} />
) : (
<IconChevronDown stroke={1.5} size='1rem' style={{ marginTop: 'auto', marginBottom: 'auto' }} />
)}
</ListItemButton>
<Collapse in={open} timeout='auto' unmountOnExit>
<List
component='div'
disablePadding
sx={{
position: 'relative',
'&:after': {
content: "''",
position: 'absolute',
left: '32px',
top: 0,
height: '100%',
width: '1px',
opacity: 1,
background: theme.palette.primary.light
}
}}
>
{menus}
</List>
</Collapse>
</>
)
}
NavCollapse.propTypes = {
menu: PropTypes.object,
level: PropTypes.number
}
export default NavCollapse
@@ -0,0 +1,61 @@
import PropTypes from 'prop-types'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Divider, List, Typography } from '@mui/material'
// project imports
import NavItem from '../NavItem'
import NavCollapse from '../NavCollapse'
// ==============================|| SIDEBAR MENU LIST GROUP ||============================== //
const NavGroup = ({ item }) => {
const theme = useTheme()
// menu list collapse & items
const items = item.children?.map((menu) => {
switch (menu.type) {
case 'collapse':
return <NavCollapse key={menu.id} menu={menu} level={1} />
case 'item':
return <NavItem key={menu.id} item={menu} level={1} navType='MENU' />
default:
return (
<Typography key={menu.id} variant='h6' color='error' align='center'>
Menu Items Error
</Typography>
)
}
})
return (
<>
<List
subheader={
item.title && (
<Typography variant='caption' sx={{ ...theme.typography.menuCaption }} display='block' gutterBottom>
{item.title}
{item.caption && (
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
{item.caption}
</Typography>
)}
</Typography>
)
}
>
{items}
</List>
{/* group divider */}
<Divider sx={{ mt: 0.25, mb: 1.25 }} />
</>
)
}
NavGroup.propTypes = {
item: PropTypes.object
}
export default NavGroup
@@ -0,0 +1,150 @@
import PropTypes from 'prop-types'
import { forwardRef, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography, useMediaQuery } from '@mui/material'
// project imports
import { MENU_OPEN, SET_MENU } from 'store/actions'
import config from 'config'
// assets
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'
// ==============================|| SIDEBAR MENU LIST ITEMS ||============================== //
const NavItem = ({ item, level, navType, onClick, onUploadFile }) => {
const theme = useTheme()
const dispatch = useDispatch()
const customization = useSelector((state) => state.customization)
const matchesSM = useMediaQuery(theme.breakpoints.down('lg'))
const Icon = item.icon
const itemIcon = item?.icon ? (
<Icon stroke={1.5} size='1.3rem' />
) : (
<FiberManualRecordIcon
sx={{
width: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6,
height: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6
}}
fontSize={level > 0 ? 'inherit' : 'medium'}
/>
)
let itemTarget = '_self'
if (item.target) {
itemTarget = '_blank'
}
let listItemProps = {
component: forwardRef(function ListItemPropsComponent(props, ref) {
return <Link ref={ref} {...props} to={`${config.basename}${item.url}`} target={itemTarget} />
})
}
if (item?.external) {
listItemProps = { component: 'a', href: item.url, target: itemTarget }
}
if (item?.id === 'loadChatflow') {
listItemProps.component = 'label'
}
const handleFileUpload = (e) => {
if (!e.target.files) return
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = (evt) => {
if (!evt?.target?.result) {
return
}
const { result } = evt.target
onUploadFile(result)
}
reader.readAsText(file)
}
const itemHandler = (id) => {
if (navType === 'SETTINGS' && id !== 'loadChatflow') {
onClick(id)
} else {
dispatch({ type: MENU_OPEN, id })
if (matchesSM) dispatch({ type: SET_MENU, opened: false })
}
}
// active menu item on page load
useEffect(() => {
if (navType === 'MENU') {
const currentIndex = document.location.pathname
.toString()
.split('/')
.findIndex((id) => id === item.id)
if (currentIndex > -1) {
dispatch({ type: MENU_OPEN, id: item.id })
}
if (!document.location.pathname.toString().split('/')[1]) {
itemHandler('chatflows')
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navType])
return (
<ListItemButton
{...listItemProps}
disabled={item.disabled}
sx={{
borderRadius: `${customization.borderRadius}px`,
mb: 0.5,
alignItems: 'flex-start',
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
py: level > 1 ? 1 : 1.25,
pl: `${level * 24}px`
}}
selected={customization.isOpen.findIndex((id) => id === item.id) > -1}
onClick={() => itemHandler(item.id)}
>
{item.id === 'loadChatflow' && <input type='file' hidden accept='.json' onChange={(e) => handleFileUpload(e)} />}
<ListItemIcon sx={{ my: 'auto', minWidth: !item?.icon ? 18 : 36 }}>{itemIcon}</ListItemIcon>
<ListItemText
primary={
<Typography variant={customization.isOpen.findIndex((id) => id === item.id) > -1 ? 'h5' : 'body1'} color='inherit'>
{item.title}
</Typography>
}
secondary={
item.caption && (
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
{item.caption}
</Typography>
)
}
/>
{item.chip && (
<Chip
color={item.chip.color}
variant={item.chip.variant}
size={item.chip.size}
label={item.chip.label}
avatar={item.chip.avatar && <Avatar>{item.chip.avatar}</Avatar>}
/>
)}
</ListItemButton>
)
}
NavItem.propTypes = {
item: PropTypes.object,
level: PropTypes.number,
navType: PropTypes.string,
onClick: PropTypes.func,
onUploadFile: PropTypes.func
}
export default NavItem
@@ -0,0 +1,27 @@
// material-ui
import { Typography } from '@mui/material'
// project imports
import NavGroup from './NavGroup'
import menuItem from 'menu-items'
// ==============================|| SIDEBAR MENU LIST ||============================== //
const MenuList = () => {
const navItems = menuItem.items.map((item) => {
switch (item.type) {
case 'group':
return <NavGroup key={item.id} item={item} />
default:
return (
<Typography key={item.id} variant='h6' color='error' align='center'>
Menu Items Error
</Typography>
)
}
})
return <>{navItems}</>
}
export default MenuList
@@ -0,0 +1,85 @@
import PropTypes from 'prop-types'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Box, Drawer, useMediaQuery } from '@mui/material'
// third-party
import PerfectScrollbar from 'react-perfect-scrollbar'
import { BrowserView, MobileView } from 'react-device-detect'
// project imports
import MenuList from './MenuList'
import LogoSection from '../LogoSection'
import { drawerWidth } from 'store/constant'
// ==============================|| SIDEBAR DRAWER ||============================== //
const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
const theme = useTheme()
const matchUpMd = useMediaQuery(theme.breakpoints.up('md'))
const drawer = (
<>
<Box sx={{ display: { xs: 'block', md: 'none' } }}>
<Box sx={{ display: 'flex', p: 2, mx: 'auto' }}>
<LogoSection />
</Box>
</Box>
<BrowserView>
<PerfectScrollbar
component='div'
style={{
height: !matchUpMd ? 'calc(100vh - 56px)' : 'calc(100vh - 88px)',
paddingLeft: '16px',
paddingRight: '16px'
}}
>
<MenuList />
</PerfectScrollbar>
</BrowserView>
<MobileView>
<Box sx={{ px: 2 }}>
<MenuList />
</Box>
</MobileView>
</>
)
const container = window !== undefined ? () => window.document.body : undefined
return (
<Box component='nav' sx={{ flexShrink: { md: 0 }, width: matchUpMd ? drawerWidth : 'auto' }} aria-label='mailbox folders'>
<Drawer
container={container}
variant={matchUpMd ? 'persistent' : 'temporary'}
anchor='left'
open={drawerOpen}
onClose={drawerToggle}
sx={{
'& .MuiDrawer-paper': {
width: drawerWidth,
background: theme.palette.background.default,
color: theme.palette.text.primary,
borderRight: 'none',
[theme.breakpoints.up('md')]: {
top: '66px'
}
}
}}
ModalProps={{ keepMounted: true }}
color='inherit'
>
{drawer}
</Drawer>
</Box>
)
}
Sidebar.propTypes = {
drawerOpen: PropTypes.bool,
drawerToggle: PropTypes.func,
window: PropTypes.object
}
export default Sidebar
+107
View File
@@ -0,0 +1,107 @@
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Outlet } from 'react-router-dom'
// material-ui
import { styled, useTheme } from '@mui/material/styles'
import { AppBar, Box, CssBaseline, Toolbar, useMediaQuery } from '@mui/material'
// project imports
import Header from './Header'
import Sidebar from './Sidebar'
import { drawerWidth } from 'store/constant'
import { SET_MENU } from 'store/actions'
// styles
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
...theme.typography.mainContent,
...(!open && {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
[theme.breakpoints.up('md')]: {
marginLeft: -(drawerWidth - 20),
width: `calc(100% - ${drawerWidth}px)`
},
[theme.breakpoints.down('md')]: {
marginLeft: '20px',
width: `calc(100% - ${drawerWidth}px)`,
padding: '16px'
},
[theme.breakpoints.down('sm')]: {
marginLeft: '10px',
width: `calc(100% - ${drawerWidth}px)`,
padding: '16px',
marginRight: '10px'
}
}),
...(open && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen
}),
marginLeft: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
width: `calc(100% - ${drawerWidth}px)`,
[theme.breakpoints.down('md')]: {
marginLeft: '20px'
},
[theme.breakpoints.down('sm')]: {
marginLeft: '10px'
}
})
}))
// ==============================|| MAIN LAYOUT ||============================== //
const MainLayout = () => {
const theme = useTheme()
const matchDownMd = useMediaQuery(theme.breakpoints.down('lg'))
// Handle left drawer
const leftDrawerOpened = useSelector((state) => state.customization.opened)
const dispatch = useDispatch()
const handleLeftDrawerToggle = () => {
dispatch({ type: SET_MENU, opened: !leftDrawerOpened })
}
useEffect(() => {
dispatch({ type: SET_MENU, opened: !matchDownMd })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [matchDownMd])
return (
<Box sx={{ display: 'flex' }}>
<CssBaseline />
{/* header */}
<AppBar
enableColorOnDark
position='fixed'
color='inherit'
elevation={0}
sx={{
bgcolor: theme.palette.background.default,
transition: leftDrawerOpened ? theme.transitions.create('width') : 'none'
}}
>
<Toolbar>
<Header handleLeftDrawerToggle={handleLeftDrawerToggle} />
</Toolbar>
</AppBar>
{/* drawer */}
<Sidebar drawerOpen={leftDrawerOpened} drawerToggle={handleLeftDrawerToggle} />
{/* main content */}
<Main theme={theme} open={leftDrawerOpened}>
<Outlet />
</Main>
</Box>
)
}
export default MainLayout
@@ -0,0 +1,11 @@
import { Outlet } from 'react-router-dom'
// ==============================|| MINIMAL LAYOUT ||============================== //
const MinimalLayout = () => (
<>
<Outlet />
</>
)
export default MinimalLayout
+39
View File
@@ -0,0 +1,39 @@
import PropTypes from 'prop-types'
import { motion } from 'framer-motion'
// ==============================|| ANIMATION FOR CONTENT ||============================== //
const NavMotion = ({ children }) => {
const motionVariants = {
initial: {
opacity: 0,
scale: 0.99
},
in: {
opacity: 1,
scale: 1
},
out: {
opacity: 0,
scale: 1.01
}
}
const motionTransition = {
type: 'tween',
ease: 'anticipate',
duration: 0.4
}
return (
<motion.div initial='initial' animate='in' exit='out' variants={motionVariants} transition={motionTransition}>
{children}
</motion.div>
)
}
NavMotion.propTypes = {
children: PropTypes.node
}
export default NavMotion
@@ -0,0 +1,26 @@
import PropTypes from 'prop-types'
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
// ==============================|| NAVIGATION SCROLL TO TOP ||============================== //
const NavigationScroll = ({ children }) => {
const location = useLocation()
const { pathname } = location
useEffect(() => {
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
})
}, [pathname])
return children || null
}
NavigationScroll.propTypes = {
children: PropTypes.node
}
export default NavigationScroll
+25
View File
@@ -0,0 +1,25 @@
// assets
import { IconHierarchy, IconKey, IconBook, IconListCheck } from '@tabler/icons'
// constant
const icons = { IconHierarchy, IconKey, IconBook, IconListCheck }
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
const dashboard = {
id: 'dashboard',
title: '',
type: 'group',
children: [
{
id: 'chatflows',
title: 'Chatflows',
type: 'item',
url: '/chatflows',
icon: icons.IconHierarchy,
breadcrumbs: true
}
]
}
export default dashboard
+9
View File
@@ -0,0 +1,9 @@
import dashboard from './dashboard'
// ==============================|| MENU ITEMS ||============================== //
const menuItems = {
items: [dashboard]
}
export default menuItems
+38
View File
@@ -0,0 +1,38 @@
// assets
import { IconTrash, IconFileUpload, IconFileExport } from '@tabler/icons'
// constant
const icons = { IconTrash, IconFileUpload, IconFileExport }
// ==============================|| SETTINGS MENU ITEMS ||============================== //
const settings = {
id: 'settings',
title: '',
type: 'group',
children: [
{
id: 'loadChatflow',
title: 'Load Chatflow',
type: 'item',
url: '',
icon: icons.IconFileUpload
},
{
id: 'exportChatflow',
title: 'Export Chatflow',
type: 'item',
url: '',
icon: icons.IconFileExport
},
{
id: 'deleteChatflow',
title: 'Delete Chatflow',
type: 'item',
url: '',
icon: icons.IconTrash
}
]
}
export default settings
+27
View File
@@ -0,0 +1,27 @@
import { lazy } from 'react'
// project imports
import Loadable from 'ui-component/loading/Loadable'
import MinimalLayout from 'layout/MinimalLayout'
// canvas routing
const Canvas = Loadable(lazy(() => import('views/canvas')))
// ==============================|| CANVAS ROUTING ||============================== //
const CanvasRoutes = {
path: '/',
element: <MinimalLayout />,
children: [
{
path: '/canvas',
element: <Canvas />
},
{
path: '/canvas/:id',
element: <Canvas />
}
]
}
export default CanvasRoutes
+27
View File
@@ -0,0 +1,27 @@
import { lazy } from 'react'
// project imports
import MainLayout from 'layout/MainLayout'
import Loadable from 'ui-component/loading/Loadable'
// chatflows routing
const Chatflows = Loadable(lazy(() => import('views/chatflows')))
// ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = {
path: '/',
element: <MainLayout />,
children: [
{
path: '/',
element: <Chatflows />
},
{
path: '/chatflows',
element: <Chatflows />
}
]
}
export default MainRoutes
+12
View File
@@ -0,0 +1,12 @@
import { useRoutes } from 'react-router-dom'
// routes
import MainRoutes from './MainRoutes'
import CanvasRoutes from './CanvasRoutes'
import config from 'config'
// ==============================|| ROUTING RENDER ||============================== //
export default function ThemeRoutes() {
return useRoutes([MainRoutes, CanvasRoutes], config.basename)
}
+132
View File
@@ -0,0 +1,132 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
)
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing
if (installingWorker == null) {
return
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.info(
'New content is available and will be used when all tabs for this page are closed. See https://bit.ly/CRA-PWA.'
)
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration)
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.info('Content is cached for offline use.')
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration)
}
}
}
}
}
})
.catch((error) => {
console.error('Error during service worker registration:', error)
})
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type')
if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload()
})
})
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config)
}
})
.catch(() => {
console.info('No internet connection found. App is running in offline mode.')
})
}
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.info(
'This web app is being served cache-first by a service worker. To learn more, visit https://bit.ly/CRA-PWA'
)
})
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config)
}
})
}
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
registration.unregister()
})
.catch((error) => {
console.error(error.message)
})
}
}
+46
View File
@@ -0,0 +1,46 @@
// action - customization reducer
export const SET_MENU = '@customization/SET_MENU'
export const MENU_TOGGLE = '@customization/MENU_TOGGLE'
export const MENU_OPEN = '@customization/MENU_OPEN'
export const SET_FONT_FAMILY = '@customization/SET_FONT_FAMILY'
export const SET_BORDER_RADIUS = '@customization/SET_BORDER_RADIUS'
export const SET_LAYOUT = '@customization/SET_LAYOUT '
export const SET_DARKMODE = '@customization/SET_DARKMODE'
// action - canvas reducer
export const REMOVE_EDGE = '@canvas/REMOVE_EDGE'
export const SET_DIRTY = '@canvas/SET_DIRTY'
export const REMOVE_DIRTY = '@canvas/REMOVE_DIRTY'
export const SET_CHATFLOW = '@canvas/SET_CHATFLOW'
// action - notifier reducer
export const ENQUEUE_SNACKBAR = 'ENQUEUE_SNACKBAR'
export const CLOSE_SNACKBAR = 'CLOSE_SNACKBAR'
export const REMOVE_SNACKBAR = 'REMOVE_SNACKBAR'
// action - dialog reducer
export const SHOW_CONFIRM = 'SHOW_CONFIRM'
export const HIDE_CONFIRM = 'HIDE_CONFIRM'
export const enqueueSnackbar = (notification) => {
const key = notification.options && notification.options.key
return {
type: ENQUEUE_SNACKBAR,
notification: {
...notification,
key: key || new Date().getTime() + Math.random()
}
}
}
export const closeSnackbar = (key) => ({
type: CLOSE_SNACKBAR,
dismissAll: !key, // dismiss all if no key has been defined
key
})
export const removeSnackbar = (key) => ({
type: REMOVE_SNACKBAR,
key
})
+5
View File
@@ -0,0 +1,5 @@
// constant
export const gridSpacing = 3
export const drawerWidth = 260
export const appDrawerWidth = 320
export const baseURL = process.env.NODE_ENV === 'production' ? window.location.origin : window.location.origin.replace(':8080', ':3000')
@@ -0,0 +1,5 @@
import React from 'react'
const ConfirmContext = React.createContext()
export default ConfirmContext
@@ -0,0 +1,16 @@
import { useReducer } from 'react'
import PropTypes from 'prop-types'
import alertReducer, { initialState } from '../reducers/dialogReducer'
import ConfirmContext from './ConfirmContext'
const ConfirmContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(alertReducer, initialState)
return <ConfirmContext.Provider value={[state, dispatch]}>{children}</ConfirmContext.Provider>
}
ConfirmContextProvider.propTypes = {
children: PropTypes.any
}
export default ConfirmContextProvider
@@ -0,0 +1,35 @@
import { createContext, useState } from 'react'
import PropTypes from 'prop-types'
const initialValue = {
reactFlowInstance: null,
setReactFlowInstance: () => {},
deleteNode: () => {}
}
export const flowContext = createContext(initialValue)
export const ReactFlowContext = ({ children }) => {
const [reactFlowInstance, setReactFlowInstance] = useState(null)
const deleteNode = (id) => {
reactFlowInstance.setNodes(reactFlowInstance.getNodes().filter((n) => n.id !== id))
reactFlowInstance.setEdges(reactFlowInstance.getEdges().filter((ns) => ns.source !== id && ns.target !== id))
}
return (
<flowContext.Provider
value={{
reactFlowInstance,
setReactFlowInstance,
deleteNode
}}
>
{children}
</flowContext.Provider>
)
}
ReactFlowContext.propTypes = {
children: PropTypes.any
}
+9
View File
@@ -0,0 +1,9 @@
import { createStore } from 'redux'
import reducer from './reducer'
// ==============================|| REDUX - MAIN STORE ||============================== //
const store = createStore(reducer)
const persister = 'Free'
export { store, persister }
+18
View File
@@ -0,0 +1,18 @@
import { combineReducers } from 'redux'
// reducer import
import customizationReducer from './reducers/customizationReducer'
import canvasReducer from './reducers/canvasReducer'
import notifierReducer from './reducers/notifierReducer'
import dialogReducer from './reducers/dialogReducer'
// ==============================|| COMBINE REDUCER ||============================== //
const reducer = combineReducers({
customization: customizationReducer,
canvas: canvasReducer,
notifier: notifierReducer,
dialog: dialogReducer
})
export default reducer
@@ -0,0 +1,39 @@
// action - state management
import * as actionTypes from '../actions'
export const initialState = {
removeEdgeId: '',
isDirty: false,
chatflow: null
}
// ==============================|| CANVAS REDUCER ||============================== //
const canvasReducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.REMOVE_EDGE:
return {
...state,
removeEdgeId: action.edgeId
}
case actionTypes.SET_DIRTY:
return {
...state,
isDirty: true
}
case actionTypes.REMOVE_DIRTY:
return {
...state,
isDirty: false
}
case actionTypes.SET_CHATFLOW:
return {
...state,
chatflow: action.chatflow
}
default:
return state
}
}
export default canvasReducer
@@ -0,0 +1,57 @@
// project imports
import config from 'config'
// action - state management
import * as actionTypes from '../actions'
export const initialState = {
isOpen: [], // for active default menu
fontFamily: config.fontFamily,
borderRadius: config.borderRadius,
opened: true,
isHorizontal: localStorage.getItem('isHorizontal') === 'true' ? true : false,
isDarkMode: localStorage.getItem('isDarkMode') === 'true' ? true : false
}
// ==============================|| CUSTOMIZATION REDUCER ||============================== //
const customizationReducer = (state = initialState, action) => {
let id
switch (action.type) {
case actionTypes.MENU_OPEN:
id = action.id
return {
...state,
isOpen: [id]
}
case actionTypes.SET_MENU:
return {
...state,
opened: action.opened
}
case actionTypes.SET_FONT_FAMILY:
return {
...state,
fontFamily: action.fontFamily
}
case actionTypes.SET_BORDER_RADIUS:
return {
...state,
borderRadius: action.borderRadius
}
case actionTypes.SET_LAYOUT:
return {
...state,
isHorizontal: action.isHorizontal
}
case actionTypes.SET_DARKMODE:
return {
...state,
isDarkMode: action.isDarkMode
}
default:
return state
}
}
export default customizationReducer
@@ -0,0 +1,28 @@
import { SHOW_CONFIRM, HIDE_CONFIRM } from '../actions'
export const initialState = {
show: false,
title: '',
description: '',
confirmButtonName: 'OK',
cancelButtonName: 'Cancel'
}
const alertReducer = (state = initialState, action) => {
switch (action.type) {
case SHOW_CONFIRM:
return {
show: true,
title: action.payload.title,
description: action.payload.description,
confirmButtonName: action.payload.confirmButtonName,
cancelButtonName: action.payload.cancelButtonName
}
case HIDE_CONFIRM:
return initialState
default:
return state
}
}
export default alertReducer
@@ -0,0 +1,40 @@
import { ENQUEUE_SNACKBAR, CLOSE_SNACKBAR, REMOVE_SNACKBAR } from '../actions'
export const initialState = {
notifications: []
}
const notifierReducer = (state = initialState, action) => {
switch (action.type) {
case ENQUEUE_SNACKBAR:
return {
...state,
notifications: [
...state.notifications,
{
key: action.key,
...action.notification
}
]
}
case CLOSE_SNACKBAR:
return {
...state,
notifications: state.notifications.map((notification) =>
action.dismissAll || notification.key === action.key ? { ...notification, dismissed: true } : { ...notification }
)
}
case REMOVE_SNACKBAR:
return {
...state,
notifications: state.notifications.filter((notification) => notification.key !== action.key)
}
default:
return state
}
}
export default notifierReducer
+204
View File
@@ -0,0 +1,204 @@
export default function componentStyleOverrides(theme) {
const bgColor = theme.colors?.grey50
return {
MuiButton: {
styleOverrides: {
root: {
fontWeight: 500,
borderRadius: '4px'
}
}
},
MuiSvgIcon: {
styleOverrides: {
root: {
color: theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit',
background: theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryLight : 'inherit'
}
}
},
MuiPaper: {
defaultProps: {
elevation: 0
},
styleOverrides: {
root: {
backgroundImage: 'none'
},
rounded: {
borderRadius: `${theme?.customization?.borderRadius}px`
}
}
},
MuiCardHeader: {
styleOverrides: {
root: {
color: theme.colors?.textDark,
padding: '24px'
},
title: {
fontSize: '1.125rem'
}
}
},
MuiCardContent: {
styleOverrides: {
root: {
padding: '24px'
}
}
},
MuiCardActions: {
styleOverrides: {
root: {
padding: '24px'
}
}
},
MuiListItemButton: {
styleOverrides: {
root: {
color: theme.darkTextPrimary,
paddingTop: '10px',
paddingBottom: '10px',
'&.Mui-selected': {
color: theme.menuSelected,
backgroundColor: theme.menuSelectedBack,
'&:hover': {
backgroundColor: theme.menuSelectedBack
},
'& .MuiListItemIcon-root': {
color: theme.menuSelected
}
},
'&:hover': {
backgroundColor: theme.menuSelectedBack,
color: theme.menuSelected,
'& .MuiListItemIcon-root': {
color: theme.menuSelected
}
}
}
}
},
MuiListItemIcon: {
styleOverrides: {
root: {
color: theme.darkTextPrimary,
minWidth: '36px'
}
}
},
MuiListItemText: {
styleOverrides: {
primary: {
color: theme.textDark
}
}
},
MuiInputBase: {
styleOverrides: {
input: {
color: theme.textDark,
'&::placeholder': {
color: theme.darkTextSecondary,
fontSize: '0.875rem'
}
}
}
},
MuiOutlinedInput: {
styleOverrides: {
root: {
background: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary800 : bgColor,
borderRadius: `${theme?.customization?.borderRadius}px`,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: theme.colors?.grey400
},
'&:hover $notchedOutline': {
borderColor: theme.colors?.primaryLight
},
'&.MuiInputBase-multiline': {
padding: 1
}
},
input: {
fontWeight: 500,
background: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary800 : bgColor,
padding: '15.5px 14px',
borderRadius: `${theme?.customization?.borderRadius}px`,
'&.MuiInputBase-inputSizeSmall': {
padding: '10px 14px',
'&.MuiInputBase-inputAdornedStart': {
paddingLeft: 0
}
}
},
inputAdornedStart: {
paddingLeft: 4
},
notchedOutline: {
borderRadius: `${theme?.customization?.borderRadius}px`
}
}
},
MuiSlider: {
styleOverrides: {
root: {
'&.Mui-disabled': {
color: theme.colors?.grey300
}
},
mark: {
backgroundColor: theme.paper,
width: '4px'
},
valueLabel: {
color: theme?.colors?.primaryLight
}
}
},
MuiDivider: {
styleOverrides: {
root: {
borderColor: theme.divider,
opacity: 1
}
}
},
MuiAvatar: {
styleOverrides: {
root: {
color: theme.colors?.primaryDark,
background: theme.colors?.primary200
}
}
},
MuiChip: {
styleOverrides: {
root: {
'&.MuiChip-deletable .MuiChip-deleteIcon': {
color: 'inherit'
}
}
}
},
MuiTooltip: {
styleOverrides: {
tooltip: {
color: theme?.customization?.isDarkMode ? theme.colors?.paper : theme.paper,
background: theme.colors?.grey700
}
}
},
MuiAutocomplete: {
styleOverrides: {
option: {
'&:hover': {
background: theme?.customization?.isDarkMode ? '#233345 !important' : ''
}
}
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
import { createTheme } from '@mui/material/styles'
// assets
import colors from 'assets/scss/_themes-vars.module.scss'
// project imports
import componentStyleOverrides from './compStyleOverride'
import themePalette from './palette'
import themeTypography from './typography'
/**
* Represent theme style and structure as per Material-UI
* @param {JsonObject} customization customization parameter object
*/
export const theme = (customization) => {
const color = colors
const themeOption = customization.isDarkMode
? {
colors: color,
heading: color.paper,
paper: color.darkPrimaryLight,
backgroundDefault: color.darkPaper,
background: color.darkPrimaryLight,
darkTextPrimary: color.paper,
darkTextSecondary: color.paper,
textDark: color.paper,
menuSelected: color.darkSecondaryDark,
menuSelectedBack: color.darkSecondaryLight,
divider: color.darkPaper,
customization
}
: {
colors: color,
heading: color.grey900,
paper: color.paper,
backgroundDefault: color.paper,
background: color.primaryLight,
darkTextPrimary: color.grey700,
darkTextSecondary: color.grey500,
textDark: color.grey900,
menuSelected: color.secondaryDark,
menuSelectedBack: color.secondaryLight,
divider: color.grey200,
customization
}
const themeOptions = {
direction: 'ltr',
palette: themePalette(themeOption),
mixins: {
toolbar: {
minHeight: '48px',
padding: '16px',
'@media (min-width: 600px)': {
minHeight: '48px'
}
}
},
typography: themeTypography(themeOption)
}
const themes = createTheme(themeOptions)
themes.components = componentStyleOverrides(themeOption)
return themes
}
export default theme
+96
View File
@@ -0,0 +1,96 @@
/**
* Color intention that you want to used in your theme
* @param {JsonObject} theme Theme customization object
*/
export default function themePalette(theme) {
return {
mode: theme?.customization?.navType,
common: {
black: theme.colors?.darkPaper
},
primary: {
light: theme.customization.isDarkMode ? theme.colors?.darkPrimaryLight : theme.colors?.primaryLight,
main: theme.colors?.primaryMain,
dark: theme.customization.isDarkMode ? theme.colors?.darkPrimaryDark : theme.colors?.primaryDark,
200: theme.customization.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.primary200,
800: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.primary800
},
secondary: {
light: theme.customization.isDarkMode ? theme.colors?.darkSecondaryLight : theme.colors?.secondaryLight,
main: theme.customization.isDarkMode ? theme.colors?.darkSecondaryMain : theme.colors?.secondaryMain,
dark: theme.customization.isDarkMode ? theme.colors?.darkSecondaryDark : theme.colors?.secondaryDark,
200: theme.colors?.secondary200,
800: theme.colors?.secondary800
},
error: {
light: theme.colors?.errorLight,
main: theme.colors?.errorMain,
dark: theme.colors?.errorDark
},
orange: {
light: theme.colors?.orangeLight,
main: theme.colors?.orangeMain,
dark: theme.colors?.orangeDark
},
warning: {
light: theme.colors?.warningLight,
main: theme.colors?.warningMain,
dark: theme.colors?.warningDark
},
success: {
light: theme.colors?.successLight,
200: theme.colors?.success200,
main: theme.colors?.successMain,
dark: theme.colors?.successDark
},
grey: {
50: theme.colors?.grey50,
100: theme.colors?.grey100,
200: theme.colors?.grey200,
300: theme.colors?.grey300,
500: theme.darkTextSecondary,
600: theme.heading,
700: theme.darkTextPrimary,
900: theme.textDark
},
dark: {
light: theme.colors?.darkTextPrimary,
main: theme.colors?.darkLevel1,
dark: theme.colors?.darkLevel2,
800: theme.colors?.darkBackground,
900: theme.colors?.darkPaper
},
text: {
primary: theme.darkTextPrimary,
secondary: theme.darkTextSecondary,
dark: theme.textDark,
hint: theme.colors?.grey100
},
background: {
paper: theme.paper,
default: theme.backgroundDefault
},
card: {
main: theme.customization.isDarkMode ? theme.colors?.darkPrimaryMain : theme.colors?.paper,
light: theme.customization.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.paper,
hover: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.paper
},
asyncSelect: {
main: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.grey50
},
canvasHeader: {
executionLight: theme.colors?.successLight,
executionDark: theme.colors?.successDark,
deployLight: theme.colors?.primaryLight,
deployDark: theme.colors?.primaryDark,
saveLight: theme.colors?.secondaryLight,
saveDark: theme.colors?.secondaryDark,
settingsLight: theme.colors?.grey300,
settingsDark: theme.colors?.grey700
},
codeEditor: {
main: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.primaryLight
}
}
}
+133
View File
@@ -0,0 +1,133 @@
/**
* Typography used in theme
* @param {JsonObject} theme theme customization object
*/
export default function themeTypography(theme) {
return {
fontFamily: theme?.customization?.fontFamily,
h6: {
fontWeight: 500,
color: theme.heading,
fontSize: '0.75rem'
},
h5: {
fontSize: '0.875rem',
color: theme.heading,
fontWeight: 500
},
h4: {
fontSize: '1rem',
color: theme.heading,
fontWeight: 600
},
h3: {
fontSize: '1.25rem',
color: theme.heading,
fontWeight: 600
},
h2: {
fontSize: '1.5rem',
color: theme.heading,
fontWeight: 700
},
h1: {
fontSize: '2.125rem',
color: theme.heading,
fontWeight: 700
},
subtitle1: {
fontSize: '0.875rem',
fontWeight: 500,
color: theme.textDark
},
subtitle2: {
fontSize: '0.75rem',
fontWeight: 400,
color: theme.darkTextSecondary
},
caption: {
fontSize: '0.75rem',
color: theme.darkTextSecondary,
fontWeight: 400
},
body1: {
fontSize: '0.875rem',
fontWeight: 400,
lineHeight: '1.334em'
},
body2: {
letterSpacing: '0em',
fontWeight: 400,
lineHeight: '1.5em',
color: theme.darkTextPrimary
},
button: {
textTransform: 'capitalize'
},
customInput: {
marginTop: 1,
marginBottom: 1,
'& > label': {
top: 23,
left: 0,
color: theme.grey500,
'&[data-shrink="false"]': {
top: 5
}
},
'& > div > input': {
padding: '30.5px 14px 11.5px !important'
},
'& legend': {
display: 'none'
},
'& fieldset': {
top: 0
}
},
mainContent: {
backgroundColor: theme.background,
width: '100%',
minHeight: 'calc(100vh - 75px)',
flexGrow: 1,
padding: '20px',
marginTop: '75px',
marginRight: '20px',
borderRadius: `${theme?.customization?.borderRadius}px`
},
menuCaption: {
fontSize: '0.875rem',
fontWeight: 500,
color: theme.heading,
padding: '6px',
textTransform: 'capitalize',
marginTop: '10px'
},
subMenuCaption: {
fontSize: '0.6875rem',
fontWeight: 500,
color: theme.darkTextSecondary,
textTransform: 'capitalize'
},
commonAvatar: {
cursor: 'pointer',
borderRadius: '8px'
},
smallAvatar: {
width: '22px',
height: '22px',
fontSize: '1rem'
},
mediumAvatar: {
width: '34px',
height: '34px',
fontSize: '1.2rem'
},
largeAvatar: {
width: '44px',
height: '44px',
fontSize: '1.5rem'
}
}
}
@@ -0,0 +1,97 @@
import PropTypes from 'prop-types'
import { forwardRef } from 'react'
// third-party
import { motion, useCycle } from 'framer-motion'
// ==============================|| ANIMATION BUTTON ||============================== //
const AnimateButton = forwardRef(function AnimateButton({ children, type, direction, offset, scale }, ref) {
let offset1
let offset2
switch (direction) {
case 'up':
case 'left':
offset1 = offset
offset2 = 0
break
case 'right':
case 'down':
default:
offset1 = 0
offset2 = offset
break
}
const [x, cycleX] = useCycle(offset1, offset2)
const [y, cycleY] = useCycle(offset1, offset2)
switch (type) {
case 'rotate':
return (
<motion.div
ref={ref}
animate={{ rotate: 360 }}
transition={{
repeat: Infinity,
repeatType: 'loop',
duration: 2,
repeatDelay: 0
}}
>
{children}
</motion.div>
)
case 'slide':
if (direction === 'up' || direction === 'down') {
return (
<motion.div
ref={ref}
animate={{ y: y !== undefined ? y : '' }}
onHoverEnd={() => cycleY()}
onHoverStart={() => cycleY()}
>
{children}
</motion.div>
)
}
return (
<motion.div ref={ref} animate={{ x: x !== undefined ? x : '' }} onHoverEnd={() => cycleX()} onHoverStart={() => cycleX()}>
{children}
</motion.div>
)
case 'scale':
default:
if (typeof scale === 'number') {
scale = {
hover: scale,
tap: scale
}
}
return (
<motion.div ref={ref} whileHover={{ scale: scale?.hover }} whileTap={{ scale: scale?.tap }}>
{children}
</motion.div>
)
}
})
AnimateButton.propTypes = {
children: PropTypes.node,
offset: PropTypes.number,
type: PropTypes.oneOf(['slide', 'scale', 'rotate']),
direction: PropTypes.oneOf(['up', 'down', 'left', 'right']),
scale: PropTypes.oneOfType([PropTypes.number, PropTypes.object])
}
AnimateButton.defaultProps = {
type: 'scale',
offset: 10,
direction: 'right',
scale: {
hover: 1,
tap: 0.9
}
}
export default AnimateButton
@@ -0,0 +1,11 @@
import { styled } from '@mui/material/styles'
import { Button } from '@mui/material'
export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({
color: 'white',
backgroundColor: theme.palette[color].main,
'&:hover': {
backgroundColor: theme.palette[color].main,
backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)`
}
}))
@@ -0,0 +1,11 @@
import { styled } from '@mui/material/styles'
import { Fab } from '@mui/material'
export const StyledFab = styled(Fab)(({ theme, color = 'primary' }) => ({
color: 'white',
backgroundColor: theme.palette[color].main,
'&:hover': {
backgroundColor: theme.palette[color].main,
backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)`
}
}))
@@ -0,0 +1,95 @@
import PropTypes from 'prop-types'
// material-ui
import { styled, useTheme } from '@mui/material/styles'
import { Box, Grid, Chip, Typography } from '@mui/material'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import SkeletonChatflowCard from 'ui-component/cards/Skeleton/ChatflowCard'
const CardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.main,
color: theme.darkTextPrimary,
overflow: 'hidden',
position: 'relative',
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
cursor: 'pointer',
'&:hover': {
background: theme.palette.card.hover,
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 20%)'
}
}))
// ===========================|| CONTRACT CARD ||=========================== //
const ItemCard = ({ isLoading, data, images, onClick }) => {
const theme = useTheme()
const chipSX = {
height: 24,
padding: '0 6px'
}
const activeChatflowSX = {
...chipSX,
color: 'white',
backgroundColor: theme.palette.success.dark
}
return (
<>
{isLoading ? (
<SkeletonChatflowCard />
) : (
<CardWrapper border={false} content={false} onClick={onClick}>
<Box sx={{ p: 2.25 }}>
<Grid container direction='column'>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Typography sx={{ fontSize: '1.5rem', fontWeight: 500 }}>{data.name}</Typography>
</div>
<Grid sx={{ mt: 1, mb: 1 }} container direction='row'>
{data.deployed && (
<Grid item>
<Chip label='Deployed' sx={activeChatflowSX} />
</Grid>
)}
</Grid>
{images && (
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
{images.map((img) => (
<div
key={img}
style={{
width: 40,
height: 40,
marginRight: 5,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }}
alt=''
src={img}
/>
</div>
))}
</div>
)}
</Grid>
</Box>
</CardWrapper>
)}
</>
)
}
ItemCard.propTypes = {
isLoading: PropTypes.bool,
data: PropTypes.object,
images: PropTypes.array,
onClick: PropTypes.func
}
export default ItemCard
@@ -0,0 +1,79 @@
import PropTypes from 'prop-types'
import { forwardRef } from 'react'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Card, CardContent, CardHeader, Divider, Typography } from '@mui/material'
// constant
const headerSX = {
'& .MuiCardHeader-action': { mr: 0 }
}
// ==============================|| CUSTOM MAIN CARD ||============================== //
const MainCard = forwardRef(function MainCard(
{
border = true,
boxShadow,
children,
content = true,
contentClass = '',
contentSX = {},
darkTitle,
secondary,
shadow,
sx = {},
title,
...others
},
ref
) {
const theme = useTheme()
return (
<Card
ref={ref}
{...others}
sx={{
border: border ? '1px solid' : 'none',
borderColor: theme.palette.primary[200] + 75,
':hover': {
boxShadow: boxShadow ? shadow || '0 2px 14px 0 rgb(32 40 45 / 8%)' : 'inherit'
},
...sx
}}
>
{/* card header and action */}
{!darkTitle && title && <CardHeader sx={headerSX} title={title} action={secondary} />}
{darkTitle && title && <CardHeader sx={headerSX} title={<Typography variant='h3'>{title}</Typography>} action={secondary} />}
{/* content & header divider */}
{title && <Divider />}
{/* card content */}
{content && (
<CardContent sx={contentSX} className={contentClass}>
{children}
</CardContent>
)}
{!content && children}
</Card>
)
})
MainCard.propTypes = {
border: PropTypes.bool,
boxShadow: PropTypes.bool,
children: PropTypes.node,
content: PropTypes.bool,
contentClass: PropTypes.string,
contentSX: PropTypes.object,
darkTitle: PropTypes.bool,
secondary: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
shadow: PropTypes.string,
sx: PropTypes.object,
title: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object])
}
export default MainCard
@@ -0,0 +1,32 @@
// material-ui
import { Card, CardContent, Grid } from '@mui/material'
import Skeleton from '@mui/material/Skeleton'
// ==============================|| SKELETON - BRIDGE CARD ||============================== //
const ChatflowCard = () => (
<Card>
<CardContent>
<Grid container direction='column'>
<Grid item>
<Grid container justifyContent='space-between'>
<Grid item>
<Skeleton variant='rectangular' width={44} height={44} />
</Grid>
<Grid item>
<Skeleton variant='rectangular' width={34} height={34} />
</Grid>
</Grid>
</Grid>
<Grid item>
<Skeleton variant='rectangular' sx={{ my: 2 }} height={40} />
</Grid>
<Grid item>
<Skeleton variant='rectangular' height={30} />
</Grid>
</Grid>
</CardContent>
</Card>
)
export default ChatflowCard
@@ -0,0 +1,39 @@
import { createPortal } from 'react-dom'
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'
import useConfirm from 'hooks/useConfirm'
import { StyledButton } from 'ui-component/button/StyledButton'
const ConfirmDialog = () => {
const { onConfirm, onCancel, confirmState } = useConfirm()
const portalElement = document.getElementById('portal')
const component = confirmState.show ? (
<Dialog
fullWidth
maxWidth='xs'
open={confirmState.show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{confirmState.title}
</DialogTitle>
<DialogContent>
<DialogContentText sx={{ color: 'black' }} id='alert-dialog-description'>
{confirmState.description}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>{confirmState.cancelButtonName}</Button>
<StyledButton variant='contained' onClick={onConfirm}>
{confirmState.confirmButtonName}
</StyledButton>
</DialogActions>
</Dialog>
) : null
return createPortal(component, portalElement)
}
export default ConfirmDialog
@@ -0,0 +1,61 @@
import { createPortal } from 'react-dom'
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { Button, Dialog, DialogActions, DialogContent, OutlinedInput, DialogTitle } from '@mui/material'
import { StyledButton } from 'ui-component/button/StyledButton'
const SaveChatflowDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const [chatflowName, setChatflowName] = useState('')
const [isReadyToSave, setIsReadyToSave] = useState(false)
useEffect(() => {
if (chatflowName) setIsReadyToSave(true)
else setIsReadyToSave(false)
}, [chatflowName])
const component = show ? (
<Dialog
open={show}
fullWidth
maxWidth='xs'
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent>
<OutlinedInput
sx={{ mt: 1 }}
id='chatflow-name'
type='text'
fullWidth
placeholder='My New Chatflow'
value={chatflowName}
onChange={(e) => setChatflowName(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
<StyledButton disabled={!isReadyToSave} variant='contained' onClick={() => onConfirm(chatflowName)}>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
</Dialog>
) : null
return createPortal(component, portalElement)
}
SaveChatflowDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default SaveChatflowDialog
@@ -0,0 +1,61 @@
import { useState } from 'react'
import { useSelector } from 'react-redux'
import { Popper, FormControl, TextField, Box, Typography } from '@mui/material'
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
import { styled } from '@mui/material/styles'
import PropTypes from 'prop-types'
const StyledPopper = styled(Popper)({
boxShadow: '0px 8px 10px -5px rgb(0 0 0 / 20%), 0px 16px 24px 2px rgb(0 0 0 / 14%), 0px 6px 30px 5px rgb(0 0 0 / 12%)',
borderRadius: '10px',
[`& .${autocompleteClasses.listbox}`]: {
boxSizing: 'border-box',
'& ul': {
padding: 10,
margin: 10
}
}
})
export const Dropdown = ({ name, value, options, onSelect }) => {
const customization = useSelector((state) => state.customization)
const findMatchingOptions = (options = [], value) => options.find((option) => option.name === value)
const getDefaultOptionValue = () => ''
let [internalValue, setInternalValue] = useState(value ?? 'choose an option')
return (
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
<Autocomplete
id={name}
size='small'
options={options || []}
value={findMatchingOptions(options, internalValue) || getDefaultOptionValue()}
onChange={(e, selection) => {
const value = selection ? selection.name : ''
setInternalValue(value)
onSelect(value)
}}
PopperComponent={StyledPopper}
renderInput={(params) => <TextField {...params} value={internalValue} />}
renderOption={(props, option) => (
<Box component='li' {...props}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant='h5'>{option.label}</Typography>
{option.description && (
<Typography sx={{ color: customization.isDarkMode ? '#9e9e9e' : '' }}>{option.description}</Typography>
)}
</div>
</Box>
)}
/>
</FormControl>
)
}
Dropdown.propTypes = {
name: PropTypes.string,
value: PropTypes.string,
options: PropTypes.array,
onSelect: PropTypes.func
}
@@ -0,0 +1,40 @@
import Editor from 'react-simple-code-editor'
import { highlight, languages } from 'prismjs/components/prism-core'
import 'prismjs/components/prism-clike'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-json'
import 'prismjs/components/prism-markup'
import './prism-dark.css'
import PropTypes from 'prop-types'
import { useTheme } from '@mui/material/styles'
export const DarkCodeEditor = ({ value, placeholder, type, style, onValueChange, onMouseUp, onBlur }) => {
const theme = useTheme()
return (
<Editor
value={value}
placeholder={placeholder}
highlight={(code) => highlight(code, type === 'json' ? languages.json : languages.js)}
padding={10}
onValueChange={onValueChange}
onMouseUp={onMouseUp}
onBlur={onBlur}
style={{
...style,
background: theme.palette.codeEditor.main
}}
textareaClassName='editor__textarea'
/>
)
}
DarkCodeEditor.propTypes = {
value: PropTypes.string,
placeholder: PropTypes.string,
type: PropTypes.string,
style: PropTypes.object,
onValueChange: PropTypes.func,
onMouseUp: PropTypes.func,
onBlur: PropTypes.func
}
@@ -0,0 +1,40 @@
import Editor from 'react-simple-code-editor'
import { highlight, languages } from 'prismjs/components/prism-core'
import 'prismjs/components/prism-clike'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-json'
import 'prismjs/components/prism-markup'
import './prism-light.css'
import PropTypes from 'prop-types'
import { useTheme } from '@mui/material/styles'
export const LightCodeEditor = ({ value, placeholder, type, style, onValueChange, onMouseUp, onBlur }) => {
const theme = useTheme()
return (
<Editor
value={value}
placeholder={placeholder}
highlight={(code) => highlight(code, type === 'json' ? languages.json : languages.js)}
padding={10}
onValueChange={onValueChange}
onMouseUp={onMouseUp}
onBlur={onBlur}
style={{
...style,
background: theme.palette.card.main
}}
textareaClassName='editor__textarea'
/>
)
}
LightCodeEditor.propTypes = {
value: PropTypes.string,
placeholder: PropTypes.string,
type: PropTypes.string,
style: PropTypes.object,
onValueChange: PropTypes.func,
onMouseUp: PropTypes.func,
onBlur: PropTypes.func
}
@@ -0,0 +1,275 @@
pre[class*='language-'],
code[class*='language-'] {
color: #d4d4d4;
font-size: 13px;
text-shadow: none;
font-family: Menlo, Monaco, Consolas, 'Andale Mono', 'Ubuntu Mono', 'Courier New', monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*='language-']::selection,
code[class*='language-']::selection,
pre[class*='language-'] *::selection,
code[class*='language-'] *::selection {
text-shadow: none;
background: #264f78;
}
@media print {
pre[class*='language-'],
code[class*='language-'] {
text-shadow: none;
}
}
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
background: #1e1e1e;
}
:not(pre) > code[class*='language-'] {
padding: 0.1em 0.3em;
border-radius: 0.3em;
color: #db4c69;
background: #1e1e1e;
}
/*********************************************************
* Tokens
*/
.namespace {
opacity: 0.7;
}
.token.doctype .token.doctype-tag {
color: #569cd6;
}
.token.doctype .token.name {
color: #9cdcfe;
}
.token.comment,
.token.prolog {
color: #6a9955;
}
.token.punctuation,
.language-html .language-css .token.punctuation,
.language-html .language-javascript .token.punctuation {
color: #d4d4d4;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.inserted,
.token.unit {
color: #b5cea8;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.deleted {
color: #ce9178;
}
.language-css .token.string.url {
text-decoration: underline;
}
.token.operator,
.token.entity {
color: #d4d4d4;
}
.token.operator.arrow {
color: #569cd6;
}
.token.atrule {
color: #ce9178;
}
.token.atrule .token.rule {
color: #c586c0;
}
.token.atrule .token.url {
color: #9cdcfe;
}
.token.atrule .token.url .token.function {
color: #dcdcaa;
}
.token.atrule .token.url .token.punctuation {
color: #d4d4d4;
}
.token.keyword {
color: #569cd6;
}
.token.keyword.module,
.token.keyword.control-flow {
color: #c586c0;
}
.token.function,
.token.function .token.maybe-class-name {
color: #dcdcaa;
}
.token.regex {
color: #d16969;
}
.token.important {
color: #569cd6;
}
.token.italic {
font-style: italic;
}
.token.constant {
color: #9cdcfe;
}
.token.class-name,
.token.maybe-class-name {
color: #4ec9b0;
}
.token.console {
color: #9cdcfe;
}
.token.parameter {
color: #9cdcfe;
}
.token.interpolation {
color: #9cdcfe;
}
.token.punctuation.interpolation-punctuation {
color: #569cd6;
}
.token.boolean {
color: #569cd6;
}
.token.property,
.token.variable,
.token.imports .token.maybe-class-name,
.token.exports .token.maybe-class-name {
color: #9cdcfe;
}
.token.selector {
color: #d7ba7d;
}
.token.escape {
color: #d7ba7d;
}
.token.tag {
color: #569cd6;
}
.token.tag .token.punctuation {
color: #808080;
}
.token.cdata {
color: #808080;
}
.token.attr-name {
color: #9cdcfe;
}
.token.attr-value,
.token.attr-value .token.punctuation {
color: #ce9178;
}
.token.attr-value .token.punctuation.attr-equals {
color: #d4d4d4;
}
.token.entity {
color: #569cd6;
}
.token.namespace {
color: #4ec9b0;
}
/*********************************************************
* Language Specific
*/
pre[class*='language-javascript'],
code[class*='language-javascript'],
pre[class*='language-jsx'],
code[class*='language-jsx'],
pre[class*='language-typescript'],
code[class*='language-typescript'],
pre[class*='language-tsx'],
code[class*='language-tsx'] {
color: #9cdcfe;
}
pre[class*='language-css'],
code[class*='language-css'] {
color: #ce9178;
}
pre[class*='language-html'],
code[class*='language-html'] {
color: #d4d4d4;
}
.language-regex .token.anchor {
color: #dcdcaa;
}
.language-html .token.punctuation {
color: #808080;
}
/*********************************************************
* Line highlighting
*/
pre[class*='language-'] > code[class*='language-'] {
position: relative;
z-index: 1;
}
.line-highlight.line-highlight {
background: #f7ebc6;
box-shadow: inset 5px 0 0 #f7d87c;
z-index: 0;
}
@@ -0,0 +1,207 @@
code[class*='language-'],
pre[class*='language-'] {
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
color: #90a4ae;
background: #fafafa;
font-family: Roboto Mono, monospace;
font-size: 1em;
line-height: 1.5em;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
code[class*='language-']::-moz-selection,
pre[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection,
pre[class*='language-'] ::-moz-selection {
background: #cceae7;
color: #263238;
}
code[class*='language-']::selection,
pre[class*='language-']::selection,
code[class*='language-'] ::selection,
pre[class*='language-'] ::selection {
background: #cceae7;
color: #263238;
}
:not(pre) > code[class*='language-'] {
white-space: normal;
border-radius: 0.2em;
padding: 0.1em;
}
pre[class*='language-'] {
overflow: auto;
position: relative;
margin: 0.5em 0;
padding: 1.25em 1em;
}
.language-css > code,
.language-sass > code,
.language-scss > code {
color: #f76d47;
}
[class*='language-'] .namespace {
opacity: 0.7;
}
.token.atrule {
color: #7c4dff;
}
.token.attr-name {
color: #39adb5;
}
.token.attr-value {
color: #f6a434;
}
.token.attribute {
color: #f6a434;
}
.token.boolean {
color: #7c4dff;
}
.token.builtin {
color: #39adb5;
}
.token.cdata {
color: #39adb5;
}
.token.char {
color: #39adb5;
}
.token.class {
color: #39adb5;
}
.token.class-name {
color: #6182b8;
}
.token.comment {
color: #aabfc9;
}
.token.constant {
color: #7c4dff;
}
.token.deleted {
color: #e53935;
}
.token.doctype {
color: #aabfc9;
}
.token.entity {
color: #e53935;
}
.token.function {
color: #7c4dff;
}
.token.hexcode {
color: #f76d47;
}
.token.id {
color: #7c4dff;
font-weight: bold;
}
.token.important {
color: #7c4dff;
font-weight: bold;
}
.token.inserted {
color: #39adb5;
}
.token.keyword {
color: #7c4dff;
}
.token.number {
color: #f76d47;
}
.token.operator {
color: #39adb5;
}
.token.prolog {
color: #aabfc9;
}
.token.property {
color: #39adb5;
}
.token.pseudo-class {
color: #f6a434;
}
.token.pseudo-element {
color: #f6a434;
}
.token.punctuation {
color: #39adb5;
}
.token.regex {
color: #6182b8;
}
.token.selector {
color: #e53935;
}
.token.string {
color: #f6a434;
}
.token.symbol {
color: #7c4dff;
}
.token.tag {
color: #e53935;
}
.token.unit {
color: #f76d47;
}
.token.url {
color: #e53935;
}
.token.variable {
color: #e53935;
}
@@ -0,0 +1,72 @@
import PropTypes from 'prop-types'
// material-ui
import { useTheme } from '@mui/material/styles'
import MuiAvatar from '@mui/material/Avatar'
// ==============================|| AVATAR ||============================== //
const Avatar = ({ color, outline, size, sx, ...others }) => {
const theme = useTheme()
const colorSX = color && !outline && { color: theme.palette.background.paper, bgcolor: `${color}.main` }
const outlineSX = outline && {
color: color ? `${color}.main` : `primary.main`,
bgcolor: theme.palette.background.paper,
border: '2px solid',
borderColor: color ? `${color}.main` : `primary.main`
}
let sizeSX = {}
switch (size) {
case 'badge':
sizeSX = {
width: theme.spacing(3.5),
height: theme.spacing(3.5)
}
break
case 'xs':
sizeSX = {
width: theme.spacing(4.25),
height: theme.spacing(4.25)
}
break
case 'sm':
sizeSX = {
width: theme.spacing(5),
height: theme.spacing(5)
}
break
case 'lg':
sizeSX = {
width: theme.spacing(9),
height: theme.spacing(9)
}
break
case 'xl':
sizeSX = {
width: theme.spacing(10.25),
height: theme.spacing(10.25)
}
break
case 'md':
sizeSX = {
width: theme.spacing(7.5),
height: theme.spacing(7.5)
}
break
default:
sizeSX = {}
}
return <MuiAvatar sx={{ ...colorSX, ...outlineSX, ...sizeSX, ...sx }} {...others} />
}
Avatar.propTypes = {
className: PropTypes.string,
color: PropTypes.string,
outline: PropTypes.bool,
size: PropTypes.string,
sx: PropTypes.object
}
export default Avatar
@@ -0,0 +1,184 @@
import PropTypes from 'prop-types'
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Box, Card, Divider, Grid, Typography } from '@mui/material'
import MuiBreadcrumbs from '@mui/material/Breadcrumbs'
// project imports
import config from 'config'
import { gridSpacing } from 'store/constant'
// assets
import { IconTallymark1 } from '@tabler/icons'
import AccountTreeTwoToneIcon from '@mui/icons-material/AccountTreeTwoTone'
import HomeIcon from '@mui/icons-material/Home'
import HomeTwoToneIcon from '@mui/icons-material/HomeTwoTone'
const linkSX = {
display: 'flex',
color: 'grey.900',
textDecoration: 'none',
alignContent: 'center',
alignItems: 'center'
}
// ==============================|| BREADCRUMBS ||============================== //
const Breadcrumbs = ({ card, divider, icon, icons, maxItems, navigation, rightAlign, separator, title, titleBottom, ...others }) => {
const theme = useTheme()
const iconStyle = {
marginRight: theme.spacing(0.75),
marginTop: `-${theme.spacing(0.25)}`,
width: '1rem',
height: '1rem',
color: theme.palette.secondary.main
}
const [main, setMain] = useState()
const [item, setItem] = useState()
// set active item state
const getCollapse = (menu) => {
if (menu.children) {
menu.children.filter((collapse) => {
if (collapse.type && collapse.type === 'collapse') {
getCollapse(collapse)
} else if (collapse.type && collapse.type === 'item') {
if (document.location.pathname === config.basename + collapse.url) {
setMain(menu)
setItem(collapse)
}
}
return false
})
}
}
useEffect(() => {
navigation?.items?.map((menu) => {
if (menu.type && menu.type === 'group') {
getCollapse(menu)
}
return false
})
})
// item separator
const SeparatorIcon = separator
const separatorIcon = separator ? <SeparatorIcon stroke={1.5} size='1rem' /> : <IconTallymark1 stroke={1.5} size='1rem' />
let mainContent
let itemContent
let breadcrumbContent = <Typography />
let itemTitle = ''
let CollapseIcon
let ItemIcon
// collapse item
if (main && main.type === 'collapse') {
CollapseIcon = main.icon ? main.icon : AccountTreeTwoToneIcon
mainContent = (
<Typography component={Link} to='#' variant='subtitle1' sx={linkSX}>
{icons && <CollapseIcon style={iconStyle} />}
{main.title}
</Typography>
)
}
// items
if (item && item.type === 'item') {
itemTitle = item.title
ItemIcon = item.icon ? item.icon : AccountTreeTwoToneIcon
itemContent = (
<Typography
variant='subtitle1'
sx={{
display: 'flex',
textDecoration: 'none',
alignContent: 'center',
alignItems: 'center',
color: 'grey.500'
}}
>
{icons && <ItemIcon style={iconStyle} />}
{itemTitle}
</Typography>
)
// main
if (item.breadcrumbs !== false) {
breadcrumbContent = (
<Card
sx={{
border: 'none'
}}
{...others}
>
<Box sx={{ p: 2, pl: card === false ? 0 : 2 }}>
<Grid
container
direction={rightAlign ? 'row' : 'column'}
justifyContent={rightAlign ? 'space-between' : 'flex-start'}
alignItems={rightAlign ? 'center' : 'flex-start'}
spacing={1}
>
{title && !titleBottom && (
<Grid item>
<Typography variant='h3' sx={{ fontWeight: 500 }}>
{item.title}
</Typography>
</Grid>
)}
<Grid item>
<MuiBreadcrumbs
sx={{ '& .MuiBreadcrumbs-separator': { width: 16, ml: 1.25, mr: 1.25 } }}
aria-label='breadcrumb'
maxItems={maxItems || 8}
separator={separatorIcon}
>
<Typography component={Link} to='/' color='inherit' variant='subtitle1' sx={linkSX}>
{icons && <HomeTwoToneIcon sx={iconStyle} />}
{icon && <HomeIcon sx={{ ...iconStyle, mr: 0 }} />}
{!icon && 'Dashboard'}
</Typography>
{mainContent}
{itemContent}
</MuiBreadcrumbs>
</Grid>
{title && titleBottom && (
<Grid item>
<Typography variant='h3' sx={{ fontWeight: 500 }}>
{item.title}
</Typography>
</Grid>
)}
</Grid>
</Box>
{card === false && divider !== false && <Divider sx={{ borderColor: theme.palette.primary.main, mb: gridSpacing }} />}
</Card>
)
}
}
return breadcrumbContent
}
Breadcrumbs.propTypes = {
card: PropTypes.bool,
divider: PropTypes.bool,
icon: PropTypes.bool,
icons: PropTypes.bool,
maxItems: PropTypes.number,
navigation: PropTypes.object,
rightAlign: PropTypes.bool,
separator: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
title: PropTypes.bool,
titleBottom: PropTypes.bool
}
export default Breadcrumbs
@@ -0,0 +1,22 @@
import logo from 'assets/images/flowise_logo.png'
import logoDark from 'assets/images/flowise_logo_dark.png'
import { useSelector } from 'react-redux'
// ==============================|| LOGO ||============================== //
const Logo = () => {
const customization = useSelector((state) => state.customization)
return (
<div style={{ alignItems: 'center', display: 'flex', flexDirection: 'row' }}>
<img
style={{ objectFit: 'contain', height: 'auto', width: 150 }}
src={customization.isDarkMode ? logoDark : logo}
alt='Flowise'
/>
</div>
)
}
export default Logo
@@ -0,0 +1,107 @@
import PropTypes from 'prop-types'
import { forwardRef } from 'react'
// material-ui
import { Collapse, Fade, Box, Grow, Slide, Zoom } from '@mui/material'
// ==============================|| TRANSITIONS ||============================== //
const Transitions = forwardRef(function Transitions({ children, position, type, direction, ...others }, ref) {
let positionSX = {
transformOrigin: '0 0 0'
}
switch (position) {
case 'top-right':
positionSX = {
transformOrigin: 'top right'
}
break
case 'top':
positionSX = {
transformOrigin: 'top'
}
break
case 'bottom-left':
positionSX = {
transformOrigin: 'bottom left'
}
break
case 'bottom-right':
positionSX = {
transformOrigin: 'bottom right'
}
break
case 'bottom':
positionSX = {
transformOrigin: 'bottom'
}
break
case 'top-left':
default:
positionSX = {
transformOrigin: '0 0 0'
}
break
}
return (
<Box ref={ref}>
{type === 'grow' && (
<Grow {...others}>
<Box sx={positionSX}>{children}</Box>
</Grow>
)}
{type === 'collapse' && (
<Collapse {...others} sx={positionSX}>
{children}
</Collapse>
)}
{type === 'fade' && (
<Fade
{...others}
timeout={{
appear: 500,
enter: 600,
exit: 400
}}
>
<Box sx={positionSX}>{children}</Box>
</Fade>
)}
{type === 'slide' && (
<Slide
{...others}
timeout={{
appear: 0,
enter: 400,
exit: 200
}}
direction={direction}
>
<Box sx={positionSX}>{children}</Box>
</Slide>
)}
{type === 'zoom' && (
<Zoom {...others}>
<Box sx={positionSX}>{children}</Box>
</Zoom>
)}
</Box>
)
})
Transitions.propTypes = {
children: PropTypes.node,
type: PropTypes.oneOf(['grow', 'fade', 'collapse', 'slide', 'zoom']),
position: PropTypes.oneOf(['top-left', 'top-right', 'top', 'bottom-left', 'bottom-right', 'bottom']),
direction: PropTypes.oneOf(['up', 'down', 'left', 'right'])
}
Transitions.defaultProps = {
type: 'grow',
position: 'top-left',
direction: 'up'
}
export default Transitions
@@ -0,0 +1,32 @@
import { useState } from 'react'
import PropTypes from 'prop-types'
import { FormControl, OutlinedInput } from '@mui/material'
export const Input = ({ inputParam, value, onChange }) => {
const [myValue, setMyValue] = useState(value ?? '')
return (
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
<OutlinedInput
id={inputParam.name}
size='small'
type={inputParam.type === 'string' ? 'text' : inputParam.type}
placeholder={inputParam.placeholder}
multiline={!!inputParam.rows}
maxRows={inputParam.rows || 0}
minRows={inputParam.rows || 0}
value={myValue}
name={inputParam.name}
onChange={(e) => {
setMyValue(e.target.value)
onChange(e.target.value)
}}
/>
</FormControl>
)
}
Input.propTypes = {
inputParam: PropTypes.object,
value: PropTypes.string,
onChange: PropTypes.func
}
@@ -0,0 +1,17 @@
import { Suspense } from 'react'
// project imports
import Loader from './Loader'
// ==============================|| LOADABLE - LAZY LOADING ||============================== //
const Loadable = (Component) =>
function WithLoader(props) {
return (
<Suspense fallback={<Loader />}>
<Component {...props} />
</Suspense>
)
}
export default Loadable
@@ -0,0 +1,21 @@
// material-ui
import LinearProgress from '@mui/material/LinearProgress'
import { styled } from '@mui/material/styles'
// styles
const LoaderWrapper = styled('div')({
position: 'fixed',
top: 0,
left: 0,
zIndex: 1301,
width: '100%'
})
// ==============================|| LOADER ||============================== //
const Loader = () => (
<LoaderWrapper>
<LinearProgress color='primary' />
</LoaderWrapper>
)
export default Loader
@@ -0,0 +1,25 @@
import { Info } from '@mui/icons-material'
import { IconButton, Tooltip } from '@mui/material'
import parser from 'html-react-parser'
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
export const TooltipWithParser = ({ title }) => {
const customization = useSelector((state) => state.customization)
return (
<Tooltip title={parser(title)} placement='right'>
<div style={{ display: 'flex', alignItems: 'center' }}>
<IconButton sx={{ height: 25, width: 25 }}>
<Info
style={{ background: 'transparent', color: customization.isDarkMode ? 'white' : 'inherit', height: 18, width: 18 }}
/>
</IconButton>
</div>
</Tooltip>
)
}
TooltipWithParser.propTypes = {
title: PropTypes.node
}
+233
View File
@@ -0,0 +1,233 @@
import moment from 'moment'
export const getUniqueNodeId = (nodeData, nodes) => {
// Get amount of same nodes
let totalSameNodes = 0
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i]
if (node.data.name === nodeData.name) {
totalSameNodes += 1
}
}
// Get unique id
let nodeId = `${nodeData.name}_${totalSameNodes}`
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i]
if (node.id === nodeId) {
totalSameNodes += 1
nodeId = `${nodeData.name}_${totalSameNodes}`
}
}
return nodeId
}
export const initializeNodeData = (nodeParams) => {
const initialValues = {}
for (let i = 0; i < nodeParams.length; i += 1) {
const input = nodeParams[i]
// Load from nodeParams default values
initialValues[input.name] = input.default || ''
// Special case for array, always initialize the item if default is not set
if (input.type === 'array' && !input.default) {
const newObj = {}
for (let j = 0; j < input.array.length; j += 1) {
newObj[input.array[j].name] = input.array[j].default || ''
}
initialValues[input.name] = [newObj]
}
}
return initialValues
}
export const initNode = (nodeData, newNodeId) => {
const inputAnchors = []
const incoming = nodeData.inputs ? nodeData.inputs.length : 0
const outgoing = 1
const whitelistTypes = ['asyncOptions', 'options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder']
for (let i = 0; i < incoming; i += 1) {
if (whitelistTypes.includes(nodeData.inputs[i].type)) continue
const newInput = {
...nodeData.inputs[i],
id: `${newNodeId}-input-${nodeData.inputs[i].name}-${nodeData.inputs[i].type}`
}
inputAnchors.push(newInput)
}
const outputAnchors = []
for (let i = 0; i < outgoing; i += 1) {
const newOutput = {
id: `${newNodeId}-output-${nodeData.name}-${nodeData.baseClasses.join('|')}`,
name: nodeData.name,
label: nodeData.type,
type: nodeData.baseClasses.join(' | ')
}
outputAnchors.push(newOutput)
}
nodeData.id = newNodeId
nodeData.inputAnchors = inputAnchors
nodeData.outputAnchors = outputAnchors
/*
Initial inputs = [
{
label: 'field_label',
name: 'field'
}
]
// Turn into inputs object with default values
Converted inputs = { 'field': 'defaultvalue' }
// Move remaining inputs that are not part of inputAnchors to inputParams
inputParams = [
{
label: 'field_label',
name: 'field'
}
]
*/
if (nodeData.inputs) {
nodeData.inputParams = nodeData.inputs.filter(({ name }) => !nodeData.inputAnchors.some((exclude) => exclude.name === name))
nodeData.inputs = initializeNodeData(nodeData.inputs)
} else {
nodeData.inputParams = []
nodeData.inputs = {}
}
return nodeData
}
export const getEdgeLabelName = (source) => {
const sourceSplit = source.split('-')
if (sourceSplit.length && sourceSplit[0].includes('ifElse')) {
const outputAnchorsIndex = sourceSplit[sourceSplit.length - 1]
return outputAnchorsIndex === '0' ? 'true' : 'false'
}
return ''
}
export const isValidConnection = (connection, reactFlowInstance) => {
const sourceHandle = connection.sourceHandle
const targetHandle = connection.targetHandle
const target = connection.target
//sourceHandle: "llmChain_0-output-llmChain-BaseChain"
//targetHandle: "mrlkAgentLLM_0-input-model-BaseLanguageModel"
const sourceTypes = sourceHandle.split('-')[sourceHandle.split('-').length - 1].split('|')
const targetTypes = targetHandle.split('-')[targetHandle.split('-').length - 1].split('|')
if (targetTypes.some((t) => sourceTypes.includes(t))) {
let targetNode = reactFlowInstance.getNode(target)
if (!targetNode) {
if (!reactFlowInstance.getEdges().find((e) => e.targetHandle === targetHandle)) {
return true
}
} else {
const targetNodeInputAnchor = targetNode.data.inputAnchors.find((ancr) => ancr.id === targetHandle)
if (
(targetNodeInputAnchor &&
!targetNodeInputAnchor?.list &&
!reactFlowInstance.getEdges().find((e) => e.targetHandle === targetHandle)) ||
targetNodeInputAnchor?.list
) {
return true
}
}
}
return false
}
export const convertDateStringToDateObject = (dateString) => {
if (dateString === undefined || !dateString) return undefined
const date = moment(dateString)
if (!date.isValid) return undefined
// Sat Sep 24 2022 07:30:14
return new Date(date.year(), date.month(), date.date(), date.hours(), date.minutes())
}
export const getFileName = (fileBase64) => {
const splitDataURI = fileBase64.split(',')
const filename = splitDataURI[splitDataURI.length - 1].split(':')[1]
return filename
}
export const getFolderName = (base64ArrayStr) => {
try {
const base64Array = JSON.parse(base64ArrayStr)
const filenames = []
for (let i = 0; i < base64Array.length; i += 1) {
const fileBase64 = base64Array[i]
const splitDataURI = fileBase64.split(',')
const filename = splitDataURI[splitDataURI.length - 1].split(':')[1]
filenames.push(filename)
}
return filenames.length ? filenames.join(',') : ''
} catch (e) {
return ''
}
}
export const generateExportFlowData = (flowData) => {
const nodes = flowData.nodes
const edges = flowData.edges
for (let i = 0; i < nodes.length; i += 1) {
nodes[i].selected = false
const node = nodes[i]
const newNodeData = {
id: node.data.id,
label: node.data.label,
name: node.data.name,
type: node.data.type,
baseClasses: node.data.baseClasses,
category: node.data.category,
description: node.data.description,
inputParams: node.data.inputParams,
inputAnchors: node.data.inputAnchors,
inputs: {},
outputAnchors: node.data.outputAnchors,
selected: false
}
// Remove password
if (node.data.inputs && Object.keys(node.data.inputs).length) {
const nodeDataInputs = {}
for (const input in node.data.inputs) {
const inputParam = node.data.inputParams.find((inp) => inp.name === input)
if (inputParam && inputParam.type === 'password') continue
nodeDataInputs[input] = node.data.inputs[input]
}
newNodeData.inputs = nodeDataInputs
}
nodes[i].data = newNodeData
}
const exportJson = {
nodes,
edges
}
return exportJson
}
export const copyToClipboard = (e) => {
const src = e.src
if (Array.isArray(src) || typeof src === 'object') {
navigator.clipboard.writeText(JSON.stringify(src, null, ' '))
} else {
navigator.clipboard.writeText(src)
}
}
+56
View File
@@ -0,0 +1,56 @@
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useSnackbar } from 'notistack'
import { removeSnackbar } from 'store/actions'
let displayed = []
const useNotifier = () => {
const dispatch = useDispatch()
const notifier = useSelector((state) => state.notifier)
const { notifications } = notifier
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const storeDisplayed = (id) => {
displayed = [...displayed, id]
}
const removeDisplayed = (id) => {
displayed = [...displayed.filter((key) => id !== key)]
}
React.useEffect(() => {
notifications.forEach(({ key, message, options = {}, dismissed = false }) => {
if (dismissed) {
// dismiss snackbar using notistack
closeSnackbar(key)
return
}
// do nothing if snackbar is already displayed
if (displayed.includes(key)) return
// display snackbar using notistack
enqueueSnackbar(message, {
key,
...options,
onClose: (event, reason, myKey) => {
if (options.onClose) {
options.onClose(event, reason, myKey)
}
},
onExited: (event, myKey) => {
// remove this snackbar from redux store
dispatch(removeSnackbar(myKey))
removeDisplayed(myKey)
}
})
// keep track of snackbars that we've displayed
storeDisplayed(key)
})
}, [notifications, closeSnackbar, enqueueSnackbar, dispatch])
}
export default useNotifier
+37
View File
@@ -0,0 +1,37 @@
import { useCallback, useContext, useEffect } from 'react'
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom'
// https://stackoverflow.com/questions/71572678/react-router-v-6-useprompt-typescript
export function useBlocker(blocker, when = true) {
const { navigator } = useContext(NavigationContext)
useEffect(() => {
if (!when) return
const unblock = navigator.block((tx) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock()
tx.retry()
}
}
blocker(autoUnblockingTx)
})
return unblock
}, [navigator, blocker, when])
}
export function usePrompt(message, when = true) {
const blocker = useCallback(
(tx) => {
if (window.confirm(message)) tx.retry()
},
[message]
)
useBlocker(blocker, when)
}
+296
View File
@@ -0,0 +1,296 @@
import { useState, useRef, useEffect } from 'react'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
// material-ui
import { useTheme } from '@mui/material/styles'
import {
Accordion,
AccordionSummary,
AccordionDetails,
Box,
ClickAwayListener,
Divider,
InputAdornment,
List,
ListItemButton,
ListItem,
ListItemAvatar,
ListItemText,
OutlinedInput,
Paper,
Popper,
Stack,
Typography
} from '@mui/material'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
// third-party
import PerfectScrollbar from 'react-perfect-scrollbar'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import Transitions from 'ui-component/extended/Transitions'
import { StyledFab } from 'ui-component/button/StyledFab'
// icons
import { IconPlus, IconSearch, IconMinus } from '@tabler/icons'
// const
import { baseURL } from 'store/constant'
// ==============================|| ADD NODES||============================== //
const AddNodes = ({ nodesData, node }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [searchValue, setSearchValue] = useState('')
const [nodes, setNodes] = useState({})
const [open, setOpen] = useState(false)
const [categoryExpanded, setCategoryExpanded] = useState({})
const anchorRef = useRef(null)
const prevOpen = useRef(open)
const ps = useRef()
const scrollTop = () => {
const curr = ps.current
if (curr) {
curr.scrollTop = 0
}
}
const filterSearch = (value) => {
setSearchValue(value)
setTimeout(() => {
if (value) {
const returnData = nodesData.filter((nd) => nd.name.toLowerCase().includes(value.toLowerCase()))
groupByCategory(returnData, true)
scrollTop()
} else if (value === '') {
groupByCategory(nodesData)
scrollTop()
}
}, 500)
}
const groupByCategory = (nodes, isFilter) => {
const accordianCategories = {}
const result = nodes.reduce(function (r, a) {
r[a.category] = r[a.category] || []
r[a.category].push(a)
accordianCategories[a.category] = isFilter ? true : false
return r
}, Object.create(null))
setNodes(result)
setCategoryExpanded(accordianCategories)
}
const handleAccordionChange = (category) => (event, isExpanded) => {
const accordianCategories = { ...categoryExpanded }
accordianCategories[category] = isExpanded
setCategoryExpanded(accordianCategories)
}
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return
}
setOpen(false)
}
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen)
}
const onDragStart = (event, node) => {
event.dataTransfer.setData('application/reactflow', JSON.stringify(node))
event.dataTransfer.effectAllowed = 'move'
}
useEffect(() => {
if (prevOpen.current === true && open === false) {
anchorRef.current.focus()
}
prevOpen.current = open
}, [open])
useEffect(() => {
if (node) setOpen(false)
}, [node])
useEffect(() => {
if (nodesData) groupByCategory(nodesData)
}, [nodesData])
return (
<>
<StyledFab
sx={{ left: 20, top: 20 }}
ref={anchorRef}
size='small'
color='primary'
aria-label='add'
title='Add Node'
onClick={handleToggle}
>
{open ? <IconMinus /> : <IconPlus />}
</StyledFab>
<Popper
placement='bottom-end'
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [-40, 14]
}
}
]
}}
sx={{ zIndex: 1000 }}
>
{({ TransitionProps }) => (
<Transitions in={open} {...TransitionProps}>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
<Box sx={{ p: 2 }}>
<Stack>
<Typography variant='h4'>Add Nodes</Typography>
</Stack>
<OutlinedInput
sx={{ width: '100%', pr: 1, pl: 2, my: 2 }}
id='input-search-node'
value={searchValue}
onChange={(e) => filterSearch(e.target.value)}
placeholder='Search nodes'
startAdornment={
<InputAdornment position='start'>
<IconSearch stroke={1.5} size='1rem' color={theme.palette.grey[500]} />
</InputAdornment>
}
aria-describedby='search-helper-text'
inputProps={{
'aria-label': 'weight'
}}
/>
<Divider />
</Box>
<PerfectScrollbar
containerRef={(el) => {
ps.current = el
}}
style={{ height: '100%', maxHeight: 'calc(100vh - 320px)', overflowX: 'hidden' }}
>
<Box sx={{ p: 2 }}>
<List
sx={{
width: '100%',
maxWidth: 370,
py: 0,
borderRadius: '10px',
[theme.breakpoints.down('md')]: {
maxWidth: 370
},
'& .MuiListItemSecondaryAction-root': {
top: 22
},
'& .MuiDivider-root': {
my: 0
},
'& .list-container': {
pl: 7
}
}}
>
{Object.keys(nodes)
.sort()
.map((category) => (
<Accordion
expanded={categoryExpanded[category] || false}
onChange={handleAccordionChange(category)}
key={category}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`nodes-accordian-${category}`}
id={`nodes-accordian-header-${category}`}
>
<Typography variant='h5'>{category}</Typography>
</AccordionSummary>
<AccordionDetails>
{nodes[category].map((node, index) => (
<div
key={node.name}
onDragStart={(event) => onDragStart(event, node)}
draggable
>
<ListItemButton
sx={{
p: 0,
borderRadius: `${customization.borderRadius}px`,
cursor: 'move'
}}
>
<ListItem alignItems='center'>
<ListItemAvatar>
<div
style={{
width: 50,
height: 50,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 10,
objectFit: 'contain'
}}
alt={node.name}
src={`${baseURL}/api/v1/node-icon/${node.name}`}
/>
</div>
</ListItemAvatar>
<ListItemText
sx={{ ml: 1 }}
primary={node.label}
secondary={node.description}
/>
</ListItem>
</ListItemButton>
{index === nodes[category].length - 1 ? null : <Divider />}
</div>
))}
</AccordionDetails>
</Accordion>
))}
</List>
</Box>
</PerfectScrollbar>
</MainCard>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</Popper>
</>
)
}
AddNodes.propTypes = {
nodesData: PropTypes.array,
node: PropTypes.object
}
export default AddNodes
@@ -0,0 +1,72 @@
import { getBezierPath, EdgeText } from 'reactflow'
import PropTypes from 'prop-types'
import { useDispatch } from 'react-redux'
import { REMOVE_EDGE } from 'store/actions'
import './index.css'
const foreignObjectSize = 40
const ButtonEdge = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, data, markerEnd }) => {
const [edgePath, edgeCenterX, edgeCenterY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
})
const dispatch = useDispatch()
const onEdgeClick = (evt, id) => {
evt.stopPropagation()
dispatch({ type: REMOVE_EDGE, edgeId: `${id}:${Date.now()}` })
}
return (
<>
<path id={id} style={style} className='react-flow__edge-path' d={edgePath} markerEnd={markerEnd} />
{data && data.label && (
<EdgeText
x={sourceX + 10}
y={sourceY + 10}
label={data.label}
labelStyle={{ fill: 'black' }}
labelBgStyle={{ fill: 'transparent' }}
labelBgPadding={[2, 4]}
labelBgBorderRadius={2}
/>
)}
<foreignObject
width={foreignObjectSize}
height={foreignObjectSize}
x={edgeCenterX - foreignObjectSize / 2}
y={edgeCenterY - foreignObjectSize / 2}
className='edgebutton-foreignobject'
requiredExtensions='http://www.w3.org/1999/xhtml'
>
<div>
<button className='edgebutton' onClick={(event) => onEdgeClick(event, id)}>
×
</button>
</div>
</foreignObject>
</>
)
}
ButtonEdge.propTypes = {
id: PropTypes.string,
sourceX: PropTypes.number,
sourceY: PropTypes.number,
targetX: PropTypes.number,
targetY: PropTypes.number,
sourcePosition: PropTypes.any,
targetPosition: PropTypes.any,
style: PropTypes.object,
data: PropTypes.object,
markerEnd: PropTypes.any
}
export default ButtonEdge
@@ -0,0 +1,291 @@
import PropTypes from 'prop-types'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { useEffect, useRef, useState } from 'react'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Avatar, Box, ButtonBase, Typography, Stack, TextField } from '@mui/material'
// icons
import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck, IconX } from '@tabler/icons'
// project imports
import Settings from 'views/settings'
import SaveChatflowDialog from 'ui-component/dialog/SaveChatflowDialog'
// API
import chatflowsApi from 'api/chatflows'
// Hooks
import useApi from 'hooks/useApi'
// utils
import { generateExportFlowData } from 'utils/genericHelper'
// ==============================|| CANVAS HEADER ||============================== //
const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFlow }) => {
const theme = useTheme()
const navigate = useNavigate()
const flowNameRef = useRef()
const settingsRef = useRef()
const [isEditingFlowName, setEditingFlowName] = useState(null)
const [flowName, setFlowName] = useState('')
const [isSettingsOpen, setSettingsOpen] = useState(false)
const [flowDialogOpen, setFlowDialogOpen] = useState(false)
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
const canvas = useSelector((state) => state.canvas)
const onSettingsItemClick = (setting) => {
setSettingsOpen(false)
if (setting === 'deleteChatflow') {
handleDeleteFlow()
} else if (setting === 'exportChatflow') {
try {
const flowData = JSON.parse(chatflow.flowData)
let dataStr = JSON.stringify(generateExportFlowData(flowData))
let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
let exportFileDefaultName = `${chatflow.name} Chatflow.json`
let linkElement = document.createElement('a')
linkElement.setAttribute('href', dataUri)
linkElement.setAttribute('download', exportFileDefaultName)
linkElement.click()
} catch (e) {
console.error(e)
}
}
}
const onUploadFile = (file) => {
setSettingsOpen(false)
handleLoadFlow(file)
}
const submitFlowName = () => {
if (chatflow.id) {
const updateBody = {
name: flowNameRef.current.value
}
updateChatflowApi.request(chatflow.id, updateBody)
}
}
const onSaveChatflowClick = () => {
if (chatflow.id) handleSaveFlow(chatflow.name)
else setFlowDialogOpen(true)
}
const onConfirmSaveName = (flowName) => {
setFlowDialogOpen(false)
handleSaveFlow(flowName)
}
useEffect(() => {
if (updateChatflowApi.data) {
setFlowName(updateChatflowApi.data.name)
}
setEditingFlowName(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updateChatflowApi.data])
useEffect(() => {
if (chatflow) {
setFlowName(chatflow.name)
}
}, [chatflow])
return (
<>
<Box>
<ButtonBase title='Back' sx={{ borderRadius: '50%' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.secondary.light,
color: theme.palette.secondary.dark,
'&:hover': {
background: theme.palette.secondary.dark,
color: theme.palette.secondary.light
}
}}
color='inherit'
onClick={() => navigate(-1)}
>
<IconChevronLeft stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
</Box>
<Box sx={{ flexGrow: 1 }}>
{!isEditingFlowName && (
<Stack flexDirection='row'>
<Typography
sx={{
fontSize: '1.5rem',
fontWeight: 600,
ml: 2
}}
>
{canvas.isDirty && <strong style={{ color: theme.palette.orange.main }}>*</strong>} {flowName}
</Typography>
{chatflow?.id && (
<ButtonBase title='Edit Name' sx={{ borderRadius: '50%' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
ml: 1,
background: theme.palette.secondary.light,
color: theme.palette.secondary.dark,
'&:hover': {
background: theme.palette.secondary.dark,
color: theme.palette.secondary.light
}
}}
color='inherit'
onClick={() => setEditingFlowName(true)}
>
<IconPencil stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
)}
</Stack>
)}
{isEditingFlowName && (
<Stack flexDirection='row'>
<TextField
size='small'
inputRef={flowNameRef}
sx={{
width: '50%',
ml: 2
}}
defaultValue={flowName}
/>
<ButtonBase title='Save Name' sx={{ borderRadius: '50%' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.success.light,
color: theme.palette.success.dark,
ml: 1,
'&:hover': {
background: theme.palette.success.dark,
color: theme.palette.success.light
}
}}
color='inherit'
onClick={submitFlowName}
>
<IconCheck stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
<ButtonBase title='Cancel' sx={{ borderRadius: '50%' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.error.light,
color: theme.palette.error.dark,
ml: 1,
'&:hover': {
background: theme.palette.error.dark,
color: theme.palette.error.light
}
}}
color='inherit'
onClick={() => setEditingFlowName(false)}
>
<IconX stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
</Stack>
)}
</Box>
<Box>
<ButtonBase title='Save Chatflow' sx={{ borderRadius: '50%', mr: 2 }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.canvasHeader.saveLight,
color: theme.palette.canvasHeader.saveDark,
'&:hover': {
background: theme.palette.canvasHeader.saveDark,
color: theme.palette.canvasHeader.saveLight
}
}}
color='inherit'
onClick={onSaveChatflowClick}
>
<IconDeviceFloppy stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
<ButtonBase ref={settingsRef} title='Settings' sx={{ borderRadius: '50%' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.canvasHeader.settingsLight,
color: theme.palette.canvasHeader.settingsDark,
'&:hover': {
background: theme.palette.canvasHeader.settingsDark,
color: theme.palette.canvasHeader.settingsLight
}
}}
onClick={() => setSettingsOpen(!isSettingsOpen)}
>
<IconSettings stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
</Box>
<Settings
chatflow={chatflow}
isSettingsOpen={isSettingsOpen}
anchorEl={settingsRef.current}
onClose={() => setSettingsOpen(false)}
onSettingsItemClick={onSettingsItemClick}
onUploadFile={onUploadFile}
/>
<SaveChatflowDialog
show={flowDialogOpen}
dialogProps={{
title: `Save New Chatflow`,
confirmButtonName: 'Save',
cancelButtonName: 'Cancel'
}}
onCancel={() => setFlowDialogOpen(false)}
onConfirm={onConfirmSaveName}
/>
</>
)
}
CanvasHeader.propTypes = {
chatflow: PropTypes.object,
handleSaveFlow: PropTypes.func,
handleDeleteFlow: PropTypes.func,
handleLoadFlow: PropTypes.func
}
export default CanvasHeader
+136
View File
@@ -0,0 +1,136 @@
import PropTypes from 'prop-types'
import { useContext } from 'react'
// material-ui
import { styled, useTheme } from '@mui/material/styles'
import { IconButton, Box, Typography, Divider } from '@mui/material'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import NodeInputHandler from './NodeInputHandler'
import NodeOutputHandler from './NodeOutputHandler'
// const
import { baseURL } from 'store/constant'
import { IconTrash } from '@tabler/icons'
import { flowContext } from 'store/context/ReactFlowContext'
const CardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.main,
color: theme.darkTextPrimary,
border: 'solid 1px',
borderColor: theme.palette.primary[200] + 75,
width: '300px',
height: 'auto',
padding: '10px',
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
'&:hover': {
borderColor: theme.palette.primary.main
}
}))
// ===========================|| CANVAS NODE ||=========================== //
const CanvasNode = ({ data }) => {
const theme = useTheme()
const { deleteNode } = useContext(flowContext)
return (
<>
<CardWrapper
content={false}
sx={{
padding: 0,
borderColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary
}}
border={false}
>
<Box>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Box item style={{ width: 50, marginRight: 10, padding: 5 }}>
<div
style={{
...theme.typography.commonAvatar,
...theme.typography.largeAvatar,
borderRadius: '50%',
backgroundColor: 'white',
cursor: 'grab'
}}
>
<img
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }}
src={`${baseURL}/api/v1/node-icon/${data.name}`}
alt='Notification'
/>
</div>
</Box>
<Box>
<Typography
sx={{
fontSize: '1rem',
fontWeight: 500
}}
>
{data.label}
</Typography>
</Box>
<div style={{ flexGrow: 1 }}></div>
<IconButton
onClick={() => {
deleteNode(data.id)
}}
sx={{ height: 35, width: 35, mr: 1 }}
>
<IconTrash />
</IconButton>
</div>
{(data.inputAnchors.length > 0 || data.inputParams.length > 0) && (
<>
<Divider />
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
<Typography
sx={{
fontWeight: 500,
textAlign: 'center'
}}
>
Inputs
</Typography>
</Box>
<Divider />
</>
)}
{data.inputAnchors.map((inputAnchor, index) => (
<NodeInputHandler key={index} inputAnchor={inputAnchor} data={data} />
))}
{data.inputParams.map((inputParam, index) => (
<NodeInputHandler key={index} inputParam={inputParam} data={data} />
))}
<Divider />
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
<Typography
sx={{
fontWeight: 500,
textAlign: 'center'
}}
>
Output
</Typography>
</Box>
<Divider />
{data.outputAnchors.map((outputAnchor, index) => (
<NodeOutputHandler key={index} outputAnchor={outputAnchor} data={data} />
))}
</Box>
</CardWrapper>
</>
)
}
CanvasNode.propTypes = {
data: PropTypes.object
}
export default CanvasNode
@@ -0,0 +1,104 @@
import PropTypes from 'prop-types'
import { Handle, Position, useUpdateNodeInternals } from 'reactflow'
import { useEffect, useRef, useState, useContext } from 'react'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Box, Typography, Tooltip } from '@mui/material'
import { Dropdown } from 'ui-component/dropdown/Dropdown'
import { Input } from 'ui-component/input/Input'
import { flowContext } from 'store/context/ReactFlowContext'
import { isValidConnection } from 'utils/genericHelper'
// ===========================|| NodeInputHandler ||=========================== //
const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
const theme = useTheme()
const ref = useRef(null)
const updateNodeInternals = useUpdateNodeInternals()
const [position, setPosition] = useState(0)
const { reactFlowInstance } = useContext(flowContext)
useEffect(() => {
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2)
updateNodeInternals(data.id)
}
}, [data.id, ref, updateNodeInternals])
useEffect(() => {
updateNodeInternals(data.id)
}, [data.id, position, updateNodeInternals])
return (
<div ref={ref}>
{inputAnchor && (
<>
<Tooltip
placement='left'
title={
<Typography sx={{ color: 'white', p: 1 }} variant='h5'>
{'Type: ' + inputAnchor.type}
</Typography>
}
>
<Handle
type='target'
position={Position.Left}
key={inputAnchor.id}
id={inputAnchor.id}
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
style={{
height: 10,
width: 10,
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
top: position
}}
/>
</Tooltip>
<Box sx={{ p: 2 }}>
<Typography>
{inputAnchor.label}
{!inputAnchor.optional && <span style={{ color: 'red' }}>&nbsp;*</span>}
</Typography>
</Box>
</>
)}
{inputParam && (
<>
<Box sx={{ p: 2 }}>
<Typography>
{inputParam.label}
{!inputParam.optional && <span style={{ color: 'red' }}>&nbsp;*</span>}
</Typography>
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
<Input
inputParam={inputParam}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
/>
)}
{inputParam.type === 'options' && (
<Dropdown
name={inputParam.name}
options={inputParam.options}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'chose an option'}
/>
)}
</Box>
</>
)}
</div>
)
}
NodeInputHandler.propTypes = {
inputAnchor: PropTypes.object,
inputParam: PropTypes.object,
data: PropTypes.object
}
export default NodeInputHandler
@@ -0,0 +1,71 @@
import PropTypes from 'prop-types'
import { Handle, Position, useUpdateNodeInternals } from 'reactflow'
import { useEffect, useRef, useState, useContext } from 'react'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Box, Typography, Tooltip } from '@mui/material'
import { flowContext } from 'store/context/ReactFlowContext'
import { isValidConnection } from 'utils/genericHelper'
// ===========================|| NodeOutputHandler ||=========================== //
const NodeOutputHandler = ({ outputAnchor, data }) => {
const theme = useTheme()
const ref = useRef(null)
const updateNodeInternals = useUpdateNodeInternals()
const [position, setPosition] = useState(0)
const { reactFlowInstance } = useContext(flowContext)
useEffect(() => {
if (ref.current && ref.current?.offsetTop && ref.current?.clientHeight) {
setTimeout(() => {
setPosition(ref.current?.offsetTop + ref.current?.clientHeight / 2)
updateNodeInternals(data.id)
}, 0)
}
}, [data.id, ref, updateNodeInternals])
useEffect(() => {
setTimeout(() => {
updateNodeInternals(data.id)
}, 0)
}, [data.id, position, updateNodeInternals])
return (
<div ref={ref}>
<Tooltip
placement='right'
title={
<Typography sx={{ color: 'white', p: 1 }} variant='h5'>
{'Type: ' + outputAnchor.type}
</Typography>
}
>
<Handle
type='source'
position={Position.Right}
key={outputAnchor.id}
id={outputAnchor.id}
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
style={{
height: 10,
width: 10,
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
top: position
}}
/>
</Tooltip>
<Box sx={{ p: 2, textAlign: 'end' }}>
<Typography>{outputAnchor.label}</Typography>
</Box>
</div>
)
}
NodeOutputHandler.propTypes = {
outputAnchor: PropTypes.object,
data: PropTypes.object
}
export default NodeOutputHandler
+37
View File
@@ -0,0 +1,37 @@
.edgebutton {
width: 20px;
height: 20px;
background: #eee;
border: 1px solid #fff;
cursor: pointer;
border-radius: 50%;
font-size: 12px;
line-height: 1;
}
.edgebutton:hover {
background: #5e35b1;
color: #eee;
box-shadow: 0 0 6px 2px rgba(0, 0, 0, 0.08);
}
.edgebutton-foreignobject div {
background: transparent;
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
min-height: 40px;
}
.reactflow-parent-wrapper {
display: flex;
flex-grow: 1;
height: 100%;
}
.reactflow-parent-wrapper .reactflow-wrapper {
flex-grow: 1;
height: 100%;
}
+515
View File
@@ -0,0 +1,515 @@
import { useEffect, useRef, useState, useCallback, useContext } from 'react'
import ReactFlow, { addEdge, Controls, Background, useNodesState, useEdgesState } from 'reactflow'
import 'reactflow/dist/style.css'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import { usePrompt } from '../../utils/usePrompt'
import {
REMOVE_DIRTY,
SET_DIRTY,
SET_CHATFLOW,
enqueueSnackbar as enqueueSnackbarAction,
closeSnackbar as closeSnackbarAction
} from 'store/actions'
// material-ui
import { Toolbar, Box, AppBar, Button } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import CanvasNode from './CanvasNode'
import ButtonEdge from './ButtonEdge'
import CanvasHeader from './CanvasHeader'
import AddNodes from './AddNodes'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
import { ChatMessage } from 'views/chatmessage/ChatMessage'
import { flowContext } from 'store/context/ReactFlowContext'
// API
import nodesApi from 'api/nodes'
import chatflowsApi from 'api/chatflows'
// Hooks
import useApi from 'hooks/useApi'
import useConfirm from 'hooks/useConfirm'
// icons
import { IconX } from '@tabler/icons'
// utils
import { getUniqueNodeId, initNode, getEdgeLabelName } from 'utils/genericHelper'
import useNotifier from 'utils/useNotifier'
const nodeTypes = { customNode: CanvasNode }
const edgeTypes = { buttonedge: ButtonEdge }
// ==============================|| CANVAS ||============================== //
const Canvas = () => {
const theme = useTheme()
const navigate = useNavigate()
const URLpath = document.location.pathname.toString().split('/')
const chatflowId = URLpath[URLpath.length - 1] === 'canvas' ? '' : URLpath[URLpath.length - 1]
const { confirm } = useConfirm()
const dispatch = useDispatch()
const canvas = useSelector((state) => state.canvas)
const [canvasDataStore, setCanvasDataStore] = useState(canvas)
const [chatflow, setChatflow] = useState(null)
const { reactFlowInstance, setReactFlowInstance } = useContext(flowContext)
// ==============================|| Snackbar ||============================== //
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
// ==============================|| ReactFlow ||============================== //
const [nodes, setNodes, onNodesChange] = useNodesState()
const [edges, setEdges, onEdgesChange] = useEdgesState()
const [selectedNode, setSelectedNode] = useState(null)
const reactFlowWrapper = useRef(null)
// ==============================|| Chatflow API ||============================== //
const getNodesApi = useApi(nodesApi.getAllNodes)
const createNewChatflowApi = useApi(chatflowsApi.createNewChatflow)
const testChatflowApi = useApi(chatflowsApi.testChatflow)
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
const getSpecificChatflowApi = useApi(chatflowsApi.getSpecificChatflow)
// ==============================|| Events & Actions ||============================== //
const onConnect = (params) => {
const newEdge = {
...params,
type: 'buttonedge',
id: `${params.source}-${params.sourceHandle}-${params.target}-${params.targetHandle}`,
data: { label: getEdgeLabelName(params.sourceHandle) }
}
const targetNodeId = params.targetHandle.split('-')[0]
const sourceNodeId = params.sourceHandle.split('-')[0]
const targetInput = params.targetHandle.split('-')[2]
setNodes((nds) =>
nds.map((node) => {
if (node.id === targetNodeId) {
setTimeout(() => setDirty(), 0)
let value
const inputAnchor = node.data.inputAnchors.find((ancr) => ancr.name === targetInput)
if (inputAnchor && inputAnchor.list) {
const newValues = node.data.inputs[targetInput] || []
newValues.push(`{{${sourceNodeId}.data.instance}}`)
value = newValues
} else {
value = `{{${sourceNodeId}.data.instance}}`
}
node.data = {
...node.data,
inputs: {
...node.data.inputs,
[targetInput]: value
}
}
}
return node
})
)
setEdges((eds) => addEdge(newEdge, eds))
setDirty()
}
const handleLoadFlow = (file) => {
try {
const flowData = JSON.parse(file)
const nodes = flowData.nodes || []
setNodes(nodes)
setEdges(flowData.edges || [])
setDirty()
} catch (e) {
console.error(e)
}
}
const handleDeleteFlow = async () => {
const confirmPayload = {
title: `Delete`,
description: `Delete chatflow ${chatflow.name}?`,
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
}
const isConfirmed = await confirm(confirmPayload)
if (isConfirmed) {
try {
await chatflowsApi.deleteChatflow(chatflow.id)
navigate(-1)
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: errorData,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
const handleSaveFlow = (chatflowName) => {
if (reactFlowInstance) {
setNodes((nds) =>
nds.map((node) => {
node.data = {
...node.data,
selected: false
}
return node
})
)
const rfInstanceObject = reactFlowInstance.toObject()
const flowData = JSON.stringify(rfInstanceObject)
if (!chatflow.id) {
const newChatflowBody = {
name: chatflowName,
deployed: false,
flowData
}
createNewChatflowApi.request(newChatflowBody)
} else {
const updateBody = {
name: chatflowName,
flowData
}
updateChatflowApi.request(chatflow.id, updateBody)
}
}
}
// eslint-disable-next-line
const onNodeClick = useCallback((event, clickedNode) => {
setSelectedNode(clickedNode)
setNodes((nds) =>
nds.map((node) => {
if (node.id === clickedNode.id) {
node.data = {
...node.data,
selected: true
}
} else {
node.data = {
...node.data,
selected: false
}
}
return node
})
)
})
const onDragOver = useCallback((event) => {
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
}, [])
const onDrop = useCallback(
(event) => {
event.preventDefault()
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect()
let nodeData = event.dataTransfer.getData('application/reactflow')
// check if the dropped element is valid
if (typeof nodeData === 'undefined' || !nodeData) {
return
}
nodeData = JSON.parse(nodeData)
const position = reactFlowInstance.project({
x: event.clientX - reactFlowBounds.left - 100,
y: event.clientY - reactFlowBounds.top - 50
})
const newNodeId = getUniqueNodeId(nodeData, reactFlowInstance.getNodes())
const newNode = {
id: newNodeId,
position,
type: 'customNode',
data: initNode(nodeData, newNodeId)
}
setSelectedNode(newNode)
setNodes((nds) =>
nds.concat(newNode).map((node) => {
if (node.id === newNode.id) {
node.data = {
...node.data,
selected: true
}
} else {
node.data = {
...node.data,
selected: false
}
}
return node
})
)
setTimeout(() => setDirty(), 0)
},
// eslint-disable-next-line
[reactFlowInstance]
)
const saveChatflowSuccess = () => {
dispatch({ type: REMOVE_DIRTY })
enqueueSnackbar({
message: 'Chatflow saved',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
const errorFailed = (message) => {
enqueueSnackbar({
message,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
const setDirty = () => {
dispatch({ type: SET_DIRTY })
}
// ==============================|| useEffect ||============================== //
// Get specific chatflow successful
useEffect(() => {
if (getSpecificChatflowApi.data) {
const chatflow = getSpecificChatflowApi.data
const initialFlow = chatflow.flowData ? JSON.parse(chatflow.flowData) : []
setNodes(initialFlow.nodes || [])
setEdges(initialFlow.edges || [])
dispatch({ type: SET_CHATFLOW, chatflow })
} else if (getSpecificChatflowApi.error) {
const error = getSpecificChatflowApi.error
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
errorFailed(`Failed to retrieve chatflow: ${errorData}`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getSpecificChatflowApi.data, getSpecificChatflowApi.error])
// Create new chatflow successful
useEffect(() => {
if (createNewChatflowApi.data) {
const chatflow = createNewChatflowApi.data
dispatch({ type: SET_CHATFLOW, chatflow })
saveChatflowSuccess()
window.history.replaceState(null, null, `/canvas/${chatflow.id}`)
} else if (createNewChatflowApi.error) {
const error = createNewChatflowApi.error
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
errorFailed(`Failed to save chatflow: ${errorData}`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [createNewChatflowApi.data, createNewChatflowApi.error])
// Update chatflow successful
useEffect(() => {
if (updateChatflowApi.data) {
dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data })
saveChatflowSuccess()
} else if (updateChatflowApi.error) {
const error = updateChatflowApi.error
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
errorFailed(`Failed to save chatflow: ${errorData}`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updateChatflowApi.data, updateChatflowApi.error])
// Test chatflow failed
useEffect(() => {
if (testChatflowApi.error) {
enqueueSnackbar({
message: 'Test chatflow failed',
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [testChatflowApi.error])
// Listen to edge button click remove redux event
useEffect(() => {
if (reactFlowInstance) {
const edges = reactFlowInstance.getEdges()
const toRemoveEdgeId = canvasDataStore.removeEdgeId.split(':')[0]
setEdges(edges.filter((edge) => edge.id !== toRemoveEdgeId))
setDirty()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [canvasDataStore.removeEdgeId])
useEffect(() => setChatflow(canvasDataStore.chatflow), [canvasDataStore.chatflow])
// Initialization
useEffect(() => {
if (chatflowId) {
getSpecificChatflowApi.request(chatflowId)
} else {
setNodes([])
setEdges([])
dispatch({
type: SET_CHATFLOW,
chatflow: {
name: 'Untitled chatflow'
}
})
}
getNodesApi.request()
// Clear dirty state before leaving and remove any ongoing test triggers and webhooks
return () => {
setTimeout(() => dispatch({ type: REMOVE_DIRTY }), 0)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
setCanvasDataStore(canvas)
}, [canvas])
useEffect(() => {
function handlePaste(e) {
const pasteData = e.clipboardData.getData('text')
//TODO: prevent paste event when input focused, temporary fix: catch chatflow syntax
if (pasteData.includes('{"nodes":[') && pasteData.includes('],"edges":[')) {
handleLoadFlow(pasteData)
}
}
window.addEventListener('paste', handlePaste)
return () => {
window.removeEventListener('paste', handlePaste)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
usePrompt('You have unsaved changes! Do you want to navigate away?', canvasDataStore.isDirty)
return (
<>
<Box>
<AppBar
enableColorOnDark
position='fixed'
color='inherit'
elevation={1}
sx={{
bgcolor: theme.palette.background.default
}}
>
<Toolbar>
<CanvasHeader
chatflow={chatflow}
handleSaveFlow={handleSaveFlow}
handleDeleteFlow={handleDeleteFlow}
handleLoadFlow={handleLoadFlow}
/>
</Toolbar>
</AppBar>
<Box sx={{ pt: '70px', height: '100vh', width: '100%' }}>
<div className='reactflow-parent-wrapper'>
<div className='reactflow-wrapper' ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onNodeClick={onNodeClick}
onEdgesChange={onEdgesChange}
onDrop={onDrop}
onDragOver={onDragOver}
onNodeDragStop={setDirty}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onConnect={onConnect}
onInit={setReactFlowInstance}
fitView
>
<Controls
style={{
display: 'flex',
flexDirection: 'row',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
/>
<Background color='#aaa' gap={16} />
<AddNodes nodesData={getNodesApi.data} node={selectedNode} />
<ChatMessage chatflowid={chatflowId} />
</ReactFlow>
</div>
</div>
</Box>
<ConfirmDialog />
</Box>
</>
)
}
export default Canvas
+113
View File
@@ -0,0 +1,113 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
// material-ui
import { Grid, Box, Stack } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import ItemCard from 'ui-component/cards/ItemCard'
import { gridSpacing } from 'store/constant'
import WorkflowEmptySVG from 'assets/images/workflow_empty.svg'
import { StyledButton } from 'ui-component/button/StyledButton'
// API
import chatflowsApi from 'api/chatflows'
// Hooks
import useApi from 'hooks/useApi'
// const
import { baseURL } from 'store/constant'
// ==============================|| CHATFLOWS ||============================== //
const Chatflows = () => {
const navigate = useNavigate()
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [isLoading, setLoading] = useState(true)
const [images, setImages] = useState({})
const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows)
const addNew = () => {
navigate('/canvas')
}
const goToCanvas = (selectedChatflow) => {
navigate(`/canvas/${selectedChatflow.id}`)
}
useEffect(() => {
getAllChatflowsApi.request()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
setLoading(getAllChatflowsApi.loading)
}, [getAllChatflowsApi.loading])
useEffect(() => {
if (getAllChatflowsApi.data) {
try {
const chatflows = getAllChatflowsApi.data
const images = {}
for (let i = 0; i < chatflows.length; i += 1) {
const flowDataStr = chatflows[i].flowData
const flowData = JSON.parse(flowDataStr)
const nodes = flowData.nodes || []
images[chatflows[i].id] = []
for (let j = 0; j < nodes.length; j += 1) {
const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}`
if (!images[chatflows[i].id].includes(imageSrc)) {
images[chatflows[i].id].push(imageSrc)
}
}
}
setImages(images)
} catch (e) {
console.error(e)
}
}
}, [getAllChatflowsApi.data])
return (
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
<Stack flexDirection='row'>
<h1>Chatflows</h1>
<Grid sx={{ mb: 1.25 }} container direction='row'>
<Box sx={{ flexGrow: 1 }} />
<Grid item>
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew}>
Add New
</StyledButton>
</Grid>
</Grid>
</Stack>
<Grid container spacing={gridSpacing}>
{!isLoading &&
getAllChatflowsApi.data &&
getAllChatflowsApi.data.map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
</Grid>
))}
</Grid>
{!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={WorkflowEmptySVG} alt='WorkflowEmptySVG' />
</Box>
<div>No Chatflows Yet</div>
</Stack>
)}
</MainCard>
)
}
export default Chatflows
@@ -0,0 +1,127 @@
.cloudform {
position: relative;
}
.messagelist {
width: 100%;
height: 100%;
overflow-y: scroll;
border-radius: 0.5rem;
}
.messagelistloading {
display: flex;
width: 100%;
justify-content: center;
margin-top: 1rem;
}
.usermessage {
padding: 1rem 1.5rem 1rem 1.5rem;
}
.usermessagewaiting-light {
padding: 1rem 1.5rem 1rem 1.5rem;
background: linear-gradient(to left, #ede7f6, #e3f2fd, #ede7f6);
background-size: 200% 200%;
background-position: -100% 0;
animation: loading-gradient 2s ease-in-out infinite;
animation-direction: alternate;
animation-name: loading-gradient;
}
.usermessagewaiting-dark {
padding: 1rem 1.5rem 1rem 1.5rem;
color: #ececf1;
background: linear-gradient(to left, #2e2352, #1d3d60, #2e2352);
background-size: 200% 200%;
background-position: -100% 0;
animation: loading-gradient 2s ease-in-out infinite;
animation-direction: alternate;
animation-name: loading-gradient;
}
@keyframes loading-gradient {
0% {
background-position: -100% 0;
}
100% {
background-position: 100% 0;
}
}
.apimessage {
padding: 1rem 1.5rem 1rem 1.5rem;
animation: fadein 0.5s;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.apimessage,
.usermessage,
.usermessagewaiting {
display: flex;
}
.markdownanswer {
line-height: 1.75;
}
.markdownanswer a:hover {
opacity: 0.8;
}
.markdownanswer a {
color: #16bed7;
font-weight: 500;
}
.markdownanswer code {
color: #15cb19;
font-weight: 500;
white-space: pre-wrap !important;
}
.markdownanswer ol,
.markdownanswer ul {
margin: 1rem;
}
.boticon,
.usericon {
margin-right: 1rem;
border-radius: 1rem;
}
.markdownanswer h1,
.markdownanswer h2,
.markdownanswer h3 {
font-size: inherit;
}
.center {
display: flex;
justify-content: center;
align-items: center;
position: relative;
flex-direction: column;
padding: 10px;
max-width: 500px;
}
.cloud {
width: '100%';
max-width: 500px;
height: 73vh;
border-radius: 0.5rem;
display: flex;
justify-content: center;
align-items: center;
}
@@ -0,0 +1,395 @@
import { useState, useRef, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import ReactMarkdown from 'react-markdown'
import PropTypes from 'prop-types'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
import {
ClickAwayListener,
Paper,
Popper,
CircularProgress,
OutlinedInput,
Divider,
InputAdornment,
IconButton,
Box,
Button
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { IconMessage, IconX, IconSend, IconEraser } from '@tabler/icons'
// project import
import { StyledFab } from 'ui-component/button/StyledFab'
import MainCard from 'ui-component/cards/MainCard'
import Transitions from 'ui-component/extended/Transitions'
import './ChatMessage.css'
// api
import chatmessageApi from 'api/chatmessage'
import predictionApi from 'api/prediction'
// Hooks
import useApi from 'hooks/useApi'
import useConfirm from 'hooks/useConfirm'
import useNotifier from 'utils/useNotifier'
export const ChatMessage = ({ chatflowid }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const { confirm } = useConfirm()
const dispatch = useDispatch()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [open, setOpen] = useState(false)
const [userInput, setUserInput] = useState('')
const [history, setHistory] = useState([])
const [loading, setLoading] = useState(false)
const [messages, setMessages] = useState([
{
message: 'Hi there! How can I help?',
type: 'apiMessage'
}
])
const messagesEndRef = useRef(null)
const inputRef = useRef(null)
const anchorRef = useRef(null)
const prevOpen = useRef(open)
const getChatmessageApi = useApi(chatmessageApi.getChatmessageFromChatflow)
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return
}
setOpen(false)
}
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen)
}
const clearChat = async () => {
const confirmPayload = {
title: `Clear Chat History`,
description: `Are you sure you want to clear all chat history?`,
confirmButtonName: 'Clear',
cancelButtonName: 'Cancel'
}
const isConfirmed = await confirm(confirmPayload)
if (isConfirmed) {
try {
await chatmessageApi.deleteChatmessage(chatflowid)
enqueueSnackbar({
message: 'Succesfully cleared all chat history',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: errorData,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
const addChatMessage = async (message, type) => {
try {
const newChatMessageBody = {
role: type,
content: message,
chatflowid: chatflowid
}
await chatmessageApi.createNewChatmessage(chatflowid, newChatMessageBody)
} catch (error) {
console.error(error)
}
}
// Handle errors
const handleError = (message = 'Oops! There seems to be an error. Please try again.') => {
setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }])
addChatMessage(message, 'apiMessage')
setLoading(false)
setUserInput('')
setTimeout(() => {
inputRef.current.focus()
}, 100)
}
// Handle form submission
const handleSubmit = async (e) => {
e.preventDefault()
if (userInput.trim() === '') {
return
}
setLoading(true)
setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage' }])
addChatMessage(userInput, 'userMessage')
// Send user question and history to API
try {
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, { question: userInput, history: history })
if (response.data) {
const data = response.data
setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }])
addChatMessage(data, 'apiMessage')
setLoading(false)
setUserInput('')
setTimeout(() => {
inputRef.current.focus()
scrollToBottom()
}, 100)
}
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
handleError(errorData)
return
}
}
// Prevent blank submissions and allow for multiline input
const handleEnter = (e) => {
if (e.key === 'Enter' && userInput) {
if (!e.shiftKey && userInput) {
handleSubmit(e)
}
} else if (e.key === 'Enter') {
e.preventDefault()
}
}
// Get chatmessages successful
useEffect(() => {
if (getChatmessageApi.data) {
const loadedMessages = []
for (const message of getChatmessageApi.data) {
loadedMessages.push({
message: message.content,
type: message.role
})
}
setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getChatmessageApi.data])
// Keep history in sync with messages
useEffect(() => {
if (messages.length >= 3) {
setHistory([[messages[messages.length - 2].message, messages[messages.length - 1].message]])
}
}, [messages])
// Auto scroll chat to bottom
useEffect(() => {
scrollToBottom()
}, [messages])
useEffect(() => {
if (prevOpen.current === true && open === false) {
anchorRef.current.focus()
}
if (open && chatflowid) {
getChatmessageApi.request(chatflowid)
scrollToBottom()
}
prevOpen.current = open
return () => {
setUserInput('')
setHistory([])
setLoading(false)
setMessages([
{
message: 'Hi there! How can I help?',
type: 'apiMessage'
}
])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, chatflowid])
return (
<>
<StyledFab
sx={{ position: 'absolute', right: 20, top: 20 }}
ref={anchorRef}
size='small'
color='secondary'
aria-label='chat'
title='Chat'
onClick={handleToggle}
>
{open ? <IconX /> : <IconMessage />}
</StyledFab>
{open && (
<StyledFab
sx={{ position: 'absolute', right: 80, top: 20 }}
onClick={clearChat}
size='small'
color='error'
aria-label='clear'
title='Clear Chat History'
>
<IconEraser />
</StyledFab>
)}
<Popper
placement='bottom-end'
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [40, 14]
}
}
]
}}
sx={{ zIndex: 1000 }}
>
{({ TransitionProps }) => (
<Transitions in={open} {...TransitionProps}>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
<div className='cloud'>
<div className='messagelist'>
{messages.map((message, index) => {
return (
// The latest message sent by the user will be animated while waiting for a response
<Box
sx={{
background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : ''
}}
key={index}
style={{ display: 'flex', alignItems: 'center' }}
className={
message.type === 'userMessage' && loading && index === messages.length - 1
? customization.isDarkMode
? 'usermessagewaiting-dark'
: 'usermessagewaiting-light'
: message.type === 'usermessagewaiting'
? 'apimessage'
: 'usermessage'
}
>
{/* Display the correct icon depending on the message type */}
{message.type === 'apiMessage' ? (
<img
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png'
alt='AI'
width='30'
height='30'
className='boticon'
/>
) : (
<img
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png'
alt='Me'
width='30'
height='30'
className='usericon'
/>
)}
<div className='markdownanswer'>
{/* Messages are being rendered in Markdown format */}
<ReactMarkdown linkTarget={'_blank'}>{message.message}</ReactMarkdown>
</div>
</Box>
)
})}
<div ref={messagesEndRef} />
</div>
</div>
<Divider />
<div className='center'>
<div className='cloudform'>
<form onSubmit={handleSubmit}>
<OutlinedInput
inputRef={inputRef}
// eslint-disable-next-line
autoFocus
sx={{ width: '50vh' }}
disabled={loading || !chatflowid}
onKeyDown={handleEnter}
id='userInput'
name='userInput'
placeholder={loading ? 'Waiting for response...' : 'Type your question...'}
value={userInput}
onChange={(e) => setUserInput(e.target.value)}
endAdornment={
<InputAdornment position='end'>
<IconButton type='submit' disabled={loading || !chatflowid} edge='end'>
{loading ? (
<div>
<CircularProgress color='inherit' size={20} />
</div>
) : (
// Send icon SVG in input field
<IconSend
color={
loading || !chatflowid
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
}
/>
)}
</IconButton>
</InputAdornment>
}
/>
</form>
</div>
</div>
</MainCard>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</Popper>
</>
)
}
ChatMessage.propTypes = { chatflowid: PropTypes.string }
+104
View File
@@ -0,0 +1,104 @@
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Box, List, Paper, Popper, ClickAwayListener } from '@mui/material'
// third-party
import PerfectScrollbar from 'react-perfect-scrollbar'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import Transitions from 'ui-component/extended/Transitions'
import NavItem from 'layout/MainLayout/Sidebar/MenuList/NavItem'
import settings from 'menu-items/settings'
// ==============================|| SETTINGS ||============================== //
const Settings = ({ chatflow, isSettingsOpen, anchorEl, onSettingsItemClick, onUploadFile, onClose }) => {
const theme = useTheme()
const [settingsMenu, setSettingsMenu] = useState([])
const [open, setOpen] = useState(false)
useEffect(() => {
if (chatflow && !chatflow.id) {
const settingsMenu = settings.children.filter((menu) => menu.id === 'loadChatflow')
setSettingsMenu(settingsMenu)
} else if (chatflow && chatflow.id) {
const settingsMenu = settings.children
setSettingsMenu(settingsMenu)
}
}, [chatflow])
useEffect(() => {
setOpen(isSettingsOpen)
}, [isSettingsOpen])
// settings list items
const items = settingsMenu.map((menu) => {
return (
<NavItem
key={menu.id}
item={menu}
level={1}
navType='SETTINGS'
onClick={(id) => onSettingsItemClick(id)}
onUploadFile={onUploadFile}
/>
)
})
return (
<>
<Popper
placement='bottom-end'
open={open}
anchorEl={anchorEl}
role={undefined}
transition
disablePortal
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [170, 20]
}
}
]
}}
sx={{ zIndex: 1000 }}
>
{({ TransitionProps }) => (
<Transitions in={open} {...TransitionProps}>
<Paper>
<ClickAwayListener onClickAway={onClose}>
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
<PerfectScrollbar style={{ height: '100%', maxHeight: 'calc(100vh - 250px)', overflowX: 'hidden' }}>
<Box sx={{ p: 2 }}>
<List>{items}</List>
</Box>
</PerfectScrollbar>
</MainCard>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</Popper>
</>
)
}
Settings.propTypes = {
chatflow: PropTypes.object,
isSettingsOpen: PropTypes.bool,
anchorEl: PropTypes.any,
onSettingsItemClick: PropTypes.func,
onUploadFile: PropTypes.func,
onClose: PropTypes.func
}
export default Settings