Merge branch 'main' into feature/LlamaIndex

# Conflicts:
#	packages/components/nodes/agents/ConversationalAgent/ConversationalAgent.ts
#	packages/components/nodes/agents/ConversationalRetrievalAgent/ConversationalRetrievalAgent.ts
#	packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts
#	packages/components/nodes/agents/OpenAIFunctionAgent/OpenAIFunctionAgent.ts
#	packages/components/nodes/chains/ConversationChain/ConversationChain.ts
#	packages/components/nodes/chains/ConversationalRetrievalQAChain/ConversationalRetrievalQAChain.ts
#	packages/components/nodes/memory/BufferMemory/BufferMemory.ts
#	packages/components/nodes/memory/BufferWindowMemory/BufferWindowMemory.ts
#	packages/components/nodes/memory/ConversationSummaryMemory/ConversationSummaryMemory.ts
#	packages/components/nodes/memory/DynamoDb/DynamoDb.ts
#	packages/components/nodes/memory/MongoDBMemory/MongoDBMemory.ts
#	packages/components/nodes/memory/MotorheadMemory/MotorheadMemory.ts
#	packages/components/nodes/memory/RedisBackedChatMemory/RedisBackedChatMemory.ts
#	packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts
#	packages/components/nodes/memory/ZepMemory/ZepMemory.ts
#	packages/components/src/utils.ts
#	packages/server/marketplaces/chatflows/Long Term Memory.json
#	packages/server/src/index.ts
#	packages/server/src/utils/index.ts
This commit is contained in:
Henry
2024-01-24 03:00:31 +00:00
468 changed files with 13562 additions and 3588 deletions
+9 -4
View File
@@ -1,6 +1,6 @@
{
"name": "flowise-ui",
"version": "1.4.1",
"version": "1.4.7",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://flowiseai.com",
"author": {
@@ -8,13 +8,20 @@
"email": "henryheng@flowiseai.com"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/view": "^6.22.3",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.0.3",
"@mui/material": "^5.11.12",
"@mui/lab": "^5.0.0-alpha.156",
"@mui/material": "^5.15.0",
"@mui/x-data-grid": "^6.8.0",
"@tabler/icons": "^1.39.1",
"@uiw/codemirror-theme-sublime": "^4.21.21",
"@uiw/codemirror-theme-vscode": "^4.21.21",
"@uiw/react-codemirror": "^4.21.21",
"clsx": "^1.1.1",
"flowise-embed": "*",
"flowise-embed-react": "*",
@@ -26,7 +33,6 @@
"lodash": "^4.17.21",
"moment": "^2.29.3",
"notistack": "^2.0.4",
"prismjs": "^1.28.0",
"prop-types": "^15.7.2",
"react": "^18.2.0",
"react-code-blocks": "^0.0.9-0",
@@ -39,7 +45,6 @@
"react-redux": "^8.0.5",
"react-router": "~6.3.0",
"react-router-dom": "~6.3.0",
"react-simple-code-editor": "^0.11.2",
"react-syntax-highlighter": "^15.5.0",
"reactflow": "^11.5.6",
"redux": "^4.0.5",
+4 -1
View File
@@ -4,7 +4,10 @@ const getAllNodes = () => client.get('/nodes')
const getSpecificNode = (name) => client.get(`/nodes/${name}`)
const executeCustomFunctionNode = (body) => client.post(`/node-custom-function`, body)
export default {
getAllNodes,
getSpecificNode
getSpecificNode,
executeCustomFunctionNode
}
+9
View File
@@ -0,0 +1,9 @@
import client from './client'
const getAvailablePrompts = (body) => client.post(`/prompts-list`, body)
const getPrompt = (body) => client.post(`/load-prompt`, body)
export default {
getAvailablePrompts,
getPrompt
}
+16
View File
@@ -0,0 +1,16 @@
import client from './client'
const getAllVariables = () => client.get('/variables')
const createVariable = (body) => client.post(`/variables`, body)
const updateVariable = (id, body) => client.put(`/variables/${id}`, body)
const deleteVariable = (id) => client.delete(`/variables/${id}`)
export default {
getAllVariables,
createVariable,
updateVariable,
deleteVariable
}
@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 13.2044C23.0012 13.3219 21.9869 13.2745 21 13.0618C19.7752 12.7978 18.5927 12.2794 17.5344 11.506L15.8458 10.2721C14.0933 8.99141 11.9531 8.63334 10 9.10241C9.30052 9.27041 8.62505 9.54449 8 9.92028M24 18.7958C23.0012 18.6783 21.9869 18.7258 21 18.9385C19.7752 19.2024 18.5927 19.7208 17.5344 20.4942L15.8458 21.7282C14.0933 23.0088 11.9531 23.3669 10 22.8978C9.30052 22.7298 8.62505 22.4557 8 22.08" stroke="#E91212" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 21.0001L3.58441 20.5618C6.8019 18.1487 11.2184 18.1213 14.4656 20.4942L16.1542 21.7282C19.3633 24.0733 23.8728 23.3248 26.1521 20.0686C27.862 17.6258 27.862 14.3745 26.1521 11.9317C23.8728 8.67548 19.3633 7.92693 16.1542 10.2721L14.4656 11.506C11.2184 13.879 6.8019 13.8515 3.58441 11.4384L3 11.0001" stroke="#1363BB" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M29 21L28.4156 20.5617C27.0917 19.5688 25.5649 18.9798 24 18.7957C23.0012 18.6782 21.9869 18.7257 21 18.9383M29 11L28.4156 11.4383C27.0917 12.4312 25.5649 13.0202 24 13.2043C23.0012 13.3218 21.9869 13.2743 21 13.0617M10 9.10229C9.30052 9.27029 8.62505 9.54437 8 9.92017C7.17438 10.4165 6.43672 11.0904 5.84789 11.9316C4.13794 14.3743 4.13794 17.6257 5.84789 20.0684C6.43672 20.9096 7.17438 21.5835 8 22.0798C8.62505 22.4556 9.30052 22.7297 10 22.8977" stroke="#E91212" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="15.9562" cy="16.0002" rx="4.3478" ry="4.34782" stroke="black" stroke-width="2"/>
<path d="M23.5651 23.6086C21.603 25.621 18.8688 26.8695 15.8445 26.8695C10.2386 26.8695 5.62933 22.5797 5.08691 17.0869" stroke="black" stroke-width="2" stroke-linecap="round"/>
<path d="M8.34766 8.23751C10.314 6.3155 13.0074 5.13037 15.9785 5.13037C21.6311 5.13037 26.2789 9.42024 26.8258 14.913" stroke="black" stroke-width="2" stroke-linecap="round"/>
<path d="M9.5217 9.47815C9.5217 10.7268 8.50948 11.739 7.26085 11.739C6.01222 11.739 5 10.7268 5 9.47815C5 8.22951 6.01222 7.21729 7.26085 7.21729C8.50948 7.21729 9.5217 8.22951 9.5217 9.47815Z" fill="black" stroke="black" stroke-width="2"/>
<path d="M28.0002 21.4347C28.0002 22.6833 26.988 23.6956 25.7394 23.6956C24.4907 23.6956 23.4785 22.6833 23.4785 21.4347C23.4785 20.186 24.4907 19.1738 25.7394 19.1738C26.988 19.1738 28.0002 20.186 28.0002 21.4347Z" fill="black" stroke="black" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

+10 -2
View File
@@ -1,8 +1,8 @@
// assets
import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot } from '@tabler/icons'
import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable } from '@tabler/icons'
// constant
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot }
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable }
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
@@ -51,6 +51,14 @@ const dashboard = {
icon: icons.IconLock,
breadcrumbs: true
},
{
id: 'variables',
title: 'Variables',
type: 'item',
url: '/variables',
icon: icons.IconVariable,
breadcrumbs: true
},
{
id: 'apikey',
title: 'API Keys',
+9 -2
View File
@@ -1,8 +1,8 @@
// assets
import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage } from '@tabler/icons'
import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff } from '@tabler/icons'
// constant
const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage }
const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff }
// ==============================|| SETTINGS MENU ITEMS ||============================== //
@@ -11,6 +11,13 @@ const settings = {
title: '',
type: 'group',
children: [
{
id: 'conversationStarters',
title: 'Starter Prompts',
type: 'item',
url: '',
icon: icons.IconPictureInPictureOff
},
{
id: 'viewMessages',
title: 'View Messages',
+7
View File
@@ -22,6 +22,9 @@ const Assistants = Loadable(lazy(() => import('views/assistants')))
// credentials routing
const Credentials = Loadable(lazy(() => import('views/credentials')))
// variables routing
const Variables = Loadable(lazy(() => import('views/variables')))
// ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = {
@@ -55,6 +58,10 @@ const MainRoutes = {
{
path: '/credentials',
element: <Credentials />
},
{
path: '/variables',
element: <Variables />
}
]
}
@@ -11,6 +11,7 @@ import FileCopyIcon from '@mui/icons-material/FileCopy'
import FileDownloadIcon from '@mui/icons-material/Downloading'
import FileDeleteIcon from '@mui/icons-material/Delete'
import FileCategoryIcon from '@mui/icons-material/Category'
import PictureInPictureAltIcon from '@mui/icons-material/PictureInPictureAlt'
import Button from '@mui/material/Button'
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
import { IconX } from '@tabler/icons'
@@ -22,12 +23,12 @@ import useConfirm from 'hooks/useConfirm'
import { uiBaseURL } from '../../store/constant'
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '../../store/actions'
import ConfirmDialog from '../dialog/ConfirmDialog'
import SaveChatflowDialog from '../dialog/SaveChatflowDialog'
import TagDialog from '../dialog/TagDialog'
import { generateExportFlowData } from '../../utils/genericHelper'
import useNotifier from '../../utils/useNotifier'
import StarterPromptsDialog from '../dialog/StarterPromptsDialog'
const StyledMenu = styled((props) => (
<Menu
@@ -79,6 +80,8 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
const [categoryDialogProps, setCategoryDialogProps] = useState({})
const [anchorEl, setAnchorEl] = useState(null)
const open = Boolean(anchorEl)
const [conversationStartersDialogOpen, setConversationStartersDialogOpen] = useState(false)
const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({})
const handleClick = (event) => {
setAnchorEl(event.currentTarget)
@@ -93,6 +96,20 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
setFlowDialogOpen(true)
}
const handleFlowStarterPrompts = () => {
setAnchorEl(null)
setConversationStartersDialogProps({
title: 'Starter Prompts - ' + chatflow.name,
chatflow: chatflow
})
setConversationStartersDialogOpen(true)
}
const saveFlowStarterPrompts = async () => {
setConversationStartersDialogOpen(false)
await updateFlowsApi.request()
}
const saveFlowRename = async (chatflowName) => {
const updateBody = {
name: chatflowName,
@@ -254,6 +271,10 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
Export
</MenuItem>
<Divider sx={{ my: 0.5 }} />
<MenuItem onClick={handleFlowStarterPrompts} disableRipple>
<PictureInPictureAltIcon />
Starter Prompts
</MenuItem>
<MenuItem onClick={handleFlowCategory} disableRipple>
<FileCategoryIcon />
Update Category
@@ -264,7 +285,6 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
Delete
</MenuItem>
</StyledMenu>
<ConfirmDialog />
<SaveChatflowDialog
show={flowDialogOpen}
dialogProps={{
@@ -281,6 +301,12 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
onClose={() => setCategoryDialogOpen(false)}
onSubmit={saveFlowCategory}
/>
<StarterPromptsDialog
show={conversationStartersDialogOpen}
dialogProps={conversationStartersDialogProps}
onConfirm={saveFlowStarterPrompts}
onCancel={() => setConversationStartersDialogOpen(false)}
/>
</div>
)
}
@@ -0,0 +1,21 @@
// material-ui
import { styled } from '@mui/material/styles'
// project imports
import MainCard from './MainCard'
const NodeCardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.main,
color: theme.darkTextPrimary,
border: 'solid 1px',
borderColor: theme.palette.primary[200] + 75,
width: '300px',
height: 'auto',
padding: '10px',
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
'&:hover': {
borderColor: theme.palette.primary.main
}
}))
export default NodeCardWrapper
@@ -0,0 +1,14 @@
.button-container {
position: absolute;
bottom: 0;
z-index: 1000;
display: flex;
overflow-x: auto;
-webkit-overflow-scrolling: touch; /* For momentum scroll on mobile devices */
scrollbar-width: none; /* For Firefox */
}
.button {
flex: 0 0 auto; /* Don't grow, don't shrink, base width on content */
margin: 5px; /* Adjust as needed for spacing between buttons */
}
@@ -0,0 +1,22 @@
import Box from '@mui/material/Box'
import PropTypes from 'prop-types'
import { Chip } from '@mui/material'
import './StarterPromptsCard.css'
const StarterPromptsCard = ({ isGrid, starterPrompts, onPromptClick }) => {
return (
<Box className={'button-container'} sx={{ maxWidth: isGrid ? 'inherit' : '400px', m: 1 }}>
{starterPrompts.map((sp, index) => (
<Chip label={sp.prompt} className={'button'} key={index} onClick={(e) => onPromptClick(sp.prompt, e)} />
))}
</Box>
)
}
StarterPromptsCard.propTypes = {
isGrid: PropTypes.bool,
starterPrompts: PropTypes.arrayOf(PropTypes.string),
onPromptClick: PropTypes.func
}
export default StarterPromptsCard
@@ -30,8 +30,8 @@ import { SwitchInput } from 'ui-component/switch/Switch'
import { Input } from 'ui-component/input/Input'
import { StyledButton } from 'ui-component/button/StyledButton'
import langsmithPNG from 'assets/images/langchain.png'
import langfusePNG from 'assets/images/langfuse.png'
import llmonitorPNG from 'assets/images/llmonitor.png'
import langfuseSVG from 'assets/images/langfuse.svg'
import llmonitorSVG from 'assets/images/lunary.svg'
// store
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
@@ -72,7 +72,7 @@ const analyticProviders = [
{
label: 'LangFuse',
name: 'langFuse',
icon: langfusePNG,
icon: langfuseSVG,
url: 'https://langfuse.com',
inputs: [
{
@@ -99,7 +99,7 @@ const analyticProviders = [
{
label: 'LLMonitor',
name: 'llmonitor',
icon: llmonitorPNG,
icon: llmonitorSVG,
url: 'https://llmonitor.com',
inputs: [
{
@@ -2,14 +2,24 @@ import { createPortal } from 'react-dom'
import { useState, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import PropTypes from 'prop-types'
import PerfectScrollbar from 'react-perfect-scrollbar'
// MUI
import { Button, Dialog, DialogActions, DialogContent, Typography } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import PerfectScrollbar from 'react-perfect-scrollbar'
import { LoadingButton } from '@mui/lab'
// Project Import
import { StyledButton } from 'ui-component/button/StyledButton'
import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor'
import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor'
import { CodeEditor } from 'ui-component/editor/CodeEditor'
// Store
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
// API
import nodesApi from 'api/nodes'
import useApi from 'hooks/useApi'
import './ExpandTextDialog.css'
const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
@@ -18,18 +28,30 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const theme = useTheme()
const dispatch = useDispatch()
const customization = useSelector((state) => state.customization)
const languageType = 'json'
const [inputValue, setInputValue] = useState('')
const [inputParam, setInputParam] = useState(null)
const [languageType, setLanguageType] = useState('json')
const [loading, setLoading] = useState(false)
const [codeExecutedResult, setCodeExecutedResult] = useState('')
const executeCustomFunctionNodeApi = useApi(nodesApi.executeCustomFunctionNode)
useEffect(() => {
if (dialogProps.value) setInputValue(dialogProps.value)
if (dialogProps.inputParam) setInputParam(dialogProps.inputParam)
if (dialogProps.inputParam) {
setInputParam(dialogProps.inputParam)
if (dialogProps.inputParam.type === 'code') {
setLanguageType('js')
}
}
return () => {
setInputValue('')
setLoading(false)
setInputParam(null)
setLanguageType('json')
setCodeExecutedResult('')
}
}, [dialogProps])
@@ -39,11 +61,35 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
useEffect(() => {
setLoading(executeCustomFunctionNodeApi.loading)
}, [executeCustomFunctionNodeApi.loading])
useEffect(() => {
if (executeCustomFunctionNodeApi.data) {
if (typeof executeCustomFunctionNodeApi.data === 'object') {
setCodeExecutedResult(JSON.stringify(executeCustomFunctionNodeApi.data, null, 2))
} else {
setCodeExecutedResult(executeCustomFunctionNodeApi.data)
}
}
}, [executeCustomFunctionNodeApi.data])
useEffect(() => {
if (executeCustomFunctionNodeApi.error) {
if (typeof executeCustomFunctionNodeApi.error === 'object' && executeCustomFunctionNodeApi.error?.response?.data) {
setCodeExecutedResult(executeCustomFunctionNodeApi.error?.response?.data)
} else if (typeof executeCustomFunctionNodeApi.error === 'string') {
setCodeExecutedResult(executeCustomFunctionNodeApi.error)
}
}
}, [executeCustomFunctionNodeApi.error])
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' && (
{inputParam && (inputParam.type === 'string' || inputParam.type === 'code') && (
<div style={{ flex: 70 }}>
<Typography sx={{ mb: 2, ml: 1 }} variant='h4'>
{inputParam.label}
@@ -54,42 +100,66 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
borderColor: theme.palette.grey['500'],
borderRadius: '12px',
height: '100%',
maxHeight: 'calc(100vh - 220px)',
maxHeight: languageType === 'js' ? 'calc(100vh - 250px)' : '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%'
}}
/>
)}
<CodeEditor
disabled={dialogProps.disabled}
value={inputValue}
height={languageType === 'js' ? 'calc(100vh - 250px)' : 'calc(100vh - 220px)'}
theme={customization.isDarkMode ? 'dark' : 'light'}
lang={languageType}
placeholder={inputParam.placeholder}
basicSetup={
languageType === 'json'
? { lineNumbers: false, foldGutter: false, autocompletion: false, highlightActiveLine: false }
: {}
}
onValueChange={(code) => setInputValue(code)}
/>
</PerfectScrollbar>
</div>
)}
</div>
{languageType === 'js' && (
<LoadingButton
sx={{
mt: 2,
'&:hover': {
backgroundColor: theme.palette.secondary.main,
backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)`
},
'&:disabled': {
backgroundColor: theme.palette.secondary.main,
backgroundImage: `linear-gradient(rgb(0 0 0/50%) 0 0)`
}
}}
loading={loading}
variant='contained'
fullWidth
color='secondary'
onClick={() => {
setLoading(true)
executeCustomFunctionNodeApi.request({ javascriptFunction: inputValue })
}}
>
Execute
</LoadingButton>
)}
{codeExecutedResult && (
<div style={{ marginTop: '15px' }}>
<CodeEditor
disabled={true}
value={codeExecutedResult}
height='max-content'
theme={customization.isDarkMode ? 'dark' : 'light'}
lang={'js'}
basicSetup={{ lineNumbers: false, foldGutter: false, autocompletion: false, highlightActiveLine: false }}
/>
</div>
)}
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
@@ -0,0 +1,600 @@
import { createPortal } from 'react-dom'
import { useState, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import rehypeMathjax from 'rehype-mathjax'
import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
// MUI
import {
Box,
Button,
Card,
CardContent,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Chip,
Grid,
InputLabel,
List,
ListItemButton,
ListItemText,
OutlinedInput,
Select,
Typography,
Stack,
IconButton,
FormControl,
Checkbox,
MenuItem
} from '@mui/material'
import MuiAccordion from '@mui/material/Accordion'
import MuiAccordionSummary from '@mui/material/AccordionSummary'
import MuiAccordionDetails from '@mui/material/AccordionDetails'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'
import ClearIcon from '@mui/icons-material/Clear'
import { styled } from '@mui/material/styles'
//Project Import
import { StyledButton } from 'ui-component/button/StyledButton'
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
import promptEmptySVG from 'assets/images/prompt_empty.svg'
import useApi from 'hooks/useApi'
import promptApi from 'api/prompt'
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
const NewLineToBr = ({ children = '' }) => {
return children.split('\n').reduce(function (arr, line) {
return arr.concat(line, <br />)
}, [])
}
const Accordion = styled((props) => <MuiAccordion disableGutters elevation={0} square {...props} />)(({ theme }) => ({
border: `1px solid ${theme.palette.divider}`,
'&:not(:last-child)': {
borderBottom: 0
},
'&:before': {
display: 'none'
}
}))
const AccordionSummary = styled((props) => (
<MuiAccordionSummary expandIcon={<ArrowForwardIosSharpIcon sx={{ fontSize: '0.9rem' }} />} {...props} />
))(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, .05)' : 'rgba(0, 0, 0, .03)',
flexDirection: 'row-reverse',
'& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': {
transform: 'rotate(180deg)'
},
'& .MuiAccordionSummary-content': {
marginLeft: theme.spacing(1)
}
}))
const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
padding: theme.spacing(2),
borderTop: '1px solid rgba(0, 0, 0, .125)'
}))
const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
const customization = useSelector((state) => state.customization)
const getAvailablePromptsApi = useApi(promptApi.getAvailablePrompts)
useEffect(() => {
if (show) {
dispatch({ type: SHOW_CANVAS_DIALOG })
} else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [show, dispatch])
useEffect(() => {
if (promptType && show) {
setLoading(true)
getAvailablePromptsApi.request({ tags: promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&' })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [promptType, show])
useEffect(() => {
if (getAvailablePromptsApi.data && getAvailablePromptsApi.data.repos) {
setAvailablePrompNameList(getAvailablePromptsApi.data.repos)
if (getAvailablePromptsApi.data.repos?.length) handleListItemClick(0, getAvailablePromptsApi.data.repos)
setLoading(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAvailablePromptsApi.data])
const ITEM_HEIGHT = 48
const ITEM_PADDING_TOP = 8
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250
}
}
}
const models = [
{ id: 101, name: 'anthropic:claude-instant-1' },
{ id: 102, name: 'anthropic:claude-instant-1.2' },
{ id: 103, name: 'anthropic:claude-2' },
{ id: 104, name: 'google:palm-2-chat-bison' },
{ id: 105, name: 'google:palm-2-codechat-bison' },
{ id: 106, name: 'google:palm-2-text-bison' },
{ id: 107, name: 'meta:llama-2-13b-chat' },
{ id: 108, name: 'meta:llama-2-70b-chat' },
{ id: 109, name: 'openai:gpt-3.5-turbo' },
{ id: 110, name: 'openai:gpt-4' },
{ id: 111, name: 'openai:text-davinci-003' }
]
const [modelName, setModelName] = useState([])
const usecases = [
{ id: 201, name: 'Agents' },
{ id: 202, name: 'Agent Stimulation' },
{ id: 203, name: 'Autonomous agents' },
{ id: 204, name: 'Classification' },
{ id: 205, name: 'Chatbots' },
{ id: 206, name: 'Code understanding' },
{ id: 207, name: 'Code writing' },
{ id: 208, name: 'Evaluation' },
{ id: 209, name: 'Extraction' },
{ id: 210, name: 'Interacting with APIs' },
{ id: 211, name: 'Multi-modal' },
{ id: 212, name: 'QA over documents' },
{ id: 213, name: 'Self-checking' },
{ id: 214, name: 'SQL' },
{ id: 215, name: 'Summarization' },
{ id: 216, name: 'Tagging' }
]
const [usecase, setUsecase] = useState([])
const languages = [
{ id: 301, name: 'Chinese' },
{ id: 302, name: 'English' },
{ id: 303, name: 'French' },
{ id: 304, name: 'German' },
{ id: 305, name: 'Russian' },
{ id: 306, name: 'Spanish' }
]
const [language, setLanguage] = useState([])
const [availablePrompNameList, setAvailablePrompNameList] = useState([])
const [selectedPrompt, setSelectedPrompt] = useState({})
const [accordionExpanded, setAccordionExpanded] = useState(['prompt'])
const [loading, setLoading] = useState(false)
const handleAccordionChange = (accordionName) => (event, isExpanded) => {
const accordians = [...accordionExpanded]
if (!isExpanded) setAccordionExpanded(accordians.filter((accr) => accr !== accordionName))
else {
accordians.push(accordionName)
setAccordionExpanded(accordians)
}
}
const handleListItemClick = async (index, overridePromptNameList = []) => {
const prompt = overridePromptNameList.length ? overridePromptNameList[index] : availablePrompNameList[index]
if (!prompt.detailed) {
const createResp = await promptApi.getPrompt({
promptName: prompt.full_name
})
if (createResp.data) {
prompt.detailed = createResp.data.templates
}
}
setSelectedPrompt(prompt)
}
const fetchPrompts = async () => {
let tags = promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&'
modelName.forEach((item) => {
tags += `tags=${item.name}&`
})
usecase.forEach((item) => {
tags += `tags=${item.name}&`
})
language.forEach((item) => {
tags += `tags=${item.name}&`
})
setLoading(true)
getAvailablePromptsApi.request({ tags: tags })
}
const removeDuplicates = (value) => {
let duplicateRemoved = []
value.forEach((item) => {
if (value.filter((o) => o.id === item.id).length === 1) {
duplicateRemoved.push(item)
}
})
return duplicateRemoved
}
const handleModelChange = (event) => {
const {
target: { value }
} = event
setModelName(removeDuplicates(value))
}
const handleUsecaseChange = (event) => {
const {
target: { value }
} = event
setUsecase(removeDuplicates(value))
}
const handleLanguageChange = (event) => {
const {
target: { value }
} = event
setLanguage(removeDuplicates(value))
}
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth={'lg'}
aria-labelledby='prompt-dialog-title'
aria-describedby='prompt-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='prompt-dialog-title'>
Langchain Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'})
</DialogTitle>
<DialogContent dividers sx={{ p: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'row', p: 2, pt: 1, alignItems: 'center' }}>
<FormControl sx={{ mr: 1, width: '30%' }}>
<InputLabel size='small' id='model-checkbox-label'>
Model
</InputLabel>
<Select
id='model-checkbox'
labelId='model-checkbox-label'
multiple
size='small'
value={modelName}
onChange={handleModelChange}
input={<OutlinedInput label='Model' />}
renderValue={(selected) => selected.map((x) => x.name).join(', ')}
endAdornment={
modelName.length ? (
<IconButton sx={{ mr: 2 }} onClick={() => setModelName([])}>
<ClearIcon style={{ width: 20, height: 20 }} />
</IconButton>
) : (
false
)
}
sx={{
'.MuiSvgIcon-root ': {
fill: customization.isDarkMode ? 'white !important' : ''
}
}}
MenuProps={MenuProps}
>
{models.map((variant) => (
<MenuItem key={variant.id} value={variant}>
<Checkbox id={variant.id} checked={modelName.findIndex((item) => item.id === variant.id) >= 0} />
<ListItemText primary={variant.name} />
</MenuItem>
))}
</Select>
</FormControl>
<FormControl sx={{ mr: 1, width: '30%' }}>
<InputLabel size='small' id='usecase-checkbox-label'>
Usecase
</InputLabel>
<Select
autoWidth={false}
labelId='usecase-checkbox-label'
id='usecase-checkbox'
multiple
size='small'
value={usecase}
onChange={handleUsecaseChange}
input={<OutlinedInput label='Usecase' />}
renderValue={(selected) => selected.map((x) => x.name).join(', ')}
endAdornment={
usecase.length ? (
<IconButton sx={{ mr: 2 }} onClick={() => setUsecase([])}>
<ClearIcon style={{ width: 20, height: 20 }} />
</IconButton>
) : (
false
)
}
sx={{
'.MuiSvgIcon-root ': {
fill: customization.isDarkMode ? 'white !important' : ''
}
}}
MenuProps={MenuProps}
>
{usecases.map((variant) => (
<MenuItem key={variant.id} value={variant}>
<Checkbox id={variant.id} checked={usecase.findIndex((item) => item.id === variant.id) >= 0} />
<ListItemText primary={variant.name} />
</MenuItem>
))}
</Select>
</FormControl>
<FormControl sx={{ mr: 1, width: '30%' }}>
<InputLabel size='small' id='language-checkbox-label'>
Language
</InputLabel>
<Select
labelId='language-checkbox-label'
id='language-checkbox'
multiple
size='small'
value={language}
onChange={handleLanguageChange}
input={<OutlinedInput label='language' />}
renderValue={(selected) => selected.map((x) => x.name).join(', ')}
endAdornment={
language.length ? (
<IconButton sx={{ mr: 2 }} onClick={() => setLanguage([])}>
<ClearIcon style={{ width: 20, height: 20 }} />
</IconButton>
) : (
false
)
}
sx={{
'.MuiSvgIcon-root ': {
fill: customization.isDarkMode ? 'white !important' : ''
}
}}
MenuProps={MenuProps}
>
{languages.map((variant) => (
<MenuItem key={variant.id} value={variant}>
<Checkbox id={variant.id} checked={language.findIndex((item) => item.id === variant.id) >= 0} />
<ListItemText primary={variant.name} />
</MenuItem>
))}
</Select>
</FormControl>
<FormControl sx={{ width: '10%' }}>
<Button disableElevation variant='outlined' onClick={fetchPrompts}>
Search
</Button>
</FormControl>
</Box>
{loading && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%', pb: 3 }} flexDirection='column'>
<Box sx={{ p: 5, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '20vh', width: 'auto' }} src={promptEmptySVG} alt='promptEmptySVG' />
</Box>
<div>Please wait....loading Prompts</div>
</Stack>
)}
{!loading && availablePrompNameList && availablePrompNameList.length === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%', pb: 3 }} flexDirection='column'>
<Box sx={{ p: 5, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '20vh', width: 'auto' }} src={promptEmptySVG} alt='promptEmptySVG' />
</Box>
<div>No Available Prompts</div>
</Stack>
)}
{!loading && availablePrompNameList && availablePrompNameList.length > 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%' }} flexDirection='column'>
<Box sx={{ width: '100%', p: 2 }}>
<Grid xs={12} container spacing={1} justifyContent='center' alignItems='center'>
<Grid xs={4} item sx={{ textAlign: 'left' }}>
<Box sx={{ width: '100%', maxWidth: 360 }}>
<Card variant='outlined' sx={{ height: 470, overflow: 'auto', borderRadius: 0 }}>
<CardContent sx={{ p: 1 }}>
<Typography sx={{ fontSize: 10 }} color='text.secondary' gutterBottom>
Available Prompts
</Typography>
<List component='nav' aria-label='secondary mailbox folder'>
{availablePrompNameList.map((item, index) => (
<ListItemButton
key={item.id}
selected={item.id === selectedPrompt?.id}
onClick={() => handleListItemClick(index)}
>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography sx={{ fontSize: 16, p: 1, fontWeight: 500 }}>
{item.full_name}
</Typography>
<div
style={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 5
}}
>
{item.tags.map((tag, index) => (
<Chip
key={index}
label={tag}
style={{ marginRight: 5, marginBottom: 5 }}
/>
))}
</div>
</div>
</ListItemButton>
))}
</List>
</CardContent>
</Card>
</Box>
</Grid>
<Grid xs={8} item sx={{ textAlign: 'left' }}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Card sx={{ height: 470, overflow: 'auto' }}>
<CardContent sx={{ p: 0.5 }}>
<Accordion
expanded={accordionExpanded.includes('prompt')}
onChange={handleAccordionChange('prompt')}
>
<AccordionSummary
aria-controls='panel2d-content'
expandIcon={<ExpandMoreIcon />}
id='panel2d-header'
>
<Typography>Prompt</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography sx={{ wordWrap: 'true' }} color='text.primary'>
{selectedPrompt?.detailed?.map((item) => (
<>
<Typography sx={{ fontSize: 12 }} color='text.secondary' gutterBottom>
{item.typeDisplay.toUpperCase()}
</Typography>
<Typography>
<p
style={{
whiteSpace: 'pre-wrap -moz-pre-wrap -pre-wrap -o-pre-wrap',
wordWrap: 'break-word',
fontFamily: 'inherit',
wordSpacing: '0.1rem',
lineHeight: '1.5rem'
}}
>
<NewLineToBr>{item.template}</NewLineToBr>
</p>
</Typography>
</>
))}
</Typography>
</AccordionDetails>
</Accordion>
<Accordion
expanded={accordionExpanded.includes('description')}
onChange={handleAccordionChange('description')}
>
<AccordionSummary
aria-controls='panel1d-content'
expandIcon={<ExpandMoreIcon />}
id='panel1d-header'
>
<Typography>Description</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography
sx={{ wordWrap: 'true', wordSpacing: '0.1rem', lineHeight: '1.5rem' }}
color='text.primary'
>
{selectedPrompt?.description}
</Typography>
</AccordionDetails>
</Accordion>
<Accordion
expanded={accordionExpanded.includes('readme')}
onChange={handleAccordionChange('readme')}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='panel3d-content'
id='panel3d-header'
>
<Typography>Readme</Typography>
</AccordionSummary>
<AccordionDetails>
<div
style={{
lineHeight: 1.75,
'& a': {
display: 'block',
marginRight: '2.5rem',
wordWrap: 'break-word',
color: '#16bed7',
fontWeight: 500
},
'& a:hover': { opacity: 0.8 },
'& code': {
color: '#0ab126',
fontWeight: 500,
whiteSpace: 'pre-wrap !important'
}
}}
>
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax, rehypeRaw]}
components={{
code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<CodeBlock
key={Math.random()}
isDialog={true}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{selectedPrompt?.readme}
</MemoizedReactMarkdown>
</div>
</AccordionDetails>
</Accordion>
</CardContent>
</Card>
</Box>
</Grid>
</Grid>
</Box>
</Stack>
)}
</DialogContent>
{availablePrompNameList && availablePrompNameList.length > 0 && (
<DialogActions>
<Button onClick={onCancel}>Cancel</Button>
<StyledButton
disabled={!selectedPrompt?.detailed}
onClick={() => onSubmit(selectedPrompt.detailed)}
variant='contained'
>
Load
</StyledButton>
</DialogActions>
)}
</Dialog>
) : null
return createPortal(component, portalElement)
}
PromptLangsmithHubDialog.propTypes = {
promptType: PropTypes.string,
show: PropTypes.bool,
onCancel: PropTypes.func,
onSubmit: PropTypes.func
}
export default PromptLangsmithHubDialog
@@ -0,0 +1,252 @@
import { createPortal } from 'react-dom'
import { useDispatch } from 'react-redux'
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from 'store/actions'
// material-ui
import {
Button,
IconButton,
Dialog,
DialogContent,
OutlinedInput,
DialogTitle,
DialogActions,
Box,
List,
InputAdornment
} from '@mui/material'
import { IconX, IconTrash, IconPlus, IconBulb } from '@tabler/icons'
// Project import
import { StyledButton } from 'ui-component/button/StyledButton'
// store
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
import useNotifier from 'utils/useNotifier'
// API
import chatflowsApi from 'api/chatflows'
const StarterPromptsDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [inputFields, setInputFields] = useState([
{
prompt: ''
}
])
const [chatbotConfig, setChatbotConfig] = useState({})
const addInputField = () => {
setInputFields([
...inputFields,
{
prompt: ''
}
])
}
const removeInputFields = (index) => {
const rows = [...inputFields]
rows.splice(index, 1)
setInputFields(rows)
}
const handleChange = (index, evnt) => {
const { name, value } = evnt.target
const list = [...inputFields]
list[index][name] = value
setInputFields(list)
}
const onSave = async () => {
try {
let value = {
starterPrompts: {
...inputFields
}
}
chatbotConfig.starterPrompts = value.starterPrompts
const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, {
chatbotConfig: JSON.stringify(chatbotConfig)
})
if (saveResp.data) {
enqueueSnackbar({
message: 'Conversation Starter Prompts 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 })
}
onConfirm()
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: `Failed to save Conversation Starter Prompts: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
useEffect(() => {
if (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) {
try {
let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig)
setChatbotConfig(chatbotConfig || {})
if (chatbotConfig.starterPrompts) {
let inputFields = []
Object.getOwnPropertyNames(chatbotConfig.starterPrompts).forEach((key) => {
if (chatbotConfig.starterPrompts[key]) {
inputFields.push(chatbotConfig.starterPrompts[key])
}
})
setInputFields(inputFields)
} else {
setInputFields([
{
prompt: ''
}
])
}
} catch (e) {
setInputFields([
{
prompt: ''
}
])
}
}
return () => {}
}, [dialogProps])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth='sm'
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title || 'Conversation Starter Prompts'}
</DialogTitle>
<DialogContent>
<div
style={{
display: 'flex',
flexDirection: 'column',
borderRadius: 10,
background: '#d8f3dc',
padding: 10
}}
>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<IconBulb size={30} color='#2d6a4f' />
<span style={{ color: '#2d6a4f', marginLeft: 10, fontWeight: 500 }}>
Starter prompts will only be shown when there is no messages on the chat
</span>
</div>
</div>
<Box sx={{ '& > :not(style)': { m: 1 }, pt: 2 }}>
<List>
{inputFields.map((data, index) => {
return (
<div key={index} style={{ display: 'flex', width: '100%' }}>
<Box sx={{ width: '95%', mb: 1 }}>
<OutlinedInput
sx={{ width: '100%' }}
key={index}
type='text'
onChange={(e) => handleChange(index, e)}
size='small'
value={data.prompt}
name='prompt'
endAdornment={
<InputAdornment position='end' sx={{ padding: '2px' }}>
{inputFields.length > 1 && (
<IconButton
sx={{ height: 30, width: 30 }}
size='small'
color='error'
disabled={inputFields.length === 1}
onClick={() => removeInputFields(index)}
edge='end'
>
<IconTrash />
</IconButton>
)}
</InputAdornment>
}
/>
</Box>
<Box sx={{ width: '5%', mb: 1 }}>
{index === inputFields.length - 1 && (
<IconButton color='primary' onClick={addInputField}>
<IconPlus />
</IconButton>
)}
</Box>
</div>
)
})}
</List>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>Cancel</Button>
<StyledButton variant='contained' onClick={onSave}>
Save
</StyledButton>
</DialogActions>
</Dialog>
) : null
return createPortal(component, portalElement)
}
StarterPromptsDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default StarterPromptsDialog
@@ -0,0 +1,48 @@
import PropTypes from 'prop-types'
import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import { json } from '@codemirror/lang-json'
import { vscodeDark } from '@uiw/codemirror-theme-vscode'
import { sublime } from '@uiw/codemirror-theme-sublime'
import { EditorView } from '@codemirror/view'
export const CodeEditor = ({ value, height, theme, lang, placeholder, disabled = false, basicSetup = {}, onValueChange }) => {
const customStyle = EditorView.baseTheme({
'&': {
color: '#191b1f',
padding: '10px'
},
'.cm-placeholder': {
color: 'rgba(120, 120, 120, 0.5)'
}
})
return (
<CodeMirror
placeholder={placeholder}
value={value}
height={height ?? 'calc(100vh - 220px)'}
theme={theme === 'dark' ? (lang === 'js' ? vscodeDark : sublime) : 'none'}
extensions={
lang === 'js'
? [javascript({ jsx: true }), EditorView.lineWrapping, customStyle]
: [json(), EditorView.lineWrapping, customStyle]
}
onChange={onValueChange}
readOnly={disabled}
editable={!disabled}
basicSetup={basicSetup}
/>
)
}
CodeEditor.propTypes = {
value: PropTypes.string,
height: PropTypes.string,
theme: PropTypes.string,
lang: PropTypes.string,
placeholder: PropTypes.string,
disabled: PropTypes.bool,
basicSetup: PropTypes.object,
onValueChange: PropTypes.func
}
@@ -1,43 +0,0 @@
import Editor from 'react-simple-code-editor'
import { highlight, languages } from 'prismjs/components/prism-core'
import 'prismjs/components/prism-clike'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-json'
import 'prismjs/components/prism-markup'
import './prism-dark.css'
import PropTypes from 'prop-types'
import { useTheme } from '@mui/material/styles'
export const DarkCodeEditor = ({ value, placeholder, disabled = false, type, style, onValueChange, onMouseUp, onBlur }) => {
const theme = useTheme()
return (
<Editor
disabled={disabled}
value={value}
placeholder={placeholder}
highlight={(code) => highlight(code, type === 'json' ? languages.json : languages.js)}
padding={10}
onValueChange={onValueChange}
onMouseUp={onMouseUp}
onBlur={onBlur}
tabSize={4}
style={{
...style,
background: theme.palette.codeEditor.main
}}
textareaClassName='editor__textarea'
/>
)
}
DarkCodeEditor.propTypes = {
value: PropTypes.string,
placeholder: PropTypes.string,
disabled: PropTypes.bool,
type: PropTypes.string,
style: PropTypes.object,
onValueChange: PropTypes.func,
onMouseUp: PropTypes.func,
onBlur: PropTypes.func
}
@@ -1,43 +0,0 @@
import Editor from 'react-simple-code-editor'
import { highlight, languages } from 'prismjs/components/prism-core'
import 'prismjs/components/prism-clike'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-json'
import 'prismjs/components/prism-markup'
import './prism-light.css'
import PropTypes from 'prop-types'
import { useTheme } from '@mui/material/styles'
export const LightCodeEditor = ({ value, placeholder, disabled = false, type, style, onValueChange, onMouseUp, onBlur }) => {
const theme = useTheme()
return (
<Editor
disabled={disabled}
value={value}
placeholder={placeholder}
highlight={(code) => highlight(code, type === 'json' ? languages.json : languages.js)}
padding={10}
onValueChange={onValueChange}
onMouseUp={onMouseUp}
onBlur={onBlur}
tabSize={4}
style={{
...style,
background: theme.palette.card.main
}}
textareaClassName='editor__textarea'
/>
)
}
LightCodeEditor.propTypes = {
value: PropTypes.string,
placeholder: PropTypes.string,
disabled: PropTypes.bool,
type: PropTypes.string,
style: PropTypes.object,
onValueChange: PropTypes.func,
onMouseUp: PropTypes.func,
onBlur: PropTypes.func
}
@@ -1,275 +0,0 @@
pre[class*='language-'],
code[class*='language-'] {
color: #d4d4d4;
font-size: 13px;
text-shadow: none;
font-family: Menlo, Monaco, Consolas, 'Andale Mono', 'Ubuntu Mono', 'Courier New', monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*='language-']::selection,
code[class*='language-']::selection,
pre[class*='language-'] *::selection,
code[class*='language-'] *::selection {
text-shadow: none;
background: #264f78;
}
@media print {
pre[class*='language-'],
code[class*='language-'] {
text-shadow: none;
}
}
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
background: #1e1e1e;
}
:not(pre) > code[class*='language-'] {
padding: 0.1em 0.3em;
border-radius: 0.3em;
color: #db4c69;
background: #1e1e1e;
}
/*********************************************************
* Tokens
*/
.namespace {
opacity: 0.7;
}
.token.doctype .token.doctype-tag {
color: #569cd6;
}
.token.doctype .token.name {
color: #9cdcfe;
}
.token.comment,
.token.prolog {
color: #6a9955;
}
.token.punctuation,
.language-html .language-css .token.punctuation,
.language-html .language-javascript .token.punctuation {
color: #d4d4d4;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.inserted,
.token.unit {
color: #b5cea8;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.deleted {
color: #ce9178;
}
.language-css .token.string.url {
text-decoration: underline;
}
.token.operator,
.token.entity {
color: #d4d4d4;
}
.token.operator.arrow {
color: #569cd6;
}
.token.atrule {
color: #ce9178;
}
.token.atrule .token.rule {
color: #c586c0;
}
.token.atrule .token.url {
color: #9cdcfe;
}
.token.atrule .token.url .token.function {
color: #dcdcaa;
}
.token.atrule .token.url .token.punctuation {
color: #d4d4d4;
}
.token.keyword {
color: #569cd6;
}
.token.keyword.module,
.token.keyword.control-flow {
color: #c586c0;
}
.token.function,
.token.function .token.maybe-class-name {
color: #dcdcaa;
}
.token.regex {
color: #d16969;
}
.token.important {
color: #569cd6;
}
.token.italic {
font-style: italic;
}
.token.constant {
color: #9cdcfe;
}
.token.class-name,
.token.maybe-class-name {
color: #4ec9b0;
}
.token.console {
color: #9cdcfe;
}
.token.parameter {
color: #9cdcfe;
}
.token.interpolation {
color: #9cdcfe;
}
.token.punctuation.interpolation-punctuation {
color: #569cd6;
}
.token.boolean {
color: #569cd6;
}
.token.property,
.token.variable,
.token.imports .token.maybe-class-name,
.token.exports .token.maybe-class-name {
color: #9cdcfe;
}
.token.selector {
color: #d7ba7d;
}
.token.escape {
color: #d7ba7d;
}
.token.tag {
color: #569cd6;
}
.token.tag .token.punctuation {
color: #808080;
}
.token.cdata {
color: #808080;
}
.token.attr-name {
color: #9cdcfe;
}
.token.attr-value,
.token.attr-value .token.punctuation {
color: #ce9178;
}
.token.attr-value .token.punctuation.attr-equals {
color: #d4d4d4;
}
.token.entity {
color: #569cd6;
}
.token.namespace {
color: #4ec9b0;
}
/*********************************************************
* Language Specific
*/
pre[class*='language-javascript'],
code[class*='language-javascript'],
pre[class*='language-jsx'],
code[class*='language-jsx'],
pre[class*='language-typescript'],
code[class*='language-typescript'],
pre[class*='language-tsx'],
code[class*='language-tsx'] {
color: #9cdcfe;
}
pre[class*='language-css'],
code[class*='language-css'] {
color: #ce9178;
}
pre[class*='language-html'],
code[class*='language-html'] {
color: #d4d4d4;
}
.language-regex .token.anchor {
color: #dcdcaa;
}
.language-html .token.punctuation {
color: #808080;
}
/*********************************************************
* Line highlighting
*/
pre[class*='language-'] > code[class*='language-'] {
position: relative;
z-index: 1;
}
.line-highlight.line-highlight {
background: #f7ebc6;
box-shadow: inset 5px 0 0 #f7d87c;
z-index: 0;
}
@@ -1,207 +0,0 @@
code[class*='language-'],
pre[class*='language-'] {
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
color: #90a4ae;
background: #fafafa;
font-family: Roboto Mono, monospace;
font-size: 1em;
line-height: 1.5em;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
code[class*='language-']::-moz-selection,
pre[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection,
pre[class*='language-'] ::-moz-selection {
background: #cceae7;
color: #263238;
}
code[class*='language-']::selection,
pre[class*='language-']::selection,
code[class*='language-'] ::selection,
pre[class*='language-'] ::selection {
background: #cceae7;
color: #263238;
}
:not(pre) > code[class*='language-'] {
white-space: normal;
border-radius: 0.2em;
padding: 0.1em;
}
pre[class*='language-'] {
overflow: auto;
position: relative;
margin: 0.5em 0;
padding: 1.25em 1em;
}
.language-css > code,
.language-sass > code,
.language-scss > code {
color: #f76d47;
}
[class*='language-'] .namespace {
opacity: 0.7;
}
.token.atrule {
color: #7c4dff;
}
.token.attr-name {
color: #39adb5;
}
.token.attr-value {
color: #f6a434;
}
.token.attribute {
color: #f6a434;
}
.token.boolean {
color: #7c4dff;
}
.token.builtin {
color: #39adb5;
}
.token.cdata {
color: #39adb5;
}
.token.char {
color: #39adb5;
}
.token.class {
color: #39adb5;
}
.token.class-name {
color: #6182b8;
}
.token.comment {
color: #aabfc9;
}
.token.constant {
color: #7c4dff;
}
.token.deleted {
color: #e53935;
}
.token.doctype {
color: #aabfc9;
}
.token.entity {
color: #e53935;
}
.token.function {
color: #7c4dff;
}
.token.hexcode {
color: #f76d47;
}
.token.id {
color: #7c4dff;
font-weight: bold;
}
.token.important {
color: #7c4dff;
font-weight: bold;
}
.token.inserted {
color: #39adb5;
}
.token.keyword {
color: #7c4dff;
}
.token.number {
color: #f76d47;
}
.token.operator {
color: #39adb5;
}
.token.prolog {
color: #aabfc9;
}
.token.property {
color: #39adb5;
}
.token.pseudo-class {
color: #f6a434;
}
.token.pseudo-element {
color: #f6a434;
}
.token.punctuation {
color: #39adb5;
}
.token.regex {
color: #6182b8;
}
.token.selector {
color: #e53935;
}
.token.string {
color: #f6a434;
}
.token.symbol {
color: #7c4dff;
}
.token.tag {
color: #e53935;
}
.token.unit {
color: #f76d47;
}
.token.url {
color: #e53935;
}
.token.variable {
color: #e53935;
}
+63 -53
View File
@@ -1,23 +1,10 @@
import { useState, useEffect, useRef } from 'react'
import PropTypes from 'prop-types'
import { FormControl, OutlinedInput, Popover } from '@mui/material'
import ExpandTextDialog from 'ui-component/dialog/ExpandTextDialog'
import { FormControl, OutlinedInput, InputBase, Popover } from '@mui/material'
import SelectVariable from 'ui-component/json/SelectVariable'
import { getAvailableNodesForVariable } from 'utils/genericHelper'
export const Input = ({
inputParam,
value,
nodes,
edges,
nodeId,
onChange,
disabled = false,
showDialog,
dialogProps,
onDialogCancel,
onDialogConfirm
}) => {
export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disabled = false }) => {
const [myValue, setMyValue] = useState(value ?? '')
const [anchorEl, setAnchorEl] = useState(null)
const [availableNodesForVariable, setAvailableNodesForVariable] = useState([])
@@ -63,39 +50,66 @@ export const Input = ({
return (
<>
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
<OutlinedInput
id={inputParam.name}
size='small'
disabled={disabled}
type={getInputType(inputParam.type)}
placeholder={inputParam.placeholder}
multiline={!!inputParam.rows}
rows={inputParam.rows ?? 1}
value={myValue}
name={inputParam.name}
onChange={(e) => {
setMyValue(e.target.value)
onChange(e.target.value)
}}
inputProps={{
step: inputParam.step ?? 1,
style: {
height: inputParam.rows ? '90px' : 'inherit'
}
}}
/>
</FormControl>
{showDialog && (
<ExpandTextDialog
show={showDialog}
dialogProps={dialogProps}
onCancel={onDialogCancel}
onConfirm={(newValue, inputParamName) => {
setMyValue(newValue)
onDialogConfirm(newValue, inputParamName)
}}
></ExpandTextDialog>
{inputParam.name === 'note' ? (
<FormControl sx={{ width: '100%', height: 'auto' }} size='small'>
<InputBase
id={nodeId}
size='small'
disabled={disabled}
type={getInputType(inputParam.type)}
placeholder={inputParam.placeholder}
multiline={!!inputParam.rows}
minRows={inputParam.rows ?? 1}
value={myValue}
name={inputParam.name}
onChange={(e) => {
setMyValue(e.target.value)
onChange(e.target.value)
}}
inputProps={{
step: inputParam.step ?? 1,
style: {
border: 'none',
background: 'none',
color: '#212121'
}
}}
sx={{
border: 'none',
background: 'none',
padding: '10px 14px',
textarea: {
'&::placeholder': {
color: '#616161'
}
}
}}
/>
</FormControl>
) : (
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
<OutlinedInput
id={inputParam.name}
size='small'
disabled={disabled}
type={getInputType(inputParam.type)}
placeholder={inputParam.placeholder}
multiline={!!inputParam.rows}
rows={inputParam.rows ?? 1}
value={myValue}
name={inputParam.name}
onChange={(e) => {
setMyValue(e.target.value)
onChange(e.target.value)
}}
inputProps={{
step: inputParam.step ?? 1,
style: {
height: inputParam.rows ? '90px' : 'inherit'
}
}}
/>
</FormControl>
)}
<div ref={ref}></div>
{inputParam?.acceptVariable && (
@@ -131,11 +145,7 @@ Input.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
onChange: PropTypes.func,
disabled: PropTypes.bool,
showDialog: PropTypes.bool,
dialogProps: PropTypes.object,
nodes: PropTypes.array,
edges: PropTypes.array,
nodeId: PropTypes.string,
onDialogCancel: PropTypes.func,
onDialogConfirm: PropTypes.func
nodeId: PropTypes.string
}
@@ -141,8 +141,17 @@ const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectA
</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}`}
primary={
node.data.inputs.chainName ??
node.data.inputs.functionName ??
node.data.inputs.variableName ??
node.data.id
}
secondary={
node.data.name === 'ifElseFunction'
? `${node.data.description}`
: `${selectedOutputAnchor?.label ?? 'output'} from ${node.data.label}`
}
/>
</ListItem>
</ListItemButton>
@@ -147,8 +147,8 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi })
}
FlowListTable.propTypes = {
data: PropTypes.object,
images: PropTypes.array,
data: PropTypes.array,
images: PropTypes.object,
filterFunction: PropTypes.func,
updateFlowsApi: PropTypes.object
}
@@ -0,0 +1,12 @@
import { styled } from '@mui/material/styles'
import Tooltip, { tooltipClasses } from '@mui/material/Tooltip'
const NodeTooltip = styled(({ className, ...props }) => <Tooltip {...props} classes={{ popper: className }} />)(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: theme.palette.nodeToolTip.background,
color: theme.palette.nodeToolTip.color,
boxShadow: theme.shadows[1]
}
}))
export default NodeTooltip
-9
View File
@@ -182,15 +182,6 @@ export const initNode = (nodeData, newNodeId) => {
return nodeData
}
export const getEdgeLabelName = (source) => {
const sourceSplit = source.split('-')
if (sourceSplit.length && sourceSplit[0].includes('ifElse')) {
const outputAnchorsIndex = sourceSplit[sourceSplit.length - 1]
return outputAnchorsIndex === '0' ? 'true' : 'false'
}
return ''
}
export const isValidConnection = (connection, reactFlowInstance) => {
const sourceHandle = connection.sourceHandle
const targetHandle = connection.targetHandle
+32 -23
View File
@@ -271,7 +271,13 @@ const AddNodes = ({ nodesData, node }) => {
'aria-label': 'weight'
}}
/>
<Tabs variant='fullWidth' value={tabValue} onChange={handleTabChange} aria-label='tabs'>
<Tabs
sx={{ position: 'relative', minHeight: '50px', height: '50px' }}
variant='fullWidth'
value={tabValue}
onChange={handleTabChange}
aria-label='tabs'
>
{['LangChain', 'LlamaIndex'].map((item, index) => (
<Tab
icon={
@@ -293,32 +299,35 @@ const AddNodes = ({ nodesData, node }) => {
</div>
}
iconPosition='start'
sx={{ minHeight: '50px', height: '50px' }}
key={index}
label={
item === 'LlamaIndex' ? (
<>
<h4>{item}</h4>
&nbsp;
<Chip
sx={{
width: 'max-content',
fontWeight: 700,
fontSize: '0.65rem',
background: theme.palette.primary.main,
color: 'white'
}}
size='small'
label='BETA'
/>
</>
) : (
<h4>{item}</h4>
)
}
label={item}
{...a11yProps(index)}
></Tab>
))}
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
borderRadius: 10,
background: 'rgb(254,252,191)',
paddingLeft: 6,
paddingRight: 6,
paddingTop: 1,
paddingBottom: 1,
width: 'max-content',
position: 'absolute',
top: 0,
right: 0,
fontSize: '0.65rem',
fontWeight: 700
}}
>
<span style={{ color: 'rgb(116,66,16)' }}>BETA</span>
</div>
</Tabs>
<Divider />
</Box>
<PerfectScrollbar
@@ -327,7 +336,7 @@ const AddNodes = ({ nodesData, node }) => {
}}
style={{ height: '100%', maxHeight: 'calc(100vh - 380px)', overflowX: 'hidden' }}
>
<Box sx={{ p: 2 }}>
<Box sx={{ p: 2, pt: 0 }}>
<List
sx={{
width: '100%',
@@ -16,6 +16,7 @@ import SaveChatflowDialog from 'ui-component/dialog/SaveChatflowDialog'
import APICodeDialog from 'views/chatflows/APICodeDialog'
import AnalyseFlowDialog from 'ui-component/dialog/AnalyseFlowDialog'
import ViewMessagesDialog from 'ui-component/dialog/ViewMessagesDialog'
import StarterPromptsDialog from 'ui-component/dialog/StarterPromptsDialog'
// API
import chatflowsApi from 'api/chatflows'
@@ -45,6 +46,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
const [apiDialogProps, setAPIDialogProps] = useState({})
const [analyseDialogOpen, setAnalyseDialogOpen] = useState(false)
const [analyseDialogProps, setAnalyseDialogProps] = useState({})
const [conversationStartersDialogOpen, setConversationStartersDialogOpen] = useState(false)
const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({})
const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false)
const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({})
@@ -56,6 +59,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
if (setting === 'deleteChatflow') {
handleDeleteFlow()
} else if (setting === 'conversationStarters') {
setConversationStartersDialogProps({
title: 'Starter Prompts - ' + chatflow.name,
chatflow: chatflow
})
setConversationStartersDialogOpen(true)
} else if (setting === 'analyseChatflow') {
setAnalyseDialogProps({
title: 'Analyse Chatflow',
@@ -376,6 +385,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
/>
<APICodeDialog show={apiDialogOpen} dialogProps={apiDialogProps} onCancel={() => setAPIDialogOpen(false)} />
<AnalyseFlowDialog show={analyseDialogOpen} dialogProps={analyseDialogProps} onCancel={() => setAnalyseDialogOpen(false)} />
<StarterPromptsDialog
show={conversationStartersDialogOpen}
dialogProps={conversationStartersDialogProps}
onConfirm={() => setConversationStartersDialogOpen(false)}
onCancel={() => setConversationStartersDialogOpen(false)}
/>
<ViewMessagesDialog
show={viewMessagesDialogOpen}
dialogProps={viewMessagesDialogProps}
+8 -30
View File
@@ -3,12 +3,13 @@ import { useContext, useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
// material-ui
import { styled, useTheme } from '@mui/material/styles'
import { useTheme } from '@mui/material/styles'
import { IconButton, Box, Typography, Divider, Button } from '@mui/material'
import Tooltip, { tooltipClasses } from '@mui/material/Tooltip'
import Tooltip from '@mui/material/Tooltip'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import NodeCardWrapper from '../../ui-component/cards/NodeCardWrapper'
import NodeTooltip from '../../ui-component/tooltip/NodeTooltip'
import NodeInputHandler from './NodeInputHandler'
import NodeOutputHandler from './NodeOutputHandler'
import AdditionalParamsDialog from 'ui-component/dialog/AdditionalParamsDialog'
@@ -20,28 +21,6 @@ import { IconTrash, IconCopy, IconInfoCircle, IconAlertTriangle } from '@tabler/
import { flowContext } from 'store/context/ReactFlowContext'
import LlamaindexPNG from 'assets/images/llamaindex.png'
const CardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.main,
color: theme.darkTextPrimary,
border: 'solid 1px',
borderColor: theme.palette.primary[200] + 75,
width: '300px',
height: 'auto',
padding: '10px',
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
'&:hover': {
borderColor: theme.palette.primary.main
}
}))
const LightTooltip = styled(({ className, ...props }) => <Tooltip {...props} classes={{ popper: className }} />)(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: theme.palette.nodeToolTip.background,
color: theme.palette.nodeToolTip.color,
boxShadow: theme.shadows[1]
}
}))
// ===========================|| CANVAS NODE ||=========================== //
const CanvasNode = ({ data }) => {
@@ -94,7 +73,7 @@ const CanvasNode = ({ data }) => {
return (
<>
<CardWrapper
<NodeCardWrapper
content={false}
sx={{
padding: 0,
@@ -102,7 +81,7 @@ const CanvasNode = ({ data }) => {
}}
border={false}
>
<LightTooltip
<NodeTooltip
open={!canvas.canvasDialogShow && open}
onClose={handleClose}
onOpen={handleOpen}
@@ -259,13 +238,12 @@ const CanvasNode = ({ data }) => {
</Typography>
</Box>
<Divider />
{data.outputAnchors.map((outputAnchor, index) => (
<NodeOutputHandler key={index} outputAnchor={outputAnchor} data={data} />
))}
</Box>
</LightTooltip>
</CardWrapper>
</NodeTooltip>
</NodeCardWrapper>
<AdditionalParamsDialog
show={showDialog}
dialogProps={dialogProps}
@@ -6,6 +6,7 @@ import { useSelector } from 'react-redux'
// material-ui
import { useTheme, styled } from '@mui/material/styles'
import { Box, Typography, Tooltip, IconButton, Button } from '@mui/material'
import IconAutoFixHigh from '@mui/icons-material/AutoFixHigh'
import { tooltipClasses } from '@mui/material/Tooltip'
import { IconArrowsMaximize, IconEdit, IconAlertTriangle } from '@tabler/icons'
@@ -21,8 +22,11 @@ import { flowContext } from 'store/context/ReactFlowContext'
import { isValidConnection } from 'utils/genericHelper'
import { JsonEditorInput } from 'ui-component/json/JsonEditor'
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
import { CodeEditor } from 'ui-component/editor/CodeEditor'
import ToolDialog from 'views/tools/ToolDialog'
import AssistantDialog from 'views/assistants/AssistantDialog'
import ExpandTextDialog from 'ui-component/dialog/ExpandTextDialog'
import FormatPromptValuesDialog from 'ui-component/dialog/FormatPromptValuesDialog'
import CredentialInputHandler from './CredentialInputHandler'
@@ -31,6 +35,7 @@ import { getInputVariables } from 'utils/genericHelper'
// const
import { FLOWISE_CREDENTIAL_ID } from 'store/constant'
import PromptLangsmithHubDialog from '../../ui-component/dialog/PromptLangsmithHubDialog'
const EDITABLE_OPTIONS = ['selectedTool', 'selectedAssistant']
@@ -56,6 +61,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString())
const [showFormatPromptValuesDialog, setShowFormatPromptValuesDialog] = useState(false)
const [formatPromptValuesDialogProps, setFormatPromptValuesDialogProps] = useState({})
const [showPromptHubDialog, setShowPromptHubDialog] = useState(false)
const onExpandDialogClicked = (value, inputParam) => {
const dialogProp = {
@@ -69,7 +75,18 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
setShowExpandDialog(true)
}
const onFormatPromptValuesClicked = (value, inputParam) => {
const onShowPromptHubButtonClicked = () => {
setShowPromptHubDialog(true)
}
const onShowPromptHubButtonSubmit = (templates) => {
setShowPromptHubDialog(false)
for (const t of templates) {
if (Object.prototype.hasOwnProperty.call(data.inputs, t.type)) {
data.inputs[t.type] = t.template
}
}
}
const onEditJSONClicked = (value, inputParam) => {
// Preset values if the field is format prompt values
let inputValue = value
if (inputParam.name === 'promptValues' && !value) {
@@ -209,6 +226,31 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
</CustomWidthTooltip>
)}
<Box sx={{ p: 2 }}>
{(data.name === 'promptTemplate' || data.name === 'chatPromptTemplate') &&
(inputParam.name === 'template' || inputParam.name === 'systemMessagePrompt') && (
<>
<Button
style={{
display: 'flex',
flexDirection: 'row',
width: '100%'
}}
disabled={disabled}
sx={{ borderRadius: 25, width: '100%', mb: 2, mt: 0 }}
variant='outlined'
onClick={() => onShowPromptHubButtonClicked()}
endIcon={<IconAutoFixHigh />}
>
Langchain Hub
</Button>
<PromptLangsmithHubDialog
promptType={inputParam.name}
show={showPromptHubDialog}
onCancel={() => setShowPromptHubDialog(false)}
onSubmit={onShowPromptHubButtonSubmit}
></PromptLangsmithHubDialog>
</>
)}
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
{inputParam.label}
@@ -216,7 +258,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
{inputParam.description && <TooltipWithParser style={{ marginLeft: 10 }} title={inputParam.description} />}
</Typography>
<div style={{ flexGrow: 1 }}></div>
{inputParam.type === 'string' && inputParam.rows && (
{((inputParam.type === 'string' && inputParam.rows) || inputParam.type === 'code') && (
<IconButton
size='small'
sx={{
@@ -238,6 +280,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
borderRadius: 10,
background: 'rgb(254,252,191)',
padding: 10,
@@ -245,7 +288,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
marginBottom: 10
}}
>
<IconAlertTriangle size={36} color='orange' />
<IconAlertTriangle size={30} color='orange' />
<span style={{ color: 'rgb(116,66,16)', marginLeft: 10 }}>{inputParam.warning}</span>
</div>
)}
@@ -260,6 +303,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
}}
/>
)}
{inputParam.type === 'file' && (
<File
disabled={disabled}
@@ -284,6 +328,23 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
/>
)}
{inputParam.type === 'code' && (
<>
<div style={{ height: '5px' }}></div>
<div style={{ height: inputParam.rows ? '100px' : '200px' }}>
<CodeEditor
disabled={disabled}
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
height={inputParam.rows ? '100px' : '200px'}
theme={customization.isDarkMode ? 'dark' : 'light'}
lang={'js'}
placeholder={inputParam.placeholder}
onValueChange={(code) => (data.inputs[inputParam.name] = code)}
basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }}
/>
</div>
</>
)}
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
<Input
key={data.inputs[inputParam.name]}
@@ -294,10 +355,6 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
nodes={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getNodes() : []}
edges={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getEdges() : []}
nodeId={data.id}
showDialog={showExpandDialog}
dialogProps={expandDialogProps}
onDialogCancel={() => setShowExpandDialog(false)}
onDialogConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)}
/>
)}
{inputParam.type === 'json' && (
@@ -313,11 +370,17 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
{inputParam?.acceptVariable && (
<>
<Button
sx={{ borderRadius: 25, width: '100%', mb: 2, mt: 2 }}
sx={{
borderRadius: 25,
width: '100%',
mb: 0,
mt: 2
}}
variant='outlined'
onClick={() => onFormatPromptValuesClicked(data.inputs[inputParam.name] ?? '', inputParam)}
disabled={disabled}
onClick={() => onEditJSONClicked(data.inputs[inputParam.name] ?? '', inputParam)}
>
Format Prompt Values
{inputParam.label}
</Button>
<FormatPromptValuesDialog
show={showFormatPromptValuesDialog}
@@ -388,6 +451,12 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
onCancel={() => setAsyncOptionEditDialog('')}
onConfirm={onConfirmAsyncOption}
></AssistantDialog>
<ExpandTextDialog
show={showExpandDialog}
dialogProps={expandDialogProps}
onCancel={() => setShowExpandDialog(false)}
onConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)}
></ExpandTextDialog>
</div>
)
}
@@ -73,42 +73,104 @@ const NodeOutputHandler = ({ outputAnchor, data, disabled = false }) => {
</Box>
</>
)}
{outputAnchor.type === 'options' && outputAnchor.options && outputAnchor.options.length > 0 && (
<>
<CustomWidthTooltip
placement='right'
title={
outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.type ?? outputAnchor.type
}
>
<Handle
type='source'
position={Position.Right}
id={outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.id ?? ''}
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
style={{
height: 10,
width: 10,
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
top: position
}}
/>
</CustomWidthTooltip>
<Box sx={{ p: 2, textAlign: 'end' }}>
<Dropdown
disabled={disabled}
disableClearable={true}
name={outputAnchor.name}
options={outputAnchor.options}
onSelect={(newValue) => {
setDropdownValue(newValue)
data.outputs[outputAnchor.name] = newValue
}}
value={data.outputs[outputAnchor.name] ?? outputAnchor.default ?? 'choose an option'}
/>
</Box>
</>
{data.name === 'ifElseFunction' && outputAnchor.type === 'options' && outputAnchor.options && (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<CustomWidthTooltip
placement='right'
title={
outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.type ??
outputAnchor.type
}
>
<Handle
type='source'
position={Position.Right}
key={outputAnchor.options.find((opt) => opt.name === 'returnTrue')?.id ?? ''}
id={outputAnchor.options.find((opt) => opt.name === 'returnTrue')?.id ?? ''}
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
style={{
height: 10,
width: 10,
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
top: position - 25
}}
/>
</CustomWidthTooltip>
<div style={{ flex: 1 }}></div>
<Box sx={{ p: 2, textAlign: 'end' }}>
<Typography>True</Typography>
</Box>
</div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<CustomWidthTooltip
placement='right'
title={
outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.type ??
outputAnchor.type
}
>
<Handle
type='source'
position={Position.Right}
key={outputAnchor.options.find((opt) => opt.name === 'returnFalse')?.id ?? ''}
id={outputAnchor.options.find((opt) => opt.name === 'returnFalse')?.id ?? ''}
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
style={{
height: 10,
width: 10,
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
top: position + 25
}}
/>
</CustomWidthTooltip>
<div style={{ flex: 1 }}></div>
<Box sx={{ p: 2, textAlign: 'end' }}>
<Typography>False</Typography>
</Box>
</div>
</div>
)}
{data.name !== 'ifElseFunction' &&
outputAnchor.type === 'options' &&
outputAnchor.options &&
outputAnchor.options.length > 0 && (
<>
<CustomWidthTooltip
placement='right'
title={
outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.type ??
outputAnchor.type
}
>
<Handle
type='source'
position={Position.Right}
id={outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.id ?? ''}
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
style={{
height: 10,
width: 10,
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
top: position
}}
/>
</CustomWidthTooltip>
<Box sx={{ p: 2, textAlign: 'end' }}>
<Dropdown
disabled={disabled}
disableClearable={true}
name={outputAnchor.name}
options={outputAnchor.options}
onSelect={(newValue) => {
setDropdownValue(newValue)
data.outputs[outputAnchor.name] = newValue
}}
value={data.outputs[outputAnchor.name] ?? outputAnchor.default ?? 'choose an option'}
/>
</Box>
</>
)}
</div>
)
}
+103
View File
@@ -0,0 +1,103 @@
import PropTypes from 'prop-types'
import { useContext, useState } from 'react'
import { useSelector } from 'react-redux'
// material-ui
import { useTheme } from '@mui/material/styles'
// project imports
import NodeCardWrapper from '../../ui-component/cards/NodeCardWrapper'
import NodeTooltip from '../../ui-component/tooltip/NodeTooltip'
import { IconButton, Box } from '@mui/material'
import { IconCopy, IconTrash } from '@tabler/icons'
import { Input } from 'ui-component/input/Input'
// const
import { flowContext } from '../../store/context/ReactFlowContext'
const StickyNote = ({ data }) => {
const theme = useTheme()
const canvas = useSelector((state) => state.canvas)
const { deleteNode, duplicateNode } = useContext(flowContext)
const [inputParam] = data.inputParams
const [open, setOpen] = useState(false)
const handleClose = () => {
setOpen(false)
}
const handleOpen = () => {
setOpen(true)
}
return (
<>
<NodeCardWrapper
content={false}
sx={{
padding: 0,
borderColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
backgroundColor: data.selected ? '#FFDC00' : '#FFE770'
}}
border={false}
>
<NodeTooltip
open={!canvas.canvasDialogShow && open}
onClose={handleClose}
onOpen={handleOpen}
disableFocusListener={true}
title={
<div
style={{
background: 'transparent',
display: 'flex',
flexDirection: 'column'
}}
>
<IconButton
title='Duplicate'
onClick={() => {
duplicateNode(data.id)
}}
sx={{ height: '35px', width: '35px', '&:hover': { color: theme?.palette.primary.main } }}
color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'}
>
<IconCopy />
</IconButton>
<IconButton
title='Delete'
onClick={() => {
deleteNode(data.id)
}}
sx={{ height: '35px', width: '35px', '&:hover': { color: 'red' } }}
color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'}
>
<IconTrash />
</IconButton>
</div>
}
placement='right-start'
>
<Box>
<Input
key={data.id}
inputParam={inputParam}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
nodes={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getNodes() : []}
edges={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getEdges() : []}
nodeId={data.id}
/>
</Box>
</NodeTooltip>
</NodeCardWrapper>
</>
)
}
StickyNote.propTypes = {
data: PropTypes.object
}
export default StickyNote
+6 -6
View File
@@ -21,6 +21,7 @@ import { useTheme } from '@mui/material/styles'
// project imports
import CanvasNode from './CanvasNode'
import ButtonEdge from './ButtonEdge'
import StickyNote from './StickyNote'
import CanvasHeader from './CanvasHeader'
import AddNodes from './AddNodes'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
@@ -40,13 +41,13 @@ import useConfirm from 'hooks/useConfirm'
import { IconX } from '@tabler/icons'
// utils
import { getUniqueNodeId, initNode, getEdgeLabelName, rearrangeToolsOrdering, getUpsertDetails } from 'utils/genericHelper'
import { getUniqueNodeId, initNode, rearrangeToolsOrdering, getUpsertDetails } from 'utils/genericHelper'
import useNotifier from 'utils/useNotifier'
// const
import { FLOWISE_CREDENTIAL_ID } from 'store/constant'
const nodeTypes = { customNode: CanvasNode }
const nodeTypes = { customNode: CanvasNode, stickyNote: StickyNote }
const edgeTypes = { buttonedge: ButtonEdge }
// ==============================|| CANVAS ||============================== //
@@ -100,8 +101,7 @@ const Canvas = () => {
const newEdge = {
...params,
type: 'buttonedge',
id: `${params.source}-${params.sourceHandle}-${params.target}-${params.targetHandle}`,
data: { label: getEdgeLabelName(params.sourceHandle) }
id: `${params.source}-${params.sourceHandle}-${params.target}-${params.targetHandle}`
}
const targetNodeId = params.targetHandle.split('-')[0]
@@ -170,7 +170,7 @@ const Canvas = () => {
try {
await chatflowsApi.deleteChatflow(chatflow.id)
localStorage.removeItem(`${chatflow.id}_INTERNAL`)
navigate(-1)
navigate('/')
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
@@ -277,7 +277,7 @@ const Canvas = () => {
const newNode = {
id: newNodeId,
position,
type: 'customNode',
type: nodeData.type !== 'StickyNote' ? 'customNode' : 'stickyNote',
data: initNode(nodeData, newNodeId)
}
@@ -135,6 +135,8 @@ const ShareChatbot = ({ isSessionMemory }) => {
if (isSessionMemory) obj.overrideConfig.generateNewSession = generateNewSession
if (chatbotConfig?.starterPrompts) obj.starterPrompts = chatbotConfig.starterPrompts
return obj
}
+2 -1
View File
@@ -12,6 +12,7 @@ import ItemCard from 'ui-component/cards/ItemCard'
import { gridSpacing } from 'store/constant'
import WorkflowEmptySVG from 'assets/images/workflow_empty.svg'
import LoginDialog from 'ui-component/dialog/LoginDialog'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
// API
import chatflowsApi from 'api/chatflows'
@@ -160,7 +161,6 @@ const Chatflows = () => {
variant='contained'
value='card'
title='Card View'
selectedColor='#00abc0'
>
<IconLayoutGrid />
</ToggleButton>
@@ -212,6 +212,7 @@ const Chatflows = () => {
</Stack>
)}
<LoginDialog show={loginDialogOpen} dialogProps={loginDialogProps} onConfirm={onLoginClick} />
<ConfirmDialog />
</MainCard>
)
}
@@ -32,6 +32,7 @@ import { baseURL, maxScroll } from 'store/constant'
import robotPNG from 'assets/images/robot.png'
import userPNG from 'assets/images/account.png'
import StarterPromptsCard from '../../ui-component/cards/StarterPromptsCard'
import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper'
export const ChatMessage = ({ open, chatflowid, isDialog }) => {
@@ -57,6 +58,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
const inputRef = useRef(null)
const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow)
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming)
const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow)
const [starterPrompts, setStarterPrompts] = useState([])
const onSourceDialogClick = (data, title) => {
setSourceDialogProps({ data, title })
@@ -104,21 +108,30 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
}, 100)
}
// Handle form submission
const handleSubmit = async (e) => {
e.preventDefault()
const handlePromptClick = async (promptStarterInput) => {
setUserInput(promptStarterInput)
handleSubmit(undefined, promptStarterInput)
}
if (userInput.trim() === '') {
// Handle form submission
const handleSubmit = async (e, promptStarterInput) => {
if (e) e.preventDefault()
if (!promptStarterInput && userInput.trim() === '') {
return
}
let input = userInput
if (promptStarterInput !== undefined && promptStarterInput.trim() !== '') input = promptStarterInput
setLoading(true)
setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage' }])
setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage' }])
// Send user question and history to API
try {
const params = {
question: userInput,
question: input,
history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?'),
chatId
}
@@ -223,10 +236,27 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
if (getIsChatflowStreamingApi.data) {
setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getIsChatflowStreamingApi.data])
useEffect(() => {
if (getChatflowConfig.data) {
if (getChatflowConfig.data?.chatbotConfig && JSON.parse(getChatflowConfig.data?.chatbotConfig)) {
let config = JSON.parse(getChatflowConfig.data?.chatbotConfig)
if (config.starterPrompts) {
let inputFields = []
Object.getOwnPropertyNames(config.starterPrompts).forEach((key) => {
if (config.starterPrompts[key]) {
inputFields.push(config.starterPrompts[key])
}
})
setStarterPrompts(inputFields)
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getChatflowConfig.data])
// Auto scroll chat to bottom
useEffect(() => {
scrollToBottom()
@@ -245,6 +275,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
if (open && chatflowid) {
getChatmessageApi.request(chatflowid)
getIsChatflowStreamingApi.request(chatflowid)
getChatflowConfig.request(chatflowid)
scrollToBottom()
socket = socketIOClient(baseURL)
@@ -412,7 +443,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
})}
</div>
</div>
<Divider />
<div style={{ position: 'relative' }}>
{messages && messages.length === 1 && (
<StarterPromptsCard starterPrompts={starterPrompts || []} onPromptClick={handlePromptClick} isGrid={isDialog} />
)}
<Divider />
</div>
<div className='center'>
<div style={{ width: '100%' }}>
<form style={{ width: '100%' }} onSubmit={handleSubmit}>
@@ -11,10 +11,10 @@ import { useTheme } from '@mui/material/styles'
// project imports
import MarketplaceCanvasNode from './MarketplaceCanvasNode'
import MarketplaceCanvasHeader from './MarketplaceCanvasHeader'
import StickyNote from '../canvas/StickyNote'
const nodeTypes = { customNode: MarketplaceCanvasNode }
const nodeTypes = { customNode: MarketplaceCanvasNode, stickyNote: StickyNote }
const edgeTypes = { buttonedge: '' }
// ==============================|| CANVAS ||============================== //
@@ -0,0 +1,68 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
const HowToUseFunctionDialog = ({ show, onCancel }) => {
const portalElement = document.getElementById('portal')
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'>
How To Use Function
</DialogTitle>
<DialogContent>
<ul>
<li style={{ marginTop: 10 }}>You can use any libraries imported in Flowise</li>
<li style={{ marginTop: 10 }}>
You can use properties specified in Output Schema as variables with prefix $:
<ul style={{ marginTop: 10 }}>
<li>
Property = <code>userid</code>
</li>
<li>
Variable = <code>$userid</code>
</li>
</ul>
</li>
<li style={{ marginTop: 10 }}>
You can get default flow config:
<ul style={{ marginTop: 10 }}>
<li>
<code>$flow.sessionId</code>
</li>
<li>
<code>$flow.chatId</code>
</li>
<li>
<code>$flow.chatflowId</code>
</li>
<li>
<code>$flow.input</code>
</li>
</ul>
</li>
<li style={{ marginTop: 10 }}>
You can get custom variables:&nbsp;<code>{`$vars.<variable-name>`}</code>
</li>
<li style={{ marginTop: 10 }}>Must return a string value at the end of function</li>
</ul>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
HowToUseFunctionDialog.propTypes = {
show: PropTypes.bool,
onCancel: PropTypes.func
}
export default HowToUseFunctionDialog
+22 -30
View File
@@ -12,9 +12,8 @@ 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'
import { CodeEditor } from 'ui-component/editor/CodeEditor'
import HowToUseFunctionDialog from './HowToUseFunctionDialog'
// Icons
import { IconX, IconFileExport } from '@tabler/icons'
@@ -34,6 +33,8 @@ import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
const exampleAPIFunc = `/*
* You can use any libraries imported in Flowise
* You can use properties specified in Output Schema as variables. Ex: Property = userid, Variable = $userid
* You can get default flow config: $flow.sessionId, $flow.chatId, $flow.chatflowId, $flow.input
* You can get custom variables: $vars.<variable-name>
* Must return a string value at the end of function
*/
@@ -56,7 +57,6 @@ try {
const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const dispatch = useDispatch()
@@ -77,6 +77,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
const [toolIcon, setToolIcon] = useState('')
const [toolSchema, setToolSchema] = useState([])
const [toolFunc, setToolFunc] = useState('')
const [showHowToDialog, setShowHowToDialog] = useState(false)
const deleteItem = useCallback(
(id) => () => {
@@ -485,37 +486,27 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
/>
</Typography>
</Stack>
<Button
style={{ marginBottom: 10, marginRight: 10 }}
color='secondary'
variant='outlined'
onClick={() => setShowHowToDialog(true)}
>
How to use Function
</Button>
{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
}}
/>
)}
<CodeEditor
disabled={dialogProps.type === 'TEMPLATE'}
value={toolFunc}
height='calc(100vh - 220px)'
theme={customization.isDarkMode ? 'dark' : 'light'}
lang={'js'}
onValueChange={(code) => setToolFunc(code)}
/>
</Box>
</DialogContent>
<DialogActions>
@@ -540,6 +531,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
)}
</DialogActions>
<ConfirmDialog />
<HowToUseFunctionDialog show={showHowToDialog} onCancel={() => setShowHowToDialog(false)} />
</Dialog>
) : null
@@ -0,0 +1,285 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
// Material
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Typography, OutlinedInput } from '@mui/material'
// Project imports
import { StyledButton } from 'ui-component/button/StyledButton'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
// Icons
import { IconX, IconVariable } from '@tabler/icons'
// API
import variablesApi from 'api/variables'
// Hooks
// utils
import useNotifier from 'utils/useNotifier'
// const
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
import { Dropdown } from '../../ui-component/dropdown/Dropdown'
const variableTypes = [
{
label: 'Static',
name: 'static',
description: 'Variable value will be read from the value entered below'
},
{
label: 'Runtime',
name: 'runtime',
description: 'Variable value will be read from .env file'
}
]
const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
// ==============================|| Snackbar ||============================== //
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [variableName, setVariableName] = useState('')
const [variableValue, setVariableValue] = useState('')
const [variableType, setVariableType] = useState('static')
const [dialogType, setDialogType] = useState('ADD')
const [variable, setVariable] = useState({})
useEffect(() => {
if (dialogProps.type === 'EDIT' && dialogProps.data) {
setVariableName(dialogProps.data.name)
setVariableValue(dialogProps.data.value)
setVariableType(dialogProps.data.type)
setDialogType('EDIT')
setVariable(dialogProps.data)
} else if (dialogProps.type === 'ADD') {
setVariableName('')
setVariableValue('')
setVariableType('static')
setDialogType('ADD')
setVariable({})
}
return () => {
setVariableName('')
setVariableValue('')
setVariableType('static')
setDialogType('ADD')
setVariable({})
}
}, [dialogProps])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const addNewVariable = async () => {
try {
const obj = {
name: variableName,
value: variableValue,
type: variableType
}
const createResp = await variablesApi.createVariable(obj)
if (createResp.data) {
enqueueSnackbar({
message: 'New Variable 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 (err) {
const errorData = typeof err === 'string' ? err : err.response?.data || `${err.response?.status}: ${err.response?.statusText}`
enqueueSnackbar({
message: `Failed to add new Variable: ${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 saveVariable = async () => {
try {
const saveObj = {
name: variableName,
value: variableValue,
type: variableType
}
const saveResp = await variablesApi.updateVariable(variable.id, saveObj)
if (saveResp.data) {
enqueueSnackbar({
message: 'Variable saved',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(saveResp.data.id)
}
} catch (error) {
const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}`
enqueueSnackbar({
message: `Failed to save Variable: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
const component = show ? (
<Dialog
fullWidth
maxWidth='sm'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div
style={{
width: 50,
height: 50,
marginRight: 10,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<IconVariable
style={{
width: '100%',
height: '100%',
padding: 7,
borderRadius: '50%',
objectFit: 'contain'
}}
/>
</div>
{dialogProps.type === 'ADD' ? 'Add Variable' : 'Edit Variable'}
</div>
</DialogTitle>
<DialogContent>
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Variable Name<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
size='small'
sx={{ mt: 1 }}
type='string'
fullWidth
key='variableName'
onChange={(e) => setVariableName(e.target.value)}
value={variableName ?? ''}
/>
</Box>
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Type<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<Dropdown
key={variableType}
name='variableType'
options={variableTypes}
onSelect={(newValue) => setVariableType(newValue)}
value={variableType ?? 'choose an option'}
/>
</Box>
{variableType === 'static' && (
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Value<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
size='small'
sx={{ mt: 1 }}
type='string'
fullWidth
key='variableValue'
onChange={(e) => setVariableValue(e.target.value)}
value={variableValue ?? ''}
/>
</Box>
)}
</DialogContent>
<DialogActions>
<StyledButton
disabled={!variableName || !variableType || (variableType === 'static' && !variableValue)}
variant='contained'
onClick={() => (dialogType === 'ADD' ? addNewVariable() : saveVariable())}
>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
<ConfirmDialog />
</Dialog>
) : null
return createPortal(component, portalElement)
}
AddEditVariableDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default AddEditVariableDialog
@@ -0,0 +1,72 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
import { CodeEditor } from 'ui-component/editor/CodeEditor'
const overrideConfig = `{
overrideConfig: {
vars: {
var1: 'abc'
}
}
}`
const HowToUseVariablesDialog = ({ show, onCancel }) => {
const portalElement = document.getElementById('portal')
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'>
How To Use Variables
</DialogTitle>
<DialogContent>
<p style={{ marginBottom: '10px' }}>Variables can be used in Custom Tool Function with the $ prefix.</p>
<CodeEditor
disabled={true}
value={`$vars.<variable-name>`}
height={'50px'}
theme={'dark'}
lang={'js'}
basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }}
/>
<p style={{ marginBottom: '10px' }}>
If variable type is Static, the value will be retrieved as it is. If variable type is Runtime, the value will be
retrieved from .env file.
</p>
<p style={{ marginBottom: '10px' }}>
You can also override variable values in API overrideConfig using <b>vars</b>:
</p>
<CodeEditor
disabled={true}
value={overrideConfig}
height={'170px'}
theme={'dark'}
lang={'js'}
basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }}
/>
<p>
Read more from{' '}
<a target='_blank' rel='noreferrer' href='https://docs.flowiseai.com/using-flowise/variables'>
docs
</a>
</p>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
HowToUseVariablesDialog.propTypes = {
show: PropTypes.bool,
onCancel: PropTypes.func
}
export default HowToUseVariablesDialog
+314
View File
@@ -0,0 +1,314 @@
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
import moment from 'moment'
// material-ui
import {
Button,
Box,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
IconButton,
Toolbar,
TextField,
InputAdornment,
ButtonGroup,
Chip
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import { StyledButton } from 'ui-component/button/StyledButton'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
// API
import variablesApi from 'api/variables'
// Hooks
import useApi from 'hooks/useApi'
import useConfirm from 'hooks/useConfirm'
// utils
import useNotifier from 'utils/useNotifier'
// Icons
import { IconTrash, IconEdit, IconX, IconPlus, IconSearch, IconVariable } from '@tabler/icons'
import VariablesEmptySVG from 'assets/images/variables_empty.svg'
// const
import AddEditVariableDialog from './AddEditVariableDialog'
import HowToUseVariablesDialog from './HowToUseVariablesDialog'
// ==============================|| Credentials ||============================== //
const Variables = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const dispatch = useDispatch()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [showVariableDialog, setShowVariableDialog] = useState(false)
const [variableDialogProps, setVariableDialogProps] = useState({})
const [variables, setVariables] = useState([])
const [showHowToDialog, setShowHowToDialog] = useState(false)
const { confirm } = useConfirm()
const getAllVariables = useApi(variablesApi.getAllVariables)
const [search, setSearch] = useState('')
const onSearchChange = (event) => {
setSearch(event.target.value)
}
function filterVariables(data) {
return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1
}
const addNew = () => {
const dialogProp = {
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Add',
data: {}
}
setVariableDialogProps(dialogProp)
setShowVariableDialog(true)
}
const edit = (variable) => {
const dialogProp = {
type: 'EDIT',
cancelButtonName: 'Cancel',
confirmButtonName: 'Save',
data: variable
}
setVariableDialogProps(dialogProp)
setShowVariableDialog(true)
}
const deleteVariable = async (variable) => {
const confirmPayload = {
title: `Delete`,
description: `Delete variable ${variable.name}?`,
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
}
const isConfirmed = await confirm(confirmPayload)
if (isConfirmed) {
try {
const deleteResp = await variablesApi.deleteVariable(variable.id)
if (deleteResp.data) {
enqueueSnackbar({
message: 'Variable 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 Variable: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
const onConfirm = () => {
setShowVariableDialog(false)
getAllVariables.request()
}
useEffect(() => {
getAllVariables.request()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllVariables.data) {
setVariables(getAllVariables.data)
}
}, [getAllVariables.data])
return (
<>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
<Stack flexDirection='row'>
<Box sx={{ flexGrow: 1 }}>
<Toolbar
disableGutters={true}
style={{
margin: 1,
padding: 1,
paddingBottom: 10,
display: 'flex',
justifyContent: 'space-between',
width: '100%'
}}
>
<h1>Variables&nbsp;</h1>
<TextField
size='small'
sx={{ display: { xs: 'none', sm: 'block' }, ml: 3 }}
variant='outlined'
placeholder='Search variable name'
onChange={onSearchChange}
InputProps={{
startAdornment: (
<InputAdornment position='start'>
<IconSearch />
</InputAdornment>
)
}}
/>
<Box sx={{ flexGrow: 1 }} />
<Button variant='outlined' sx={{ mr: 2 }} onClick={() => setShowHowToDialog(true)}>
How To Use
</Button>
<ButtonGroup
sx={{ maxHeight: 40 }}
disableElevation
variant='contained'
aria-label='outlined primary button group'
>
<ButtonGroup disableElevation aria-label='outlined primary button group'>
<StyledButton
variant='contained'
sx={{ color: 'white', mr: 1, height: 37 }}
onClick={addNew}
startIcon={<IconPlus />}
>
Add Variable
</StyledButton>
</ButtonGroup>
</ButtonGroup>
</Toolbar>
</Box>
</Stack>
{variables.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={VariablesEmptySVG}
alt='VariablesEmptySVG'
/>
</Box>
<div>No Variables Yet</div>
</Stack>
)}
{variables.length > 0 && (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Value</TableCell>
<TableCell>Type</TableCell>
<TableCell>Last Updated</TableCell>
<TableCell>Created</TableCell>
<TableCell> </TableCell>
<TableCell> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{variables.filter(filterVariables).map((variable, index) => (
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component='th' scope='row'>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<div
style={{
width: 25,
height: 25,
marginRight: 10,
borderRadius: '50%'
}}
>
<IconVariable
style={{
width: '100%',
height: '100%',
borderRadius: '50%',
objectFit: 'contain'
}}
/>
</div>
{variable.name}
</div>
</TableCell>
<TableCell>{variable.value}</TableCell>
<TableCell>
<Chip
color={variable.type === 'static' ? 'info' : 'secondary'}
size='small'
label={variable.type}
/>
</TableCell>
<TableCell>{moment(variable.updatedDate).format('DD-MMM-YY')}</TableCell>
<TableCell>{moment(variable.createdDate).format('DD-MMM-YY')}</TableCell>
<TableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(variable)}>
<IconEdit />
</IconButton>
</TableCell>
<TableCell>
<IconButton title='Delete' color='error' onClick={() => deleteVariable(variable)}>
<IconTrash />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</MainCard>
<AddEditVariableDialog
show={showVariableDialog}
dialogProps={variableDialogProps}
onCancel={() => setShowVariableDialog(false)}
onConfirm={onConfirm}
></AddEditVariableDialog>
<HowToUseVariablesDialog show={showHowToDialog} onCancel={() => setShowHowToDialog(false)}></HowToUseVariablesDialog>
<ConfirmDialog />
</>
)
}
export default Variables