mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 15:00:57 +03:00
Merge branch 'main' into feature/Milvus
# Conflicts: # packages/components/package.json
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
<!-- markdownlint-disable MD030 -->
|
||||
|
||||
# 流程界面
|
||||
|
||||
[English](./README.md) | 中文
|
||||
|
||||
Flowise 的 React 前端界面。
|
||||
|
||||

|
||||
|
||||
安装:
|
||||
|
||||
```bash
|
||||
npm i flowise-ui
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
本仓库中的源代码在[MIT 许可证](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md)下提供。
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
# Flowise UI
|
||||
|
||||
English | [中文](./README-ZH.md)
|
||||
|
||||
React frontend ui for Flowise.
|
||||
|
||||

|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
webpack: {
|
||||
configure: {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
resolve: {
|
||||
fullySpecified: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flowise-ui",
|
||||
"version": "1.2.5",
|
||||
"version": "1.3.1",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://flowiseai.com",
|
||||
"author": {
|
||||
@@ -13,8 +13,12 @@
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.0.3",
|
||||
"@mui/material": "^5.11.12",
|
||||
"@mui/x-data-grid": "^6.8.0",
|
||||
"@tabler/icons": "^1.39.1",
|
||||
"clsx": "^1.1.1",
|
||||
"flowise-embed": "*",
|
||||
"flowise-embed-react": "*",
|
||||
"flowise-react-json-view": "*",
|
||||
"formik": "^2.2.6",
|
||||
"framer-motion": "^4.1.13",
|
||||
"history": "^5.0.0",
|
||||
@@ -26,26 +30,31 @@
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^18.2.0",
|
||||
"react-code-blocks": "^0.0.9-0",
|
||||
"react-color": "^2.19.3",
|
||||
"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",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"reactflow": "^11.5.6",
|
||||
"redux": "^4.0.5",
|
||||
"rehype-mathjax": "^4.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"socket.io-client": "^4.6.1",
|
||||
"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"
|
||||
"start": "craco start",
|
||||
"dev": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"eject": "craco eject"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
@@ -66,6 +75,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.15.8",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@craco/craco": "^7.1.0",
|
||||
"@testing-library/jest-dom": "^5.11.10",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Flowise - LangchainJS UI</title>
|
||||
<title>Flowise - Low-code LLM apps builder</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="title" content="Flowise - Low-code LLM apps builder" />
|
||||
<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" />
|
||||
@@ -17,13 +17,13 @@
|
||||
<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:title" content="Flowise - Low-code LLM apps builder" />
|
||||
<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:title" content="Flowise - Low-code LLM apps builder" />
|
||||
<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" />
|
||||
|
||||
@@ -4,16 +4,22 @@ const getAllChatflows = () => client.get('/chatflows')
|
||||
|
||||
const getSpecificChatflow = (id) => client.get(`/chatflows/${id}`)
|
||||
|
||||
const getSpecificChatflowFromPublicEndpoint = (id) => client.get(`/public-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}`)
|
||||
|
||||
const getIsChatflowStreaming = (id) => client.get(`/chatflows-streaming/${id}`)
|
||||
|
||||
export default {
|
||||
getAllChatflows,
|
||||
getSpecificChatflow,
|
||||
getSpecificChatflowFromPublicEndpoint,
|
||||
createNewChatflow,
|
||||
updateChatflow,
|
||||
deleteChatflow
|
||||
deleteChatflow,
|
||||
getIsChatflowStreaming
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ apiClient.interceptors.request.use(function (config) {
|
||||
|
||||
if (username && password) {
|
||||
config.auth = {
|
||||
username: username.toLocaleLowerCase(),
|
||||
password: password.toLocaleLowerCase()
|
||||
username,
|
||||
password
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import client from './client'
|
||||
|
||||
const getConfig = (id) => client.get(`/flow-config/${id}`)
|
||||
const getNodeConfig = (body) => client.post(`/node-config`, body)
|
||||
|
||||
export default {
|
||||
getConfig
|
||||
getConfig,
|
||||
getNodeConfig
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import client from './client'
|
||||
|
||||
const getAllCredentials = () => client.get('/credentials')
|
||||
|
||||
const getCredentialsByName = (componentCredentialName) => client.get(`/credentials?credentialName=${componentCredentialName}`)
|
||||
|
||||
const getAllComponentsCredentials = () => client.get('/components-credentials')
|
||||
|
||||
const getSpecificCredential = (id) => client.get(`/credentials/${id}`)
|
||||
|
||||
const getSpecificComponentCredential = (name) => client.get(`/components-credentials/${name}`)
|
||||
|
||||
const createCredential = (body) => client.post(`/credentials`, body)
|
||||
|
||||
const updateCredential = (id, body) => client.put(`/credentials/${id}`, body)
|
||||
|
||||
const deleteCredential = (id) => client.delete(`/credentials/${id}`)
|
||||
|
||||
export default {
|
||||
getAllCredentials,
|
||||
getCredentialsByName,
|
||||
getAllComponentsCredentials,
|
||||
getSpecificCredential,
|
||||
getSpecificComponentCredential,
|
||||
createCredential,
|
||||
updateCredential,
|
||||
deleteCredential
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import client from './client'
|
||||
|
||||
const getAllMarketplaces = () => client.get('/marketplaces')
|
||||
const getAllChatflowsMarketplaces = () => client.get('/marketplaces/chatflows')
|
||||
const getAllToolsMarketplaces = () => client.get('/marketplaces/tools')
|
||||
|
||||
export default {
|
||||
getAllMarketplaces
|
||||
getAllChatflowsMarketplaces,
|
||||
getAllToolsMarketplaces
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import client from './client'
|
||||
|
||||
const getAllTools = () => client.get('/tools')
|
||||
|
||||
const getSpecificTool = (id) => client.get(`/tools/${id}`)
|
||||
|
||||
const createNewTool = (body) => client.post(`/tools`, body)
|
||||
|
||||
const updateTool = (id, body) => client.put(`/tools/${id}`, body)
|
||||
|
||||
const deleteTool = (id) => client.delete(`/tools/${id}`)
|
||||
|
||||
export default {
|
||||
getAllTools,
|
||||
getSpecificTool,
|
||||
createNewTool,
|
||||
updateTool,
|
||||
deleteTool
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.8 KiB |
@@ -27,9 +27,10 @@ import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import MainCard from 'ui-component/cards/MainCard'
|
||||
import Transitions from 'ui-component/extended/Transitions'
|
||||
import { BackdropLoader } from 'ui-component/loading/BackdropLoader'
|
||||
import AboutDialog from 'ui-component/dialog/AboutDialog'
|
||||
|
||||
// assets
|
||||
import { IconLogout, IconSettings, IconFileExport, IconFileDownload } from '@tabler/icons'
|
||||
import { IconLogout, IconSettings, IconFileExport, IconFileDownload, IconInfoCircle } from '@tabler/icons'
|
||||
|
||||
// API
|
||||
import databaseApi from 'api/database'
|
||||
@@ -49,6 +50,7 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [aboutDialogOpen, setAboutDialogOpen] = useState(false)
|
||||
|
||||
const anchorRef = useRef(null)
|
||||
const uploadRef = useRef(null)
|
||||
@@ -215,6 +217,18 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant='body2'>Export Database</Typography>} />
|
||||
</ListItemButton>
|
||||
<ListItemButton
|
||||
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setAboutDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<IconInfoCircle stroke={1.5} size='1.3rem' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant='body2'>About Flowise</Typography>} />
|
||||
</ListItemButton>
|
||||
{localStorage.getItem('username') && localStorage.getItem('password') && (
|
||||
<ListItemButton
|
||||
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||
@@ -237,6 +251,7 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
</Popper>
|
||||
<input ref={uploadRef} type='file' hidden accept='.json' onChange={(e) => handleFileUpload(e)} />
|
||||
<BackdropLoader open={loading} />
|
||||
<AboutDialog show={aboutDialogOpen} onCancel={() => setAboutDialogOpen(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// assets
|
||||
import { IconHierarchy, IconBuildingStore, IconKey } from '@tabler/icons'
|
||||
import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock } from '@tabler/icons'
|
||||
|
||||
// constant
|
||||
const icons = { IconHierarchy, IconBuildingStore, IconKey }
|
||||
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock }
|
||||
|
||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||
|
||||
@@ -27,6 +27,22 @@ const dashboard = {
|
||||
icon: icons.IconBuildingStore,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
title: 'Tools',
|
||||
type: 'item',
|
||||
url: '/tools',
|
||||
icon: icons.IconTool,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'credentials',
|
||||
title: 'Credentials',
|
||||
type: 'item',
|
||||
url: '/credentials',
|
||||
icon: icons.IconLock,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'apikey',
|
||||
title: 'API Keys',
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { lazy } from 'react'
|
||||
|
||||
// project imports
|
||||
import Loadable from 'ui-component/loading/Loadable'
|
||||
import MinimalLayout from 'layout/MinimalLayout'
|
||||
|
||||
// canvas routing
|
||||
const ChatbotFull = Loadable(lazy(() => import('views/chatbot')))
|
||||
|
||||
// ==============================|| CANVAS ROUTING ||============================== //
|
||||
|
||||
const ChatbotRoutes = {
|
||||
path: '/',
|
||||
element: <MinimalLayout />,
|
||||
children: [
|
||||
{
|
||||
path: '/chatbot/:id',
|
||||
element: <ChatbotFull />
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default ChatbotRoutes
|
||||
@@ -13,6 +13,12 @@ const Marketplaces = Loadable(lazy(() => import('views/marketplaces')))
|
||||
// apikey routing
|
||||
const APIKey = Loadable(lazy(() => import('views/apikey')))
|
||||
|
||||
// tools routing
|
||||
const Tools = Loadable(lazy(() => import('views/tools')))
|
||||
|
||||
// credentials routing
|
||||
const Credentials = Loadable(lazy(() => import('views/credentials')))
|
||||
|
||||
// ==============================|| MAIN ROUTING ||============================== //
|
||||
|
||||
const MainRoutes = {
|
||||
@@ -34,6 +40,14 @@ const MainRoutes = {
|
||||
{
|
||||
path: '/apikey',
|
||||
element: <APIKey />
|
||||
},
|
||||
{
|
||||
path: '/tools',
|
||||
element: <Tools />
|
||||
},
|
||||
{
|
||||
path: '/credentials',
|
||||
element: <Credentials />
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ import { useRoutes } from 'react-router-dom'
|
||||
// routes
|
||||
import MainRoutes from './MainRoutes'
|
||||
import CanvasRoutes from './CanvasRoutes'
|
||||
import ChatbotRoutes from './ChatbotRoutes'
|
||||
import config from 'config'
|
||||
|
||||
// ==============================|| ROUTING RENDER ||============================== //
|
||||
|
||||
export default function ThemeRoutes() {
|
||||
return useRoutes([MainRoutes, CanvasRoutes], config.basename)
|
||||
return useRoutes([MainRoutes, CanvasRoutes, ChatbotRoutes], config.basename)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ export const SET_DARKMODE = '@customization/SET_DARKMODE'
|
||||
export const SET_DIRTY = '@canvas/SET_DIRTY'
|
||||
export const REMOVE_DIRTY = '@canvas/REMOVE_DIRTY'
|
||||
export const SET_CHATFLOW = '@canvas/SET_CHATFLOW'
|
||||
export const SHOW_CANVAS_DIALOG = '@canvas/SHOW_CANVAS_DIALOG'
|
||||
export const HIDE_CANVAS_DIALOG = '@canvas/HIDE_CANVAS_DIALOG'
|
||||
export const SET_COMPONENT_NODES = '@canvas/SET_COMPONENT_NODES'
|
||||
export const SET_COMPONENT_CREDENTIALS = '@canvas/SET_COMPONENT_CREDENTIALS'
|
||||
|
||||
// action - notifier reducer
|
||||
export const ENQUEUE_SNACKBAR = 'ENQUEUE_SNACKBAR'
|
||||
|
||||
@@ -5,3 +5,4 @@ export const appDrawerWidth = 320
|
||||
export const maxScroll = 100000
|
||||
export const baseURL = process.env.NODE_ENV === 'production' ? window.location.origin : window.location.origin.replace(':8080', ':3000')
|
||||
export const uiBaseURL = window.location.origin
|
||||
export const FLOWISE_CREDENTIAL_ID = 'FLOWISE_CREDENTIAL_ID'
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createContext, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { getUniqueNodeId } from 'utils/genericHelper'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { SET_DIRTY } from 'store/actions'
|
||||
|
||||
const initialValue = {
|
||||
reactFlowInstance: null,
|
||||
@@ -14,17 +16,20 @@ const initialValue = {
|
||||
export const flowContext = createContext(initialValue)
|
||||
|
||||
export const ReactFlowContext = ({ children }) => {
|
||||
const dispatch = useDispatch()
|
||||
const [reactFlowInstance, setReactFlowInstance] = useState(null)
|
||||
|
||||
const deleteNode = (nodeid) => {
|
||||
deleteConnectedInput(nodeid, 'node')
|
||||
reactFlowInstance.setNodes(reactFlowInstance.getNodes().filter((n) => n.id !== nodeid))
|
||||
reactFlowInstance.setEdges(reactFlowInstance.getEdges().filter((ns) => ns.source !== nodeid && ns.target !== nodeid))
|
||||
dispatch({ type: SET_DIRTY })
|
||||
}
|
||||
|
||||
const deleteEdge = (edgeid) => {
|
||||
deleteConnectedInput(edgeid, 'edge')
|
||||
reactFlowInstance.setEdges(reactFlowInstance.getEdges().filter((edge) => edge.id !== edgeid))
|
||||
dispatch({ type: SET_DIRTY })
|
||||
}
|
||||
|
||||
const deleteConnectedInput = (id, type) => {
|
||||
@@ -103,6 +108,7 @@ export const ReactFlowContext = ({ children }) => {
|
||||
}
|
||||
|
||||
reactFlowInstance.setNodes([...nodes, duplicatedNode])
|
||||
dispatch({ type: SET_DIRTY })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ import * as actionTypes from '../actions'
|
||||
|
||||
export const initialState = {
|
||||
isDirty: false,
|
||||
chatflow: null
|
||||
chatflow: null,
|
||||
canvasDialogShow: false,
|
||||
componentNodes: [],
|
||||
componentCredentials: []
|
||||
}
|
||||
|
||||
// ==============================|| CANVAS REDUCER ||============================== //
|
||||
@@ -25,6 +28,26 @@ const canvasReducer = (state = initialState, action) => {
|
||||
...state,
|
||||
chatflow: action.chatflow
|
||||
}
|
||||
case actionTypes.SHOW_CANVAS_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
canvasDialogShow: true
|
||||
}
|
||||
case actionTypes.HIDE_CANVAS_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
canvasDialogShow: false
|
||||
}
|
||||
case actionTypes.SET_COMPONENT_NODES:
|
||||
return {
|
||||
...state,
|
||||
componentNodes: action.componentNodes
|
||||
}
|
||||
case actionTypes.SET_COMPONENT_CREDENTIALS:
|
||||
return {
|
||||
...state,
|
||||
componentCredentials: action.componentCredentials
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -1,6 +1,39 @@
|
||||
export default function componentStyleOverrides(theme) {
|
||||
const bgColor = theme.colors?.grey50
|
||||
return {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
body: {
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: theme?.customization?.isDarkMode
|
||||
? `${theme.colors?.grey500} ${theme.colors?.darkPrimaryMain}`
|
||||
: `${theme.colors?.grey300} ${theme.paper}`,
|
||||
'&::-webkit-scrollbar, & *::-webkit-scrollbar': {
|
||||
width: 12,
|
||||
height: 12,
|
||||
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryMain : theme.paper
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb': {
|
||||
borderRadius: 8,
|
||||
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.grey500 : theme.colors?.grey300,
|
||||
minHeight: 24,
|
||||
border: `3px solid ${theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryMain : theme.paper}`
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:focus, & *::-webkit-scrollbar-thumb:focus': {
|
||||
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.grey500
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:active, & *::-webkit-scrollbar-thumb:active': {
|
||||
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.grey500
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover, & *::-webkit-scrollbar-thumb:hover': {
|
||||
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.grey500
|
||||
},
|
||||
'&::-webkit-scrollbar-corner, & *::-webkit-scrollbar-corner': {
|
||||
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryMain : theme.paper
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
@@ -103,6 +136,9 @@ export default function componentStyleOverrides(theme) {
|
||||
'&::placeholder': {
|
||||
color: theme.darkTextSecondary,
|
||||
fontSize: '0.875rem'
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
WebkitTextFillColor: theme?.customization?.isDarkMode ? theme.colors?.grey500 : theme.darkTextSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ export default function themePalette(theme) {
|
||||
return {
|
||||
mode: theme?.customization?.navType,
|
||||
common: {
|
||||
black: theme.colors?.darkPaper
|
||||
black: theme.colors?.darkPaper,
|
||||
dark: theme.colors?.darkPrimaryMain
|
||||
},
|
||||
primary: {
|
||||
light: theme.customization.isDarkMode ? theme.colors?.darkPrimaryLight : theme.colors?.primaryLight,
|
||||
@@ -89,6 +90,10 @@ export default function themePalette(theme) {
|
||||
},
|
||||
codeEditor: {
|
||||
main: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.primaryLight
|
||||
},
|
||||
nodeToolTip: {
|
||||
background: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.paper,
|
||||
color: theme.customization.isDarkMode ? theme.colors?.paper : 'rgba(0, 0, 0, 0.87)'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// material-ui
|
||||
import { styled, useTheme } from '@mui/material/styles'
|
||||
import { Box, Grid, Chip, Typography } from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import { Box, Grid, Typography } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard'
|
||||
@@ -28,19 +28,6 @@ const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
// ===========================|| 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 ? (
|
||||
@@ -49,11 +36,42 @@ const ItemCard = ({ isLoading, data, images, onClick }) => {
|
||||
<CardWrapper border={false} content={false} onClick={onClick}>
|
||||
<Box sx={{ p: 2.25 }}>
|
||||
<Grid container direction='column'>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{data.iconSrc && (
|
||||
<div
|
||||
style={{
|
||||
width: 35,
|
||||
height: 35,
|
||||
marginRight: 10,
|
||||
borderRadius: '50%',
|
||||
background: `url(${data.iconSrc})`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center center'
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{!data.iconSrc && data.color && (
|
||||
<div
|
||||
style={{
|
||||
width: 35,
|
||||
height: 35,
|
||||
marginRight: 10,
|
||||
borderRadius: '50%',
|
||||
background: data.color
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
<Typography
|
||||
sx={{ fontSize: '1.5rem', fontWeight: 500, overflowWrap: 'break-word', whiteSpace: 'pre-line' }}
|
||||
>
|
||||
{data.name}
|
||||
{data.templateName || data.name}
|
||||
</Typography>
|
||||
</div>
|
||||
{data.description && (
|
||||
@@ -61,13 +79,6 @@ const ItemCard = ({ isLoading, data, images, onClick }) => {
|
||||
{data.description}
|
||||
</span>
|
||||
)}
|
||||
<Grid sx={{ mt: 1, mb: 1 }} container direction='row'>
|
||||
{data.deployed && (
|
||||
<Grid item>
|
||||
<Chip label='Deployed' sx={activeChatflowSX} />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
{images && (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Dialog, DialogContent, DialogTitle, TableContainer, Table, TableHead, TableRow, TableCell, TableBody, Paper } from '@mui/material'
|
||||
import moment from 'moment'
|
||||
import axios from 'axios'
|
||||
|
||||
const fetchLatestVer = async ({ api }) => {
|
||||
let apiReturn = await axios
|
||||
.get(api)
|
||||
.then(async function (response) {
|
||||
return response.data
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error(error)
|
||||
})
|
||||
return apiReturn
|
||||
}
|
||||
|
||||
const AboutDialog = ({ show, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const [data, setData] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
const fetchData = async (api) => {
|
||||
let response = await fetchLatestVer({ api })
|
||||
setData(response)
|
||||
}
|
||||
|
||||
fetchData('https://api.github.com/repos/FlowiseAI/Flowise/releases/latest')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [show])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
onClose={onCancel}
|
||||
open={show}
|
||||
fullWidth
|
||||
maxWidth='sm'
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
Flowise Version
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{data && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table aria-label='simple table'>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Latest Version</TableCell>
|
||||
<TableCell>Published At</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
<TableCell component='th' scope='row'>
|
||||
<a target='_blank' rel='noreferrer' href={data.html_url}>
|
||||
{data.name}
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell>{moment(data.published_at).fromNow()}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
AboutDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
onCancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default AboutDialog
|
||||
@@ -1,12 +1,15 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Dialog, DialogContent } from '@mui/material'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import NodeInputHandler from 'views/canvas/NodeInputHandler'
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
|
||||
|
||||
const AdditionalParamsDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const [inputParams, setInputParams] = useState([])
|
||||
const [data, setData] = useState({})
|
||||
@@ -21,6 +24,12 @@ const AdditionalParamsDialog = ({ show, dialogProps, onCancel }) => {
|
||||
}
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
onClose={onCancel}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'
|
||||
import useConfirm from 'hooks/useConfirm'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
|
||||
@@ -20,9 +20,7 @@ const ConfirmDialog = () => {
|
||||
{confirmState.title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText sx={{ color: 'black' }} id='alert-dialog-description'>
|
||||
{confirmState.description}
|
||||
</DialogContentText>
|
||||
<span>{confirmState.description}</span>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>{confirmState.cancelButtonName}</Button>
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Box,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Typography,
|
||||
Stack
|
||||
} from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor'
|
||||
import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor'
|
||||
|
||||
import './EditPromptValuesDialog.css'
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
const EditPromptValuesDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const languageType = 'json'
|
||||
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [inputParam, setInputParam] = useState(null)
|
||||
const [textCursorPosition, setTextCursorPosition] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.value) setInputValue(dialogProps.value)
|
||||
if (dialogProps.inputParam) setInputParam(dialogProps.inputParam)
|
||||
|
||||
return () => {
|
||||
setInputValue('')
|
||||
setInputParam(null)
|
||||
setTextCursorPosition({})
|
||||
}
|
||||
}, [dialogProps])
|
||||
|
||||
const onMouseUp = (e) => {
|
||||
if (e.target && e.target.selectionEnd && e.target.value) {
|
||||
const cursorPosition = e.target.selectionEnd
|
||||
const textBeforeCursorPosition = e.target.value.substring(0, cursorPosition)
|
||||
const textAfterCursorPosition = e.target.value.substring(cursorPosition, e.target.value.length)
|
||||
const body = {
|
||||
textBeforeCursorPosition,
|
||||
textAfterCursorPosition
|
||||
}
|
||||
setTextCursorPosition(body)
|
||||
} else {
|
||||
setTextCursorPosition({})
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectOutputResponseClick = (node, isUserQuestion = false) => {
|
||||
let variablePath = isUserQuestion ? `question` : `${node.id}.data.instance`
|
||||
if (textCursorPosition) {
|
||||
let newInput = ''
|
||||
if (textCursorPosition.textBeforeCursorPosition === undefined && textCursorPosition.textAfterCursorPosition === undefined)
|
||||
newInput = `${inputValue}${`{{${variablePath}}}`}`
|
||||
else newInput = `${textCursorPosition.textBeforeCursorPosition}{{${variablePath}}}${textCursorPosition.textAfterCursorPosition}`
|
||||
setInputValue(newInput)
|
||||
}
|
||||
}
|
||||
|
||||
const component = show ? (
|
||||
<Dialog open={show} fullWidth maxWidth='md' aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description'>
|
||||
<DialogContent>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
{inputParam && inputParam.type === 'string' && (
|
||||
<div style={{ flex: 70 }}>
|
||||
<Typography sx={{ mb: 2, ml: 1 }} variant='h4'>
|
||||
{inputParam.label}
|
||||
</Typography>
|
||||
<PerfectScrollbar
|
||||
style={{
|
||||
border: '1px solid',
|
||||
borderColor: theme.palette.grey['500'],
|
||||
borderRadius: '12px',
|
||||
height: '100%',
|
||||
maxHeight: 'calc(100vh - 220px)',
|
||||
overflowX: 'hidden',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
{customization.isDarkMode ? (
|
||||
<DarkCodeEditor
|
||||
disabled={dialogProps.disabled}
|
||||
value={inputValue}
|
||||
onValueChange={(code) => setInputValue(code)}
|
||||
placeholder={inputParam.placeholder}
|
||||
type={languageType}
|
||||
onMouseUp={(e) => onMouseUp(e)}
|
||||
onBlur={(e) => onMouseUp(e)}
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 'calc(100vh - 220px)',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<LightCodeEditor
|
||||
disabled={dialogProps.disabled}
|
||||
value={inputValue}
|
||||
onValueChange={(code) => setInputValue(code)}
|
||||
placeholder={inputParam.placeholder}
|
||||
type={languageType}
|
||||
onMouseUp={(e) => onMouseUp(e)}
|
||||
onBlur={(e) => onMouseUp(e)}
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 'calc(100vh - 220px)',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
)}
|
||||
{!dialogProps.disabled && inputParam && inputParam.acceptVariable && (
|
||||
<div style={{ flex: 30 }}>
|
||||
<Stack flexDirection='row' sx={{ mb: 1, ml: 2 }}>
|
||||
<Typography variant='h4'>Select Variable</Typography>
|
||||
</Stack>
|
||||
<PerfectScrollbar style={{ height: '100%', maxHeight: 'calc(100vh - 220px)', overflowX: 'hidden' }}>
|
||||
<Box sx={{ pl: 2, pr: 2 }}>
|
||||
<List>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
p: 0,
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
|
||||
mb: 1
|
||||
}}
|
||||
disabled={dialogProps.disabled}
|
||||
onClick={() => onSelectOutputResponseClick(null, true)}
|
||||
>
|
||||
<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='AI'
|
||||
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png'
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
sx={{ ml: 1 }}
|
||||
primary='question'
|
||||
secondary={`User's question from chatbox`}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
{dialogProps.availableNodesForVariable &&
|
||||
dialogProps.availableNodesForVariable.length > 0 &&
|
||||
dialogProps.availableNodesForVariable.map((node, index) => {
|
||||
const selectedOutputAnchor = node.data.outputAnchors[0].options.find(
|
||||
(ancr) => ancr.name === node.data.outputs['output']
|
||||
)
|
||||
return (
|
||||
<ListItemButton
|
||||
key={index}
|
||||
sx={{
|
||||
p: 0,
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
|
||||
mb: 1
|
||||
}}
|
||||
disabled={dialogProps.disabled}
|
||||
onClick={() => onSelectOutputResponseClick(node)}
|
||||
>
|
||||
<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.data.name}
|
||||
src={`${baseURL}/api/v1/node-icon/${node.data.name}`}
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
sx={{ ml: 1 }}
|
||||
primary={
|
||||
node.data.inputs.chainName ? node.data.inputs.chainName : node.data.id
|
||||
}
|
||||
secondary={`${selectedOutputAnchor?.label ?? 'output'} from ${
|
||||
node.data.label
|
||||
}`}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
|
||||
<StyledButton disabled={dialogProps.disabled} variant='contained' onClick={() => onConfirm(inputValue, inputParam.name)}>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
EditPromptValuesDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default EditPromptValuesDialog
|
||||
@@ -0,0 +1,113 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Button, Dialog, DialogActions, DialogContent, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor'
|
||||
import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor'
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
|
||||
|
||||
import './ExpandTextDialog.css'
|
||||
|
||||
const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const languageType = 'json'
|
||||
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [inputParam, setInputParam] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.value) setInputValue(dialogProps.value)
|
||||
if (dialogProps.inputParam) setInputParam(dialogProps.inputParam)
|
||||
|
||||
return () => {
|
||||
setInputValue('')
|
||||
setInputParam(null)
|
||||
}
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog open={show} fullWidth maxWidth='md' aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description'>
|
||||
<DialogContent>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
{inputParam && inputParam.type === 'string' && (
|
||||
<div style={{ flex: 70 }}>
|
||||
<Typography sx={{ mb: 2, ml: 1 }} variant='h4'>
|
||||
{inputParam.label}
|
||||
</Typography>
|
||||
<PerfectScrollbar
|
||||
style={{
|
||||
border: '1px solid',
|
||||
borderColor: theme.palette.grey['500'],
|
||||
borderRadius: '12px',
|
||||
height: '100%',
|
||||
maxHeight: 'calc(100vh - 220px)',
|
||||
overflowX: 'hidden',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
{customization.isDarkMode ? (
|
||||
<DarkCodeEditor
|
||||
disabled={dialogProps.disabled}
|
||||
value={inputValue}
|
||||
onValueChange={(code) => setInputValue(code)}
|
||||
placeholder={inputParam.placeholder}
|
||||
type={languageType}
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 'calc(100vh - 220px)',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<LightCodeEditor
|
||||
disabled={dialogProps.disabled}
|
||||
value={inputValue}
|
||||
onValueChange={(code) => setInputValue(code)}
|
||||
placeholder={inputParam.placeholder}
|
||||
type={languageType}
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 'calc(100vh - 220px)',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
|
||||
<StyledButton disabled={dialogProps.disabled} variant='contained' onClick={() => onConfirm(inputValue, inputParam.name)}>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
ExpandTextDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default ExpandTextDialog
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import { JsonEditorInput } from 'ui-component/json/JsonEditor'
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
|
||||
|
||||
const FormatPromptValuesDialog = ({ show, dialogProps, onChange, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
onClose={onCancel}
|
||||
open={show}
|
||||
fullWidth
|
||||
maxWidth='sm'
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
Format Prompt Values
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<PerfectScrollbar
|
||||
style={{
|
||||
height: '100%',
|
||||
maxHeight: 'calc(100vh - 220px)',
|
||||
overflowX: 'hidden'
|
||||
}}
|
||||
>
|
||||
<JsonEditorInput
|
||||
onChange={(newValue) => onChange(newValue)}
|
||||
value={dialogProps.value}
|
||||
isDarkMode={customization.isDarkMode}
|
||||
inputParam={dialogProps.inputParam}
|
||||
nodes={dialogProps.nodes}
|
||||
edges={dialogProps.edges}
|
||||
nodeId={dialogProps.nodeId}
|
||||
/>
|
||||
</PerfectScrollbar>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
FormatPromptValuesDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default FormatPromptValuesDialog
|
||||
@@ -0,0 +1,141 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// Material
|
||||
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
|
||||
import { TableViewOnly } from 'ui-component/table/Table'
|
||||
|
||||
// Store
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
// API
|
||||
import configApi from 'api/config'
|
||||
import useApi from 'hooks/useApi'
|
||||
|
||||
const NodeInfoDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const getNodeConfigApi = useApi(configApi.getNodeConfig)
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.data) {
|
||||
getNodeConfigApi.request(dialogProps.data)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
onClose={onCancel}
|
||||
open={show}
|
||||
fullWidth
|
||||
maxWidth='md'
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
{dialogProps.data && dialogProps.data.name && dialogProps.data.label && (
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
marginRight: 10,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 7,
|
||||
borderRadius: '50%',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
alt={dialogProps.data.name}
|
||||
src={`${baseURL}/api/v1/node-icon/${dialogProps.data.name}`}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginLeft: 10 }}>
|
||||
{dialogProps.data.label}
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: 'max-content',
|
||||
borderRadius: 15,
|
||||
background: 'rgb(254,252,191)',
|
||||
padding: 5,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
marginTop: 5,
|
||||
marginBottom: 5
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'rgb(116,66,16)', fontSize: '0.825rem' }}>{dialogProps.data.id}</span>
|
||||
</div>
|
||||
{dialogProps.data.version && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: 'max-content',
|
||||
borderRadius: 15,
|
||||
background: '#e9edc9',
|
||||
padding: 5,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
marginTop: 5,
|
||||
marginLeft: 10,
|
||||
marginBottom: 5
|
||||
}}
|
||||
>
|
||||
<span style={{ color: '#606c38', fontSize: '0.825rem' }}>version {dialogProps.data.version}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{dialogProps.data?.description && (
|
||||
<div
|
||||
style={{
|
||||
padding: 10,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<span>{dialogProps.data.description}</span>
|
||||
</div>
|
||||
)}
|
||||
{getNodeConfigApi.data && getNodeConfigApi.data.length > 0 && (
|
||||
<TableViewOnly rows={getNodeConfigApi.data} columns={Object.keys(getNodeConfigApi.data[0]).slice(-3)} />
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
NodeInfoDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default NodeInfoDialog
|
||||
@@ -0,0 +1,57 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
|
||||
import ReactJson from 'flowise-react-json-view'
|
||||
|
||||
const SourceDocDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const [data, setData] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.data) setData(dialogProps.data)
|
||||
|
||||
return () => {
|
||||
setData({})
|
||||
}
|
||||
}, [dialogProps])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
onClose={onCancel}
|
||||
open={show}
|
||||
fullWidth
|
||||
maxWidth='sm'
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
Source Document
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<ReactJson
|
||||
theme={customization.isDarkMode ? 'ocean' : 'rjv-default'}
|
||||
style={{ padding: 10, borderRadius: 10 }}
|
||||
src={data}
|
||||
name={null}
|
||||
quotesOnKeys={false}
|
||||
enableClipboard={false}
|
||||
displayDataTypes={false}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
SourceDocDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default SourceDocDialog
|
||||
@@ -0,0 +1,178 @@
|
||||
import { useState, useEffect, Fragment } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import axios from 'axios'
|
||||
|
||||
// Material
|
||||
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
|
||||
import { Popper, CircularProgress, TextField, Box, Typography } from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
// API
|
||||
import credentialsApi from 'api/credentials'
|
||||
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const fetchList = async ({ name, nodeData }) => {
|
||||
const loadMethod = nodeData.inputParams.find((param) => param.name === name)?.loadMethod
|
||||
const username = localStorage.getItem('username')
|
||||
const password = localStorage.getItem('password')
|
||||
|
||||
let lists = await axios
|
||||
.post(
|
||||
`${baseURL}/api/v1/node-load-method/${nodeData.name}`,
|
||||
{ ...nodeData, loadMethod },
|
||||
{ auth: username && password ? { username, password } : undefined }
|
||||
)
|
||||
.then(async function (response) {
|
||||
return response.data
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error(error)
|
||||
})
|
||||
return lists
|
||||
}
|
||||
|
||||
export const AsyncDropdown = ({
|
||||
name,
|
||||
nodeData,
|
||||
value,
|
||||
onSelect,
|
||||
isCreateNewOption,
|
||||
onCreateNew,
|
||||
credentialNames = [],
|
||||
disabled = false,
|
||||
disableClearable = false
|
||||
}) => {
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [options, setOptions] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const findMatchingOptions = (options = [], value) => options.find((option) => option.name === value)
|
||||
const getDefaultOptionValue = () => ''
|
||||
const addNewOption = [{ label: '- Create New -', name: '-create-' }]
|
||||
let [internalValue, setInternalValue] = useState(value ?? 'choose an option')
|
||||
|
||||
const fetchCredentialList = async () => {
|
||||
try {
|
||||
let names = ''
|
||||
if (credentialNames.length > 1) {
|
||||
names = credentialNames.join('&credentialName=')
|
||||
} else {
|
||||
names = credentialNames[0]
|
||||
}
|
||||
const resp = await credentialsApi.getCredentialsByName(names)
|
||||
if (resp.data) {
|
||||
const returnList = []
|
||||
for (let i = 0; i < resp.data.length; i += 1) {
|
||||
const data = {
|
||||
label: resp.data[i].name,
|
||||
name: resp.data[i].id
|
||||
}
|
||||
returnList.push(data)
|
||||
}
|
||||
return returnList
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
;(async () => {
|
||||
const fetchData = async () => {
|
||||
let response = credentialNames.length ? await fetchCredentialList() : await fetchList({ name, nodeData })
|
||||
if (isCreateNewOption) setOptions([...response, ...addNewOption])
|
||||
else setOptions([...response])
|
||||
setLoading(false)
|
||||
}
|
||||
fetchData()
|
||||
})()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Autocomplete
|
||||
id={name}
|
||||
disabled={disabled}
|
||||
disableClearable={disableClearable}
|
||||
size='small'
|
||||
sx={{ width: '100%' }}
|
||||
open={open}
|
||||
onOpen={() => {
|
||||
setOpen(true)
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
options={options}
|
||||
value={findMatchingOptions(options, internalValue) || getDefaultOptionValue()}
|
||||
onChange={(e, selection) => {
|
||||
const value = selection ? selection.name : ''
|
||||
if (isCreateNewOption && value === '-create-') {
|
||||
onCreateNew()
|
||||
} else {
|
||||
setInternalValue(value)
|
||||
onSelect(value)
|
||||
}
|
||||
}}
|
||||
PopperComponent={StyledPopper}
|
||||
loading={loading}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
value={internalValue}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<Fragment>
|
||||
{loading ? <CircularProgress color='inherit' size={20} /> : null}
|
||||
{params.InputProps.endAdornment}
|
||||
</Fragment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
AsyncDropdown.propTypes = {
|
||||
name: PropTypes.string,
|
||||
nodeData: PropTypes.object,
|
||||
value: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
onCreateNew: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
credentialNames: PropTypes.array,
|
||||
disableClearable: PropTypes.bool,
|
||||
isCreateNewOption: PropTypes.bool
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export const DarkCodeEditor = ({ value, placeholder, disabled = false, type, sty
|
||||
onValueChange={onValueChange}
|
||||
onMouseUp={onMouseUp}
|
||||
onBlur={onBlur}
|
||||
tabSize={4}
|
||||
style={{
|
||||
...style,
|
||||
background: theme.palette.codeEditor.main
|
||||
|
||||
@@ -21,6 +21,7 @@ export const LightCodeEditor = ({ value, placeholder, disabled = false, type, st
|
||||
onValueChange={onValueChange}
|
||||
onMouseUp={onMouseUp}
|
||||
onBlur={onBlur}
|
||||
tabSize={4}
|
||||
style={{
|
||||
...style,
|
||||
background: theme.palette.card.main
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { DataGrid } from '@mui/x-data-grid'
|
||||
import { IconPlus } from '@tabler/icons'
|
||||
import { Button } from '@mui/material'
|
||||
|
||||
export const Grid = ({ columns, rows, style, disabled = false, onRowUpdate, addNewRow }) => {
|
||||
const handleProcessRowUpdate = (newRow) => {
|
||||
onRowUpdate(newRow)
|
||||
return newRow
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!disabled && (
|
||||
<Button variant='outlined' onClick={addNewRow} startIcon={<IconPlus />}>
|
||||
Add Item
|
||||
</Button>
|
||||
)}
|
||||
{rows && columns && (
|
||||
<div style={{ marginTop: 10, height: 300, width: '100%', ...style }}>
|
||||
<DataGrid
|
||||
processRowUpdate={handleProcessRowUpdate}
|
||||
isCellEditable={() => {
|
||||
return !disabled
|
||||
}}
|
||||
onProcessRowUpdateError={(error) => console.error(error)}
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Grid.propTypes = {
|
||||
rows: PropTypes.array,
|
||||
columns: PropTypes.array,
|
||||
style: PropTypes.any,
|
||||
disabled: PropTypes.bool,
|
||||
addNewRow: PropTypes.func,
|
||||
onRowUpdate: PropTypes.func
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { FormControl, OutlinedInput } from '@mui/material'
|
||||
import EditPromptValuesDialog from 'ui-component/dialog/EditPromptValuesDialog'
|
||||
import ExpandTextDialog from 'ui-component/dialog/ExpandTextDialog'
|
||||
|
||||
export const Input = ({ inputParam, value, onChange, disabled = false, showDialog, dialogProps, onDialogCancel, onDialogConfirm }) => {
|
||||
const [myValue, setMyValue] = useState(value ?? '')
|
||||
@@ -37,6 +37,7 @@ export const Input = ({ inputParam, value, onChange, disabled = false, showDialo
|
||||
onChange(e.target.value)
|
||||
}}
|
||||
inputProps={{
|
||||
step: inputParam.step ?? 1,
|
||||
style: {
|
||||
height: inputParam.rows ? '90px' : 'inherit'
|
||||
}
|
||||
@@ -44,7 +45,7 @@ export const Input = ({ inputParam, value, onChange, disabled = false, showDialo
|
||||
/>
|
||||
</FormControl>
|
||||
{showDialog && (
|
||||
<EditPromptValuesDialog
|
||||
<ExpandTextDialog
|
||||
show={showDialog}
|
||||
dialogProps={dialogProps}
|
||||
onCancel={onDialogCancel}
|
||||
@@ -52,7 +53,7 @@ export const Input = ({ inputParam, value, onChange, disabled = false, showDialo
|
||||
setMyValue(newValue)
|
||||
onDialogConfirm(newValue, inputParamName)
|
||||
}}
|
||||
></EditPromptValuesDialog>
|
||||
></ExpandTextDialog>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -60,7 +61,7 @@ export const Input = ({ inputParam, value, onChange, disabled = false, showDialo
|
||||
|
||||
Input.propTypes = {
|
||||
inputParam: PropTypes.object,
|
||||
value: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
showDialog: PropTypes.bool,
|
||||
|
||||
@@ -1,10 +1,32 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { FormControl } from '@mui/material'
|
||||
import ReactJson from 'react-json-view'
|
||||
import { FormControl, Popover } from '@mui/material'
|
||||
import ReactJson from 'flowise-react-json-view'
|
||||
import SelectVariable from './SelectVariable'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { getAvailableNodesForVariable } from 'utils/genericHelper'
|
||||
|
||||
export const JsonEditorInput = ({ value, onChange, disabled = false, isDarkMode = false }) => {
|
||||
export const JsonEditorInput = ({ value, onChange, inputParam, nodes, edges, nodeId, disabled = false, isDarkMode = false }) => {
|
||||
const [myValue, setMyValue] = useState(value ? JSON.parse(value) : {})
|
||||
const [availableNodesForVariable, setAvailableNodesForVariable] = useState([])
|
||||
const [mouseUpKey, setMouseUpKey] = useState('')
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const openPopOver = Boolean(anchorEl)
|
||||
|
||||
const handleClosePopOver = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const setNewVal = (val) => {
|
||||
const newVal = cloneDeep(myValue)
|
||||
newVal[mouseUpKey] = val
|
||||
onChange(JSON.stringify(newVal))
|
||||
setMyValue((params) => ({
|
||||
...params,
|
||||
[mouseUpKey]: val
|
||||
}))
|
||||
}
|
||||
|
||||
const onClipboardCopy = (e) => {
|
||||
const src = e.src
|
||||
@@ -15,6 +37,13 @@ export const JsonEditorInput = ({ value, onChange, disabled = false, isDarkMode
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!disabled && nodes && edges && nodeId && inputParam) {
|
||||
const nodesForVariable = inputParam?.acceptVariable ? getAvailableNodesForVariable(nodes, edges, nodeId, inputParam.id) : []
|
||||
setAvailableNodesForVariable(nodesForVariable)
|
||||
}
|
||||
}, [disabled, inputParam, nodes, edges, nodeId])
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
|
||||
@@ -30,28 +59,60 @@ export const JsonEditorInput = ({ value, onChange, disabled = false, isDarkMode
|
||||
/>
|
||||
)}
|
||||
{!disabled && (
|
||||
<ReactJson
|
||||
theme={isDarkMode ? 'ocean' : 'rjv-default'}
|
||||
style={{ padding: 10, borderRadius: 10 }}
|
||||
src={myValue}
|
||||
name={null}
|
||||
quotesOnKeys={false}
|
||||
displayDataTypes={false}
|
||||
enableClipboard={(e) => onClipboardCopy(e)}
|
||||
onEdit={(edit) => {
|
||||
setMyValue(edit.updated_src)
|
||||
onChange(JSON.stringify(edit.updated_src))
|
||||
}}
|
||||
onAdd={() => {
|
||||
//console.log(add)
|
||||
}}
|
||||
onDelete={(deleteobj) => {
|
||||
setMyValue(deleteobj.updated_src)
|
||||
onChange(JSON.stringify(deleteobj.updated_src))
|
||||
}}
|
||||
/>
|
||||
<div key={JSON.stringify(myValue)}>
|
||||
<ReactJson
|
||||
theme={isDarkMode ? 'ocean' : 'rjv-default'}
|
||||
style={{ padding: 10, borderRadius: 10 }}
|
||||
src={myValue}
|
||||
name={null}
|
||||
quotesOnKeys={false}
|
||||
displayDataTypes={false}
|
||||
enableClipboard={(e) => onClipboardCopy(e)}
|
||||
onMouseUp={(event) => {
|
||||
if (inputParam?.acceptVariable) {
|
||||
setMouseUpKey(event.name)
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
}}
|
||||
onEdit={(edit) => {
|
||||
setMyValue(edit.updated_src)
|
||||
onChange(JSON.stringify(edit.updated_src))
|
||||
}}
|
||||
onAdd={() => {
|
||||
//console.log(add)
|
||||
}}
|
||||
onDelete={(deleteobj) => {
|
||||
setMyValue(deleteobj.updated_src)
|
||||
onChange(JSON.stringify(deleteobj.updated_src))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</FormControl>
|
||||
{inputParam?.acceptVariable && (
|
||||
<Popover
|
||||
open={openPopOver}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClosePopOver}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
>
|
||||
<SelectVariable
|
||||
disabled={disabled}
|
||||
availableNodesForVariable={availableNodesForVariable}
|
||||
onSelectAndReturnVal={(val) => {
|
||||
setNewVal(val)
|
||||
handleClosePopOver()
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -60,5 +121,9 @@ JsonEditorInput.propTypes = {
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
isDarkMode: PropTypes.bool
|
||||
isDarkMode: PropTypes.bool,
|
||||
inputParam: PropTypes.object,
|
||||
nodes: PropTypes.array,
|
||||
edges: PropTypes.array,
|
||||
nodeId: PropTypes.string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Box, List, ListItemButton, ListItem, ListItemAvatar, ListItemText, Typography, Stack } from '@mui/material'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import robotPNG from 'assets/images/robot.png'
|
||||
import chatPNG from 'assets/images/chathistory.png'
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectAndReturnVal }) => {
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const onSelectOutputResponseClick = (node, prefix) => {
|
||||
let variablePath = node ? `${node.id}.data.instance` : prefix
|
||||
const newInput = `{{${variablePath}}}`
|
||||
onSelectAndReturnVal(newInput)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!disabled && (
|
||||
<div style={{ flex: 30 }}>
|
||||
<Stack flexDirection='row' sx={{ mb: 1, ml: 2, mt: 2 }}>
|
||||
<Typography variant='h5'>Select Variable</Typography>
|
||||
</Stack>
|
||||
<PerfectScrollbar style={{ height: '100%', maxHeight: 'calc(100vh - 220px)', overflowX: 'hidden' }}>
|
||||
<Box sx={{ pl: 2, pr: 2 }}>
|
||||
<List>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
p: 0,
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
|
||||
mb: 1
|
||||
}}
|
||||
disabled={disabled}
|
||||
onClick={() => onSelectOutputResponseClick(null, 'question')}
|
||||
>
|
||||
<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='AI'
|
||||
src={robotPNG}
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
<ListItemText sx={{ ml: 1 }} primary='question' secondary={`User's question from chatbox`} />
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
p: 0,
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
|
||||
mb: 1
|
||||
}}
|
||||
disabled={disabled}
|
||||
onClick={() => onSelectOutputResponseClick(null, 'chat_history')}
|
||||
>
|
||||
<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='chatHistory'
|
||||
src={chatPNG}
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
sx={{ ml: 1 }}
|
||||
primary='chat_history'
|
||||
secondary={`Past conversation history between user and AI`}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
{availableNodesForVariable &&
|
||||
availableNodesForVariable.length > 0 &&
|
||||
availableNodesForVariable.map((node, index) => {
|
||||
const selectedOutputAnchor = node.data.outputAnchors[0].options.find(
|
||||
(ancr) => ancr.name === node.data.outputs['output']
|
||||
)
|
||||
return (
|
||||
<ListItemButton
|
||||
key={index}
|
||||
sx={{
|
||||
p: 0,
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
|
||||
mb: 1
|
||||
}}
|
||||
disabled={disabled}
|
||||
onClick={() => onSelectOutputResponseClick(node)}
|
||||
>
|
||||
<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.data.name}
|
||||
src={`${baseURL}/api/v1/node-icon/${node.data.name}`}
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
sx={{ ml: 1 }}
|
||||
primary={node.data.inputs.chainName ? node.data.inputs.chainName : node.data.id}
|
||||
secondary={`${selectedOutputAnchor?.label ?? 'output'} from ${node.data.label}`}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
SelectVariable.propTypes = {
|
||||
availableNodesForVariable: PropTypes.array,
|
||||
disabled: PropTypes.bool,
|
||||
onSelectAndReturnVal: PropTypes.func
|
||||
}
|
||||
|
||||
export default SelectVariable
|
||||
@@ -0,0 +1,123 @@
|
||||
import { IconClipboard, IconDownload } from '@tabler/icons'
|
||||
import { memo, useState } from 'react'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Box, IconButton, Popover, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
const programmingLanguages = {
|
||||
javascript: '.js',
|
||||
python: '.py',
|
||||
java: '.java',
|
||||
c: '.c',
|
||||
cpp: '.cpp',
|
||||
'c++': '.cpp',
|
||||
'c#': '.cs',
|
||||
ruby: '.rb',
|
||||
php: '.php',
|
||||
swift: '.swift',
|
||||
'objective-c': '.m',
|
||||
kotlin: '.kt',
|
||||
typescript: '.ts',
|
||||
go: '.go',
|
||||
perl: '.pl',
|
||||
rust: '.rs',
|
||||
scala: '.scala',
|
||||
haskell: '.hs',
|
||||
lua: '.lua',
|
||||
shell: '.sh',
|
||||
sql: '.sql',
|
||||
html: '.html',
|
||||
css: '.css'
|
||||
}
|
||||
|
||||
export const CodeBlock = memo(({ language, chatflowid, isDialog, value }) => {
|
||||
const theme = useTheme()
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const openPopOver = Boolean(anchorEl)
|
||||
|
||||
const handleClosePopOver = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const copyToClipboard = (event) => {
|
||||
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||
return
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(value)
|
||||
setAnchorEl(event.currentTarget)
|
||||
setTimeout(() => {
|
||||
handleClosePopOver()
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const downloadAsFile = () => {
|
||||
const fileExtension = programmingLanguages[language] || '.file'
|
||||
const suggestedFileName = `file-${chatflowid}${fileExtension}`
|
||||
const fileName = suggestedFileName
|
||||
|
||||
if (!fileName) {
|
||||
// user pressed cancel on prompt
|
||||
return
|
||||
}
|
||||
|
||||
const blob = new Blob([value], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.download = fileName
|
||||
link.href = url
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: isDialog ? '' : 300 }}>
|
||||
<Box sx={{ color: 'white', background: theme.palette?.common.dark, p: 1, borderTopLeftRadius: 10, borderTopRightRadius: 10 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
{language}
|
||||
<div style={{ flex: 1 }}></div>
|
||||
<IconButton size='small' title='Copy' color='success' onClick={copyToClipboard}>
|
||||
<IconClipboard />
|
||||
</IconButton>
|
||||
<Popover
|
||||
open={openPopOver}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClosePopOver}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
>
|
||||
<Typography variant='h6' sx={{ pl: 1, pr: 1, color: 'white', background: theme.palette.success.dark }}>
|
||||
Copied!
|
||||
</Typography>
|
||||
</Popover>
|
||||
<IconButton size='small' title='Download' color='primary' onClick={downloadAsFile}>
|
||||
<IconDownload />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<SyntaxHighlighter language={language} style={oneDark} customStyle={{ margin: 0 }}>
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
CodeBlock.displayName = 'CodeBlock'
|
||||
|
||||
CodeBlock.propTypes = {
|
||||
language: PropTypes.string,
|
||||
chatflowid: PropTypes.string,
|
||||
isDialog: PropTypes.bool,
|
||||
value: PropTypes.string
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { memo } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
export const MemoizedReactMarkdown = memo(ReactMarkdown, (prevProps, nextProps) => prevProps.children === nextProps.children)
|
||||
@@ -16,9 +16,11 @@ export const TableViewOnly = ({ columns, rows }) => {
|
||||
<TableBody>
|
||||
{rows.map((row, index) => (
|
||||
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
{Object.keys(row).map((key, index) => (
|
||||
<TableCell key={index}>{row[key]}</TableCell>
|
||||
))}
|
||||
{Object.keys(row)
|
||||
.slice(-3)
|
||||
.map((key, index) => (
|
||||
<TableCell key={index}>{row[key]}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -39,8 +39,9 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
const incoming = nodeData.inputs ? nodeData.inputs.length : 0
|
||||
const outgoing = 1
|
||||
|
||||
const whitelistTypes = ['options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder']
|
||||
const whitelistTypes = ['asyncOptions', 'options', 'string', 'number', 'boolean', 'password', 'json', 'code', 'date', 'file', 'folder']
|
||||
|
||||
// Inputs
|
||||
for (let i = 0; i < incoming; i += 1) {
|
||||
const newInput = {
|
||||
...nodeData.inputs[i],
|
||||
@@ -53,6 +54,16 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Credential
|
||||
if (nodeData.credential) {
|
||||
const newInput = {
|
||||
...nodeData.credential,
|
||||
id: `${newNodeId}-input-${nodeData.credential.name}-${nodeData.credential.type}`
|
||||
}
|
||||
inputParams.unshift(newInput)
|
||||
}
|
||||
|
||||
// Outputs
|
||||
const outputAnchors = []
|
||||
for (let i = 0; i < outgoing; i += 1) {
|
||||
if (nodeData.outputs && nodeData.outputs.length) {
|
||||
@@ -129,6 +140,8 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
}
|
||||
]
|
||||
*/
|
||||
|
||||
// Inputs
|
||||
if (nodeData.inputs) {
|
||||
nodeData.inputAnchors = inputAnchors
|
||||
nodeData.inputParams = inputParams
|
||||
@@ -139,13 +152,17 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
nodeData.inputs = {}
|
||||
}
|
||||
|
||||
// Outputs
|
||||
if (nodeData.outputs) {
|
||||
nodeData.outputs = initializeDefaultNodeData(outputAnchors)
|
||||
} else {
|
||||
nodeData.outputs = {}
|
||||
}
|
||||
|
||||
nodeData.outputAnchors = outputAnchors
|
||||
|
||||
// Credential
|
||||
if (nodeData.credential) nodeData.credential = ''
|
||||
|
||||
nodeData.id = newNodeId
|
||||
|
||||
return nodeData
|
||||
@@ -168,8 +185,10 @@ export const isValidConnection = (connection, reactFlowInstance) => {
|
||||
//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('|')
|
||||
let sourceTypes = sourceHandle.split('-')[sourceHandle.split('-').length - 1].split('|')
|
||||
sourceTypes = sourceTypes.map((s) => s.trim())
|
||||
let targetTypes = targetHandle.split('-')[targetHandle.split('-').length - 1].split('|')
|
||||
targetTypes = targetTypes.map((t) => t.trim())
|
||||
|
||||
if (targetTypes.some((t) => sourceTypes.includes(t))) {
|
||||
let targetNode = reactFlowInstance.getNode(target)
|
||||
@@ -249,6 +268,7 @@ export const generateExportFlowData = (flowData) => {
|
||||
const newNodeData = {
|
||||
id: node.data.id,
|
||||
label: node.data.label,
|
||||
version: node.data.version,
|
||||
name: node.data.name,
|
||||
type: node.data.type,
|
||||
baseClasses: node.data.baseClasses,
|
||||
@@ -285,7 +305,7 @@ export const generateExportFlowData = (flowData) => {
|
||||
}
|
||||
|
||||
export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle) => {
|
||||
// example edge id = "llmChain_0-llmChain_0-output-outputPrediction-string-llmChain_1-llmChain_1-input-promptValues-string"
|
||||
// example edge id = "llmChain_0-llmChain_0-output-outputPrediction-string|json-llmChain_1-llmChain_1-input-promptValues-string"
|
||||
// {source} -{sourceHandle} -{target} -{targetHandle}
|
||||
const parentNodes = []
|
||||
const inputEdges = edges.filter((edg) => edg.target === target && edg.targetHandle === targetHandle)
|
||||
@@ -314,3 +334,78 @@ export const rearrangeToolsOrdering = (newValues, sourceNodeId) => {
|
||||
|
||||
newValues.sort((a, b) => sortKey(a) - sortKey(b))
|
||||
}
|
||||
|
||||
export const throttle = (func, limit) => {
|
||||
let lastFunc
|
||||
let lastRan
|
||||
|
||||
return (...args) => {
|
||||
if (!lastRan) {
|
||||
func(...args)
|
||||
lastRan = Date.now()
|
||||
} else {
|
||||
clearTimeout(lastFunc)
|
||||
lastFunc = setTimeout(() => {
|
||||
if (Date.now() - lastRan >= limit) {
|
||||
func(...args)
|
||||
lastRan = Date.now()
|
||||
}
|
||||
}, limit - (Date.now() - lastRan))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const generateRandomGradient = () => {
|
||||
function randomColor() {
|
||||
var color = 'rgb('
|
||||
for (var i = 0; i < 3; i++) {
|
||||
var random = Math.floor(Math.random() * 256)
|
||||
color += random
|
||||
if (i < 2) {
|
||||
color += ','
|
||||
}
|
||||
}
|
||||
color += ')'
|
||||
return color
|
||||
}
|
||||
|
||||
var gradient = 'linear-gradient(' + randomColor() + ', ' + randomColor() + ')'
|
||||
|
||||
return gradient
|
||||
}
|
||||
|
||||
export const getInputVariables = (paramValue) => {
|
||||
let returnVal = paramValue
|
||||
const variableStack = []
|
||||
const inputVariables = []
|
||||
let startIdx = 0
|
||||
const endIdx = returnVal.length
|
||||
|
||||
while (startIdx < endIdx) {
|
||||
const substr = returnVal.substring(startIdx, startIdx + 1)
|
||||
|
||||
// Store the opening double curly bracket
|
||||
if (substr === '{') {
|
||||
variableStack.push({ substr, startIdx: startIdx + 1 })
|
||||
}
|
||||
|
||||
// Found the complete variable
|
||||
if (substr === '}' && variableStack.length > 0 && variableStack[variableStack.length - 1].substr === '{') {
|
||||
const variableStartIdx = variableStack[variableStack.length - 1].startIdx
|
||||
const variableEndIdx = startIdx
|
||||
const variableFullPath = returnVal.substring(variableStartIdx, variableEndIdx)
|
||||
inputVariables.push(variableFullPath)
|
||||
variableStack.pop()
|
||||
}
|
||||
startIdx += 1
|
||||
}
|
||||
return inputVariables
|
||||
}
|
||||
|
||||
export const isValidURL = (url) => {
|
||||
try {
|
||||
return new URL(url)
|
||||
} catch (err) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// material-ui
|
||||
@@ -34,16 +34,18 @@ import Transitions from 'ui-component/extended/Transitions'
|
||||
import { StyledFab } from 'ui-component/button/StyledFab'
|
||||
|
||||
// icons
|
||||
import { IconPlus, IconSearch, IconMinus } from '@tabler/icons'
|
||||
import { IconPlus, IconSearch, IconMinus, IconX } from '@tabler/icons'
|
||||
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
import { SET_COMPONENT_NODES } from 'store/actions'
|
||||
|
||||
// ==============================|| ADD NODES||============================== //
|
||||
|
||||
const AddNodes = ({ nodesData, node }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [nodes, setNodes] = useState({})
|
||||
@@ -61,11 +63,20 @@ const AddNodes = ({ nodesData, node }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getSearchedNodes = (value) => {
|
||||
const passed = nodesData.filter((nd) => {
|
||||
const passesQuery = nd.name.toLowerCase().includes(value.toLowerCase())
|
||||
const passesCategory = nd.category.toLowerCase().includes(value.toLowerCase())
|
||||
return passesQuery || passesCategory
|
||||
})
|
||||
return passed
|
||||
}
|
||||
|
||||
const filterSearch = (value) => {
|
||||
setSearchValue(value)
|
||||
setTimeout(() => {
|
||||
if (value) {
|
||||
const returnData = nodesData.filter((nd) => nd.name.toLowerCase().includes(value.toLowerCase()))
|
||||
const returnData = getSearchedNodes(value)
|
||||
groupByCategory(returnData, true)
|
||||
scrollTop()
|
||||
} else if (value === '') {
|
||||
@@ -122,8 +133,11 @@ const AddNodes = ({ nodesData, node }) => {
|
||||
}, [node])
|
||||
|
||||
useEffect(() => {
|
||||
if (nodesData) groupByCategory(nodesData)
|
||||
}, [nodesData])
|
||||
if (nodesData) {
|
||||
groupByCategory(nodesData)
|
||||
dispatch({ type: SET_COMPONENT_NODES, componentNodes: nodesData })
|
||||
}
|
||||
}, [nodesData, dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -167,7 +181,7 @@ const AddNodes = ({ nodesData, node }) => {
|
||||
<Typography variant='h4'>Add Nodes</Typography>
|
||||
</Stack>
|
||||
<OutlinedInput
|
||||
sx={{ width: '100%', pr: 1, pl: 2, my: 2 }}
|
||||
sx={{ width: '100%', pr: 2, pl: 2, my: 2 }}
|
||||
id='input-search-node'
|
||||
value={searchValue}
|
||||
onChange={(e) => filterSearch(e.target.value)}
|
||||
@@ -177,6 +191,28 @@ const AddNodes = ({ nodesData, node }) => {
|
||||
<IconSearch stroke={1.5} size='1rem' color={theme.palette.grey[500]} />
|
||||
</InputAdornment>
|
||||
}
|
||||
endAdornment={
|
||||
<InputAdornment
|
||||
position='end'
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
color: theme.palette.grey[500],
|
||||
'&:hover': {
|
||||
color: theme.palette.grey[900]
|
||||
}
|
||||
}}
|
||||
title='Clear Search'
|
||||
>
|
||||
<IconX
|
||||
stroke={1.5}
|
||||
size='1rem'
|
||||
onClick={() => filterSearch('')}
|
||||
style={{
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
</InputAdornment>
|
||||
}
|
||||
aria-describedby='search-helper-text'
|
||||
inputProps={{
|
||||
'aria-label': 'weight'
|
||||
@@ -218,6 +254,7 @@ const AddNodes = ({ nodesData, node }) => {
|
||||
expanded={categoryExpanded[category] || false}
|
||||
onChange={handleAccordionChange(category)}
|
||||
key={category}
|
||||
disableGutters
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
// material-ui
|
||||
@@ -13,7 +13,7 @@ import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck,
|
||||
// project imports
|
||||
import Settings from 'views/settings'
|
||||
import SaveChatflowDialog from 'ui-component/dialog/SaveChatflowDialog'
|
||||
import APICodeDialog from 'ui-component/dialog/APICodeDialog'
|
||||
import APICodeDialog from 'views/chatflows/APICodeDialog'
|
||||
|
||||
// API
|
||||
import chatflowsApi from 'api/chatflows'
|
||||
@@ -24,11 +24,13 @@ import useApi from 'hooks/useApi'
|
||||
// utils
|
||||
import { generateExportFlowData } from 'utils/genericHelper'
|
||||
import { uiBaseURL } from 'store/constant'
|
||||
import { SET_CHATFLOW } from 'store/actions'
|
||||
|
||||
// ==============================|| CANVAS HEADER ||============================== //
|
||||
|
||||
const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFlow }) => {
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
const flowNameRef = useRef()
|
||||
const settingsRef = useRef()
|
||||
@@ -88,8 +90,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
|
||||
}
|
||||
|
||||
const onAPIDialogClick = () => {
|
||||
// If file type is file, isFormDataRequired = true
|
||||
let isFormDataRequired = false
|
||||
|
||||
try {
|
||||
const flowData = JSON.parse(chatflow.flowData)
|
||||
const nodes = flowData.nodes
|
||||
@@ -103,11 +105,27 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
// If sessionId memory, isSessionMemory = true
|
||||
let isSessionMemory = false
|
||||
try {
|
||||
const flowData = JSON.parse(chatflow.flowData)
|
||||
const nodes = flowData.nodes
|
||||
for (const node of nodes) {
|
||||
if (node.data.inputParams.find((param) => param.name === 'sessionId')) {
|
||||
isSessionMemory = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
setAPIDialogProps({
|
||||
title: 'Embed in website or use as API',
|
||||
chatflowid: chatflow.id,
|
||||
chatflowApiKeyId: chatflow.apikeyid,
|
||||
isFormDataRequired
|
||||
isFormDataRequired,
|
||||
isSessionMemory
|
||||
})
|
||||
setAPIDialogOpen(true)
|
||||
}
|
||||
@@ -125,6 +143,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
|
||||
useEffect(() => {
|
||||
if (updateChatflowApi.data) {
|
||||
setFlowName(updateChatflowApi.data.name)
|
||||
dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data })
|
||||
}
|
||||
setEditingFlowName(false)
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useContext, useState } from 'react'
|
||||
import { useContext, useState, useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import { styled, useTheme } from '@mui/material/styles'
|
||||
import { IconButton, Box, Typography, Divider, Button } from '@mui/material'
|
||||
import Tooltip, { tooltipClasses } from '@mui/material/Tooltip'
|
||||
|
||||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard'
|
||||
import NodeInputHandler from './NodeInputHandler'
|
||||
import NodeOutputHandler from './NodeOutputHandler'
|
||||
import AdditionalParamsDialog from 'ui-component/dialog/AdditionalParamsDialog'
|
||||
import NodeInfoDialog from 'ui-component/dialog/NodeInfoDialog'
|
||||
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
import { IconTrash, IconCopy } from '@tabler/icons'
|
||||
import { IconTrash, IconCopy, IconInfoCircle, IconAlertTriangle } from '@tabler/icons'
|
||||
import { flowContext } from 'store/context/ReactFlowContext'
|
||||
|
||||
const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
@@ -30,14 +33,39 @@ const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
}
|
||||
}))
|
||||
|
||||
const LightTooltip = styled(({ className, ...props }) => <Tooltip {...props} classes={{ popper: className }} />)(({ theme }) => ({
|
||||
[`& .${tooltipClasses.tooltip}`]: {
|
||||
backgroundColor: theme.palette.nodeToolTip.background,
|
||||
color: theme.palette.nodeToolTip.color,
|
||||
boxShadow: theme.shadows[1]
|
||||
}
|
||||
}))
|
||||
|
||||
// ===========================|| CANVAS NODE ||=========================== //
|
||||
|
||||
const CanvasNode = ({ data }) => {
|
||||
const theme = useTheme()
|
||||
const canvas = useSelector((state) => state.canvas)
|
||||
const { deleteNode, duplicateNode } = useContext(flowContext)
|
||||
|
||||
const [showDialog, setShowDialog] = useState(false)
|
||||
const [dialogProps, setDialogProps] = useState({})
|
||||
const [showInfoDialog, setShowInfoDialog] = useState(false)
|
||||
const [infoDialogProps, setInfoDialogProps] = useState({})
|
||||
const [warningMessage, setWarningMessage] = useState('')
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const nodeOutdatedMessage = (oldVersion, newVersion) => `Node version ${oldVersion} outdated\nUpdate to latest version ${newVersion}`
|
||||
|
||||
const nodeVersionEmptyMessage = (newVersion) => `Node outdated\nUpdate to latest version ${newVersion}`
|
||||
|
||||
const onDialogClicked = () => {
|
||||
const dialogProps = {
|
||||
@@ -50,6 +78,17 @@ const CanvasNode = ({ data }) => {
|
||||
setShowDialog(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const componentNode = canvas.componentNodes.find((nd) => nd.name === data.name)
|
||||
if (componentNode) {
|
||||
if (!data.version) {
|
||||
setWarningMessage(nodeVersionEmptyMessage(componentNode.version))
|
||||
} else {
|
||||
if (componentNode.version > data.version) setWarningMessage(nodeOutdatedMessage(data.version, componentNode.version))
|
||||
}
|
||||
}
|
||||
}, [canvas.componentNodes, data.name, data.version])
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardWrapper
|
||||
@@ -60,118 +99,158 @@ const CanvasNode = ({ data }) => {
|
||||
}}
|
||||
border={false}
|
||||
>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Box 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
|
||||
title='Duplicate'
|
||||
onClick={() => {
|
||||
duplicateNode(data.id)
|
||||
}}
|
||||
sx={{ height: 35, width: 35, '&:hover': { color: theme?.palette.primary.main } }}
|
||||
color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'}
|
||||
>
|
||||
<IconCopy />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
title='Delete'
|
||||
onClick={() => {
|
||||
deleteNode(data.id)
|
||||
}}
|
||||
sx={{ height: 35, width: 35, mr: 1, '&:hover': { color: 'red' } }}
|
||||
color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'}
|
||||
>
|
||||
<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} />
|
||||
))}
|
||||
{data.inputParams.find((param) => param.additionalParams) && (
|
||||
<LightTooltip
|
||||
open={!canvas.canvasDialogShow && open}
|
||||
onClose={handleClose}
|
||||
onOpen={handleOpen}
|
||||
disableFocusListener={true}
|
||||
title={
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginTop:
|
||||
data.inputParams.filter((param) => param.additionalParams).length ===
|
||||
data.inputParams.length + data.inputAnchors.length
|
||||
? 20
|
||||
: 0
|
||||
background: 'transparent',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Button sx={{ borderRadius: 25, width: '90%', mb: 2 }} variant='outlined' onClick={onDialogClicked}>
|
||||
Additional Parameters
|
||||
</Button>
|
||||
<IconButton
|
||||
title='Duplicate'
|
||||
onClick={() => {
|
||||
duplicateNode(data.id)
|
||||
}}
|
||||
sx={{ height: '35px', width: '35px', '&:hover': { color: theme?.palette.primary.main } }}
|
||||
color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'}
|
||||
>
|
||||
<IconCopy />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
title='Delete'
|
||||
onClick={() => {
|
||||
deleteNode(data.id)
|
||||
}}
|
||||
sx={{ height: '35px', width: '35px', '&:hover': { color: 'red' } }}
|
||||
color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'}
|
||||
>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
title='Info'
|
||||
onClick={() => {
|
||||
setInfoDialogProps({ data })
|
||||
setShowInfoDialog(true)
|
||||
}}
|
||||
sx={{ height: '35px', width: '35px', '&:hover': { color: theme?.palette.secondary.main } }}
|
||||
color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'}
|
||||
>
|
||||
<IconInfoCircle />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
<Divider />
|
||||
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
Output
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider />
|
||||
}
|
||||
placement='right-start'
|
||||
>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Box 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,
|
||||
mr: 2
|
||||
}}
|
||||
>
|
||||
{data.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
{warningMessage && (
|
||||
<>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
<Tooltip title={<span style={{ whiteSpace: 'pre-line' }}>{warningMessage}</span>} placement='top'>
|
||||
<IconButton sx={{ height: 35, width: 35 }}>
|
||||
<IconAlertTriangle size={35} color='orange' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</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} />
|
||||
))}
|
||||
{data.inputParams.find((param) => param.additionalParams) && (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginTop:
|
||||
data.inputParams.filter((param) => param.additionalParams).length ===
|
||||
data.inputParams.length + data.inputAnchors.length
|
||||
? 20
|
||||
: 0
|
||||
}}
|
||||
>
|
||||
<Button sx={{ borderRadius: 25, width: '90%', mb: 2 }} variant='outlined' onClick={onDialogClicked}>
|
||||
Additional Parameters
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
{data.outputAnchors.map((outputAnchor, index) => (
|
||||
<NodeOutputHandler key={index} outputAnchor={outputAnchor} data={data} />
|
||||
))}
|
||||
</Box>
|
||||
</LightTooltip>
|
||||
</CardWrapper>
|
||||
<AdditionalParamsDialog
|
||||
show={showDialog}
|
||||
dialogProps={dialogProps}
|
||||
onCancel={() => setShowDialog(false)}
|
||||
></AdditionalParamsDialog>
|
||||
<NodeInfoDialog show={showInfoDialog} dialogProps={infoDialogProps} onCancel={() => setShowInfoDialog(false)}></NodeInfoDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
// material-ui
|
||||
import { IconButton } from '@mui/material'
|
||||
import { IconEdit } from '@tabler/icons'
|
||||
|
||||
// project import
|
||||
import { AsyncDropdown } from 'ui-component/dropdown/AsyncDropdown'
|
||||
import AddEditCredentialDialog from 'views/credentials/AddEditCredentialDialog'
|
||||
import CredentialListDialog from 'views/credentials/CredentialListDialog'
|
||||
|
||||
// API
|
||||
import credentialsApi from 'api/credentials'
|
||||
|
||||
// ===========================|| CredentialInputHandler ||=========================== //
|
||||
|
||||
const CredentialInputHandler = ({ inputParam, data, onSelect, disabled = false }) => {
|
||||
const ref = useRef(null)
|
||||
const [credentialId, setCredentialId] = useState(data?.credential ?? '')
|
||||
const [showCredentialListDialog, setShowCredentialListDialog] = useState(false)
|
||||
const [credentialListDialogProps, setCredentialListDialogProps] = useState({})
|
||||
const [showSpecificCredentialDialog, setShowSpecificCredentialDialog] = useState(false)
|
||||
const [specificCredentialDialogProps, setSpecificCredentialDialogProps] = useState({})
|
||||
const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString())
|
||||
|
||||
const editCredential = (credentialId) => {
|
||||
const dialogProp = {
|
||||
type: 'EDIT',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Save',
|
||||
credentialId
|
||||
}
|
||||
setSpecificCredentialDialogProps(dialogProp)
|
||||
setShowSpecificCredentialDialog(true)
|
||||
}
|
||||
|
||||
const addAsyncOption = async () => {
|
||||
try {
|
||||
let names = ''
|
||||
if (inputParam.credentialNames.length > 1) {
|
||||
names = inputParam.credentialNames.join('&')
|
||||
} else {
|
||||
names = inputParam.credentialNames[0]
|
||||
}
|
||||
const componentCredentialsResp = await credentialsApi.getSpecificComponentCredential(names)
|
||||
if (componentCredentialsResp.data) {
|
||||
if (Array.isArray(componentCredentialsResp.data)) {
|
||||
const dialogProp = {
|
||||
title: 'Add New Credential',
|
||||
componentsCredentials: componentCredentialsResp.data
|
||||
}
|
||||
setCredentialListDialogProps(dialogProp)
|
||||
setShowCredentialListDialog(true)
|
||||
} else {
|
||||
const dialogProp = {
|
||||
type: 'ADD',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Add',
|
||||
credentialComponent: componentCredentialsResp.data
|
||||
}
|
||||
setSpecificCredentialDialogProps(dialogProp)
|
||||
setShowSpecificCredentialDialog(true)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const onConfirmAsyncOption = (selectedCredentialId = '') => {
|
||||
setCredentialId(selectedCredentialId)
|
||||
setReloadTimestamp(Date.now().toString())
|
||||
setSpecificCredentialDialogProps({})
|
||||
setShowSpecificCredentialDialog(false)
|
||||
onSelect(selectedCredentialId)
|
||||
}
|
||||
|
||||
const onCredentialSelected = (credentialComponent) => {
|
||||
setShowCredentialListDialog(false)
|
||||
const dialogProp = {
|
||||
type: 'ADD',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Add',
|
||||
credentialComponent
|
||||
}
|
||||
setSpecificCredentialDialogProps(dialogProp)
|
||||
setShowSpecificCredentialDialog(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{inputParam && (
|
||||
<>
|
||||
{inputParam.type === 'credential' && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }} />
|
||||
<div key={reloadTimestamp} style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<AsyncDropdown
|
||||
disabled={disabled}
|
||||
name={inputParam.name}
|
||||
nodeData={data}
|
||||
value={credentialId ?? 'choose an option'}
|
||||
isCreateNewOption={true}
|
||||
credentialNames={inputParam.credentialNames}
|
||||
onSelect={(newValue) => {
|
||||
setCredentialId(newValue)
|
||||
onSelect(newValue)
|
||||
}}
|
||||
onCreateNew={() => addAsyncOption(inputParam.name)}
|
||||
/>
|
||||
{credentialId && (
|
||||
<IconButton title='Edit' color='primary' size='small' onClick={() => editCredential(credentialId)}>
|
||||
<IconEdit />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showSpecificCredentialDialog && (
|
||||
<AddEditCredentialDialog
|
||||
show={showSpecificCredentialDialog}
|
||||
dialogProps={specificCredentialDialogProps}
|
||||
onCancel={() => setShowSpecificCredentialDialog(false)}
|
||||
onConfirm={onConfirmAsyncOption}
|
||||
></AddEditCredentialDialog>
|
||||
)}
|
||||
{showCredentialListDialog && (
|
||||
<CredentialListDialog
|
||||
show={showCredentialListDialog}
|
||||
dialogProps={credentialListDialogProps}
|
||||
onCancel={() => setShowCredentialListDialog(false)}
|
||||
onCredentialSelected={onCredentialSelected}
|
||||
></CredentialListDialog>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
CredentialInputHandler.propTypes = {
|
||||
inputParam: PropTypes.object,
|
||||
data: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CredentialInputHandler
|
||||
@@ -5,19 +5,31 @@ import { useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import { useTheme, styled } from '@mui/material/styles'
|
||||
import { Box, Typography, Tooltip, IconButton } from '@mui/material'
|
||||
import { Box, Typography, Tooltip, IconButton, Button } from '@mui/material'
|
||||
import { tooltipClasses } from '@mui/material/Tooltip'
|
||||
import { IconArrowsMaximize } from '@tabler/icons'
|
||||
import { IconArrowsMaximize, IconEdit, IconAlertTriangle } from '@tabler/icons'
|
||||
|
||||
// project import
|
||||
import { Dropdown } from 'ui-component/dropdown/Dropdown'
|
||||
import { AsyncDropdown } from 'ui-component/dropdown/AsyncDropdown'
|
||||
import { Input } from 'ui-component/input/Input'
|
||||
import { File } from 'ui-component/file/File'
|
||||
import { SwitchInput } from 'ui-component/switch/Switch'
|
||||
import { flowContext } from 'store/context/ReactFlowContext'
|
||||
import { isValidConnection, getAvailableNodesForVariable } from 'utils/genericHelper'
|
||||
import { isValidConnection } from 'utils/genericHelper'
|
||||
import { JsonEditorInput } from 'ui-component/json/JsonEditor'
|
||||
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
|
||||
import ToolDialog from 'views/tools/ToolDialog'
|
||||
import FormatPromptValuesDialog from 'ui-component/dialog/FormatPromptValuesDialog'
|
||||
import CredentialInputHandler from './CredentialInputHandler'
|
||||
|
||||
// utils
|
||||
import { getInputVariables } from 'utils/genericHelper'
|
||||
|
||||
// const
|
||||
import { FLOWISE_CREDENTIAL_ID } from 'store/constant'
|
||||
|
||||
const EDITABLE_TOOLS = ['selectedTool']
|
||||
|
||||
const CustomWidthTooltip = styled(({ className, ...props }) => <Tooltip {...props} classes={{ popper: className }} />)({
|
||||
[`& .${tooltipClasses.tooltip}`]: {
|
||||
@@ -36,6 +48,11 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
const [position, setPosition] = useState(0)
|
||||
const [showExpandDialog, setShowExpandDialog] = useState(false)
|
||||
const [expandDialogProps, setExpandDialogProps] = useState({})
|
||||
const [showAsyncOptionDialog, setAsyncOptionEditDialog] = useState('')
|
||||
const [asyncOptionEditDialogProps, setAsyncOptionEditDialogProps] = useState({})
|
||||
const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString())
|
||||
const [showFormatPromptValuesDialog, setShowFormatPromptValuesDialog] = useState(false)
|
||||
const [formatPromptValuesDialogProps, setFormatPromptValuesDialogProps] = useState({})
|
||||
|
||||
const onExpandDialogClicked = (value, inputParam) => {
|
||||
const dialogProp = {
|
||||
@@ -45,22 +62,75 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
confirmButtonName: 'Save',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
|
||||
if (!disabled) {
|
||||
const nodes = reactFlowInstance.getNodes()
|
||||
const edges = reactFlowInstance.getEdges()
|
||||
const nodesForVariable = inputParam.acceptVariable ? getAvailableNodesForVariable(nodes, edges, data.id, inputParam.id) : []
|
||||
dialogProp.availableNodesForVariable = nodesForVariable
|
||||
}
|
||||
setExpandDialogProps(dialogProp)
|
||||
setShowExpandDialog(true)
|
||||
}
|
||||
|
||||
const onFormatPromptValuesClicked = (value, inputParam) => {
|
||||
// Preset values if the field is format prompt values
|
||||
let inputValue = value
|
||||
if (inputParam.name === 'promptValues' && !value) {
|
||||
const obj = {}
|
||||
const templateValue =
|
||||
(data.inputs['template'] ?? '') + (data.inputs['systemMessagePrompt'] ?? '') + (data.inputs['humanMessagePrompt'] ?? '')
|
||||
const inputVariables = getInputVariables(templateValue)
|
||||
for (const inputVariable of inputVariables) {
|
||||
obj[inputVariable] = ''
|
||||
}
|
||||
if (Object.keys(obj).length) inputValue = JSON.stringify(obj)
|
||||
}
|
||||
const dialogProp = {
|
||||
value: inputValue,
|
||||
inputParam,
|
||||
nodes: reactFlowInstance.getNodes(),
|
||||
edges: reactFlowInstance.getEdges(),
|
||||
nodeId: data.id
|
||||
}
|
||||
setFormatPromptValuesDialogProps(dialogProp)
|
||||
setShowFormatPromptValuesDialog(true)
|
||||
}
|
||||
|
||||
const onExpandDialogSave = (newValue, inputParamName) => {
|
||||
setShowExpandDialog(false)
|
||||
data.inputs[inputParamName] = newValue
|
||||
}
|
||||
|
||||
const editAsyncOption = (inputParamName, inputValue) => {
|
||||
if (inputParamName === 'selectedTool') {
|
||||
setAsyncOptionEditDialogProps({
|
||||
title: 'Edit Tool',
|
||||
type: 'EDIT',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Save',
|
||||
toolId: inputValue
|
||||
})
|
||||
}
|
||||
setAsyncOptionEditDialog(inputParamName)
|
||||
}
|
||||
|
||||
const addAsyncOption = (inputParamName) => {
|
||||
if (inputParamName === 'selectedTool') {
|
||||
setAsyncOptionEditDialogProps({
|
||||
title: 'Add New Tool',
|
||||
type: 'ADD',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Add'
|
||||
})
|
||||
}
|
||||
setAsyncOptionEditDialog(inputParamName)
|
||||
}
|
||||
|
||||
const onConfirmAsyncOption = (selectedOptionId = '') => {
|
||||
if (!selectedOptionId) {
|
||||
data.inputs[showAsyncOptionDialog] = ''
|
||||
} else {
|
||||
data.inputs[showAsyncOptionDialog] = selectedOptionId
|
||||
setReloadTimestamp(Date.now().toString())
|
||||
}
|
||||
setAsyncOptionEditDialogProps({})
|
||||
setAsyncOptionEditDialog('')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
||||
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2)
|
||||
@@ -95,6 +165,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
<Typography>
|
||||
{inputAnchor.label}
|
||||
{!inputAnchor.optional && <span style={{ color: 'red' }}> *</span>}
|
||||
{inputAnchor.description && <TooltipWithParser style={{ marginLeft: 10 }} title={inputAnchor.description} />}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
@@ -144,6 +215,33 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
{inputParam.warning && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
borderRadius: 10,
|
||||
background: 'rgb(254,252,191)',
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<IconAlertTriangle size={36} color='orange' />
|
||||
<span style={{ color: 'rgb(116,66,16)', marginLeft: 10 }}>{inputParam.warning}</span>
|
||||
</div>
|
||||
)}
|
||||
{inputParam.type === 'credential' && (
|
||||
<CredentialInputHandler
|
||||
disabled={disabled}
|
||||
data={data}
|
||||
inputParam={inputParam}
|
||||
onSelect={(newValue) => {
|
||||
data.credential = newValue
|
||||
data.inputs[FLOWISE_CREDENTIAL_ID] = newValue // in case data.credential is not updated
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{inputParam.type === 'file' && (
|
||||
<File
|
||||
disabled={disabled}
|
||||
@@ -161,6 +259,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
)}
|
||||
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
|
||||
<Input
|
||||
key={data.inputs[inputParam.name]}
|
||||
disabled={disabled}
|
||||
inputParam={inputParam}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
@@ -172,12 +271,33 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
/>
|
||||
)}
|
||||
{inputParam.type === 'json' && (
|
||||
<JsonEditorInput
|
||||
disabled={disabled}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
|
||||
isDarkMode={customization.isDarkMode}
|
||||
/>
|
||||
<>
|
||||
{!inputParam?.acceptVariable && (
|
||||
<JsonEditorInput
|
||||
disabled={disabled}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
|
||||
isDarkMode={customization.isDarkMode}
|
||||
/>
|
||||
)}
|
||||
{inputParam?.acceptVariable && (
|
||||
<>
|
||||
<Button
|
||||
sx={{ borderRadius: 25, width: '100%', mb: 2, mt: 2 }}
|
||||
variant='outlined'
|
||||
onClick={() => onFormatPromptValuesClicked(data.inputs[inputParam.name] ?? '', inputParam)}
|
||||
>
|
||||
Format Prompt Values
|
||||
</Button>
|
||||
<FormatPromptValuesDialog
|
||||
show={showFormatPromptValuesDialog}
|
||||
dialogProps={formatPromptValuesDialogProps}
|
||||
onCancel={() => setShowFormatPromptValuesDialog(false)}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
></FormatPromptValuesDialog>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{inputParam.type === 'options' && (
|
||||
<Dropdown
|
||||
@@ -185,12 +305,44 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
|
||||
name={inputParam.name}
|
||||
options={inputParam.options}
|
||||
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'chose an option'}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
|
||||
/>
|
||||
)}
|
||||
{inputParam.type === 'asyncOptions' && (
|
||||
<>
|
||||
{data.inputParams.length === 1 && <div style={{ marginTop: 10 }} />}
|
||||
<div key={reloadTimestamp} style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<AsyncDropdown
|
||||
disabled={disabled}
|
||||
name={inputParam.name}
|
||||
nodeData={data}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
|
||||
isCreateNewOption={EDITABLE_TOOLS.includes(inputParam.name)}
|
||||
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
onCreateNew={() => addAsyncOption(inputParam.name)}
|
||||
/>
|
||||
{EDITABLE_TOOLS.includes(inputParam.name) && data.inputs[inputParam.name] && (
|
||||
<IconButton
|
||||
title='Edit'
|
||||
color='primary'
|
||||
size='small'
|
||||
onClick={() => editAsyncOption(inputParam.name, data.inputs[inputParam.name])}
|
||||
>
|
||||
<IconEdit />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<ToolDialog
|
||||
show={EDITABLE_TOOLS.includes(showAsyncOptionDialog)}
|
||||
dialogProps={asyncOptionEditDialogProps}
|
||||
onCancel={() => setAsyncOptionEditDialog('')}
|
||||
onConfirm={onConfirmAsyncOption}
|
||||
></ToolDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
enqueueSnackbar as enqueueSnackbarAction,
|
||||
closeSnackbar as closeSnackbarAction
|
||||
} from 'store/actions'
|
||||
import { omit, cloneDeep } from 'lodash'
|
||||
|
||||
// material-ui
|
||||
import { Toolbar, Box, AppBar, Button } from '@mui/material'
|
||||
@@ -23,7 +24,7 @@ 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 { ChatPopUp } from 'views/chatmessage/ChatPopUp'
|
||||
import { flowContext } from 'store/context/ReactFlowContext'
|
||||
|
||||
// API
|
||||
@@ -41,6 +42,9 @@ import { IconX } from '@tabler/icons'
|
||||
import { getUniqueNodeId, initNode, getEdgeLabelName, rearrangeToolsOrdering } from 'utils/genericHelper'
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
|
||||
// const
|
||||
import { FLOWISE_CREDENTIAL_ID } from 'store/constant'
|
||||
|
||||
const nodeTypes = { customNode: CanvasNode }
|
||||
const edgeTypes = { buttonedge: ButtonEdge }
|
||||
|
||||
@@ -185,23 +189,28 @@ const Canvas = () => {
|
||||
|
||||
const handleSaveFlow = (chatflowName) => {
|
||||
if (reactFlowInstance) {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
node.data = {
|
||||
...node.data,
|
||||
selected: false
|
||||
}
|
||||
return node
|
||||
})
|
||||
)
|
||||
const nodes = reactFlowInstance.getNodes().map((node) => {
|
||||
const nodeData = cloneDeep(node.data)
|
||||
if (Object.prototype.hasOwnProperty.call(nodeData.inputs, FLOWISE_CREDENTIAL_ID)) {
|
||||
nodeData.credential = nodeData.inputs[FLOWISE_CREDENTIAL_ID]
|
||||
nodeData.inputs = omit(nodeData.inputs, [FLOWISE_CREDENTIAL_ID])
|
||||
}
|
||||
node.data = {
|
||||
...nodeData,
|
||||
selected: false
|
||||
}
|
||||
return node
|
||||
})
|
||||
|
||||
const rfInstanceObject = reactFlowInstance.toObject()
|
||||
rfInstanceObject.nodes = nodes
|
||||
const flowData = JSON.stringify(rfInstanceObject)
|
||||
|
||||
if (!chatflow.id) {
|
||||
const newChatflowBody = {
|
||||
name: chatflowName,
|
||||
deployed: false,
|
||||
isPublic: false,
|
||||
flowData
|
||||
}
|
||||
createNewChatflowApi.request(newChatflowBody)
|
||||
@@ -502,6 +511,7 @@ const Canvas = () => {
|
||||
onConnect={onConnect}
|
||||
onInit={setReactFlowInstance}
|
||||
fitView
|
||||
deleteKeyCode={canvas.canvasDialogShow ? null : ['Backspace', 'Delete']}
|
||||
minZoom={0.1}
|
||||
>
|
||||
<Controls
|
||||
@@ -514,7 +524,7 @@ const Canvas = () => {
|
||||
/>
|
||||
<Background color='#aaa' gap={16} />
|
||||
<AddNodes nodesData={getNodesApi.data} node={selectedNode} />
|
||||
<ChatMessage chatflowid={chatflowId} />
|
||||
<ChatPopUp chatflowid={chatflowId} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FullPageChat } from 'flowise-embed-react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
// Project import
|
||||
import LoginDialog from 'ui-component/dialog/LoginDialog'
|
||||
|
||||
// API
|
||||
import chatflowsApi from 'api/chatflows'
|
||||
|
||||
// Hooks
|
||||
import useApi from 'hooks/useApi'
|
||||
|
||||
//Const
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
// ==============================|| Chatbot ||============================== //
|
||||
|
||||
const ChatbotFull = () => {
|
||||
const URLpath = document.location.pathname.toString().split('/')
|
||||
const chatflowId = URLpath[URLpath.length - 1] === 'chatbot' ? '' : URLpath[URLpath.length - 1]
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [chatflow, setChatflow] = useState(null)
|
||||
const [chatbotTheme, setChatbotTheme] = useState({})
|
||||
const [loginDialogOpen, setLoginDialogOpen] = useState(false)
|
||||
const [loginDialogProps, setLoginDialogProps] = useState({})
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [chatbotOverrideConfig, setChatbotOverrideConfig] = useState({})
|
||||
|
||||
const getSpecificChatflowFromPublicApi = useApi(chatflowsApi.getSpecificChatflowFromPublicEndpoint)
|
||||
const getSpecificChatflowApi = useApi(chatflowsApi.getSpecificChatflow)
|
||||
|
||||
const onLoginClick = (username, password) => {
|
||||
localStorage.setItem('username', username)
|
||||
localStorage.setItem('password', password)
|
||||
navigate(0)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getSpecificChatflowFromPublicApi.request(chatflowId)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (getSpecificChatflowFromPublicApi.error) {
|
||||
if (getSpecificChatflowFromPublicApi.error?.response?.status === 401) {
|
||||
if (localStorage.getItem('username') && localStorage.getItem('password')) {
|
||||
getSpecificChatflowApi.request(chatflowId)
|
||||
} else {
|
||||
setLoginDialogProps({
|
||||
title: 'Login',
|
||||
confirmButtonName: 'Login'
|
||||
})
|
||||
setLoginDialogOpen(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getSpecificChatflowFromPublicApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
if (getSpecificChatflowApi.error) {
|
||||
if (getSpecificChatflowApi.error?.response?.status === 401) {
|
||||
setLoginDialogProps({
|
||||
title: 'Login',
|
||||
confirmButtonName: 'Login'
|
||||
})
|
||||
setLoginDialogOpen(true)
|
||||
}
|
||||
}
|
||||
}, [getSpecificChatflowApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
if (getSpecificChatflowFromPublicApi.data || getSpecificChatflowApi.data) {
|
||||
const chatflowData = getSpecificChatflowFromPublicApi.data || getSpecificChatflowApi.data
|
||||
setChatflow(chatflowData)
|
||||
if (chatflowData.chatbotConfig) {
|
||||
try {
|
||||
const parsedConfig = JSON.parse(chatflowData.chatbotConfig)
|
||||
setChatbotTheme(parsedConfig)
|
||||
if (parsedConfig.overrideConfig) {
|
||||
// Generate new sessionId
|
||||
if (parsedConfig.overrideConfig.generateNewSession) {
|
||||
parsedConfig.overrideConfig.sessionId = Date.now().toString()
|
||||
}
|
||||
setChatbotOverrideConfig(parsedConfig.overrideConfig)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setChatbotTheme({})
|
||||
setChatbotOverrideConfig({})
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [getSpecificChatflowFromPublicApi.data, getSpecificChatflowApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(getSpecificChatflowFromPublicApi.loading || getSpecificChatflowApi.loading)
|
||||
}, [getSpecificChatflowFromPublicApi.loading, getSpecificChatflowApi.loading])
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLoading ? (
|
||||
<>
|
||||
{!chatflow || chatflow.apikeyid ? (
|
||||
<p>Invalid Chatbot</p>
|
||||
) : (
|
||||
<FullPageChat
|
||||
chatflowid={chatflow.id}
|
||||
apiHost={baseURL}
|
||||
chatflowConfig={chatbotOverrideConfig}
|
||||
theme={{ chatWindow: chatbotTheme }}
|
||||
/>
|
||||
)}
|
||||
<LoginDialog show={loginDialogOpen} dialogProps={loginDialogProps} onConfirm={onLoginClick} />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatbotFull
|
||||
+249
-67
@@ -4,11 +4,25 @@ import { useState, useEffect } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Tabs, Tab, Dialog, DialogContent, DialogTitle, Box } from '@mui/material'
|
||||
import {
|
||||
Tabs,
|
||||
Tab,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Box,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { CopyBlock, atomOneDark } from 'react-code-blocks'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
|
||||
// Project import
|
||||
import { Dropdown } from 'ui-component/dropdown/Dropdown'
|
||||
import ShareChatbot from './ShareChatbot'
|
||||
import EmbedChat from './EmbedChat'
|
||||
|
||||
// Const
|
||||
import { baseURL } from 'store/constant'
|
||||
@@ -19,6 +33,7 @@ import pythonSVG from 'assets/images/python.svg'
|
||||
import javascriptSVG from 'assets/images/javascript.svg'
|
||||
import cURLSVG from 'assets/images/cURL.svg'
|
||||
import EmbedSVG from 'assets/images/embed.svg'
|
||||
import ShareChatbotSVG from 'assets/images/sharing.png'
|
||||
|
||||
// API
|
||||
import apiKeyApi from 'api/apikey'
|
||||
@@ -30,6 +45,8 @@ import useApi from 'hooks/useApi'
|
||||
import { CheckboxInput } from 'ui-component/checkbox/Checkbox'
|
||||
import { TableViewOnly } from 'ui-component/table/Table'
|
||||
|
||||
import { IconBulb } from '@tabler/icons'
|
||||
|
||||
function TabPanel(props) {
|
||||
const { children, value, index, ...other } = props
|
||||
return (
|
||||
@@ -79,7 +96,7 @@ const getConfigExamplesForJS = (configData, bodyType) => {
|
||||
else if (config.type === 'number') exampleVal = `1`
|
||||
else if (config.name === 'files') exampleVal = `input.files[0]`
|
||||
finalStr += bodyType === 'json' ? `\n "${config.name}": ${exampleVal},` : `formData.append("${config.name}", ${exampleVal})\n`
|
||||
if (i === loop - 1 && bodyType !== 'json') `formData.append("question", "Hey, how are you?")\n`
|
||||
if (i === loop - 1 && bodyType !== 'json') finalStr += `formData.append("question", "Hey, how are you?")\n`
|
||||
}
|
||||
return finalStr
|
||||
}
|
||||
@@ -94,7 +111,7 @@ const getConfigExamplesForPython = (configData, bodyType) => {
|
||||
if (config.type === 'string') exampleVal = `"example"`
|
||||
else if (config.type === 'boolean') exampleVal = `true`
|
||||
else if (config.type === 'number') exampleVal = `1`
|
||||
else if (config.name === 'files') exampleVal = `('example${config.type}', open('example${config.type}', 'rb'))`
|
||||
else if (config.name === 'files') continue
|
||||
finalStr += bodyType === 'json' ? `\n "${config.name}": ${exampleVal},` : `\n "${config.name}": ${exampleVal},`
|
||||
if (i === loop - 1 && bodyType !== 'json') finalStr += `\n "question": "Hey, how are you?"\n`
|
||||
}
|
||||
@@ -119,30 +136,24 @@ const getConfigExamplesForCurl = (configData, bodyType) => {
|
||||
return finalStr
|
||||
}
|
||||
|
||||
const embedCode = (chatflowid) => {
|
||||
return `<script type="module">
|
||||
import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed@latest/dist/web.js"
|
||||
Chatbot.init({
|
||||
chatflowid: "${chatflowid}",
|
||||
apiHost: "${baseURL}",
|
||||
})
|
||||
</script>`
|
||||
}
|
||||
|
||||
const APICodeDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useDispatch()
|
||||
const codes = ['Embed', 'Python', 'JavaScript', 'cURL']
|
||||
|
||||
const codes = ['Embed', 'Python', 'JavaScript', 'cURL', 'Share Chatbot']
|
||||
const [value, setValue] = useState(0)
|
||||
const [keyOptions, setKeyOptions] = useState([])
|
||||
const [apiKeys, setAPIKeys] = useState([])
|
||||
const [chatflowApiKeyId, setChatflowApiKeyId] = useState('')
|
||||
const [selectedApiKey, setSelectedApiKey] = useState({})
|
||||
const [checkboxVal, setCheckbox] = useState(false)
|
||||
const [nodeConfig, setNodeConfig] = useState({})
|
||||
const [nodeConfigExpanded, setNodeConfigExpanded] = useState({})
|
||||
|
||||
const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys)
|
||||
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
|
||||
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming)
|
||||
const getConfigApi = useApi(configApi.getConfig)
|
||||
|
||||
const onCheckBoxChanged = (newVal) => {
|
||||
@@ -165,12 +176,36 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => {
|
||||
updateChatflowApi.request(dialogProps.chatflowid, updateBody)
|
||||
}
|
||||
|
||||
const groupByNodeLabel = (nodes, isFilter = false) => {
|
||||
const accordianNodes = {}
|
||||
const result = nodes.reduce(function (r, a) {
|
||||
r[a.node] = r[a.node] || []
|
||||
r[a.node].push(a)
|
||||
accordianNodes[a.node] = isFilter ? true : false
|
||||
return r
|
||||
}, Object.create(null))
|
||||
setNodeConfig(result)
|
||||
setNodeConfigExpanded(accordianNodes)
|
||||
}
|
||||
|
||||
const handleAccordionChange = (nodeLabel) => (event, isExpanded) => {
|
||||
const accordianNodes = { ...nodeConfigExpanded }
|
||||
accordianNodes[nodeLabel] = isExpanded
|
||||
setNodeConfigExpanded(accordianNodes)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateChatflowApi.data) {
|
||||
dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data })
|
||||
}
|
||||
}, [updateChatflowApi.data, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
if (getConfigApi.data) {
|
||||
groupByNodeLabel(getConfigApi.data)
|
||||
}
|
||||
}, [getConfigApi.data])
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue)
|
||||
}
|
||||
@@ -195,7 +230,10 @@ output = query({
|
||||
"${baseURL}/api/v1/prediction/${dialogProps.chatflowid}",
|
||||
{
|
||||
method: "POST",
|
||||
body: data
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}
|
||||
);
|
||||
const result = await response.json();
|
||||
@@ -209,9 +247,8 @@ query({"question": "Hey, how are you?"}).then((response) => {
|
||||
} else if (codeLang === 'cURL') {
|
||||
return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\
|
||||
-X POST \\
|
||||
-d '{"question": "Hey, how are you?"}'`
|
||||
} else if (codeLang === 'Embed') {
|
||||
return embedCode(dialogProps.chatflowid)
|
||||
-d '{"question": "Hey, how are you?"}' \\
|
||||
-H "Content-Type: application/json"`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -236,9 +273,12 @@ output = query({
|
||||
const response = await fetch(
|
||||
"${baseURL}/api/v1/prediction/${dialogProps.chatflowid}",
|
||||
{
|
||||
headers: { Authorization: "Bearer ${selectedApiKey?.apiKey}" },
|
||||
headers: {
|
||||
Authorization: "Bearer ${selectedApiKey?.apiKey}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: data
|
||||
body: JSON.stringify(data)
|
||||
}
|
||||
);
|
||||
const result = await response.json();
|
||||
@@ -253,9 +293,8 @@ query({"question": "Hey, how are you?"}).then((response) => {
|
||||
return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\
|
||||
-X POST \\
|
||||
-d '{"question": "Hey, how are you?"}' \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "Authorization: Bearer ${selectedApiKey?.apiKey}"`
|
||||
} else if (codeLang === 'Embed') {
|
||||
return embedCode(dialogProps.chatflowid)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -263,7 +302,7 @@ query({"question": "Hey, how are you?"}).then((response) => {
|
||||
const getLang = (codeLang) => {
|
||||
if (codeLang === 'Python') {
|
||||
return 'python'
|
||||
} else if (codeLang === 'JavaScript' || codeLang === 'Embed') {
|
||||
} else if (codeLang === 'JavaScript') {
|
||||
return 'javascript'
|
||||
} else if (codeLang === 'cURL') {
|
||||
return 'bash'
|
||||
@@ -280,6 +319,8 @@ query({"question": "Hey, how are you?"}).then((response) => {
|
||||
return EmbedSVG
|
||||
} else if (codeLang === 'cURL') {
|
||||
return cURLSVG
|
||||
} else if (codeLang === 'Share Chatbot') {
|
||||
return ShareChatbotSVG
|
||||
}
|
||||
return pythonSVG
|
||||
}
|
||||
@@ -288,15 +329,20 @@ query({"question": "Hey, how are you?"}).then((response) => {
|
||||
|
||||
const getConfigCodeWithFormData = (codeLang, configData) => {
|
||||
if (codeLang === 'Python') {
|
||||
configData = unshiftFiles(configData)
|
||||
const fileType = configData[0].type
|
||||
return `import requests
|
||||
|
||||
API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}"
|
||||
|
||||
# use form data to upload files
|
||||
form_data = {${getConfigExamplesForPython(configData, 'formData')}}
|
||||
form_data = {
|
||||
"files": ${`('example${fileType}', open('example${fileType}', 'rb'))`}
|
||||
}
|
||||
body_data = {${getConfigExamplesForPython(configData, 'formData')}}
|
||||
|
||||
def query(form_data):
|
||||
response = requests.post(API_URL, files=form_data)
|
||||
response = requests.post(API_URL, files=form_data, data=body_data)
|
||||
return response.json()
|
||||
|
||||
output = query(form_data)
|
||||
@@ -323,7 +369,8 @@ query(formData).then((response) => {
|
||||
`
|
||||
} else if (codeLang === 'cURL') {
|
||||
return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\
|
||||
-X POST \\${getConfigExamplesForCurl(configData, 'formData')}`
|
||||
-X POST \\${getConfigExamplesForCurl(configData, 'formData')} \\
|
||||
-H "Content-Type: multipart/form-data"`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -332,16 +379,21 @@ query(formData).then((response) => {
|
||||
|
||||
const getConfigCodeWithFormDataWithAuth = (codeLang, configData) => {
|
||||
if (codeLang === 'Python') {
|
||||
configData = unshiftFiles(configData)
|
||||
const fileType = configData[0].type
|
||||
return `import requests
|
||||
|
||||
API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}"
|
||||
headers = {"Authorization": "Bearer ${selectedApiKey?.apiKey}"}
|
||||
|
||||
# use form data to upload files
|
||||
form_data = {${getConfigExamplesForPython(configData, 'formData')}}
|
||||
form_data = {
|
||||
"files": ${`('example${fileType}', open('example${fileType}', 'rb'))`}
|
||||
}
|
||||
body_data = {${getConfigExamplesForPython(configData, 'formData')}}
|
||||
|
||||
def query(form_data):
|
||||
response = requests.post(API_URL, headers=headers, files=form_data)
|
||||
response = requests.post(API_URL, headers=headers, files=form_data, data=body_data)
|
||||
return response.json()
|
||||
|
||||
output = query(form_data)
|
||||
@@ -370,6 +422,7 @@ query(formData).then((response) => {
|
||||
} else if (codeLang === 'cURL') {
|
||||
return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\
|
||||
-X POST \\${getConfigExamplesForCurl(configData, 'formData')} \\
|
||||
-H "Content-Type: multipart/form-data" \\
|
||||
-H "Authorization: Bearer ${selectedApiKey?.apiKey}"`
|
||||
}
|
||||
return ''
|
||||
@@ -399,7 +452,10 @@ output = query({
|
||||
"${baseURL}/api/v1/prediction/${dialogProps.chatflowid}",
|
||||
{
|
||||
method: "POST",
|
||||
body: data
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}
|
||||
);
|
||||
const result = await response.json();
|
||||
@@ -417,7 +473,8 @@ query({
|
||||
} else if (codeLang === 'cURL') {
|
||||
return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\
|
||||
-X POST \\
|
||||
-d '{"question": "Hey, how are you?", "overrideConfig": {${getConfigExamplesForCurl(configData, 'json')}}'`
|
||||
-d '{"question": "Hey, how are you?", "overrideConfig": {${getConfigExamplesForCurl(configData, 'json')}}' \\
|
||||
-H "Content-Type: application/json"`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -446,9 +503,12 @@ output = query({
|
||||
const response = await fetch(
|
||||
"${baseURL}/api/v1/prediction/${dialogProps.chatflowid}",
|
||||
{
|
||||
headers: { Authorization: "Bearer ${selectedApiKey?.apiKey}" },
|
||||
headers: {
|
||||
Authorization: "Bearer ${selectedApiKey?.apiKey}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: data
|
||||
body: JSON.stringify(data)
|
||||
}
|
||||
);
|
||||
const result = await response.json();
|
||||
@@ -467,11 +527,38 @@ query({
|
||||
return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\
|
||||
-X POST \\
|
||||
-d '{"question": "Hey, how are you?", "overrideConfig": {${getConfigExamplesForCurl(configData, 'json')}}' \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "Authorization: Bearer ${selectedApiKey?.apiKey}"`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const getMultiConfigCodeWithFormData = (codeLang) => {
|
||||
if (codeLang === 'Python') {
|
||||
return `body_data = {
|
||||
"openAIApiKey[chatOpenAI_0]": "sk-my-openai-1st-key",
|
||||
"openAIApiKey[openAIEmbeddings_0]": "sk-my-openai-2nd-key"
|
||||
}`
|
||||
} else if (codeLang === 'JavaScript') {
|
||||
return `formData.append("openAIApiKey[chatOpenAI_0]", "sk-my-openai-1st-key")
|
||||
formData.append("openAIApiKey[openAIEmbeddings_0]", "sk-my-openai-2nd-key")`
|
||||
} else if (codeLang === 'cURL') {
|
||||
return `-F "openAIApiKey[chatOpenAI_0]=sk-my-openai-1st-key" \\
|
||||
-F "openAIApiKey[openAIEmbeddings_0]=sk-my-openai-2nd-key" \\`
|
||||
}
|
||||
}
|
||||
|
||||
const getMultiConfigCode = () => {
|
||||
return `{
|
||||
"overrideConfig": {
|
||||
"openAIApiKey": {
|
||||
"chatOpenAI_0": "sk-my-openai-1st-key",
|
||||
"openAIEmbeddings_0": "sk-my-openai-2nd-key"
|
||||
}
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllAPIKeysApi.data) {
|
||||
const options = [
|
||||
@@ -503,6 +590,7 @@ query({
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
getAllAPIKeysApi.request()
|
||||
getIsChatflowStreamingApi.request(dialogProps.chatflowid)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -537,57 +625,151 @@ query({
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
{value !== 0 && (
|
||||
<div style={{ flex: 20 }}>
|
||||
<Dropdown
|
||||
name='SelectKey'
|
||||
disableClearable={true}
|
||||
options={keyOptions}
|
||||
onSelect={(newValue) => onApiKeySelected(newValue)}
|
||||
value={dialogProps.chatflowApiKeyId ?? chatflowApiKeyId ?? 'Choose an API key'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ flex: 20 }}>
|
||||
<Dropdown
|
||||
name='SelectKey'
|
||||
disableClearable={true}
|
||||
options={keyOptions}
|
||||
onSelect={(newValue) => onApiKeySelected(newValue)}
|
||||
value={dialogProps.chatflowApiKeyId ?? chatflowApiKeyId ?? 'Choose an API key'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}></div>
|
||||
{codes.map((codeLang, index) => (
|
||||
<TabPanel key={index} value={value} index={index}>
|
||||
{value === 0 && (
|
||||
{(codeLang === 'Embed' || codeLang === 'Share Chatbot') && chatflowApiKeyId && (
|
||||
<>
|
||||
<span>
|
||||
Paste this anywhere in the <code>{`<body>`}</code> tag of your html file
|
||||
</span>
|
||||
<div style={{ height: 10 }}></div>
|
||||
<p>You cannot use API key while embedding/sharing chatbot.</p>
|
||||
<p>
|
||||
Please select <b>"No Authorization"</b> from the dropdown at the top right corner.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<CopyBlock
|
||||
theme={atomOneDark}
|
||||
text={chatflowApiKeyId ? getCodeWithAuthorization(codeLang) : getCode(codeLang)}
|
||||
language={getLang(codeLang)}
|
||||
showLineNumbers={false}
|
||||
wrapLines
|
||||
/>
|
||||
{value !== 0 && <CheckboxInput label='Show Input Config' value={checkboxVal} onChange={onCheckBoxChanged} />}
|
||||
{value !== 0 && checkboxVal && getConfigApi.data && getConfigApi.data.length > 0 && (
|
||||
{codeLang === 'Embed' && !chatflowApiKeyId && <EmbedChat chatflowid={dialogProps.chatflowid} />}
|
||||
{codeLang !== 'Embed' && codeLang !== 'Share Chatbot' && (
|
||||
<>
|
||||
<TableViewOnly rows={getConfigApi.data} columns={Object.keys(getConfigApi.data[0])} />
|
||||
<CopyBlock
|
||||
theme={atomOneDark}
|
||||
text={
|
||||
chatflowApiKeyId
|
||||
? dialogProps.isFormDataRequired
|
||||
? getConfigCodeWithFormDataWithAuth(codeLang, getConfigApi.data)
|
||||
: getConfigCodeWithAuthorization(codeLang, getConfigApi.data)
|
||||
: dialogProps.isFormDataRequired
|
||||
? getConfigCodeWithFormData(codeLang, getConfigApi.data)
|
||||
: getConfigCode(codeLang, getConfigApi.data)
|
||||
}
|
||||
text={chatflowApiKeyId ? getCodeWithAuthorization(codeLang) : getCode(codeLang)}
|
||||
language={getLang(codeLang)}
|
||||
showLineNumbers={false}
|
||||
wrapLines
|
||||
/>
|
||||
<CheckboxInput label='Show Input Config' value={checkboxVal} onChange={onCheckBoxChanged} />
|
||||
{checkboxVal && getConfigApi.data && getConfigApi.data.length > 0 && (
|
||||
<>
|
||||
{Object.keys(nodeConfig)
|
||||
.sort()
|
||||
.map((nodeLabel) => (
|
||||
<Accordion
|
||||
expanded={nodeConfigExpanded[nodeLabel] || false}
|
||||
onChange={handleAccordionChange(nodeLabel)}
|
||||
key={nodeLabel}
|
||||
disableGutters
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`nodes-accordian-${nodeLabel}`}
|
||||
id={`nodes-accordian-header-${nodeLabel}`}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Typography variant='h5'>{nodeLabel}</Typography>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: 'max-content',
|
||||
borderRadius: 15,
|
||||
background: 'rgb(254,252,191)',
|
||||
padding: 5,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
marginLeft: 10
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'rgb(116,66,16)', fontSize: '0.825rem' }}>
|
||||
{nodeConfig[nodeLabel][0].nodeId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<TableViewOnly
|
||||
rows={nodeConfig[nodeLabel]}
|
||||
columns={Object.keys(nodeConfig[nodeLabel][0]).slice(-3)}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
<CopyBlock
|
||||
theme={atomOneDark}
|
||||
text={
|
||||
chatflowApiKeyId
|
||||
? dialogProps.isFormDataRequired
|
||||
? getConfigCodeWithFormDataWithAuth(codeLang, getConfigApi.data)
|
||||
: getConfigCodeWithAuthorization(codeLang, getConfigApi.data)
|
||||
: dialogProps.isFormDataRequired
|
||||
? getConfigCodeWithFormData(codeLang, getConfigApi.data)
|
||||
: getConfigCode(codeLang, getConfigApi.data)
|
||||
}
|
||||
language={getLang(codeLang)}
|
||||
showLineNumbers={false}
|
||||
wrapLines
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 10,
|
||||
background: '#d8f3dc',
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<IconBulb size={30} color='#2d6a4f' />
|
||||
<span style={{ color: '#2d6a4f', marginLeft: 10, fontWeight: 500 }}>
|
||||
You can also specify multiple values for a config parameter by specifying the node id
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ padding: 10 }}>
|
||||
<CopyBlock
|
||||
theme={atomOneDark}
|
||||
text={
|
||||
dialogProps.isFormDataRequired
|
||||
? getMultiConfigCodeWithFormData(codeLang)
|
||||
: getMultiConfigCode()
|
||||
}
|
||||
language={getLang(codeLang)}
|
||||
showLineNumbers={false}
|
||||
wrapLines
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{getIsChatflowStreamingApi.data?.isStreaming && (
|
||||
<p>
|
||||
Read
|
||||
<a rel='noreferrer' target='_blank' href='https://docs.flowiseai.com/how-to-use#streaming'>
|
||||
here
|
||||
</a>
|
||||
on how to stream response back to application
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{codeLang === 'Share Chatbot' && !chatflowApiKeyId && (
|
||||
<ShareChatbot isSessionMemory={dialogProps.isSessionMemory} />
|
||||
)}
|
||||
</TabPanel>
|
||||
))}
|
||||
</DialogContent>
|
||||
@@ -0,0 +1,324 @@
|
||||
import { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Tabs, Tab, Box } from '@mui/material'
|
||||
import { CopyBlock, atomOneDark } from 'react-code-blocks'
|
||||
|
||||
// Project import
|
||||
import { CheckboxInput } from 'ui-component/checkbox/Checkbox'
|
||||
|
||||
// Const
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
function TabPanel(props) {
|
||||
const { children, value, index, ...other } = props
|
||||
return (
|
||||
<div
|
||||
role='tabpanel'
|
||||
hidden={value !== index}
|
||||
id={`attachment-tabpanel-${index}`}
|
||||
aria-labelledby={`attachment-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && <Box sx={{ p: 1 }}>{children}</Box>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
TabPanel.propTypes = {
|
||||
children: PropTypes.node,
|
||||
index: PropTypes.number.isRequired,
|
||||
value: PropTypes.number.isRequired
|
||||
}
|
||||
|
||||
function a11yProps(index) {
|
||||
return {
|
||||
id: `attachment-tab-${index}`,
|
||||
'aria-controls': `attachment-tabpanel-${index}`
|
||||
}
|
||||
}
|
||||
|
||||
const embedPopupHtmlCode = (chatflowid) => {
|
||||
return `<script type="module">
|
||||
import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js"
|
||||
Chatbot.init({
|
||||
chatflowid: "${chatflowid}",
|
||||
apiHost: "${baseURL}",
|
||||
})
|
||||
</script>`
|
||||
}
|
||||
|
||||
const embedPopupReactCode = (chatflowid) => {
|
||||
return `import { BubbleChat } from 'flowise-embed-react'
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<BubbleChat chatflowid="${chatflowid}" apiHost="${baseURL}" />
|
||||
);
|
||||
};`
|
||||
}
|
||||
|
||||
const embedFullpageHtmlCode = (chatflowid) => {
|
||||
return `<flowise-fullchatbot></flowise-fullchatbot>
|
||||
<script type="module">
|
||||
import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js"
|
||||
Chatbot.initFull({
|
||||
chatflowid: "${chatflowid}",
|
||||
apiHost: "${baseURL}",
|
||||
})
|
||||
</script>`
|
||||
}
|
||||
|
||||
const embedFullpageReactCode = (chatflowid) => {
|
||||
return `import { FullPageChat } from "flowise-embed-react"
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<FullPageChat
|
||||
chatflowid="${chatflowid}"
|
||||
apiHost="${baseURL}"
|
||||
/>
|
||||
);
|
||||
};`
|
||||
}
|
||||
|
||||
const buttonConfig = (isReact = false) => {
|
||||
return isReact
|
||||
? `button: {
|
||||
backgroundColor: "#3B81F6",
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
size: "medium",
|
||||
iconColor: "white",
|
||||
customIconSrc: "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/svg/google-messages.svg",
|
||||
}`
|
||||
: `button: {
|
||||
backgroundColor: "#3B81F6",
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
size: "medium",
|
||||
iconColor: "white",
|
||||
customIconSrc: "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/svg/google-messages.svg",
|
||||
}`
|
||||
}
|
||||
|
||||
const chatwindowConfig = (isReact = false) => {
|
||||
return isReact
|
||||
? `chatWindow: {
|
||||
welcomeMessage: "Hello! This is custom welcome message",
|
||||
backgroundColor: "#ffffff",
|
||||
height: 700,
|
||||
width: 400,
|
||||
fontSize: 16,
|
||||
poweredByTextColor: "#303235",
|
||||
botMessage: {
|
||||
backgroundColor: "#f7f8ff",
|
||||
textColor: "#303235",
|
||||
showAvatar: true,
|
||||
avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png",
|
||||
},
|
||||
userMessage: {
|
||||
backgroundColor: "#3B81F6",
|
||||
textColor: "#ffffff",
|
||||
showAvatar: true,
|
||||
avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png",
|
||||
},
|
||||
textInput: {
|
||||
placeholder: "Type your question",
|
||||
backgroundColor: "#ffffff",
|
||||
textColor: "#303235",
|
||||
sendButtonColor: "#3B81F6",
|
||||
}
|
||||
}`
|
||||
: `chatWindow: {
|
||||
welcomeMessage: "Hello! This is custom welcome message",
|
||||
backgroundColor: "#ffffff",
|
||||
height: 700,
|
||||
width: 400,
|
||||
fontSize: 16,
|
||||
poweredByTextColor: "#303235",
|
||||
botMessage: {
|
||||
backgroundColor: "#f7f8ff",
|
||||
textColor: "#303235",
|
||||
showAvatar: true,
|
||||
avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png",
|
||||
},
|
||||
userMessage: {
|
||||
backgroundColor: "#3B81F6",
|
||||
textColor: "#ffffff",
|
||||
showAvatar: true,
|
||||
avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png",
|
||||
},
|
||||
textInput: {
|
||||
placeholder: "Type your question",
|
||||
backgroundColor: "#ffffff",
|
||||
textColor: "#303235",
|
||||
sendButtonColor: "#3B81F6",
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
const embedPopupHtmlCodeCustomization = (chatflowid) => {
|
||||
return `<script type="module">
|
||||
import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js"
|
||||
Chatbot.init({
|
||||
chatflowid: "${chatflowid}",
|
||||
apiHost: "${baseURL}",
|
||||
chatflowConfig: {
|
||||
// topK: 2
|
||||
},
|
||||
theme: {
|
||||
${buttonConfig()},
|
||||
${chatwindowConfig()}
|
||||
}
|
||||
})
|
||||
</script>`
|
||||
}
|
||||
|
||||
const embedPopupReactCodeCustomization = (chatflowid) => {
|
||||
return `import { BubbleChat } from 'flowise-embed-react'
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<BubbleChat
|
||||
chatflowid="${chatflowid}"
|
||||
apiHost="${baseURL}"
|
||||
theme={{
|
||||
${buttonConfig(true)},
|
||||
${chatwindowConfig(true)}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};`
|
||||
}
|
||||
|
||||
const embedFullpageHtmlCodeCustomization = (chatflowid) => {
|
||||
return `<flowise-fullchatbot></flowise-fullchatbot>
|
||||
<script type="module">
|
||||
import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js"
|
||||
Chatbot.initFull({
|
||||
chatflowid: "${chatflowid}",
|
||||
apiHost: "${baseURL}",
|
||||
theme: {
|
||||
${chatwindowConfig()}
|
||||
}
|
||||
})
|
||||
</script>`
|
||||
}
|
||||
|
||||
const embedFullpageReactCodeCustomization = (chatflowid) => {
|
||||
return `import { FullPageChat } from "flowise-embed-react"
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<FullPageChat
|
||||
chatflowid="${chatflowid}"
|
||||
apiHost="${baseURL}"
|
||||
theme={{
|
||||
${chatwindowConfig(true)}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};`
|
||||
}
|
||||
|
||||
const EmbedChat = ({ chatflowid }) => {
|
||||
const codes = ['Popup Html', 'Fullpage Html', 'Popup React', 'Fullpage React']
|
||||
const [value, setValue] = useState(0)
|
||||
const [embedChatCheckboxVal, setEmbedChatCheckbox] = useState(false)
|
||||
|
||||
const onCheckBoxEmbedChatChanged = (newVal) => {
|
||||
setEmbedChatCheckbox(newVal)
|
||||
}
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue)
|
||||
}
|
||||
|
||||
const getCode = (codeLang) => {
|
||||
switch (codeLang) {
|
||||
case 'Popup Html':
|
||||
return embedPopupHtmlCode(chatflowid)
|
||||
case 'Fullpage Html':
|
||||
return embedFullpageHtmlCode(chatflowid)
|
||||
case 'Popup React':
|
||||
return embedPopupReactCode(chatflowid)
|
||||
case 'Fullpage React':
|
||||
return embedFullpageReactCode(chatflowid)
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const getCodeCustomization = (codeLang) => {
|
||||
switch (codeLang) {
|
||||
case 'Popup Html':
|
||||
return embedPopupHtmlCodeCustomization(chatflowid)
|
||||
case 'Fullpage Html':
|
||||
return embedFullpageHtmlCodeCustomization(chatflowid)
|
||||
case 'Popup React':
|
||||
return embedPopupReactCodeCustomization(chatflowid)
|
||||
case 'Fullpage React':
|
||||
return embedFullpageReactCodeCustomization(chatflowid)
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<div style={{ flex: 80 }}>
|
||||
<Tabs value={value} onChange={handleChange} aria-label='tabs'>
|
||||
{codes.map((codeLang, index) => (
|
||||
<Tab key={index} label={codeLang} {...a11yProps(index)}></Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}></div>
|
||||
{codes.map((codeLang, index) => (
|
||||
<TabPanel key={index} value={value} index={index}>
|
||||
{(value === 0 || value === 1) && (
|
||||
<>
|
||||
<span>
|
||||
Paste this anywhere in the <code>{`<body>`}</code> tag of your html file.
|
||||
<p>
|
||||
You can also specify a
|
||||
<a
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
href='https://www.npmjs.com/package/flowise-embed?activeTab=versions'
|
||||
>
|
||||
version
|
||||
</a>
|
||||
: <code>{`https://cdn.jsdelivr.net/npm/flowise-embed@<version>/dist/web.js`}</code>
|
||||
</p>
|
||||
</span>
|
||||
<div style={{ height: 10 }}></div>
|
||||
</>
|
||||
)}
|
||||
<CopyBlock theme={atomOneDark} text={getCode(codeLang)} language='javascript' showLineNumbers={false} wrapLines />
|
||||
|
||||
<CheckboxInput label='Show Embed Chat Config' value={embedChatCheckboxVal} onChange={onCheckBoxEmbedChatChanged} />
|
||||
|
||||
{embedChatCheckboxVal && (
|
||||
<CopyBlock
|
||||
theme={atomOneDark}
|
||||
text={getCodeCustomization(codeLang)}
|
||||
language='javascript'
|
||||
showLineNumbers={false}
|
||||
wrapLines
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
EmbedChat.propTypes = {
|
||||
chatflowid: PropTypes.string
|
||||
}
|
||||
|
||||
export default EmbedChat
|
||||
@@ -0,0 +1,495 @@
|
||||
import { useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from 'store/actions'
|
||||
import { SketchPicker } from 'react-color'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Box, Typography, Button, Switch, OutlinedInput, Popover, Stack, IconButton } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Project import
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
|
||||
|
||||
// Icons
|
||||
import { IconX, IconCopy, IconArrowUpRightCircle } from '@tabler/icons'
|
||||
|
||||
// API
|
||||
import chatflowsApi from 'api/chatflows'
|
||||
|
||||
// utils
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
|
||||
// Const
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
const defaultConfig = {
|
||||
backgroundColor: '#ffffff',
|
||||
fontSize: 16,
|
||||
poweredByTextColor: '#303235',
|
||||
botMessage: {
|
||||
backgroundColor: '#f7f8ff',
|
||||
textColor: '#303235'
|
||||
},
|
||||
userMessage: {
|
||||
backgroundColor: '#3B81F6',
|
||||
textColor: '#ffffff'
|
||||
},
|
||||
textInput: {
|
||||
backgroundColor: '#ffffff',
|
||||
textColor: '#303235',
|
||||
sendButtonColor: '#3B81F6'
|
||||
}
|
||||
}
|
||||
|
||||
const ShareChatbot = ({ isSessionMemory }) => {
|
||||
const dispatch = useDispatch()
|
||||
const theme = useTheme()
|
||||
const chatflow = useSelector((state) => state.canvas.chatflow)
|
||||
const chatflowid = chatflow.id
|
||||
const chatbotConfig = chatflow.chatbotConfig ? JSON.parse(chatflow.chatbotConfig) : {}
|
||||
|
||||
useNotifier()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const [isPublicChatflow, setChatflowIsPublic] = useState(chatflow.isPublic ?? false)
|
||||
const [generateNewSession, setGenerateNewSession] = useState(chatbotConfig?.generateNewSession ?? false)
|
||||
|
||||
const [welcomeMessage, setWelcomeMessage] = useState(chatbotConfig?.welcomeMessage ?? '')
|
||||
const [backgroundColor, setBackgroundColor] = useState(chatbotConfig?.backgroundColor ?? defaultConfig.backgroundColor)
|
||||
const [fontSize, setFontSize] = useState(chatbotConfig?.fontSize ?? defaultConfig.fontSize)
|
||||
const [poweredByTextColor, setPoweredByTextColor] = useState(chatbotConfig?.poweredByTextColor ?? defaultConfig.poweredByTextColor)
|
||||
|
||||
const [botMessageBackgroundColor, setBotMessageBackgroundColor] = useState(
|
||||
chatbotConfig?.botMessage?.backgroundColor ?? defaultConfig.botMessage.backgroundColor
|
||||
)
|
||||
const [botMessageTextColor, setBotMessageTextColor] = useState(
|
||||
chatbotConfig?.botMessage?.textColor ?? defaultConfig.botMessage.textColor
|
||||
)
|
||||
const [botMessageAvatarSrc, setBotMessageAvatarSrc] = useState(chatbotConfig?.botMessage?.avatarSrc ?? '')
|
||||
const [botMessageShowAvatar, setBotMessageShowAvatar] = useState(chatbotConfig?.botMessage?.showAvatar ?? false)
|
||||
|
||||
const [userMessageBackgroundColor, setUserMessageBackgroundColor] = useState(
|
||||
chatbotConfig?.userMessage?.backgroundColor ?? defaultConfig.userMessage.backgroundColor
|
||||
)
|
||||
const [userMessageTextColor, setUserMessageTextColor] = useState(
|
||||
chatbotConfig?.userMessage?.textColor ?? defaultConfig.userMessage.textColor
|
||||
)
|
||||
const [userMessageAvatarSrc, setUserMessageAvatarSrc] = useState(chatbotConfig?.userMessage?.avatarSrc ?? '')
|
||||
const [userMessageShowAvatar, setUserMessageShowAvatar] = useState(chatbotConfig?.userMessage?.showAvatar ?? false)
|
||||
|
||||
const [textInputBackgroundColor, setTextInputBackgroundColor] = useState(
|
||||
chatbotConfig?.textInput?.backgroundColor ?? defaultConfig.textInput.backgroundColor
|
||||
)
|
||||
const [textInputTextColor, setTextInputTextColor] = useState(chatbotConfig?.textInput?.textColor ?? defaultConfig.textInput.textColor)
|
||||
const [textInputPlaceholder, setTextInputPlaceholder] = useState(chatbotConfig?.textInput?.placeholder ?? '')
|
||||
const [textInputSendButtonColor, setTextInputSendButtonColor] = useState(
|
||||
chatbotConfig?.textInput?.sendButtonColor ?? defaultConfig.textInput.sendButtonColor
|
||||
)
|
||||
|
||||
const [colorAnchorEl, setColorAnchorEl] = useState(null)
|
||||
const [selectedColorConfig, setSelectedColorConfig] = useState('')
|
||||
const [sketchPickerColor, setSketchPickerColor] = useState('')
|
||||
const openColorPopOver = Boolean(colorAnchorEl)
|
||||
|
||||
const [copyAnchorEl, setCopyAnchorEl] = useState(null)
|
||||
const openCopyPopOver = Boolean(copyAnchorEl)
|
||||
|
||||
const formatObj = () => {
|
||||
const obj = {
|
||||
botMessage: {
|
||||
showAvatar: false
|
||||
},
|
||||
userMessage: {
|
||||
showAvatar: false
|
||||
},
|
||||
textInput: {},
|
||||
overrideConfig: {}
|
||||
}
|
||||
if (welcomeMessage) obj.welcomeMessage = welcomeMessage
|
||||
if (backgroundColor) obj.backgroundColor = backgroundColor
|
||||
if (fontSize) obj.fontSize = fontSize
|
||||
if (poweredByTextColor) obj.poweredByTextColor = poweredByTextColor
|
||||
|
||||
if (botMessageBackgroundColor) obj.botMessage.backgroundColor = botMessageBackgroundColor
|
||||
if (botMessageTextColor) obj.botMessage.textColor = botMessageTextColor
|
||||
if (botMessageAvatarSrc) obj.botMessage.avatarSrc = botMessageAvatarSrc
|
||||
if (botMessageShowAvatar) obj.botMessage.showAvatar = botMessageShowAvatar
|
||||
|
||||
if (userMessageBackgroundColor) obj.userMessage.backgroundColor = userMessageBackgroundColor
|
||||
if (userMessageTextColor) obj.userMessage.textColor = userMessageTextColor
|
||||
if (userMessageAvatarSrc) obj.userMessage.avatarSrc = userMessageAvatarSrc
|
||||
if (userMessageShowAvatar) obj.userMessage.showAvatar = userMessageShowAvatar
|
||||
|
||||
if (textInputBackgroundColor) obj.textInput.backgroundColor = textInputBackgroundColor
|
||||
if (textInputTextColor) obj.textInput.textColor = textInputTextColor
|
||||
if (textInputPlaceholder) obj.textInput.placeholder = textInputPlaceholder
|
||||
if (textInputSendButtonColor) obj.textInput.sendButtonColor = textInputSendButtonColor
|
||||
|
||||
if (isSessionMemory) obj.overrideConfig.generateNewSession = generateNewSession
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
const onSave = async () => {
|
||||
try {
|
||||
const saveResp = await chatflowsApi.updateChatflow(chatflowid, {
|
||||
chatbotConfig: JSON.stringify(formatObj())
|
||||
})
|
||||
if (saveResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Chatbot Configuration Saved',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to save Chatbot Configuration: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onSwitchChange = async (checked) => {
|
||||
try {
|
||||
const saveResp = await chatflowsApi.updateChatflow(chatflowid, { isPublic: checked })
|
||||
if (saveResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Chatbot Configuration Saved',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to save Chatbot Configuration: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleClosePopOver = () => {
|
||||
setColorAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleCloseCopyPopOver = () => {
|
||||
setCopyAnchorEl(null)
|
||||
}
|
||||
|
||||
const onColorSelected = (hexColor) => {
|
||||
switch (selectedColorConfig) {
|
||||
case 'backgroundColor':
|
||||
setBackgroundColor(hexColor)
|
||||
break
|
||||
case 'poweredByTextColor':
|
||||
setPoweredByTextColor(hexColor)
|
||||
break
|
||||
case 'botMessageBackgroundColor':
|
||||
setBotMessageBackgroundColor(hexColor)
|
||||
break
|
||||
case 'botMessageTextColor':
|
||||
setBotMessageTextColor(hexColor)
|
||||
break
|
||||
case 'userMessageBackgroundColor':
|
||||
setUserMessageBackgroundColor(hexColor)
|
||||
break
|
||||
case 'userMessageTextColor':
|
||||
setUserMessageTextColor(hexColor)
|
||||
break
|
||||
case 'textInputBackgroundColor':
|
||||
setTextInputBackgroundColor(hexColor)
|
||||
break
|
||||
case 'textInputTextColor':
|
||||
setTextInputTextColor(hexColor)
|
||||
break
|
||||
case 'textInputSendButtonColor':
|
||||
setTextInputSendButtonColor(hexColor)
|
||||
break
|
||||
}
|
||||
setSketchPickerColor(hexColor)
|
||||
}
|
||||
|
||||
const onTextChanged = (value, fieldName) => {
|
||||
switch (fieldName) {
|
||||
case 'welcomeMessage':
|
||||
setWelcomeMessage(value)
|
||||
break
|
||||
case 'fontSize':
|
||||
setFontSize(value)
|
||||
break
|
||||
case 'botMessageAvatarSrc':
|
||||
setBotMessageAvatarSrc(value)
|
||||
break
|
||||
case 'userMessageAvatarSrc':
|
||||
setUserMessageAvatarSrc(value)
|
||||
break
|
||||
case 'textInputPlaceholder':
|
||||
setTextInputPlaceholder(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const onBooleanChanged = (value, fieldName) => {
|
||||
switch (fieldName) {
|
||||
case 'botMessageShowAvatar':
|
||||
setBotMessageShowAvatar(value)
|
||||
break
|
||||
case 'userMessageShowAvatar':
|
||||
setUserMessageShowAvatar(value)
|
||||
break
|
||||
case 'generateNewSession':
|
||||
setGenerateNewSession(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const colorField = (color, fieldName, fieldLabel) => {
|
||||
return (
|
||||
<Box sx={{ pt: 2, pb: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography sx={{ mb: 1 }}>{fieldLabel}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
border: '1px solid #616161',
|
||||
marginRight: '10px',
|
||||
backgroundColor: color ?? '#ffffff',
|
||||
borderRadius: '5px'
|
||||
}}
|
||||
onClick={(event) => {
|
||||
setSelectedColorConfig(fieldName)
|
||||
setSketchPickerColor(color ?? '#ffffff')
|
||||
setColorAnchorEl(event.currentTarget)
|
||||
}}
|
||||
></Box>
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const booleanField = (value, fieldName, fieldLabel) => {
|
||||
return (
|
||||
<Box sx={{ pt: 2, pb: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography sx={{ mb: 1 }}>{fieldLabel}</Typography>
|
||||
<Switch
|
||||
id={fieldName}
|
||||
checked={value}
|
||||
onChange={(event) => {
|
||||
onBooleanChanged(event.target.checked, fieldName)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const textField = (message, fieldName, fieldLabel, fieldType = 'string', placeholder = '') => {
|
||||
return (
|
||||
<Box sx={{ pt: 2, pb: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography sx={{ mb: 1 }}>{fieldLabel}</Typography>
|
||||
<OutlinedInput
|
||||
id={fieldName}
|
||||
type={fieldType}
|
||||
fullWidth
|
||||
value={message}
|
||||
placeholder={placeholder}
|
||||
name={fieldName}
|
||||
onChange={(e) => {
|
||||
onTextChanged(e.target.value, fieldName)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction='row'>
|
||||
<Typography
|
||||
sx={{
|
||||
p: 1,
|
||||
borderRadius: 10,
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
width: 'max-content',
|
||||
height: 'max-content'
|
||||
}}
|
||||
variant='h5'
|
||||
>
|
||||
{`${baseURL}/chatbot/${chatflowid}`}
|
||||
</Typography>
|
||||
<IconButton
|
||||
title='Copy Link'
|
||||
color='success'
|
||||
onClick={(event) => {
|
||||
navigator.clipboard.writeText(`${baseURL}/chatbot/${chatflowid}`)
|
||||
setCopyAnchorEl(event.currentTarget)
|
||||
setTimeout(() => {
|
||||
handleCloseCopyPopOver()
|
||||
}, 1500)
|
||||
}}
|
||||
>
|
||||
<IconCopy />
|
||||
</IconButton>
|
||||
<IconButton title='Open New Tab' color='primary' onClick={() => window.open(`${baseURL}/chatbot/${chatflowid}`, '_blank')}>
|
||||
<IconArrowUpRightCircle />
|
||||
</IconButton>
|
||||
<div style={{ flex: 1 }} />
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Switch
|
||||
checked={isPublicChatflow}
|
||||
onChange={(event) => {
|
||||
setChatflowIsPublic(event.target.checked)
|
||||
onSwitchChange(event.target.checked)
|
||||
}}
|
||||
/>
|
||||
<Typography>Make Public</Typography>
|
||||
<TooltipWithParser
|
||||
style={{ marginLeft: 10 }}
|
||||
title={'Making public will allow anyone to access the chatbot without username & password'}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
{textField(welcomeMessage, 'welcomeMessage', 'Welcome Message', 'string', 'Hello! This is custom welcome message')}
|
||||
{colorField(backgroundColor, 'backgroundColor', 'Background Color')}
|
||||
{textField(fontSize, 'fontSize', 'Font Size', 'number')}
|
||||
{colorField(poweredByTextColor, 'poweredByTextColor', 'PoweredBy TextColor')}
|
||||
|
||||
{/*BOT Message*/}
|
||||
<Typography variant='h4' sx={{ mb: 1, mt: 2 }}>
|
||||
Bot Message
|
||||
</Typography>
|
||||
{colorField(botMessageBackgroundColor, 'botMessageBackgroundColor', 'Background Color')}
|
||||
{colorField(botMessageTextColor, 'botMessageTextColor', 'Text Color')}
|
||||
{textField(
|
||||
botMessageAvatarSrc,
|
||||
'botMessageAvatarSrc',
|
||||
'Avatar Link',
|
||||
'string',
|
||||
`https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png`
|
||||
)}
|
||||
{booleanField(botMessageShowAvatar, 'botMessageShowAvatar', 'Show Avatar')}
|
||||
|
||||
{/*USER Message*/}
|
||||
<Typography variant='h4' sx={{ mb: 1, mt: 2 }}>
|
||||
User Message
|
||||
</Typography>
|
||||
{colorField(userMessageBackgroundColor, 'userMessageBackgroundColor', 'Background Color')}
|
||||
{colorField(userMessageTextColor, 'userMessageTextColor', 'Text Color')}
|
||||
{textField(
|
||||
userMessageAvatarSrc,
|
||||
'userMessageAvatarSrc',
|
||||
'Avatar Link',
|
||||
'string',
|
||||
`https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png`
|
||||
)}
|
||||
{booleanField(userMessageShowAvatar, 'userMessageShowAvatar', 'Show Avatar')}
|
||||
|
||||
{/*TEXT Input*/}
|
||||
<Typography variant='h4' sx={{ mb: 1, mt: 2 }}>
|
||||
Text Input
|
||||
</Typography>
|
||||
{colorField(textInputBackgroundColor, 'textInputBackgroundColor', 'Background Color')}
|
||||
{colorField(textInputTextColor, 'textInputTextColor', 'Text Color')}
|
||||
{textField(textInputPlaceholder, 'textInputPlaceholder', 'TextInput Placeholder', 'string', `Type question..`)}
|
||||
{colorField(textInputSendButtonColor, 'textInputSendButtonColor', 'TextIntput Send Button Color')}
|
||||
|
||||
{/*Session Memory Input*/}
|
||||
{isSessionMemory && (
|
||||
<>
|
||||
<Typography variant='h4' sx={{ mb: 1, mt: 2 }}>
|
||||
Session Memory
|
||||
</Typography>
|
||||
{booleanField(generateNewSession, 'generateNewSession', 'Start new session when chatbot link is opened or refreshed')}
|
||||
</>
|
||||
)}
|
||||
|
||||
<StyledButton style={{ marginBottom: 10, marginTop: 10 }} variant='contained' onClick={() => onSave()}>
|
||||
Save Changes
|
||||
</StyledButton>
|
||||
<Popover
|
||||
open={openColorPopOver}
|
||||
anchorEl={colorAnchorEl}
|
||||
onClose={handleClosePopOver}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
>
|
||||
<SketchPicker color={sketchPickerColor} onChange={(color) => onColorSelected(color.hex)} />
|
||||
</Popover>
|
||||
<Popover
|
||||
open={openCopyPopOver}
|
||||
anchorEl={copyAnchorEl}
|
||||
onClose={handleCloseCopyPopOver}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
>
|
||||
<Typography variant='h6' sx={{ pl: 1, pr: 1, color: 'white', background: theme.palette.success.dark }}>
|
||||
Copied!
|
||||
</Typography>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ShareChatbot.propTypes = {
|
||||
isSessionMemory: PropTypes.bool
|
||||
}
|
||||
|
||||
export default ShareChatbot
|
||||
@@ -0,0 +1,62 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { Dialog, DialogContent, DialogTitle, Button } from '@mui/material'
|
||||
import { ChatMessage } from './ChatMessage'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
import { IconEraser } from '@tabler/icons'
|
||||
|
||||
const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
open={show}
|
||||
fullWidth
|
||||
maxWidth='md'
|
||||
onClose={onCancel}
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
sx={{ overflow: 'visible' }}
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
{dialogProps.title}
|
||||
<div style={{ flex: 1 }}></div>
|
||||
{customization.isDarkMode && (
|
||||
<StyledButton
|
||||
variant='outlined'
|
||||
color='error'
|
||||
title='Clear Conversation'
|
||||
onClick={onClear}
|
||||
startIcon={<IconEraser />}
|
||||
>
|
||||
Clear Chat
|
||||
</StyledButton>
|
||||
)}
|
||||
{!customization.isDarkMode && (
|
||||
<Button variant='outlined' color='error' title='Clear Conversation' onClick={onClear} startIcon={<IconEraser />}>
|
||||
Clear Chat
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', justifyContent: 'flex-end', flexDirection: 'column' }}>
|
||||
<ChatMessage isDialog={true} open={dialogProps.open} chatflowid={dialogProps.chatflowid} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
ChatExpandDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onClear: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default ChatExpandDialog
|
||||
@@ -2,6 +2,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -75,12 +76,15 @@
|
||||
}
|
||||
|
||||
.markdownanswer a {
|
||||
display: block;
|
||||
margin-right: 2.5rem;
|
||||
word-wrap: break-word;
|
||||
color: #16bed7;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.markdownanswer code {
|
||||
color: #15cb19;
|
||||
color: #0ab126;
|
||||
font-weight: 500;
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
@@ -92,6 +96,7 @@
|
||||
|
||||
.boticon,
|
||||
.usericon {
|
||||
margin-top: 1rem;
|
||||
margin-right: 1rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
@@ -119,3 +124,13 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cloud-dialog {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow-y: scroll;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -1,53 +1,43 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||
import socketIOClient from 'socket.io-client'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import rehypeMathjax from 'rehype-mathjax'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
|
||||
import {
|
||||
ClickAwayListener,
|
||||
Paper,
|
||||
Popper,
|
||||
CircularProgress,
|
||||
OutlinedInput,
|
||||
Divider,
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
Box,
|
||||
Button
|
||||
} from '@mui/material'
|
||||
import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { IconMessage, IconX, IconSend, IconEraser } from '@tabler/icons'
|
||||
import { IconSend } 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 { CodeBlock } from 'ui-component/markdown/CodeBlock'
|
||||
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
|
||||
import SourceDocDialog from 'ui-component/dialog/SourceDocDialog'
|
||||
import './ChatMessage.css'
|
||||
|
||||
// api
|
||||
import chatmessageApi from 'api/chatmessage'
|
||||
import chatflowsApi from 'api/chatflows'
|
||||
import predictionApi from 'api/prediction'
|
||||
|
||||
// Hooks
|
||||
import useApi from 'hooks/useApi'
|
||||
import useConfirm from 'hooks/useConfirm'
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
|
||||
import { maxScroll } from 'store/constant'
|
||||
// Const
|
||||
import { baseURL, maxScroll } from 'store/constant'
|
||||
|
||||
export const ChatMessage = ({ chatflowid }) => {
|
||||
import robotPNG from 'assets/images/robot.png'
|
||||
import userPNG from 'assets/images/account.png'
|
||||
import { isValidURL } from 'utils/genericHelper'
|
||||
|
||||
export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const { confirm } = useConfirm()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const ps = useRef()
|
||||
|
||||
useNotifier()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [userInput, setUserInput] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [messages, setMessages] = useState([
|
||||
@@ -56,85 +46,78 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||
type: 'apiMessage'
|
||||
}
|
||||
])
|
||||
const [socketIOClientId, setSocketIOClientId] = useState('')
|
||||
const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false)
|
||||
const [sourceDialogOpen, setSourceDialogOpen] = useState(false)
|
||||
const [sourceDialogProps, setSourceDialogProps] = useState({})
|
||||
|
||||
const inputRef = useRef(null)
|
||||
const anchorRef = useRef(null)
|
||||
const prevOpen = useRef(open)
|
||||
const getChatmessageApi = useApi(chatmessageApi.getChatmessageFromChatflow)
|
||||
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming)
|
||||
|
||||
const handleClose = (event) => {
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||
return
|
||||
}
|
||||
setOpen(false)
|
||||
const onSourceDialogClick = (data) => {
|
||||
setSourceDialogProps({ data })
|
||||
setSourceDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen((prevOpen) => !prevOpen)
|
||||
const onURLClick = (data) => {
|
||||
window.open(data, '_blank')
|
||||
}
|
||||
|
||||
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 removeDuplicateURL = (message) => {
|
||||
const visitedURLs = []
|
||||
const newSourceDocuments = []
|
||||
message.sourceDocuments.forEach((source) => {
|
||||
if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) {
|
||||
visitedURLs.push(source.metadata.source)
|
||||
newSourceDocuments.push(source)
|
||||
} else if (!isValidURL(source.metadata.source)) {
|
||||
newSourceDocuments.push(source)
|
||||
}
|
||||
}
|
||||
})
|
||||
return newSourceDocuments
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (ps.current) {
|
||||
ps.current.scrollTo({ top: maxScroll, behavior: 'smooth' })
|
||||
ps.current.scrollTo({ top: maxScroll })
|
||||
}
|
||||
}
|
||||
|
||||
const addChatMessage = async (message, type) => {
|
||||
const onChange = useCallback((e) => setUserInput(e.target.value), [setUserInput])
|
||||
|
||||
const addChatMessage = async (message, type, sourceDocuments) => {
|
||||
try {
|
||||
const newChatMessageBody = {
|
||||
role: type,
|
||||
content: message,
|
||||
chatflowid: chatflowid
|
||||
}
|
||||
if (sourceDocuments) newChatMessageBody.sourceDocuments = JSON.stringify(sourceDocuments)
|
||||
await chatmessageApi.createNewChatmessage(chatflowid, newChatMessageBody)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const updateLastMessage = (text) => {
|
||||
setMessages((prevMessages) => {
|
||||
let allMessages = [...cloneDeep(prevMessages)]
|
||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||
allMessages[allMessages.length - 1].message += text
|
||||
return allMessages
|
||||
})
|
||||
}
|
||||
|
||||
const updateLastMessageSourceDocuments = (sourceDocuments) => {
|
||||
setMessages((prevMessages) => {
|
||||
let allMessages = [...cloneDeep(prevMessages)]
|
||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||
allMessages[allMessages.length - 1].sourceDocuments = sourceDocuments
|
||||
return allMessages
|
||||
})
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
const handleError = (message = 'Oops! There seems to be an error. Please try again.') => {
|
||||
message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, '')
|
||||
@@ -143,7 +126,7 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||
setLoading(false)
|
||||
setUserInput('')
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus()
|
||||
inputRef.current?.focus()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
@@ -157,22 +140,39 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||
|
||||
setLoading(true)
|
||||
setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage' }])
|
||||
addChatMessage(userInput, 'userMessage')
|
||||
// waiting for first chatmessage saved, the first chatmessage will be used in sendMessageAndGetPrediction
|
||||
await addChatMessage(userInput, 'userMessage')
|
||||
|
||||
// Send user question and history to API
|
||||
try {
|
||||
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, {
|
||||
const params = {
|
||||
question: userInput,
|
||||
history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?')
|
||||
})
|
||||
}
|
||||
if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId
|
||||
|
||||
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params)
|
||||
|
||||
if (response.data) {
|
||||
const data = response.data
|
||||
setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }])
|
||||
addChatMessage(data, 'apiMessage')
|
||||
if (typeof data === 'object' && data.text && data.sourceDocuments) {
|
||||
if (!isChatFlowAvailableToStream) {
|
||||
setMessages((prevMessages) => [
|
||||
...prevMessages,
|
||||
{ message: data.text, sourceDocuments: data.sourceDocuments, type: 'apiMessage' }
|
||||
])
|
||||
}
|
||||
addChatMessage(data.text, 'apiMessage', data.sourceDocuments)
|
||||
} else {
|
||||
if (!isChatFlowAvailableToStream) {
|
||||
setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }])
|
||||
}
|
||||
addChatMessage(data, 'apiMessage')
|
||||
}
|
||||
setLoading(false)
|
||||
setUserInput('')
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus()
|
||||
inputRef.current?.focus()
|
||||
scrollToBottom()
|
||||
}, 100)
|
||||
}
|
||||
@@ -185,7 +185,9 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||
|
||||
// Prevent blank submissions and allow for multiline input
|
||||
const handleEnter = (e) => {
|
||||
if (e.key === 'Enter' && userInput) {
|
||||
// Check if IME composition is in progress
|
||||
const isIMEComposition = e.isComposing || e.keyCode === 229
|
||||
if (e.key === 'Enter' && userInput && !isIMEComposition) {
|
||||
if (!e.shiftKey && userInput) {
|
||||
handleSubmit(e)
|
||||
}
|
||||
@@ -199,10 +201,12 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||
if (getChatmessageApi.data) {
|
||||
const loadedMessages = []
|
||||
for (const message of getChatmessageApi.data) {
|
||||
loadedMessages.push({
|
||||
const obj = {
|
||||
message: message.content,
|
||||
type: message.role
|
||||
})
|
||||
}
|
||||
if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
|
||||
loadedMessages.push(obj)
|
||||
}
|
||||
setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
|
||||
}
|
||||
@@ -210,22 +214,49 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getChatmessageApi.data])
|
||||
|
||||
// Get chatflow streaming capability
|
||||
useEffect(() => {
|
||||
if (getIsChatflowStreamingApi.data) {
|
||||
setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getIsChatflowStreamingApi.data])
|
||||
|
||||
// Auto scroll chat to bottom
|
||||
useEffect(() => {
|
||||
scrollToBottom()
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
anchorRef.current.focus()
|
||||
if (isDialog && inputRef) {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus()
|
||||
}, 100)
|
||||
}
|
||||
}, [isDialog, inputRef])
|
||||
|
||||
useEffect(() => {
|
||||
let socket
|
||||
if (open && chatflowid) {
|
||||
getChatmessageApi.request(chatflowid)
|
||||
getIsChatflowStreamingApi.request(chatflowid)
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
prevOpen.current = open
|
||||
socket = socketIOClient(baseURL)
|
||||
|
||||
socket.on('connect', () => {
|
||||
setSocketIOClientId(socket.id)
|
||||
})
|
||||
|
||||
socket.on('start', () => {
|
||||
setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage' }])
|
||||
})
|
||||
|
||||
socket.on('sourceDocuments', updateLastMessageSourceDocuments)
|
||||
|
||||
socket.on('token', updateLastMessage)
|
||||
}
|
||||
|
||||
return () => {
|
||||
setUserInput('')
|
||||
@@ -236,6 +267,10 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||
type: 'apiMessage'
|
||||
}
|
||||
])
|
||||
if (socket) {
|
||||
socket.disconnect()
|
||||
setSocketIOClientId('')
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -243,151 +278,143 @@ export const ChatMessage = ({ 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 ref={ps} 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'
|
||||
<div className={isDialog ? 'cloud-dialog' : 'cloud'}>
|
||||
<div ref={ps} className='messagelist'>
|
||||
{messages &&
|
||||
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' }}
|
||||
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={robotPNG} alt='AI' width='30' height='30' className='boticon' />
|
||||
) : (
|
||||
<img src={userPNG} alt='Me' width='30' height='30' className='usericon' />
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
|
||||
<div className='markdownanswer'>
|
||||
{/* Messages are being rendered in Markdown format */}
|
||||
<MemoizedReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeMathjax]}
|
||||
components={{
|
||||
code({ inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
return !inline ? (
|
||||
<CodeBlock
|
||||
key={Math.random()}
|
||||
chatflowid={chatflowid}
|
||||
isDialog={isDialog}
|
||||
language={(match && match[1]) || ''}
|
||||
value={String(children).replace(/\n$/, '')}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* 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'
|
||||
}}
|
||||
>
|
||||
{message.message}
|
||||
</MemoizedReactMarkdown>
|
||||
</div>
|
||||
{message.sourceDocuments && (
|
||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||
{removeDuplicateURL(message).map((source, index) => {
|
||||
const URL = isValidURL(source.metadata.source)
|
||||
return (
|
||||
<Chip
|
||||
size='small'
|
||||
key={index}
|
||||
label={
|
||||
URL
|
||||
? URL.pathname.substring(0, 15) === '/'
|
||||
? URL.host
|
||||
: `${URL.pathname.substring(0, 15)}...`
|
||||
: `${source.pageContent.substring(0, 15)}...`
|
||||
}
|
||||
component='a'
|
||||
sx={{ mr: 1, mb: 1 }}
|
||||
variant='outlined'
|
||||
clickable
|
||||
onClick={() =>
|
||||
URL ? onURLClick(source.metadata.source) : onSourceDialogClick(source)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className='center'>
|
||||
<div style={{ width: '100%' }}>
|
||||
<form style={{ width: '100%' }} onSubmit={handleSubmit}>
|
||||
<OutlinedInput
|
||||
inputRef={inputRef}
|
||||
// eslint-disable-next-line
|
||||
autoFocus
|
||||
sx={{ width: '100%' }}
|
||||
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>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className='center'>
|
||||
<div style={{ width: '100%' }}>
|
||||
<form style={{ width: '100%' }} onSubmit={handleSubmit}>
|
||||
<OutlinedInput
|
||||
inputRef={inputRef}
|
||||
// eslint-disable-next-line
|
||||
autoFocus
|
||||
sx={{ width: '100%' }}
|
||||
disabled={loading || !chatflowid}
|
||||
onKeyDown={handleEnter}
|
||||
id='userInput'
|
||||
name='userInput'
|
||||
placeholder={loading ? 'Waiting for response...' : 'Type your question...'}
|
||||
value={userInput}
|
||||
onChange={onChange}
|
||||
multiline={true}
|
||||
maxRows={isDialog ? 7 : 2}
|
||||
endAdornment={
|
||||
<InputAdornment position='end' sx={{ padding: '15px' }}>
|
||||
<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>
|
||||
<SourceDocDialog show={sourceDialogOpen} dialogProps={sourceDialogProps} onCancel={() => setSourceDialogOpen(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ChatMessage.propTypes = { chatflowid: PropTypes.string }
|
||||
ChatMessage.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
chatflowid: PropTypes.string,
|
||||
isDialog: PropTypes.bool
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { ClickAwayListener, Paper, Popper, Button } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { IconMessage, IconX, IconEraser, IconArrowsMaximize } 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 } from './ChatMessage'
|
||||
import ChatExpandDialog from './ChatExpandDialog'
|
||||
|
||||
// api
|
||||
import chatmessageApi from 'api/chatmessage'
|
||||
|
||||
// Hooks
|
||||
import useConfirm from 'hooks/useConfirm'
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
|
||||
// Const
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||
|
||||
export const ChatPopUp = ({ chatflowid }) => {
|
||||
const theme = useTheme()
|
||||
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 [showExpandDialog, setShowExpandDialog] = useState(false)
|
||||
const [expandDialogProps, setExpandDialogProps] = useState({})
|
||||
|
||||
const anchorRef = useRef(null)
|
||||
const prevOpen = useRef(open)
|
||||
|
||||
const handleClose = (event) => {
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||
return
|
||||
}
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen((prevOpen) => !prevOpen)
|
||||
}
|
||||
|
||||
const expandChat = () => {
|
||||
const props = {
|
||||
open: true,
|
||||
chatflowid: chatflowid
|
||||
}
|
||||
setExpandDialogProps(props)
|
||||
setShowExpandDialog(true)
|
||||
}
|
||||
|
||||
const resetChatDialog = () => {
|
||||
const props = {
|
||||
...expandDialogProps,
|
||||
open: false
|
||||
}
|
||||
setExpandDialogProps(props)
|
||||
setTimeout(() => {
|
||||
const resetProps = {
|
||||
...expandDialogProps,
|
||||
open: true
|
||||
}
|
||||
setExpandDialogProps(resetProps)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
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)
|
||||
resetChatDialog()
|
||||
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>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
anchorRef.current.focus()
|
||||
}
|
||||
prevOpen.current = open
|
||||
|
||||
// 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>
|
||||
)}
|
||||
{open && (
|
||||
<StyledFab
|
||||
sx={{ position: 'absolute', right: 140, top: 20 }}
|
||||
onClick={expandChat}
|
||||
size='small'
|
||||
color='primary'
|
||||
aria-label='expand'
|
||||
title='Expand Chat'
|
||||
>
|
||||
<IconArrowsMaximize />
|
||||
</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]}>
|
||||
<ChatMessage chatflowid={chatflowid} open={open} />
|
||||
</MainCard>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Transitions>
|
||||
)}
|
||||
</Popper>
|
||||
<ChatExpandDialog
|
||||
show={showExpandDialog}
|
||||
dialogProps={expandDialogProps}
|
||||
onClear={clearChat}
|
||||
onCancel={() => setShowExpandDialog(false)}
|
||||
></ChatExpandDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ChatPopUp.propTypes = { chatflowid: PropTypes.string }
|
||||
@@ -0,0 +1,283 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||
import parser from 'html-react-parser'
|
||||
|
||||
// Material
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Stack, OutlinedInput, Typography } from '@mui/material'
|
||||
|
||||
// Project imports
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
|
||||
import CredentialInputHandler from './CredentialInputHandler'
|
||||
|
||||
// Icons
|
||||
import { IconX } from '@tabler/icons'
|
||||
|
||||
// API
|
||||
import credentialsApi from 'api/credentials'
|
||||
|
||||
// Hooks
|
||||
import useApi from 'hooks/useApi'
|
||||
|
||||
// utils
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
|
||||
|
||||
const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// ==============================|| Snackbar ||============================== //
|
||||
|
||||
useNotifier()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const getSpecificCredentialApi = useApi(credentialsApi.getSpecificCredential)
|
||||
const getSpecificComponentCredentialApi = useApi(credentialsApi.getSpecificComponentCredential)
|
||||
|
||||
const [credential, setCredential] = useState({})
|
||||
const [name, setName] = useState('')
|
||||
const [credentialData, setCredentialData] = useState({})
|
||||
const [componentCredential, setComponentCredential] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
if (getSpecificCredentialApi.data) {
|
||||
setCredential(getSpecificCredentialApi.data)
|
||||
if (getSpecificCredentialApi.data.name) {
|
||||
setName(getSpecificCredentialApi.data.name)
|
||||
}
|
||||
if (getSpecificCredentialApi.data.plainDataObj) {
|
||||
setCredentialData(getSpecificCredentialApi.data.plainDataObj)
|
||||
}
|
||||
getSpecificComponentCredentialApi.request(getSpecificCredentialApi.data.credentialName)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getSpecificCredentialApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getSpecificComponentCredentialApi.data) {
|
||||
setComponentCredential(getSpecificComponentCredentialApi.data)
|
||||
}
|
||||
}, [getSpecificComponentCredentialApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.type === 'EDIT' && dialogProps.data) {
|
||||
// When credential dialog is opened from Credentials dashboard
|
||||
getSpecificCredentialApi.request(dialogProps.data.id)
|
||||
} else if (dialogProps.type === 'EDIT' && dialogProps.credentialId) {
|
||||
// When credential dialog is opened from node in canvas
|
||||
getSpecificCredentialApi.request(dialogProps.credentialId)
|
||||
} else if (dialogProps.type === 'ADD' && dialogProps.credentialComponent) {
|
||||
// When credential dialog is to add a new credential
|
||||
setName('')
|
||||
setCredential({})
|
||||
setCredentialData({})
|
||||
setComponentCredential(dialogProps.credentialComponent)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const addNewCredential = async () => {
|
||||
try {
|
||||
const obj = {
|
||||
name,
|
||||
credentialName: componentCredential.name,
|
||||
plainDataObj: credentialData
|
||||
}
|
||||
const createResp = await credentialsApi.createCredential(obj)
|
||||
if (createResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'New Credential added',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onConfirm(createResp.data.id)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to add new Credential: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
const saveCredential = async () => {
|
||||
try {
|
||||
const saveResp = await credentialsApi.updateCredential(credential.id, {
|
||||
name,
|
||||
credentialName: componentCredential.name,
|
||||
plainDataObj: credentialData
|
||||
})
|
||||
if (saveResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Credential saved',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onConfirm(saveResp.data.id)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to save Credential: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth='sm'
|
||||
open={show}
|
||||
onClose={onCancel}
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
{componentCredential && componentCredential.label && (
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
marginRight: 10,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 7,
|
||||
borderRadius: '50%',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
alt={componentCredential.name}
|
||||
src={`${baseURL}/api/v1/components-credentials-icon/${componentCredential.name}`}
|
||||
/>
|
||||
</div>
|
||||
{componentCredential.label}
|
||||
</div>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{componentCredential && componentCredential.description && (
|
||||
<Box sx={{ pl: 2, pr: 2 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
borderRadius: 10,
|
||||
background: 'rgb(254,252,191)',
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'rgb(116,66,16)' }}>{parser(componentCredential.description)}</span>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
{componentCredential && componentCredential.label && (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Stack sx={{ position: 'relative' }} direction='row'>
|
||||
<Typography variant='overline'>
|
||||
Credential Name
|
||||
<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
</Stack>
|
||||
<OutlinedInput
|
||||
id='credName'
|
||||
type='string'
|
||||
fullWidth
|
||||
placeholder={componentCredential.label}
|
||||
value={name}
|
||||
name='name'
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{componentCredential &&
|
||||
componentCredential.inputs &&
|
||||
componentCredential.inputs.map((inputParam, index) => (
|
||||
<CredentialInputHandler key={index} inputParam={inputParam} data={credentialData} />
|
||||
))}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<StyledButton
|
||||
disabled={!name}
|
||||
variant='contained'
|
||||
onClick={() => (dialogProps.type === 'ADD' ? addNewCredential() : saveCredential())}
|
||||
>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
<ConfirmDialog />
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
AddEditCredentialDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default AddEditCredentialDialog
|
||||
@@ -0,0 +1,137 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import { Box, Typography, IconButton } from '@mui/material'
|
||||
import { IconArrowsMaximize, IconAlertTriangle } from '@tabler/icons'
|
||||
|
||||
// project import
|
||||
import { Dropdown } from 'ui-component/dropdown/Dropdown'
|
||||
import { Input } from 'ui-component/input/Input'
|
||||
import { SwitchInput } from 'ui-component/switch/Switch'
|
||||
import { JsonEditorInput } from 'ui-component/json/JsonEditor'
|
||||
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
|
||||
|
||||
// ===========================|| NodeInputHandler ||=========================== //
|
||||
|
||||
const CredentialInputHandler = ({ inputParam, data, disabled = false }) => {
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const ref = useRef(null)
|
||||
|
||||
const [showExpandDialog, setShowExpandDialog] = useState(false)
|
||||
const [expandDialogProps, setExpandDialogProps] = useState({})
|
||||
|
||||
const onExpandDialogClicked = (value, inputParam) => {
|
||||
const dialogProp = {
|
||||
value,
|
||||
inputParam,
|
||||
disabled,
|
||||
confirmButtonName: 'Save',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
setExpandDialogProps(dialogProp)
|
||||
setShowExpandDialog(true)
|
||||
}
|
||||
|
||||
const onExpandDialogSave = (newValue, inputParamName) => {
|
||||
setShowExpandDialog(false)
|
||||
data[inputParamName] = newValue
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{inputParam && (
|
||||
<>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
{inputParam.label}
|
||||
{!inputParam.optional && <span style={{ color: 'red' }}> *</span>}
|
||||
{inputParam.description && <TooltipWithParser style={{ marginLeft: 10 }} title={inputParam.description} />}
|
||||
</Typography>
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
{inputParam.type === 'string' && inputParam.rows && (
|
||||
<IconButton
|
||||
size='small'
|
||||
sx={{
|
||||
height: 25,
|
||||
width: 25
|
||||
}}
|
||||
title='Expand'
|
||||
color='primary'
|
||||
onClick={() => onExpandDialogClicked(data[inputParam.name] ?? inputParam.default ?? '', inputParam)}
|
||||
>
|
||||
<IconArrowsMaximize />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
{inputParam.warning && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
borderRadius: 10,
|
||||
background: 'rgb(254,252,191)',
|
||||
padding: 10,
|
||||
marginTop: 10,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<IconAlertTriangle size={36} color='orange' />
|
||||
<span style={{ color: 'rgb(116,66,16)', marginLeft: 10 }}>{inputParam.warning}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inputParam.type === 'boolean' && (
|
||||
<SwitchInput
|
||||
disabled={disabled}
|
||||
onChange={(newValue) => (data[inputParam.name] = newValue)}
|
||||
value={data[inputParam.name] ?? inputParam.default ?? false}
|
||||
/>
|
||||
)}
|
||||
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
|
||||
<Input
|
||||
key={data[inputParam.name]}
|
||||
disabled={disabled}
|
||||
inputParam={inputParam}
|
||||
onChange={(newValue) => (data[inputParam.name] = newValue)}
|
||||
value={data[inputParam.name] ?? inputParam.default ?? ''}
|
||||
showDialog={showExpandDialog}
|
||||
dialogProps={expandDialogProps}
|
||||
onDialogCancel={() => setShowExpandDialog(false)}
|
||||
onDialogConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)}
|
||||
/>
|
||||
)}
|
||||
{inputParam.type === 'json' && (
|
||||
<JsonEditorInput
|
||||
disabled={disabled}
|
||||
onChange={(newValue) => (data[inputParam.name] = newValue)}
|
||||
value={data[inputParam.name] ?? inputParam.default ?? ''}
|
||||
isDarkMode={customization.isDarkMode}
|
||||
/>
|
||||
)}
|
||||
{inputParam.type === 'options' && (
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
name={inputParam.name}
|
||||
options={inputParam.options}
|
||||
onSelect={(newValue) => (data[inputParam.name] = newValue)}
|
||||
value={data[inputParam.name] ?? inputParam.default ?? 'choose an option'}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
CredentialInputHandler.propTypes = {
|
||||
inputAnchor: PropTypes.object,
|
||||
inputParam: PropTypes.object,
|
||||
data: PropTypes.object,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CredentialInputHandler
|
||||
@@ -0,0 +1,179 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Box,
|
||||
OutlinedInput,
|
||||
InputAdornment
|
||||
} from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { IconSearch, IconX } from '@tabler/icons'
|
||||
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
|
||||
|
||||
const CredentialListDialog = ({ show, dialogProps, onCancel, onCredentialSelected }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const dispatch = useDispatch()
|
||||
const theme = useTheme()
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [componentsCredentials, setComponentsCredentials] = useState([])
|
||||
|
||||
const filterSearch = (value) => {
|
||||
setSearchValue(value)
|
||||
setTimeout(() => {
|
||||
if (value) {
|
||||
const searchData = dialogProps.componentsCredentials.filter((crd) => crd.name.toLowerCase().includes(value.toLowerCase()))
|
||||
setComponentsCredentials(searchData)
|
||||
} else if (value === '') {
|
||||
setComponentsCredentials(dialogProps.componentsCredentials)
|
||||
}
|
||||
// scrollTop()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.componentsCredentials) {
|
||||
setComponentsCredentials(dialogProps.componentsCredentials)
|
||||
}
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth='xs'
|
||||
open={show}
|
||||
onClose={onCancel}
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
{dialogProps.title}
|
||||
<Box sx={{ p: 2 }}>
|
||||
<OutlinedInput
|
||||
sx={{ width: '100%', pr: 2, pl: 2, my: 2 }}
|
||||
id='input-search-credential'
|
||||
value={searchValue}
|
||||
onChange={(e) => filterSearch(e.target.value)}
|
||||
placeholder='Search credential'
|
||||
startAdornment={
|
||||
<InputAdornment position='start'>
|
||||
<IconSearch stroke={1.5} size='1rem' color={theme.palette.grey[500]} />
|
||||
</InputAdornment>
|
||||
}
|
||||
endAdornment={
|
||||
<InputAdornment
|
||||
position='end'
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
color: theme.palette.grey[500],
|
||||
'&:hover': {
|
||||
color: theme.palette.grey[900]
|
||||
}
|
||||
}}
|
||||
title='Clear Search'
|
||||
>
|
||||
<IconX
|
||||
stroke={1.5}
|
||||
size='1rem'
|
||||
onClick={() => filterSearch('')}
|
||||
style={{
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
</InputAdornment>
|
||||
}
|
||||
aria-describedby='search-helper-text'
|
||||
inputProps={{
|
||||
'aria-label': 'weight'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<List
|
||||
sx={{
|
||||
width: '100%',
|
||||
py: 0,
|
||||
borderRadius: '10px',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
maxWidth: 370
|
||||
},
|
||||
'& .MuiListItemSecondaryAction-root': {
|
||||
top: 22
|
||||
},
|
||||
'& .MuiDivider-root': {
|
||||
my: 0
|
||||
},
|
||||
'& .list-container': {
|
||||
pl: 7
|
||||
}
|
||||
}}
|
||||
>
|
||||
{[...componentsCredentials].map((componentCredential) => (
|
||||
<div key={componentCredential.name}>
|
||||
<ListItemButton
|
||||
onClick={() => onCredentialSelected(componentCredential)}
|
||||
sx={{ p: 0, borderRadius: `${customization.borderRadius}px` }}
|
||||
>
|
||||
<ListItem alignItems='center'>
|
||||
<ListItemAvatar>
|
||||
<div
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 7,
|
||||
borderRadius: '50%',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
alt={componentCredential.name}
|
||||
src={`${baseURL}/api/v1/components-credentials-icon/${componentCredential.name}`}
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
<ListItemText sx={{ ml: 1 }} primary={componentCredential.label} />
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
</div>
|
||||
))}
|
||||
</List>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
CredentialListDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onCredentialSelected: PropTypes.func
|
||||
}
|
||||
|
||||
export default CredentialListDialog
|
||||
@@ -0,0 +1,276 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||
import moment from 'moment'
|
||||
|
||||
// material-ui
|
||||
import { Button, Box, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
import CredentialListDialog from './CredentialListDialog'
|
||||
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
|
||||
import AddEditCredentialDialog from './AddEditCredentialDialog'
|
||||
|
||||
// API
|
||||
import credentialsApi from 'api/credentials'
|
||||
|
||||
// Hooks
|
||||
import useApi from 'hooks/useApi'
|
||||
import useConfirm from 'hooks/useConfirm'
|
||||
|
||||
// utils
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
|
||||
// Icons
|
||||
import { IconTrash, IconEdit, IconX, IconPlus } from '@tabler/icons'
|
||||
import CredentialEmptySVG from 'assets/images/credential_empty.svg'
|
||||
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
import { SET_COMPONENT_CREDENTIALS } from 'store/actions'
|
||||
|
||||
// ==============================|| Credentials ||============================== //
|
||||
|
||||
const Credentials = () => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const dispatch = useDispatch()
|
||||
useNotifier()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const [showCredentialListDialog, setShowCredentialListDialog] = useState(false)
|
||||
const [credentialListDialogProps, setCredentialListDialogProps] = useState({})
|
||||
const [showSpecificCredentialDialog, setShowSpecificCredentialDialog] = useState(false)
|
||||
const [specificCredentialDialogProps, setSpecificCredentialDialogProps] = useState({})
|
||||
const [credentials, setCredentials] = useState([])
|
||||
const [componentsCredentials, setComponentsCredentials] = useState([])
|
||||
|
||||
const { confirm } = useConfirm()
|
||||
|
||||
const getAllCredentialsApi = useApi(credentialsApi.getAllCredentials)
|
||||
const getAllComponentsCredentialsApi = useApi(credentialsApi.getAllComponentsCredentials)
|
||||
|
||||
const listCredential = () => {
|
||||
const dialogProp = {
|
||||
title: 'Add New Credential',
|
||||
componentsCredentials
|
||||
}
|
||||
setCredentialListDialogProps(dialogProp)
|
||||
setShowCredentialListDialog(true)
|
||||
}
|
||||
|
||||
const addNew = (credentialComponent) => {
|
||||
const dialogProp = {
|
||||
type: 'ADD',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Add',
|
||||
credentialComponent
|
||||
}
|
||||
setSpecificCredentialDialogProps(dialogProp)
|
||||
setShowSpecificCredentialDialog(true)
|
||||
}
|
||||
|
||||
const edit = (credential) => {
|
||||
const dialogProp = {
|
||||
type: 'EDIT',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Save',
|
||||
data: credential
|
||||
}
|
||||
setSpecificCredentialDialogProps(dialogProp)
|
||||
setShowSpecificCredentialDialog(true)
|
||||
}
|
||||
|
||||
const deleteCredential = async (credential) => {
|
||||
const confirmPayload = {
|
||||
title: `Delete`,
|
||||
description: `Delete credential ${credential.name}?`,
|
||||
confirmButtonName: 'Delete',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
const isConfirmed = await confirm(confirmPayload)
|
||||
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
const deleteResp = await credentialsApi.deleteCredential(credential.id)
|
||||
if (deleteResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Credential deleted',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onConfirm()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to delete Credential: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onCredentialSelected = (credentialComponent) => {
|
||||
setShowCredentialListDialog(false)
|
||||
addNew(credentialComponent)
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
setShowCredentialListDialog(false)
|
||||
setShowSpecificCredentialDialog(false)
|
||||
getAllCredentialsApi.request()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAllCredentialsApi.request()
|
||||
getAllComponentsCredentialsApi.request()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllCredentialsApi.data) {
|
||||
setCredentials(getAllCredentialsApi.data)
|
||||
}
|
||||
}, [getAllCredentialsApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllComponentsCredentialsApi.data) {
|
||||
setComponentsCredentials(getAllComponentsCredentialsApi.data)
|
||||
dispatch({ type: SET_COMPONENT_CREDENTIALS, componentsCredentials: getAllComponentsCredentialsApi.data })
|
||||
}
|
||||
}, [getAllComponentsCredentialsApi.data, dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
||||
<Stack flexDirection='row'>
|
||||
<h1>Credentials </h1>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
<StyledButton
|
||||
variant='contained'
|
||||
sx={{ color: 'white', mr: 1, height: 37 }}
|
||||
onClick={listCredential}
|
||||
startIcon={<IconPlus />}
|
||||
>
|
||||
Add Credential
|
||||
</StyledButton>
|
||||
</Stack>
|
||||
{credentials.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={CredentialEmptySVG}
|
||||
alt='CredentialEmptySVG'
|
||||
/>
|
||||
</Box>
|
||||
<div>No Credentials Yet</div>
|
||||
</Stack>
|
||||
)}
|
||||
{credentials.length > 0 && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Last Updated</TableCell>
|
||||
<TableCell>Created</TableCell>
|
||||
<TableCell> </TableCell>
|
||||
<TableCell> </TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{credentials.map((credential, index) => (
|
||||
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
<TableCell component='th' scope='row'>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginRight: 10,
|
||||
borderRadius: '50%'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
alt={credential.credentialName}
|
||||
src={`${baseURL}/api/v1/components-credentials-icon/${credential.credentialName}`}
|
||||
/>
|
||||
</div>
|
||||
{credential.name}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{moment(credential.updatedDate).format('DD-MMM-YY')}</TableCell>
|
||||
<TableCell>{moment(credential.createdDate).format('DD-MMM-YY')}</TableCell>
|
||||
<TableCell>
|
||||
<IconButton title='Edit' color='primary' onClick={() => edit(credential)}>
|
||||
<IconEdit />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton title='Delete' color='error' onClick={() => deleteCredential(credential)}>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</MainCard>
|
||||
<CredentialListDialog
|
||||
show={showCredentialListDialog}
|
||||
dialogProps={credentialListDialogProps}
|
||||
onCancel={() => setShowCredentialListDialog(false)}
|
||||
onCredentialSelected={onCredentialSelected}
|
||||
></CredentialListDialog>
|
||||
<AddEditCredentialDialog
|
||||
show={showSpecificCredentialDialog}
|
||||
dialogProps={specificCredentialDialogProps}
|
||||
onCancel={() => setShowSpecificCredentialDialog(false)}
|
||||
onConfirm={onConfirm}
|
||||
></AddEditCredentialDialog>
|
||||
<ConfirmDialog />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Credentials
|
||||
@@ -1,16 +1,19 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// material-ui
|
||||
import { Grid, Box, Stack } from '@mui/material'
|
||||
import { Grid, Box, Stack, Tabs, Tab } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { IconHierarchy, IconTool } from '@tabler/icons'
|
||||
|
||||
// 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 ToolDialog from 'views/tools/ToolDialog'
|
||||
|
||||
// API
|
||||
import marketplacesApi from 'api/marketplaces'
|
||||
@@ -21,6 +24,27 @@ import useApi from 'hooks/useApi'
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
function TabPanel(props) {
|
||||
const { children, value, index, ...other } = props
|
||||
return (
|
||||
<div
|
||||
role='tabpanel'
|
||||
hidden={value !== index}
|
||||
id={`attachment-tabpanel-${index}`}
|
||||
aria-labelledby={`attachment-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && <Box sx={{ p: 1 }}>{children}</Box>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
TabPanel.propTypes = {
|
||||
children: PropTypes.node,
|
||||
index: PropTypes.number.isRequired,
|
||||
value: PropTypes.number.isRequired
|
||||
}
|
||||
|
||||
// ==============================|| Marketplace ||============================== //
|
||||
|
||||
const Marketplace = () => {
|
||||
@@ -29,29 +53,66 @@ const Marketplace = () => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [isChatflowsLoading, setChatflowsLoading] = useState(true)
|
||||
const [isToolsLoading, setToolsLoading] = useState(true)
|
||||
const [images, setImages] = useState({})
|
||||
const tabItems = ['Chatflows', 'Tools']
|
||||
const [value, setValue] = useState(0)
|
||||
const [showToolDialog, setShowToolDialog] = useState(false)
|
||||
const [toolDialogProps, setToolDialogProps] = useState({})
|
||||
|
||||
const getAllMarketplacesApi = useApi(marketplacesApi.getAllMarketplaces)
|
||||
const getAllChatflowsMarketplacesApi = useApi(marketplacesApi.getAllChatflowsMarketplaces)
|
||||
const getAllToolsMarketplacesApi = useApi(marketplacesApi.getAllToolsMarketplaces)
|
||||
|
||||
const onUseTemplate = (selectedTool) => {
|
||||
const dialogProp = {
|
||||
title: 'Add New Tool',
|
||||
type: 'IMPORT',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Add',
|
||||
data: selectedTool
|
||||
}
|
||||
setToolDialogProps(dialogProp)
|
||||
setShowToolDialog(true)
|
||||
}
|
||||
|
||||
const goToTool = (selectedTool) => {
|
||||
const dialogProp = {
|
||||
title: selectedTool.templateName,
|
||||
type: 'TEMPLATE',
|
||||
data: selectedTool
|
||||
}
|
||||
setToolDialogProps(dialogProp)
|
||||
setShowToolDialog(true)
|
||||
}
|
||||
|
||||
const goToCanvas = (selectedChatflow) => {
|
||||
navigate(`/marketplace/${selectedChatflow.id}`, { state: selectedChatflow })
|
||||
}
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAllMarketplacesApi.request()
|
||||
getAllChatflowsMarketplacesApi.request()
|
||||
getAllToolsMarketplacesApi.request()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(getAllMarketplacesApi.loading)
|
||||
}, [getAllMarketplacesApi.loading])
|
||||
setChatflowsLoading(getAllChatflowsMarketplacesApi.loading)
|
||||
}, [getAllChatflowsMarketplacesApi.loading])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllMarketplacesApi.data) {
|
||||
setToolsLoading(getAllToolsMarketplacesApi.loading)
|
||||
}, [getAllToolsMarketplacesApi.loading])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllChatflowsMarketplacesApi.data) {
|
||||
try {
|
||||
const chatflows = getAllMarketplacesApi.data
|
||||
const chatflows = getAllChatflowsMarketplacesApi.data
|
||||
const images = {}
|
||||
for (let i = 0; i < chatflows.length; i += 1) {
|
||||
const flowDataStr = chatflows[i].flowData
|
||||
@@ -70,31 +131,83 @@ const Marketplace = () => {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}, [getAllMarketplacesApi.data])
|
||||
}, [getAllChatflowsMarketplacesApi.data])
|
||||
|
||||
return (
|
||||
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
||||
<Stack flexDirection='row'>
|
||||
<h1>Marketplace</h1>
|
||||
</Stack>
|
||||
<Grid container spacing={gridSpacing}>
|
||||
{!isLoading &&
|
||||
getAllMarketplacesApi.data &&
|
||||
getAllMarketplacesApi.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 && (!getAllMarketplacesApi.data || getAllMarketplacesApi.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 Marketplace Yet</div>
|
||||
<>
|
||||
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
||||
<Stack flexDirection='row'>
|
||||
<h1>Marketplace</h1>
|
||||
</Stack>
|
||||
)}
|
||||
</MainCard>
|
||||
<Tabs sx={{ mb: 2 }} variant='fullWidth' value={value} onChange={handleChange} aria-label='tabs'>
|
||||
{tabItems.map((item, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
icon={index === 0 ? <IconHierarchy /> : <IconTool />}
|
||||
iconPosition='start'
|
||||
label={<span style={{ fontSize: '1.1rem' }}>{item}</span>}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
{tabItems.map((item, index) => (
|
||||
<TabPanel key={index} value={value} index={index}>
|
||||
{item === 'Chatflows' && (
|
||||
<Grid container spacing={gridSpacing}>
|
||||
{!isChatflowsLoading &&
|
||||
getAllChatflowsMarketplacesApi.data &&
|
||||
getAllChatflowsMarketplacesApi.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>
|
||||
)}
|
||||
{item === 'Tools' && (
|
||||
<Grid container spacing={gridSpacing}>
|
||||
{!isToolsLoading &&
|
||||
getAllToolsMarketplacesApi.data &&
|
||||
getAllToolsMarketplacesApi.data.map((data, index) => (
|
||||
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
|
||||
<ItemCard data={data} onClick={() => goToTool(data)} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</TabPanel>
|
||||
))}
|
||||
{!isChatflowsLoading && (!getAllChatflowsMarketplacesApi.data || getAllChatflowsMarketplacesApi.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 Marketplace Yet</div>
|
||||
</Stack>
|
||||
)}
|
||||
{!isToolsLoading && (!getAllToolsMarketplacesApi.data || getAllToolsMarketplacesApi.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 Marketplace Yet</div>
|
||||
</Stack>
|
||||
)}
|
||||
</MainCard>
|
||||
<ToolDialog
|
||||
show={showToolDialog}
|
||||
dialogProps={toolDialogProps}
|
||||
onCancel={() => setShowToolDialog(false)}
|
||||
onConfirm={() => setShowToolDialog(false)}
|
||||
onUseTemplate={(tool) => onUseTemplate(tool)}
|
||||
></ToolDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,571 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
import { Box, Typography, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, OutlinedInput } from '@mui/material'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
import { Grid } from 'ui-component/grid/Grid'
|
||||
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
|
||||
import { GridActionsCellItem } from '@mui/x-data-grid'
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
|
||||
import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor'
|
||||
import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Icons
|
||||
import { IconX, IconFileExport } from '@tabler/icons'
|
||||
|
||||
// API
|
||||
import toolsApi from 'api/tools'
|
||||
|
||||
// Hooks
|
||||
import useConfirm from 'hooks/useConfirm'
|
||||
import useApi from 'hooks/useApi'
|
||||
|
||||
// utils
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
import { generateRandomGradient } from 'utils/genericHelper'
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
|
||||
|
||||
const exampleAPIFunc = `/*
|
||||
* You can use any libraries imported in Flowise
|
||||
* You can use properties specified in Output Schema as variables. Ex: Property = userid, Variable = $userid
|
||||
* Must return a string value at the end of function
|
||||
*/
|
||||
|
||||
const fetch = require('node-fetch');
|
||||
const url = 'https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t_weather=true';
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const text = await response.text();
|
||||
return text;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return '';
|
||||
}`
|
||||
|
||||
const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const theme = useTheme()
|
||||
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// ==============================|| Snackbar ||============================== //
|
||||
|
||||
useNotifier()
|
||||
const { confirm } = useConfirm()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const getSpecificToolApi = useApi(toolsApi.getSpecificTool)
|
||||
|
||||
const [toolId, setToolId] = useState('')
|
||||
const [toolName, setToolName] = useState('')
|
||||
const [toolDesc, setToolDesc] = useState('')
|
||||
const [toolIcon, setToolIcon] = useState('')
|
||||
const [toolSchema, setToolSchema] = useState([])
|
||||
const [toolFunc, setToolFunc] = useState('')
|
||||
|
||||
const deleteItem = useCallback(
|
||||
(id) => () => {
|
||||
setTimeout(() => {
|
||||
setToolSchema((prevRows) => prevRows.filter((row) => row.id !== id))
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const addNewRow = () => {
|
||||
setTimeout(() => {
|
||||
setToolSchema((prevRows) => {
|
||||
let allRows = [...cloneDeep(prevRows)]
|
||||
const lastRowId = allRows.length ? allRows[allRows.length - 1].id + 1 : 1
|
||||
allRows.push({
|
||||
id: lastRowId,
|
||||
property: '',
|
||||
description: '',
|
||||
type: '',
|
||||
required: false
|
||||
})
|
||||
return allRows
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const onRowUpdate = (newRow) => {
|
||||
setTimeout(() => {
|
||||
setToolSchema((prevRows) => {
|
||||
let allRows = [...cloneDeep(prevRows)]
|
||||
const indexToUpdate = allRows.findIndex((row) => row.id === newRow.id)
|
||||
if (indexToUpdate >= 0) {
|
||||
allRows[indexToUpdate] = { ...newRow }
|
||||
}
|
||||
return allRows
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{ field: 'property', headerName: 'Property', editable: true, flex: 1 },
|
||||
{
|
||||
field: 'type',
|
||||
headerName: 'Type',
|
||||
type: 'singleSelect',
|
||||
valueOptions: ['string', 'number', 'boolean', 'date'],
|
||||
editable: true,
|
||||
width: 120
|
||||
},
|
||||
{ field: 'description', headerName: 'Description', editable: true, flex: 1 },
|
||||
{ field: 'required', headerName: 'Required', type: 'boolean', editable: true, width: 80 },
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
width: 80,
|
||||
getActions: (params) => [
|
||||
<GridActionsCellItem key={'Delete'} icon={<DeleteIcon />} label='Delete' onClick={deleteItem(params.id)} />
|
||||
]
|
||||
}
|
||||
],
|
||||
[deleteItem]
|
||||
)
|
||||
|
||||
const formatSchema = (schema) => {
|
||||
try {
|
||||
const parsedSchema = JSON.parse(schema)
|
||||
return parsedSchema.map((sch, index) => {
|
||||
return {
|
||||
...sch,
|
||||
id: index
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
if (getSpecificToolApi.data) {
|
||||
setToolId(getSpecificToolApi.data.id)
|
||||
setToolName(getSpecificToolApi.data.name)
|
||||
setToolDesc(getSpecificToolApi.data.description)
|
||||
setToolSchema(formatSchema(getSpecificToolApi.data.schema))
|
||||
if (getSpecificToolApi.data.func) setToolFunc(getSpecificToolApi.data.func)
|
||||
else setToolFunc('')
|
||||
}
|
||||
}, [getSpecificToolApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.type === 'EDIT' && dialogProps.data) {
|
||||
// When tool dialog is opened from Tools dashboard
|
||||
setToolId(dialogProps.data.id)
|
||||
setToolName(dialogProps.data.name)
|
||||
setToolDesc(dialogProps.data.description)
|
||||
setToolIcon(dialogProps.data.iconSrc)
|
||||
setToolSchema(formatSchema(dialogProps.data.schema))
|
||||
if (dialogProps.data.func) setToolFunc(dialogProps.data.func)
|
||||
else setToolFunc('')
|
||||
} else if (dialogProps.type === 'EDIT' && dialogProps.toolId) {
|
||||
// When tool dialog is opened from CustomTool node in canvas
|
||||
getSpecificToolApi.request(dialogProps.toolId)
|
||||
} else if (dialogProps.type === 'IMPORT' && dialogProps.data) {
|
||||
// When tool dialog is to import existing tool
|
||||
setToolName(dialogProps.data.name)
|
||||
setToolDesc(dialogProps.data.description)
|
||||
setToolIcon(dialogProps.data.iconSrc)
|
||||
setToolSchema(formatSchema(dialogProps.data.schema))
|
||||
if (dialogProps.data.func) setToolFunc(dialogProps.data.func)
|
||||
else setToolFunc('')
|
||||
} else if (dialogProps.type === 'TEMPLATE' && dialogProps.data) {
|
||||
// When tool dialog is a template
|
||||
setToolName(dialogProps.data.name)
|
||||
setToolDesc(dialogProps.data.description)
|
||||
setToolIcon(dialogProps.data.iconSrc)
|
||||
setToolSchema(formatSchema(dialogProps.data.schema))
|
||||
if (dialogProps.data.func) setToolFunc(dialogProps.data.func)
|
||||
else setToolFunc('')
|
||||
} else if (dialogProps.type === 'ADD') {
|
||||
// When tool dialog is to add a new tool
|
||||
setToolId('')
|
||||
setToolName('')
|
||||
setToolDesc('')
|
||||
setToolIcon('')
|
||||
setToolSchema([])
|
||||
setToolFunc('')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dialogProps])
|
||||
|
||||
const useToolTemplate = () => {
|
||||
onUseTemplate(dialogProps.data)
|
||||
}
|
||||
|
||||
const exportTool = async () => {
|
||||
try {
|
||||
const toolResp = await toolsApi.getSpecificTool(toolId)
|
||||
if (toolResp.data) {
|
||||
const toolData = toolResp.data
|
||||
delete toolData.id
|
||||
delete toolData.createdDate
|
||||
delete toolData.updatedDate
|
||||
let dataStr = JSON.stringify(toolData)
|
||||
let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
|
||||
|
||||
let exportFileDefaultName = `${toolName}-CustomTool.json`
|
||||
|
||||
let linkElement = document.createElement('a')
|
||||
linkElement.setAttribute('href', dataUri)
|
||||
linkElement.setAttribute('download', exportFileDefaultName)
|
||||
linkElement.click()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to export Tool: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
const addNewTool = async () => {
|
||||
try {
|
||||
const obj = {
|
||||
name: toolName,
|
||||
description: toolDesc,
|
||||
color: generateRandomGradient(),
|
||||
schema: JSON.stringify(toolSchema),
|
||||
func: toolFunc,
|
||||
iconSrc: toolIcon
|
||||
}
|
||||
const createResp = await toolsApi.createNewTool(obj)
|
||||
if (createResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'New Tool added',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onConfirm(createResp.data.id)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to add new Tool: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
const saveTool = async () => {
|
||||
try {
|
||||
const saveResp = await toolsApi.updateTool(toolId, {
|
||||
name: toolName,
|
||||
description: toolDesc,
|
||||
schema: JSON.stringify(toolSchema),
|
||||
func: toolFunc,
|
||||
iconSrc: toolIcon
|
||||
})
|
||||
if (saveResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Tool saved',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onConfirm(saveResp.data.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to save Tool: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
const deleteTool = async () => {
|
||||
const confirmPayload = {
|
||||
title: `Delete Tool`,
|
||||
description: `Delete tool ${toolName}?`,
|
||||
confirmButtonName: 'Delete',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
const isConfirmed = await confirm(confirmPayload)
|
||||
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
const delResp = await toolsApi.deleteTool(toolId)
|
||||
if (delResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Tool deleted',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onConfirm()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to delete Tool: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth='md'
|
||||
open={show}
|
||||
onClose={onCancel}
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
{dialogProps.title}
|
||||
<div style={{ flex: 1 }} />
|
||||
{dialogProps.type === 'EDIT' && (
|
||||
<Button variant='outlined' onClick={() => exportTool()} startIcon={<IconFileExport />}>
|
||||
Export
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Stack sx={{ position: 'relative' }} direction='row'>
|
||||
<Typography variant='overline'>
|
||||
Tool Name
|
||||
<span style={{ color: 'red' }}> *</span>
|
||||
<TooltipWithParser
|
||||
style={{ marginLeft: 10 }}
|
||||
title={'Tool name must be small capital letter with underscore. Ex: my_tool'}
|
||||
/>
|
||||
</Typography>
|
||||
</Stack>
|
||||
<OutlinedInput
|
||||
id='toolName'
|
||||
type='string'
|
||||
fullWidth
|
||||
disabled={dialogProps.type === 'TEMPLATE'}
|
||||
placeholder='My New Tool'
|
||||
value={toolName}
|
||||
name='toolName'
|
||||
onChange={(e) => setToolName(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Stack sx={{ position: 'relative' }} direction='row'>
|
||||
<Typography variant='overline'>
|
||||
Tool description
|
||||
<span style={{ color: 'red' }}> *</span>
|
||||
<TooltipWithParser
|
||||
style={{ marginLeft: 10 }}
|
||||
title={'Description of what the tool does. This is for ChatGPT to determine when to use this tool.'}
|
||||
/>
|
||||
</Typography>
|
||||
</Stack>
|
||||
<OutlinedInput
|
||||
id='toolDesc'
|
||||
type='string'
|
||||
fullWidth
|
||||
disabled={dialogProps.type === 'TEMPLATE'}
|
||||
placeholder='Description of what the tool does. This is for ChatGPT to determine when to use this tool.'
|
||||
multiline={true}
|
||||
rows={3}
|
||||
value={toolDesc}
|
||||
name='toolDesc'
|
||||
onChange={(e) => setToolDesc(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Stack sx={{ position: 'relative' }} direction='row'>
|
||||
<Typography variant='overline'>Tool Icon Src</Typography>
|
||||
</Stack>
|
||||
<OutlinedInput
|
||||
id='toolIcon'
|
||||
type='string'
|
||||
fullWidth
|
||||
disabled={dialogProps.type === 'TEMPLATE'}
|
||||
placeholder='https://raw.githubusercontent.com/gilbarbara/logos/main/logos/airtable.svg'
|
||||
value={toolIcon}
|
||||
name='toolIcon'
|
||||
onChange={(e) => setToolIcon(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Stack sx={{ position: 'relative' }} direction='row'>
|
||||
<Typography variant='overline'>
|
||||
Output Schema
|
||||
<TooltipWithParser style={{ marginLeft: 10 }} title={'What should be the output response in JSON format?'} />
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Grid
|
||||
columns={columns}
|
||||
rows={toolSchema}
|
||||
disabled={dialogProps.type === 'TEMPLATE'}
|
||||
addNewRow={addNewRow}
|
||||
onRowUpdate={onRowUpdate}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Stack sx={{ position: 'relative' }} direction='row'>
|
||||
<Typography variant='overline'>
|
||||
Javascript Function
|
||||
<TooltipWithParser
|
||||
style={{ marginLeft: 10 }}
|
||||
title='Function to execute when tool is being used. You can use properties specified in Output Schema as variables. For example, if the property is <code>userid</code>, you can use as <code>$userid</code>. Return value must be a string. You can also override the code from API by following this <a target="_blank" href="https://docs.flowiseai.com/tools/custom-tool#override-function-from-api">guide</a>'
|
||||
/>
|
||||
</Typography>
|
||||
</Stack>
|
||||
{dialogProps.type !== 'TEMPLATE' && (
|
||||
<Button style={{ marginBottom: 10 }} variant='outlined' onClick={() => setToolFunc(exampleAPIFunc)}>
|
||||
See Example
|
||||
</Button>
|
||||
)}
|
||||
{customization.isDarkMode ? (
|
||||
<DarkCodeEditor
|
||||
value={toolFunc}
|
||||
disabled={dialogProps.type === 'TEMPLATE'}
|
||||
onValueChange={(code) => setToolFunc(code)}
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 'calc(100vh - 220px)',
|
||||
width: '100%',
|
||||
borderRadius: 5
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<LightCodeEditor
|
||||
value={toolFunc}
|
||||
disabled={dialogProps.type === 'TEMPLATE'}
|
||||
onValueChange={(code) => setToolFunc(code)}
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 'calc(100vh - 220px)',
|
||||
width: '100%',
|
||||
border: `1px solid ${theme.palette.grey[300]}`,
|
||||
borderRadius: 5
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{dialogProps.type === 'EDIT' && (
|
||||
<StyledButton color='error' variant='contained' onClick={() => deleteTool()}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
)}
|
||||
{dialogProps.type === 'TEMPLATE' && (
|
||||
<StyledButton color='secondary' variant='contained' onClick={useToolTemplate}>
|
||||
Use Template
|
||||
</StyledButton>
|
||||
)}
|
||||
{dialogProps.type !== 'TEMPLATE' && (
|
||||
<StyledButton
|
||||
disabled={!(toolName && toolDesc)}
|
||||
variant='contained'
|
||||
onClick={() => (dialogProps.type === 'ADD' || dialogProps.type === 'IMPORT' ? addNewTool() : saveTool())}
|
||||
>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
)}
|
||||
</DialogActions>
|
||||
<ConfirmDialog />
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
ToolDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onUseTemplate: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default ToolDialog
|
||||
@@ -0,0 +1,155 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import { Grid, Box, Stack, Button } 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 ToolEmptySVG from 'assets/images/tools_empty.svg'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
import ToolDialog from './ToolDialog'
|
||||
|
||||
// API
|
||||
import toolsApi from 'api/tools'
|
||||
|
||||
// Hooks
|
||||
import useApi from 'hooks/useApi'
|
||||
|
||||
// icons
|
||||
import { IconPlus, IconFileImport } from '@tabler/icons'
|
||||
|
||||
// ==============================|| CHATFLOWS ||============================== //
|
||||
|
||||
const Tools = () => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const getAllToolsApi = useApi(toolsApi.getAllTools)
|
||||
|
||||
const [showDialog, setShowDialog] = useState(false)
|
||||
const [dialogProps, setDialogProps] = useState({})
|
||||
|
||||
const inputRef = useRef(null)
|
||||
|
||||
const onUploadFile = (file) => {
|
||||
try {
|
||||
const dialogProp = {
|
||||
title: 'Add New Tool',
|
||||
type: 'IMPORT',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Save',
|
||||
data: JSON.parse(file)
|
||||
}
|
||||
setDialogProps(dialogProp)
|
||||
setShowDialog(true)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
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 addNew = () => {
|
||||
const dialogProp = {
|
||||
title: 'Add New Tool',
|
||||
type: 'ADD',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Add'
|
||||
}
|
||||
setDialogProps(dialogProp)
|
||||
setShowDialog(true)
|
||||
}
|
||||
|
||||
const edit = (selectedTool) => {
|
||||
const dialogProp = {
|
||||
title: 'Edit Tool',
|
||||
type: 'EDIT',
|
||||
cancelButtonName: 'Cancel',
|
||||
confirmButtonName: 'Save',
|
||||
data: selectedTool
|
||||
}
|
||||
setDialogProps(dialogProp)
|
||||
setShowDialog(true)
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
setShowDialog(false)
|
||||
getAllToolsApi.request()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAllToolsApi.request()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
||||
<Stack flexDirection='row'>
|
||||
<h1>Tools</h1>
|
||||
<Grid sx={{ mb: 1.25 }} container direction='row'>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Grid item>
|
||||
<Button
|
||||
variant='outlined'
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() => inputRef.current.click()}
|
||||
startIcon={<IconFileImport />}
|
||||
>
|
||||
Load
|
||||
</Button>
|
||||
<input ref={inputRef} type='file' hidden accept='.json' onChange={(e) => handleFileUpload(e)} />
|
||||
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew} startIcon={<IconPlus />}>
|
||||
Create
|
||||
</StyledButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
<Grid container spacing={gridSpacing}>
|
||||
{!getAllToolsApi.loading &&
|
||||
getAllToolsApi.data &&
|
||||
getAllToolsApi.data.map((data, index) => (
|
||||
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
|
||||
<ItemCard data={data} onClick={() => edit(data)} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
{!getAllToolsApi.loading && (!getAllToolsApi.data || getAllToolsApi.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={ToolEmptySVG} alt='ToolEmptySVG' />
|
||||
</Box>
|
||||
<div>No Tools Created Yet</div>
|
||||
</Stack>
|
||||
)}
|
||||
</MainCard>
|
||||
<ToolDialog
|
||||
show={showDialog}
|
||||
dialogProps={dialogProps}
|
||||
onCancel={() => setShowDialog(false)}
|
||||
onConfirm={onConfirm}
|
||||
></ToolDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tools
|
||||
Reference in New Issue
Block a user