Merge branch 'FlowiseAI:main' into bug/ChatInput

This commit is contained in:
Vikram Segta
2023-07-06 10:33:05 +05:30
committed by GitHub
122 changed files with 4906 additions and 1302 deletions
+16
View File
@@ -0,0 +1,16 @@
module.exports = {
webpack: {
configure: {
module: {
rules: [
{
test: /\.m?js$/,
resolve: {
fullySpecified: false
}
}
]
}
}
}
}
+12 -7
View File
@@ -1,6 +1,6 @@
{
"name": "flowise-ui",
"version": "1.2.11",
"version": "1.2.13",
"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,10 +30,10 @@
"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",
@@ -46,11 +50,11 @@
"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": [
@@ -71,6 +75,7 @@
},
"devDependencies": {
"@babel/eslint-parser": "^7.15.8",
"@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",
+4 -4
View File
@@ -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" />
+3
View File
@@ -4,6 +4,8 @@ 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)
@@ -15,6 +17,7 @@ const getIsChatflowStreaming = (id) => client.get(`/chatflows-streaming/${id}`)
export default {
getAllChatflows,
getSpecificChatflow,
getSpecificChatflowFromPublicEndpoint,
createNewChatflow,
updateChatflow,
deleteChatflow,
+4 -2
View File
@@ -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
}
+19
View File
@@ -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: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

+10 -2
View File
@@ -1,8 +1,8 @@
// assets
import { IconHierarchy, IconBuildingStore, IconKey } from '@tabler/icons'
import { IconHierarchy, IconBuildingStore, IconKey, IconTool } from '@tabler/icons'
// constant
const icons = { IconHierarchy, IconBuildingStore, IconKey }
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool }
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
@@ -27,6 +27,14 @@ const dashboard = {
icon: icons.IconBuildingStore,
breadcrumbs: true
},
{
id: 'tools',
title: 'Tools',
type: 'item',
url: '/tools',
icon: icons.IconTool,
breadcrumbs: true
},
{
id: 'apikey',
title: 'API Keys',
+23
View File
@@ -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
+7
View File
@@ -13,6 +13,9 @@ const Marketplaces = Loadable(lazy(() => import('views/marketplaces')))
// apikey routing
const APIKey = Loadable(lazy(() => import('views/apikey')))
// apikey routing
const Tools = Loadable(lazy(() => import('views/tools')))
// ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = {
@@ -34,6 +37,10 @@ const MainRoutes = {
{
path: '/apikey',
element: <APIKey />
},
{
path: '/tools',
element: <Tools />
}
]
}
+2 -1
View File
@@ -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)
}
@@ -136,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
}
}
}
+35 -24
View File
@@ -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={{
@@ -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,105 @@
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, 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 './ExpandTextDialog.css'
const ExpandTextDialog = ({ 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)
useEffect(() => {
if (dialogProps.value) setInputValue(dialogProps.value)
if (dialogProps.inputParam) setInputParam(dialogProps.inputParam)
return () => {
setInputValue('')
setInputParam(null)
}
}, [dialogProps])
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,56 @@
import { createPortal } from 'react-dom'
import { useSelector } 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'
const FormatPromptValuesDialog = ({ show, dialogProps, onChange, onCancel }) => {
const portalElement = document.getElementById('portal')
const customization = useSelector((state) => state.customization)
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
@@ -3,7 +3,7 @@ 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 'react-json-view'
import ReactJson from 'flowise-react-json-view'
const SourceDocDialog = ({ show, dialogProps, onCancel }) => {
const portalElement = document.getElementById('portal')
@@ -0,0 +1,147 @@
import { useState, useEffect, Fragment } from 'react'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import axios from 'axios'
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
import { Popper, CircularProgress, TextField, Box, Typography } from '@mui/material'
import { styled } from '@mui/material/styles'
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,
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')
useEffect(() => {
setLoading(true)
;(async () => {
const fetchData = async () => {
let response = 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,
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
+43
View File
@@ -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
}
+4 -3
View File
@@ -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: 0.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>
)}
</>
)
+90 -25
View File
@@ -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,126 @@
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 { baseURL } from 'store/constant'
const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectAndReturnVal }) => {
const customization = useSelector((state) => state.customization)
const onSelectOutputResponseClick = (node, isUserQuestion = false) => {
let variablePath = isUserQuestion ? `question` : `${node.id}.data.instance`
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, 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>
{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
+49 -2
View File
@@ -39,7 +39,7 @@ 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']
for (let i = 0; i < incoming; i += 1) {
const newInput = {
@@ -285,7 +285,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)
@@ -334,3 +334,50 @@ export const throttle = (func, limit) => {
}
}
}
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
}
+5 -2
View File
@@ -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()
@@ -125,6 +127,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
useEffect(() => {
if (updateChatflowApi.data) {
setFlowName(updateChatflowApi.data.name)
dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data })
}
setEditingFlowName(false)
+136 -17
View File
@@ -5,19 +5,26 @@ 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 } 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 { getInputVariables } from 'utils/genericHelper'
const EDITABLE_TOOLS = ['selectedTool']
const CustomWidthTooltip = styled(({ className, ...props }) => <Tooltip {...props} classes={{ popper: className }} />)({
[`& .${tooltipClasses.tooltip}`]: {
@@ -36,6 +43,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 +57,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)
@@ -162,6 +227,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)}
@@ -173,12 +239,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
@@ -186,12 +273,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>
)
}
+1
View File
@@ -202,6 +202,7 @@ const Canvas = () => {
const newChatflowBody = {
name: chatflowName,
deployed: false,
isPublic: false,
flowData
}
createNewChatflowApi.request(newChatflowBody)
+109
View File
@@ -0,0 +1,109 @@
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 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 {
setChatbotTheme(JSON.parse(chatflowData.chatbotConfig))
} catch (e) {
console.error(e)
setChatbotTheme({})
}
}
}
}, [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} theme={{ chatWindow: chatbotTheme }} />
)}
<LoginDialog show={loginDialogOpen} dialogProps={loginDialogProps} onConfirm={onLoginClick} />
</>
) : null}
</>
)
}
export default ChatbotFull
@@ -9,6 +9,8 @@ import { CopyBlock, atomOneDark } from 'react-code-blocks'
// 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 +21,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'
@@ -119,77 +122,18 @@ const getConfigExamplesForCurl = (configData, bodyType) => {
return finalStr
}
const embedCode = (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 embedCodeCustomization = (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: {
button: {
backgroundColor: "#3B81F6",
right: 20,
bottom: 20,
size: "medium",
iconColor: "white",
customIconSrc: "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/svg/google-messages.svg",
},
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",
}
}
}
})
</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 [embedChatCheckboxVal, setEmbedChatCheckbox] = useState(false)
const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys)
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
@@ -203,10 +147,6 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => {
}
}
const onCheckBoxEmbedChatChanged = (newVal) => {
setEmbedChatCheckbox(newVal)
}
const onApiKeySelected = (keyValue) => {
if (keyValue === 'addnewkey') {
navigate('/apikey')
@@ -265,8 +205,6 @@ query({"question": "Hey, how are you?"}).then((response) => {
return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\
-X POST \\
-d '{"question": "Hey, how are you?"}'`
} else if (codeLang === 'Embed') {
return embedCode(dialogProps.chatflowid)
}
return ''
}
@@ -309,8 +247,6 @@ query({"question": "Hey, how are you?"}).then((response) => {
-X POST \\
-d '{"question": "Hey, how are you?"}' \\
-H "Authorization: Bearer ${selectedApiKey?.apiKey}"`
} else if (codeLang === 'Embed') {
return embedCode(dialogProps.chatflowid)
}
return ''
}
@@ -318,7 +254,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'
@@ -335,6 +271,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
}
@@ -593,93 +531,70 @@ 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.
<p>
You can also specify a&nbsp;
<a
rel='noreferrer'
target='_blank'
href='https://www.npmjs.com/package/flowise-embed?activeTab=versions'
>
version
</a>
:&nbsp;<code>{`https://cdn.jsdelivr.net/npm/flowise-embed@<version>/dist/web.js`}</code>
</p>
</span>
<div style={{ height: 10 }}></div>
<p>You cannot use API key while embedding/sharing chatbot.</p>
<p>
Please select <b>&quot;No Authorization&quot;</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 && (
<>
<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)
}
language={getLang(codeLang)}
showLineNumbers={false}
wrapLines
/>
</>
)}
{getIsChatflowStreamingApi.data?.isStreaming && (
<p>
Read&nbsp;
<a rel='noreferrer' target='_blank' href='https://docs.flowiseai.com/how-to-use#streaming'>
here
</a>
&nbsp;on how to stream response back to application
</p>
)}
</>
)}
{value === 0 && (
<CheckboxInput
label='Show Embed Chat Config'
value={embedChatCheckboxVal}
onChange={onCheckBoxEmbedChatChanged}
/>
)}
{value === 0 && embedChatCheckboxVal && (
<CopyBlock
theme={atomOneDark}
text={embedCodeCustomization(dialogProps.chatflowid)}
language={getLang('Embed')}
showLineNumbers={false}
wrapLines
/>
)}
{value !== 0 && getIsChatflowStreamingApi.data?.isStreaming && (
<p>
Read&nbsp;
<a rel='noreferrer' target='_blank' href='https://docs.flowiseai.com/how-to-use#streaming'>
here
</a>
&nbsp;on how to stream response back to application
</p>
)}
{codeLang === 'Share Chatbot' && !chatflowApiKeyId && <ShareChatbot />}
</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&nbsp;
<a
rel='noreferrer'
target='_blank'
href='https://www.npmjs.com/package/flowise-embed?activeTab=versions'
>
version
</a>
:&nbsp;<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,473 @@
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 { 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 = () => {
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 [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: {}
}
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
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
}
}
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')}
<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>
</>
)
}
export default ShareChatbot
@@ -163,7 +163,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
// 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)
}
+143 -30
View File
@@ -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>
</>
)
}
+564
View File
@@ -0,0 +1,564 @@
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'
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&current_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 (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' }}>&nbsp;*</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' }}>&nbsp;*</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.'
/>
</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
+155
View File
@@ -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