Feature/lang graph (#2319)

* add langgraph

* datasource: initial commit

* datasource: datasource details and chunks

* datasource: Document Store Node

* more changes

* Document Store - Base functionality

* Document Store Loader Component

* Document Store Loader Component

* before merging the modularity PR

* after merging the modularity PR

* preview mode

* initial draft PR

* fixes

* minor updates and  fixes

* preview with loader and splitter

* preview with credential

* show stored chunks

* preview update...

* edit config

* save, preview and other changes

* save, preview and other changes

* save, process and other changes

* save, process and other changes

* alpha1 - for internal testing

* rerouting urls

* bug fix on new leader create

* pagination support for chunks

* delete document store

* Update pnpm-lock.yaml

* doc store card view

* Update store files to use updated storage functions, Document Store Table View and other changes

* ui changes

* add expanded chunk dialog, improve ui

* change throw Error to InternalError

* Bug Fixes and removal of subFolder, adding of view chunks for store

* lint fixes

* merge changes

* DocumentStoreStatus component

* ui changes for doc store

* add remove metadata key field, add custom document loader

* add chatflows used doc store chips

* add types/interfaces to DocumentStore Services

* document loader list dialog title bar color change

* update interfaces

* Whereused Chatflow Name and Added chunkNo to retain order of created chunks.

* use typeorm order chunkNo, ui changes

* update tabler icons react

* cleanup agents

* add pysandbox tool

* add abort functionality, loading next agent

* add empty view svg

* update chatflow tool with chatId

* rename to agentflows

* update worker for prompt input values

* update dashboard to agentflows, agentcanvas

* fix marketplace use template

* add agentflow templates

* resolve merge conflict

* update baseURL

---------

Co-authored-by: vinodkiran <vinodkiran@usa.net>
Co-authored-by: Vinod Paidimarry <vinodkiran@outlook.in>
This commit is contained in:
Henry Heng
2024-05-21 16:36:42 +01:00
committed by GitHub
parent 95f1090bed
commit 8ebc4dcfd5
92 changed files with 7216 additions and 701 deletions
+218
View File
@@ -0,0 +1,218 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
// material-ui
import { Box, Skeleton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import MainCard from '@/ui-component/cards/MainCard'
import ItemCard from '@/ui-component/cards/ItemCard'
import { gridSpacing } from '@/store/constant'
import AgentsEmptySVG from '@/assets/images/agents_empty.svg'
import LoginDialog from '@/ui-component/dialog/LoginDialog'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import { FlowListTable } from '@/ui-component/table/FlowListTable'
import { StyledButton } from '@/ui-component/button/StyledButton'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
// API
import chatflowsApi from '@/api/chatflows'
// Hooks
import useApi from '@/hooks/useApi'
// const
import { baseURL } from '@/store/constant'
// icons
import { IconPlus, IconLayoutGrid, IconList } from '@tabler/icons-react'
// ==============================|| AGENTS ||============================== //
const Agentflows = () => {
const navigate = useNavigate()
const theme = useTheme()
const [isLoading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [images, setImages] = useState({})
const [search, setSearch] = useState('')
const [loginDialogOpen, setLoginDialogOpen] = useState(false)
const [loginDialogProps, setLoginDialogProps] = useState({})
const getAllAgentflows = useApi(chatflowsApi.getAllAgentflows)
const [view, setView] = useState(localStorage.getItem('flowDisplayStyle') || 'card')
const handleChange = (event, nextView) => {
if (nextView === null) return
localStorage.setItem('flowDisplayStyle', nextView)
setView(nextView)
}
const onSearchChange = (event) => {
setSearch(event.target.value)
}
function filterFlows(data) {
return (
data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 ||
(data.category && data.category.toLowerCase().indexOf(search.toLowerCase()) > -1)
)
}
const onLoginClick = (username, password) => {
localStorage.setItem('username', username)
localStorage.setItem('password', password)
navigate(0)
}
const addNew = () => {
navigate('/agentcanvas')
}
const goToCanvas = (selectedAgentflow) => {
navigate(`/agentcanvas/${selectedAgentflow.id}`)
}
useEffect(() => {
getAllAgentflows.request()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllAgentflows.error) {
if (getAllAgentflows.error?.response?.status === 401) {
setLoginDialogProps({
title: 'Login',
confirmButtonName: 'Login'
})
setLoginDialogOpen(true)
} else {
setError(getAllAgentflows.error)
}
}
}, [getAllAgentflows.error])
useEffect(() => {
setLoading(getAllAgentflows.loading)
}, [getAllAgentflows.loading])
useEffect(() => {
if (getAllAgentflows.data) {
try {
const agentflows = getAllAgentflows.data
const images = {}
for (let i = 0; i < agentflows.length; i += 1) {
const flowDataStr = agentflows[i].flowData
const flowData = JSON.parse(flowDataStr)
const nodes = flowData.nodes || []
images[agentflows[i].id] = []
for (let j = 0; j < nodes.length; j += 1) {
const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}`
if (!images[agentflows[i].id].includes(imageSrc)) {
images[agentflows[i].id].push(imageSrc)
}
}
}
setImages(images)
} catch (e) {
console.error(e)
}
}
}, [getAllAgentflows.data])
return (
<MainCard>
{error ? (
<ErrorBoundary error={error} />
) : (
<Stack flexDirection='column' sx={{ gap: 3 }}>
<ViewHeader onSearchChange={onSearchChange} search={true} searchPlaceholder='Search Name or Category' title='Agents'>
<ToggleButtonGroup
sx={{ borderRadius: 2, maxHeight: 40 }}
value={view}
color='primary'
exclusive
onChange={handleChange}
>
<ToggleButton
sx={{
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
}}
variant='contained'
value='card'
title='Card View'
>
<IconLayoutGrid />
</ToggleButton>
<ToggleButton
sx={{
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
}}
variant='contained'
value='list'
title='List View'
>
<IconList />
</ToggleButton>
</ToggleButtonGroup>
<StyledButton variant='contained' onClick={addNew} startIcon={<IconPlus />} sx={{ borderRadius: 2, height: 40 }}>
Add New
</StyledButton>
</ViewHeader>
{!view || view === 'card' ? (
<>
{isLoading && !getAllAgentflows.data ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllAgentflows.data?.filter(filterFlows).map((data, index) => (
<ItemCard key={index} onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
))}
</Box>
)}
</>
) : (
<FlowListTable
isAgentCanvas={true}
data={getAllAgentflows.data}
images={images}
isLoading={isLoading}
filterFunction={filterFlows}
updateFlowsApi={getAllAgentflows}
setError={setError}
/>
)}
{!isLoading && (!getAllAgentflows.data || getAllAgentflows.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '12vh', width: 'auto' }}
src={AgentsEmptySVG}
alt='AgentsEmptySVG'
/>
</Box>
<div>No Agents Yet</div>
</Stack>
)}
</Stack>
)}
<LoginDialog show={loginDialogOpen} dialogProps={loginDialogProps} onConfirm={onLoginClick} />
<ConfirmDialog />
</MainCard>
)
}
export default Agentflows
+1 -1
View File
@@ -355,7 +355,7 @@ const APIKey = () => {
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
style={{ objectFit: 'cover', height: '20vh', width: 'auto' }}
src={APIEmptySVG}
alt='APIEmptySVG'
/>
+4 -4
View File
@@ -7,7 +7,7 @@ import { Box, Stack, Button, Skeleton } from '@mui/material'
import MainCard from '@/ui-component/cards/MainCard'
import ItemCard from '@/ui-component/cards/ItemCard'
import { gridSpacing } from '@/store/constant'
import ToolEmptySVG from '@/assets/images/tools_empty.svg'
import AssistantEmptySVG from '@/assets/images/assistant_empty.svg'
import { StyledButton } from '@/ui-component/button/StyledButton'
import AssistantDialog from './AssistantDialog'
import LoadAssistantDialog from './LoadAssistantDialog'
@@ -145,9 +145,9 @@ const Assistants = () => {
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={ToolEmptySVG}
alt='ToolEmptySVG'
style={{ objectFit: 'cover', height: '20vh', width: 'auto' }}
src={AssistantEmptySVG}
alt='AssistantEmptySVG'
/>
</Box>
<div>No Assistants Added Yet</div>
+130 -70
View File
@@ -53,7 +53,10 @@ function a11yProps(index) {
}
}
const AddNodes = ({ nodesData, node }) => {
const blacklistCategoriesForAgentCanvas = ['Agents', 'Memory', 'Record Manager']
const allowedAgentModel = {}
const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const dispatch = useDispatch()
@@ -103,7 +106,17 @@ const AddNodes = ({ nodesData, node }) => {
}
const getSearchedNodes = (value) => {
const passed = nodesData.filter((nd) => {
if (isAgentCanvas) {
const nodes = nodesData.filter((nd) => !blacklistCategoriesForAgentCanvas.includes(nd.category))
const passed = nodes.filter((nd) => {
const passesQuery = nd.name.toLowerCase().includes(value.toLowerCase())
const passesCategory = nd.category.toLowerCase().includes(value.toLowerCase())
return passesQuery || passesCategory
})
return passed
}
const nodes = nodesData.filter((nd) => nd.category !== 'Multi Agents')
const passed = nodes.filter((nd) => {
const passesQuery = nd.name.toLowerCase().includes(value.toLowerCase())
const passesCategory = nd.category.toLowerCase().includes(value.toLowerCase())
return passesQuery || passesCategory
@@ -136,17 +149,57 @@ const AddNodes = ({ nodesData, node }) => {
}
const groupByCategory = (nodes, newTabValue, isFilter) => {
const taggedNodes = groupByTags(nodes, newTabValue)
const accordianCategories = {}
const result = taggedNodes.reduce(function (r, a) {
r[a.category] = r[a.category] || []
r[a.category].push(a)
accordianCategories[a.category] = isFilter ? true : false
return r
}, Object.create(null))
setNodes(result)
categorizeVectorStores(result, accordianCategories, isFilter)
setCategoryExpanded(accordianCategories)
if (isAgentCanvas) {
const accordianCategories = {}
const result = nodes.reduce(function (r, a) {
r[a.category] = r[a.category] || []
r[a.category].push(a)
accordianCategories[a.category] = isFilter ? true : false
return r
}, Object.create(null))
const filteredResult = {}
for (const category in result) {
// Filter out blacklisted categories
if (!blacklistCategoriesForAgentCanvas.includes(category)) {
// Filter out LlamaIndex nodes
const nodes = result[category].filter((nd) => !nd.tags || !nd.tags.includes('LlamaIndex'))
if (!nodes.length) continue
// Only allow specific models for specific categories
if (Object.keys(allowedAgentModel).includes(category)) {
const allowedModels = allowedAgentModel[category]
filteredResult[category] = nodes.filter((nd) => allowedModels.includes(nd.name))
} else {
filteredResult[category] = nodes
}
}
}
setNodes(filteredResult)
categorizeVectorStores(filteredResult, accordianCategories, isFilter)
accordianCategories['Multi Agents'] = true
setCategoryExpanded(accordianCategories)
} else {
const taggedNodes = groupByTags(nodes, newTabValue)
const accordianCategories = {}
const result = taggedNodes.reduce(function (r, a) {
r[a.category] = r[a.category] || []
r[a.category].push(a)
accordianCategories[a.category] = isFilter ? true : false
return r
}, Object.create(null))
const filteredResult = {}
for (const category in result) {
if (category === 'Multi Agents') {
continue
}
filteredResult[category] = result[category]
}
setNodes(filteredResult)
categorizeVectorStores(filteredResult, accordianCategories, isFilter)
setCategoryExpanded(accordianCategories)
}
}
const handleAccordionChange = (category) => (event, isExpanded) => {
@@ -271,62 +324,64 @@ const AddNodes = ({ nodesData, node }) => {
'aria-label': 'weight'
}}
/>
<Tabs
sx={{ position: 'relative', minHeight: '50px', height: '50px' }}
variant='fullWidth'
value={tabValue}
onChange={handleTabChange}
aria-label='tabs'
>
{['LangChain', 'LlamaIndex'].map((item, index) => (
<Tab
icon={
<div
style={{
borderRadius: '50%'
}}
>
<img
style={{
width: '25px',
height: '25px',
borderRadius: '50%',
objectFit: 'contain'
}}
src={index === 0 ? LangChainPNG : LlamaindexPNG}
alt={item}
/>
</div>
}
iconPosition='start'
sx={{ minHeight: '50px', height: '50px' }}
key={index}
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
}}
{!isAgentCanvas && (
<Tabs
sx={{ position: 'relative', minHeight: '50px', height: '50px' }}
variant='fullWidth'
value={tabValue}
onChange={handleTabChange}
aria-label='tabs'
>
<span style={{ color: 'rgb(116,66,16)' }}>BETA</span>
</div>
</Tabs>
{['LangChain', 'LlamaIndex'].map((item, index) => (
<Tab
icon={
<div
style={{
borderRadius: '50%'
}}
>
<img
style={{
width: '25px',
height: '25px',
borderRadius: '50%',
objectFit: 'contain'
}}
src={index === 0 ? LangChainPNG : LlamaindexPNG}
alt={item}
/>
</div>
}
iconPosition='start'
sx={{ minHeight: '50px', height: '50px' }}
key={index}
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>
@@ -334,7 +389,11 @@ const AddNodes = ({ nodesData, node }) => {
containerRef={(el) => {
ps.current = el
}}
style={{ height: '100%', maxHeight: 'calc(100vh - 380px)', overflowX: 'hidden' }}
style={{
height: '100%',
maxHeight: `calc(100vh - ${isAgentCanvas ? '300' : '380'}px)`,
overflowX: 'hidden'
}}
>
<Box sx={{ p: 2, pt: 0 }}>
<List
@@ -503,7 +562,8 @@ const AddNodes = ({ nodesData, node }) => {
AddNodes.propTypes = {
nodesData: PropTypes.array,
node: PropTypes.object
node: PropTypes.object,
isAgentCanvas: PropTypes.bool
}
export default AddNodes
+17 -10
View File
@@ -32,7 +32,7 @@ import ViewLeadsDialog from '@/ui-component/dialog/ViewLeadsDialog'
// ==============================|| CANVAS HEADER ||============================== //
const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFlow }) => {
const CanvasHeader = ({ chatflow, isAgentCanvas, handleSaveFlow, handleDeleteFlow, handleLoadFlow }) => {
const theme = useTheme()
const dispatch = useDispatch()
const navigate = useNavigate()
@@ -54,6 +54,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
const [chatflowConfigurationDialogOpen, setChatflowConfigurationDialogOpen] = useState(false)
const [chatflowConfigurationDialogProps, setChatflowConfigurationDialogProps] = useState({})
const title = isAgentCanvas ? 'Agents' : 'Chatflow'
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
const canvas = useSelector((state) => state.canvas)
@@ -82,14 +84,17 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
setUpsertHistoryDialogOpen(true)
} else if (setting === 'chatflowConfiguration') {
setChatflowConfigurationDialogProps({
title: 'Chatflow Configuration',
title: `${title} Configuration`,
chatflow: chatflow
})
setChatflowConfigurationDialogOpen(true)
} else if (setting === 'duplicateChatflow') {
try {
localStorage.setItem('duplicatedFlowData', chatflow.flowData)
window.open(`${uiBaseURL}/canvas`, '_blank')
let flowData = chatflow.flowData
const parsedFlowData = JSON.parse(flowData)
flowData = JSON.stringify(parsedFlowData)
localStorage.setItem('duplicatedFlowData', flowData)
window.open(`${uiBaseURL}/${isAgentCanvas ? 'agentcanvas' : 'canvas'}`, '_blank')
} catch (e) {
console.error(e)
}
@@ -99,7 +104,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
let dataStr = JSON.stringify(generateExportFlowData(flowData), null, 2)
let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
let exportFileDefaultName = `${chatflow.name} Chatflow.json`
let exportFileDefaultName = `${chatflow.name} ${title}.json`
let linkElement = document.createElement('a')
linkElement.setAttribute('href', dataUri)
@@ -192,12 +197,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
// if configuration dialog is open, update its data
if (chatflowConfigurationDialogOpen) {
setChatflowConfigurationDialogProps({
title: 'Chatflow Configuration',
title: `${title} Configuration`,
chatflow
})
}
}
}, [chatflow, chatflowConfigurationDialogOpen])
}, [chatflow, title, chatflowConfigurationDialogOpen])
return (
<>
@@ -346,7 +351,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
</Avatar>
</ButtonBase>
)}
<ButtonBase title='Save Chatflow' sx={{ borderRadius: '50%', mr: 2 }}>
<ButtonBase title={`Save ${title}`} sx={{ borderRadius: '50%', mr: 2 }}>
<Avatar
variant='rounded'
sx={{
@@ -394,11 +399,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
onClose={() => setSettingsOpen(false)}
onSettingsItemClick={onSettingsItemClick}
onUploadFile={onUploadFile}
isAgentCanvas={isAgentCanvas}
/>
<SaveChatflowDialog
show={flowDialogOpen}
dialogProps={{
title: `Save New Chatflow`,
title: `Save New ${title}`,
confirmButtonName: 'Save',
cancelButtonName: 'Cancel'
}}
@@ -431,7 +437,8 @@ CanvasHeader.propTypes = {
chatflow: PropTypes.object,
handleSaveFlow: PropTypes.func,
handleDeleteFlow: PropTypes.func,
handleLoadFlow: PropTypes.func
handleLoadFlow: PropTypes.func,
isAgentCanvas: PropTypes.bool
}
export default CanvasHeader
@@ -109,18 +109,27 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
data.inputs.selectedLinks = links
}
const getJSONValue = (templateValue) => {
if (!templateValue) return ''
const obj = {}
const inputVariables = getInputVariables(templateValue)
for (const inputVariable of inputVariables) {
obj[inputVariable] = ''
}
if (Object.keys(obj).length) return JSON.stringify(obj)
return ''
}
const onEditJSONClicked = (value, inputParam) => {
// Preset values if the field is format prompt values
let inputValue = value
if (inputParam.name === 'promptValues' && !value) {
const obj = {}
const templateValue =
(data.inputs['template'] ?? '') + (data.inputs['systemMessagePrompt'] ?? '') + (data.inputs['humanMessagePrompt'] ?? '')
const inputVariables = getInputVariables(templateValue)
for (const inputVariable of inputVariables) {
obj[inputVariable] = ''
}
if (Object.keys(obj).length) inputValue = JSON.stringify(obj)
(data.inputs['template'] ?? '') +
(data.inputs['systemMessagePrompt'] ?? '') +
(data.inputs['humanMessagePrompt'] ?? '') +
(data.inputs['workerPrompt'] ?? '')
inputValue = getJSONValue(templateValue)
}
const dialogProp = {
value: inputValue,
@@ -386,7 +395,12 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
<JsonEditorInput
disabled={disabled}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
value={
data.inputs[inputParam.name] ||
inputParam.default ||
getJSONValue(data.inputs['workerPrompt']) ||
''
}
isDarkMode={customization.isDarkMode}
/>
)}
+19 -37
View File
@@ -4,7 +4,6 @@ import 'reactflow/dist/style.css'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate, useLocation } from 'react-router-dom'
import { usePrompt } from '@/utils/usePrompt'
import {
REMOVE_DIRTY,
SET_DIRTY,
@@ -50,6 +49,7 @@ import {
updateOutdatedNodeEdge
} from '@/utils/genericHelper'
import useNotifier from '@/utils/useNotifier'
import { usePrompt } from '@/utils/usePrompt'
// const
import { FLOWISE_CREDENTIAL_ID } from '@/store/constant'
@@ -67,7 +67,10 @@ const Canvas = () => {
const templateFlowData = state ? state.templateFlowData : ''
const URLpath = document.location.pathname.toString().split('/')
const chatflowId = URLpath[URLpath.length - 1] === 'canvas' ? '' : URLpath[URLpath.length - 1]
const chatflowId =
URLpath[URLpath.length - 1] === 'canvas' || URLpath[URLpath.length - 1] === 'agentcanvas' ? '' : URLpath[URLpath.length - 1]
const isAgentCanvas = URLpath.includes('agentcanvas') ? true : false
const canvasTitle = URLpath.includes('agentcanvas') ? 'Agent' : 'Chatflow'
const { confirm } = useConfirm()
@@ -75,7 +78,6 @@ const Canvas = () => {
const canvas = useSelector((state) => state.canvas)
const [canvasDataStore, setCanvasDataStore] = useState(canvas)
const [chatflow, setChatflow] = useState(null)
const { reactFlowInstance, setReactFlowInstance } = useContext(flowContext)
// ==============================|| Snackbar ||============================== //
@@ -99,7 +101,6 @@ const Canvas = () => {
const getNodesApi = useApi(nodesApi.getAllNodes)
const createNewChatflowApi = useApi(chatflowsApi.createNewChatflow)
const testChatflowApi = useApi(chatflowsApi.testChatflow)
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
const getSpecificChatflowApi = useApi(chatflowsApi.getSpecificChatflow)
@@ -159,7 +160,7 @@ const Canvas = () => {
setNodes(nodes)
setEdges(flowData.edges || [])
setDirty()
setTimeout(() => setDirty(), 0)
} catch (e) {
console.error(e)
}
@@ -168,7 +169,7 @@ const Canvas = () => {
const handleDeleteFlow = async () => {
const confirmPayload = {
title: `Delete`,
description: `Delete chatflow ${chatflow.name}?`,
description: `Delete ${canvasTitle} ${chatflow.name}?`,
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
}
@@ -178,7 +179,7 @@ const Canvas = () => {
try {
await chatflowsApi.deleteChatflow(chatflow.id)
localStorage.removeItem(`${chatflow.id}_INTERNAL`)
navigate('/')
navigate(isAgentCanvas ? '/agentflows' : '/')
} catch (error) {
enqueueSnackbar({
message: typeof error.response.data === 'object' ? error.response.data.message : error.response.data,
@@ -221,7 +222,8 @@ const Canvas = () => {
name: chatflowName,
deployed: false,
isPublic: false,
flowData
flowData,
type: isAgentCanvas ? 'MULTIAGENT' : 'CHATFLOW'
}
createNewChatflowApi.request(newChatflowBody)
} else {
@@ -339,7 +341,7 @@ const Canvas = () => {
const saveChatflowSuccess = () => {
dispatch({ type: REMOVE_DIRTY })
enqueueSnackbar({
message: 'Chatflow saved',
message: `${canvasTitle} saved`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
@@ -404,7 +406,7 @@ const Canvas = () => {
setEdges(initialFlow.edges || [])
dispatch({ type: SET_CHATFLOW, chatflow })
} else if (getSpecificChatflowApi.error) {
errorFailed(`Failed to retrieve chatflow: ${getSpecificChatflowApi.error.response.data.message}`)
errorFailed(`Failed to retrieve ${canvasTitle}: ${getSpecificChatflowApi.error.response.data.message}`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -416,9 +418,9 @@ const Canvas = () => {
const chatflow = createNewChatflowApi.data
dispatch({ type: SET_CHATFLOW, chatflow })
saveChatflowSuccess()
window.history.replaceState(null, null, `/canvas/${chatflow.id}`)
window.history.replaceState(state, null, `/${isAgentCanvas ? 'agentcanvas' : 'canvas'}/${chatflow.id}`)
} else if (createNewChatflowApi.error) {
errorFailed(`Failed to save chatflow: ${createNewChatflowApi.error.response.data.message}`)
errorFailed(`Failed to save ${canvasTitle}: ${createNewChatflowApi.error.response.data.message}`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -430,33 +432,12 @@ const Canvas = () => {
dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data })
saveChatflowSuccess()
} else if (updateChatflowApi.error) {
errorFailed(`Failed to save chatflow: ${updateChatflowApi.error.response.data.message}`)
errorFailed(`Failed to save ${canvasTitle}: ${updateChatflowApi.error.response.data.message}`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updateChatflowApi.data, updateChatflowApi.error])
// Test chatflow failed
useEffect(() => {
if (testChatflowApi.error) {
enqueueSnackbar({
message: 'Test chatflow failed',
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [testChatflowApi.error])
useEffect(() => {
setChatflow(canvasDataStore.chatflow)
if (canvasDataStore.chatflow) {
@@ -485,7 +466,7 @@ const Canvas = () => {
dispatch({
type: SET_CHATFLOW,
chatflow: {
name: 'Untitled chatflow'
name: `Untitled ${canvasTitle}`
}
})
}
@@ -550,6 +531,7 @@ const Canvas = () => {
handleSaveFlow={handleSaveFlow}
handleDeleteFlow={handleDeleteFlow}
handleLoadFlow={handleLoadFlow}
isAgentCanvas={isAgentCanvas}
/>
</Toolbar>
</AppBar>
@@ -582,7 +564,7 @@ const Canvas = () => {
}}
/>
<Background color='#aaa' gap={16} />
<AddNodes nodesData={getNodesApi.data} node={selectedNode} />
<AddNodes isAgentCanvas={isAgentCanvas} nodesData={getNodesApi.data} node={selectedNode} />
{isSyncNodesButtonEnabled && (
<Fab
sx={{
@@ -604,7 +586,7 @@ const Canvas = () => {
</Fab>
)}
{isUpsertButtonEnabled && <VectorStorePopUp chatflowid={chatflowId} />}
<ChatPopUp chatflowid={chatflowId} />
<ChatPopUp isAgentCanvas={isAgentCanvas} chatflowid={chatflowId} />
</ReactFlow>
</div>
</div>
@@ -14,7 +14,8 @@ import {
Accordion,
AccordionSummary,
AccordionDetails,
Typography
Typography,
Stack
} from '@mui/material'
import { CopyBlock, atomOneDark } from 'react-code-blocks'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
@@ -118,16 +119,34 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => {
updateChatflowApi.request(dialogProps.chatflowid, updateBody)
}
const groupByNodeLabel = (nodes, isFilter = false) => {
const accordianNodes = {}
const result = nodes.reduce(function (r, a) {
r[a.node] = r[a.node] || []
r[a.node].push(a)
accordianNodes[a.node] = isFilter ? true : false
return r
}, Object.create(null))
const groupByNodeLabel = (nodes) => {
const result = {}
nodes.forEach((item) => {
const { node, nodeId, label, name, type } = item
if (!result[node]) {
result[node] = {
nodeIds: [],
params: []
}
}
if (!result[node].nodeIds.includes(nodeId)) result[node].nodeIds.push(nodeId)
const param = { label, name, type }
if (!result[node].params.some((existingParam) => JSON.stringify(existingParam) === JSON.stringify(param))) {
result[node].params.push(param)
}
})
// Sort the nodeIds array
for (const node in result) {
result[node].nodeIds.sort()
}
setNodeConfig(result)
setNodeConfigExpanded(accordianNodes)
}
const handleAccordionChange = (nodeLabel) => (event, isExpanded) => {
@@ -481,12 +500,16 @@ query({
const getMultiConfigCodeWithFormData = (codeLang) => {
if (codeLang === 'Python') {
return `body_data = {
"openAIApiKey[chatOpenAI_0]": "sk-my-openai-1st-key",
"openAIApiKey[openAIEmbeddings_0]": "sk-my-openai-2nd-key"
return `# Specify multiple values for a config parameter by specifying the node id
body_data = {
"openAIApiKey": {
"chatOpenAI_0": "sk-my-openai-1st-key",
"openAIEmbeddings_0": "sk-my-openai-2nd-key"
}
}`
} else if (codeLang === 'JavaScript') {
return `formData.append("openAIApiKey[chatOpenAI_0]", "sk-my-openai-1st-key")
return `// Specify multiple values for a config parameter by specifying the node id
formData.append("openAIApiKey[chatOpenAI_0]", "sk-my-openai-1st-key")
formData.append("openAIApiKey[openAIEmbeddings_0]", "sk-my-openai-2nd-key")`
} else if (codeLang === 'cURL') {
return `-F "openAIApiKey[chatOpenAI_0]=sk-my-openai-1st-key" \\
@@ -619,35 +642,34 @@ formData.append("openAIApiKey[openAIEmbeddings_0]", "sk-my-openai-2nd-key")`
aria-controls={`nodes-accordian-${nodeLabel}`}
id={`nodes-accordian-header-${nodeLabel}`}
>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Stack flexDirection='row' sx={{ gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
<Typography variant='h5'>{nodeLabel}</Typography>
<div
style={{
display: 'flex',
flexDirection: 'row',
width: 'max-content',
borderRadius: 15,
background: 'rgb(254,252,191)',
padding: 5,
paddingLeft: 10,
paddingRight: 10,
marginLeft: 10
}}
>
<span style={{ color: 'rgb(116,66,16)', fontSize: '0.825rem' }}>
{nodeConfig[nodeLabel][0].nodeId}
</span>
</div>
</div>
{nodeConfig[nodeLabel].nodeIds.length > 0 &&
nodeConfig[nodeLabel].nodeIds.map((nodeId, index) => (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'row',
width: 'max-content',
borderRadius: 15,
background: 'rgb(254,252,191)',
padding: 5,
paddingLeft: 10,
paddingRight: 10
}}
>
<span style={{ color: 'rgb(116,66,16)', fontSize: '0.825rem' }}>
{nodeId}
</span>
</div>
))}
</Stack>
</AccordionSummary>
<AccordionDetails>
<TableViewOnly
rows={nodeConfig[nodeLabel].map((obj) => {
// eslint-disable-next-line
const { node, nodeId, ...rest } = obj
return rest
})}
columns={Object.keys(nodeConfig[nodeLabel][0]).slice(-3)}
rows={nodeConfig[nodeLabel].params}
columns={Object.keys(nodeConfig[nodeLabel].params[0]).slice(-3)}
/>
</AccordionDetails>
</Accordion>
+1 -1
View File
@@ -197,7 +197,7 @@ const Chatflows = () => {
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
style={{ objectFit: 'cover', height: '25vh', width: 'auto' }}
src={WorkflowEmptySVG}
alt='WorkflowEmptySVG'
/>
@@ -7,7 +7,7 @@ import { ChatMessage } from './ChatMessage'
import { StyledButton } from '@/ui-component/button/StyledButton'
import { IconEraser } from '@tabler/icons-react'
const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel, previews, setPreviews }) => {
const ChatExpandDialog = ({ show, dialogProps, isAgentCanvas, onClear, onCancel, previews, setPreviews }) => {
const portalElement = document.getElementById('portal')
const customization = useSelector((state) => state.customization)
@@ -50,6 +50,7 @@ const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel, previews, setP
<ChatMessage
isDialog={true}
open={dialogProps.open}
isAgentCanvas={isAgentCanvas}
chatflowid={dialogProps.chatflowid}
previews={previews}
setPreviews={setPreviews}
@@ -64,6 +65,7 @@ const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel, previews, setP
ChatExpandDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
isAgentCanvas: PropTypes.bool,
onClear: PropTypes.func,
onCancel: PropTypes.func,
previews: PropTypes.array,
+360 -33
View File
@@ -1,5 +1,5 @@
import { useState, useRef, useEffect, useCallback, Fragment } from 'react'
import { useSelector } from 'react-redux'
import { useSelector, useDispatch } from 'react-redux'
import PropTypes from 'prop-types'
import socketIOClient from 'socket.io-client'
import { cloneDeep } from 'lodash'
@@ -8,6 +8,7 @@ import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import axios from 'axios'
import { v4 as uuidv4 } from 'uuid'
import {
Box,
@@ -20,13 +21,28 @@ import {
IconButton,
InputAdornment,
OutlinedInput,
Typography
Typography,
CardContent,
Stack
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { IconCircleDot, IconDownload, IconSend, IconMicrophone, IconPhotoPlus, IconTrash, IconX, IconTool } from '@tabler/icons-react'
import {
IconCircleDot,
IconDownload,
IconSend,
IconMicrophone,
IconPhotoPlus,
IconTrash,
IconX,
IconTool,
IconSquareFilled
} from '@tabler/icons-react'
import robotPNG from '@/assets/images/robot.png'
import userPNG from '@/assets/images/account.png'
import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png'
import multiagent_workerPNG from '@/assets/images/multiagent_worker.png'
import audioUploadSVG from '@/assets/images/wave-sound.jpg'
import nextAgentGIF from '@/assets/images/next-agent.gif'
// project import
import { CodeBlock } from '@/ui-component/markdown/CodeBlock'
@@ -34,12 +50,12 @@ import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMark
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog'
import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'
import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'
import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-component/button/ImageButton'
import CopyToClipboardButton from '@/ui-component/button/CopyToClipboardButton'
import ThumbsUpButton from '@/ui-component/button/ThumbsUpButton'
import ThumbsDownButton from '@/ui-component/button/ThumbsDownButton'
import './ChatMessage.css'
import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'
import './audio-recording.css'
// api
@@ -54,9 +70,11 @@ import useApi from '@/hooks/useApi'
// Const
import { baseURL, maxScroll } from '@/store/constant'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
// Utils
import { isValidURL, removeDuplicateURL, setLocalStorageChatflow, getLocalStorageChatflow } from '@/utils/genericHelper'
import useNotifier from '@/utils/useNotifier'
const messageImageStyle = {
width: '128px',
@@ -64,12 +82,18 @@ const messageImageStyle = {
objectFit: 'cover'
}
export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews }) => {
export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setPreviews }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const ps = useRef()
const dispatch = useDispatch()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [userInput, setUserInput] = useState('')
const [loading, setLoading] = useState(false)
const [messages, setMessages] = useState([
@@ -83,7 +107,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
const [isChatFlowAvailableForSpeech, setIsChatFlowAvailableForSpeech] = useState(false)
const [sourceDialogOpen, setSourceDialogOpen] = useState(false)
const [sourceDialogProps, setSourceDialogProps] = useState({})
const [chatId, setChatId] = useState(undefined)
const [chatId, setChatId] = useState(uuidv4())
const [isMessageStopping, setIsMessageStopping] = useState(false)
const inputRef = useRef(null)
const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow)
@@ -287,6 +312,28 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
}
}
const handleAbort = async () => {
setIsMessageStopping(true)
try {
await chatmessageApi.abortMessage(chatflowid, chatId)
} catch (error) {
setIsMessageStopping(false)
enqueueSnackbar({
message: typeof error.response.data === 'object' ? error.response.data.message : error.response.data,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
const handleDeletePreview = (itemToDelete) => {
if (itemToDelete.type === 'file') {
URL.revokeObjectURL(itemToDelete.preview) // Clean up for file
@@ -357,6 +404,56 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
})
}
const updateLastMessageAgentReasoning = (agentReasoning) => {
setMessages((prevMessages) => {
let allMessages = [...cloneDeep(prevMessages)]
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
allMessages[allMessages.length - 1].agentReasoning = JSON.parse(agentReasoning)
return allMessages
})
}
const updateLastMessageNextAgent = (nextAgent) => {
setMessages((prevMessages) => {
let allMessages = [...cloneDeep(prevMessages)]
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning
if (lastAgentReasoning && lastAgentReasoning.length > 0) {
lastAgentReasoning.push({ nextAgent })
}
allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning
return allMessages
})
}
const abortMessage = () => {
setIsMessageStopping(false)
setMessages((prevMessages) => {
let allMessages = [...cloneDeep(prevMessages)]
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning
if (lastAgentReasoning && lastAgentReasoning.length > 0) {
allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning.filter((reasoning) => !reasoning.nextAgent)
}
return allMessages
})
setTimeout(() => {
inputRef.current?.focus()
}, 100)
enqueueSnackbar({
message: 'Message stopped',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
const updateLastMessageUsedTools = (usedTools) => {
setMessages((prevMessages) => {
let allMessages = [...cloneDeep(prevMessages)]
@@ -441,7 +538,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
return allMessages
})
if (!chatId) setChatId(data.chatId)
setChatId(data.chatId)
if (input === '' && data.question) {
// the response contains the question even if it was in an audio format
@@ -468,6 +565,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
sourceDocuments: data?.sourceDocuments,
usedTools: data?.usedTools,
fileAnnotations: data?.fileAnnotations,
agentReasoning: data?.agentReasoning,
type: 'apiMessage',
feedback: null
}
@@ -535,6 +633,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools)
if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations)
if (message.agentReasoning) obj.agentReasoning = JSON.parse(message.agentReasoning)
if (message.fileUploads) {
obj.fileUploads = JSON.parse(message.fileUploads)
obj.fileUploads.forEach((file) => {
@@ -656,6 +755,12 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
socket.on('fileAnnotations', updateLastMessageFileAnnotations)
socket.on('token', updateLastMessage)
socket.on('agentReasoning', updateLastMessageAgentReasoning)
socket.on('nextAgent', updateLastMessageNextAgent)
socket.on('abort', abortMessage)
}
return () => {
@@ -779,7 +884,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
const result = await leadsApi.addLead(body)
if (result.data) {
const data = result.data
if (!chatId) setChatId(data.chatId)
setChatId(data.chatId)
setLocalStorageChatflow(chatflowid, data.chatId, { lead: { name: leadName, email: leadEmail, phone: leadPhone } })
setIsLeadSaved(true)
setLeadEmail(leadEmail)
@@ -865,7 +970,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
}}
>
{message.usedTools.map((tool, index) => {
return (
return tool ? (
<Chip
size='small'
key={index}
@@ -877,7 +982,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
icon={<IconTool size={15} />}
onClick={() => onSourceDialogClick(tool, 'Used Tools')}
/>
)
) : null
})}
</div>
)}
@@ -925,6 +1030,183 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
})}
</div>
)}
{message.agentReasoning && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
{message.agentReasoning.map((agent, index) => {
return agent.nextAgent ? (
<Card
key={index}
sx={{
border: customization.isDarkMode ? 'none' : '1px solid #e0e0e0',
borderRadius: `${customization.borderRadius}px`,
background: customization.isDarkMode
? `linear-gradient(to top, #303030, #212121)`
: `linear-gradient(to top, #f6f3fb, #f2f8fc)`,
mb: 1
}}
>
<CardContent>
<Stack
sx={{
alignItems: 'center',
justifyContent: 'flex-start',
width: '100%'
}}
flexDirection='row'
>
<Box sx={{ height: 'auto', pr: 1 }}>
<img
style={{
objectFit: 'cover',
height: '35px',
width: 'auto'
}}
src={nextAgentGIF}
alt='agentPNG'
/>
</Box>
<div>{agent.nextAgent}</div>
</Stack>
</CardContent>
</Card>
) : (
<Card
key={index}
sx={{
border: customization.isDarkMode ? 'none' : '1px solid #e0e0e0',
borderRadius: `${customization.borderRadius}px`,
background: customization.isDarkMode
? `linear-gradient(to top, #303030, #212121)`
: `linear-gradient(to top, #f6f3fb, #f2f8fc)`,
mb: 1
}}
>
<CardContent>
<Stack
sx={{
alignItems: 'center',
justifyContent: 'flex-start',
width: '100%'
}}
flexDirection='row'
>
<Box sx={{ height: 'auto', pr: 1 }}>
<img
style={{
objectFit: 'cover',
height: '25px',
width: 'auto'
}}
src={
agent.instructions
? multiagent_supervisorPNG
: multiagent_workerPNG
}
alt='agentPNG'
/>
</Box>
<div>{agent.agentName}</div>
</Stack>
{agent.usedTools && agent.usedTools.length > 0 && (
<div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
{agent.usedTools.map((tool, index) => {
return tool !== null ? (
<Chip
size='small'
key={index}
label={tool.tool}
component='a'
sx={{ mr: 1, mt: 1 }}
variant='outlined'
clickable
icon={<IconTool size={15} />}
onClick={() => onSourceDialogClick(tool, 'Used Tools')}
/>
) : null
})}
</div>
)}
{agent.messages.length > 0 && (
<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()}
chatflowid={chatflowid}
isDialog={isDialog}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{agent.messages.length > 1
? agent.messages.join('\\n')
: agent.messages[0]}
</MemoizedReactMarkdown>
)}
{agent.instructions && <p>{agent.instructions}</p>}
{agent.messages.length === 0 && !agent.instructions && <p>Finished</p>}
{agent.sourceDocuments && agent.sourceDocuments.length > 0 && (
<div
style={{
display: 'block',
flexDirection: 'row',
width: '100%'
}}
>
{removeDuplicateURL(agent).map((source, index) => {
const URL =
source && source.metadata && source.metadata.source
? isValidURL(source.metadata.source)
: undefined
return (
<Chip
size='small'
key={index}
label={
URL
? URL.pathname.substring(0, 15) === '/'
? URL.host
: `${URL.pathname.substring(0, 15)}...`
: `${source.pageContent.substring(0, 15)}...`
}
component='a'
sx={{ mr: 1, mb: 1 }}
variant='outlined'
clickable
onClick={() =>
URL
? onURLClick(source.metadata.source)
: onSourceDialogClick(source)
}
/>
)
})}
</div>
)}
</CardContent>
</Card>
)
})}
</div>
)}
<div className='markdownanswer'>
{message.type === 'leadCaptureMessage' &&
!getLocalStorageChatflow(chatflowid)?.lead &&
@@ -1310,30 +1592,74 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
</IconButton>
</InputAdornment>
)}
<InputAdornment position='end' sx={{ padding: '15px' }}>
<IconButton
type='submit'
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
edge='end'
>
{loading ? (
<div>
<CircularProgress color='inherit' size={20} />
</div>
) : (
// Send icon SVG in input field
<IconSend
color={
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
}
/>
{!isAgentCanvas && (
<InputAdornment position='end' sx={{ padding: '15px' }}>
<IconButton
type='submit'
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
edge='end'
>
{loading ? (
<div>
<CircularProgress color='inherit' size={20} />
</div>
) : (
// Send icon SVG in input field
<IconSend
color={
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
}
/>
)}
</IconButton>
</InputAdornment>
)}
{isAgentCanvas && (
<>
{!loading && (
<InputAdornment position='end' sx={{ padding: '15px' }}>
<IconButton
type='submit'
disabled={loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)}
edge='end'
>
<IconSend
color={
loading || !chatflowid || (leadsConfig?.status && !isLeadSaved)
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
}
/>
</IconButton>
</InputAdornment>
)}
</IconButton>
</InputAdornment>
{loading && (
<InputAdornment position='end' sx={{ padding: '15px', mr: 1 }}>
<IconButton
edge='end'
title={isMessageStopping ? 'Stopping...' : 'Stop'}
style={{ border: !isMessageStopping ? '2px solid red' : 'none' }}
onClick={() => handleAbort()}
disabled={isMessageStopping}
>
{isMessageStopping ? (
<div>
<CircularProgress color='error' size={20} />
</div>
) : (
<IconSquareFilled size={15} color='red' />
)}
</IconButton>
</InputAdornment>
)}
</>
)}
</>
}
/>
@@ -1356,6 +1682,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
ChatMessage.propTypes = {
open: PropTypes.bool,
chatflowid: PropTypes.string,
isAgentCanvas: PropTypes.bool,
isDialog: PropTypes.bool,
previews: PropTypes.array,
setPreviews: PropTypes.func
@@ -26,7 +26,7 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba
// Utils
import { getLocalStorageChatflow, removeLocalStorageChatHistory } from '@/utils/genericHelper'
export const ChatPopUp = ({ chatflowid }) => {
export const ChatPopUp = ({ chatflowid, isAgentCanvas }) => {
const theme = useTheme()
const { confirm } = useConfirm()
const dispatch = useDispatch()
@@ -201,7 +201,13 @@ export const ChatPopUp = ({ chatflowid }) => {
boxShadow
shadow={theme.shadows[16]}
>
<ChatMessage chatflowid={chatflowid} open={open} previews={previews} setPreviews={setPreviews} />
<ChatMessage
isAgentCanvas={isAgentCanvas}
chatflowid={chatflowid}
open={open}
previews={previews}
setPreviews={setPreviews}
/>
</MainCard>
</ClickAwayListener>
</Paper>
@@ -211,6 +217,7 @@ export const ChatPopUp = ({ chatflowid }) => {
<ChatExpandDialog
show={showExpandDialog}
dialogProps={expandDialogProps}
isAgentCanvas={isAgentCanvas}
onClear={clearChat}
onCancel={() => setShowExpandDialog(false)}
previews={previews}
@@ -220,4 +227,4 @@ export const ChatPopUp = ({ chatflowid }) => {
)
}
ChatPopUp.propTypes = { chatflowid: PropTypes.string }
ChatPopUp.propTypes = { chatflowid: PropTypes.string, isAgentCanvas: PropTypes.bool }
+1 -1
View File
@@ -327,7 +327,7 @@ const Documents = () => {
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
style={{ objectFit: 'cover', height: '20vh', width: 'auto' }}
src={doc_store_empty}
alt='doc_store_empty'
/>
@@ -46,8 +46,9 @@ const MarketplaceCanvas = () => {
}, [flowData])
const onChatflowCopy = (flowData) => {
const isAgentCanvas = (flowData?.nodes || []).some((node) => node.data.category === 'Multi Agents')
const templateFlowData = JSON.stringify(flowData)
navigate(`/canvas`, { state: { templateFlowData } })
navigate(`/${isAgentCanvas ? 'agentcanvas' : 'canvas'}`, { state: { templateFlowData } })
}
return (
+3 -3
View File
@@ -63,7 +63,7 @@ TabPanel.propTypes = {
}
const badges = ['POPULAR', 'NEW']
const types = ['Chatflow', 'Tool']
const types = ['Chatflow', 'Agentflow', 'Tool']
const framework = ['Langchain', 'LlamaIndex']
const MenuProps = {
PaperProps: {
@@ -413,7 +413,7 @@ const Marketplace = () => {
badgeContent={data.badge}
color={data.badge === 'POPULAR' ? 'primary' : 'error'}
>
{data.type === 'Chatflow' && (
{(data.type === 'Chatflow' || data.type === 'Agentflow') && (
<ItemCard
onClick={() => goToCanvas(data)}
data={data}
@@ -425,7 +425,7 @@ const Marketplace = () => {
)}
</Badge>
)}
{!data.badge && data.type === 'Chatflow' && (
{!data.badge && (data.type === 'Chatflow' || data.type === 'Agentflow') && (
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
)}
{!data.badge && data.type === 'Tool' && (
+9 -5
View File
@@ -14,10 +14,11 @@ import PerfectScrollbar from 'react-perfect-scrollbar'
import MainCard from '@/ui-component/cards/MainCard'
import Transitions from '@/ui-component/extended/Transitions'
import settings from '@/menu-items/settings'
import agentsettings from '@/menu-items/agentsettings'
// ==============================|| SETTINGS ||============================== //
const Settings = ({ chatflow, isSettingsOpen, anchorEl, onSettingsItemClick, onUploadFile, onClose }) => {
const Settings = ({ chatflow, isSettingsOpen, anchorEl, isAgentCanvas, onSettingsItemClick, onUploadFile, onClose }) => {
const theme = useTheme()
const [settingsMenu, setSettingsMenu] = useState([])
const customization = useSelector((state) => state.customization)
@@ -42,13 +43,15 @@ const Settings = ({ chatflow, isSettingsOpen, anchorEl, onSettingsItemClick, onU
useEffect(() => {
if (chatflow && !chatflow.id) {
const settingsMenu = settings.children.filter((menu) => menu.id === 'loadChatflow')
const menus = isAgentCanvas ? agentsettings : settings
const settingsMenu = menus.children.filter((menu) => menu.id === 'loadChatflow')
setSettingsMenu(settingsMenu)
} else if (chatflow && chatflow.id) {
const settingsMenu = settings.children
const menus = isAgentCanvas ? agentsettings : settings
const settingsMenu = menus.children
setSettingsMenu(settingsMenu)
}
}, [chatflow])
}, [chatflow, isAgentCanvas])
useEffect(() => {
setOpen(isSettingsOpen)
@@ -147,7 +150,8 @@ Settings.propTypes = {
anchorEl: PropTypes.any,
onSettingsItemClick: PropTypes.func,
onUploadFile: PropTypes.func,
onClose: PropTypes.func
onClose: PropTypes.func,
isAgentCanvas: PropTypes.bool
}
export default Settings
+1 -1
View File
@@ -165,7 +165,7 @@ const Tools = () => {
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
style={{ objectFit: 'cover', height: '20vh', width: 'auto' }}
src={ToolEmptySVG}
alt='ToolEmptySVG'
/>
+1 -1
View File
@@ -218,7 +218,7 @@ const Variables = () => {
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
style={{ objectFit: 'cover', height: '20vh', width: 'auto' }}
src={VariablesEmptySVG}
alt='VariablesEmptySVG'
/>
@@ -23,7 +23,7 @@ import { CheckboxInput } from '@/ui-component/checkbox/Checkbox'
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
import { TableViewOnly } from '@/ui-component/table/Table'
import { IconX } from '@tabler/icons-react'
import { IconX, IconBulb } from '@tabler/icons-react'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import pythonSVG from '@/assets/images/python.svg'
import javascriptSVG from '@/assets/images/javascript.svg'
@@ -216,6 +216,36 @@ query(formData).then((response) => {
return ''
}
const getMultiConfigCodeWithFormData = (codeLang) => {
if (codeLang === 'Python') {
return `# Specify multiple values for a config parameter by specifying the node id
body_data = {
"openAIApiKey": {
"chatOpenAI_0": "sk-my-openai-1st-key",
"openAIEmbeddings_0": "sk-my-openai-2nd-key"
}
}`
} else if (codeLang === 'JavaScript') {
return `// Specify multiple values for a config parameter by specifying the node id
formData.append("openAIApiKey[chatOpenAI_0]", "sk-my-openai-1st-key")
formData.append("openAIApiKey[openAIEmbeddings_0]", "sk-my-openai-2nd-key")`
} else if (codeLang === 'cURL') {
return `-F "openAIApiKey[chatOpenAI_0]=sk-my-openai-1st-key" \\
-F "openAIApiKey[openAIEmbeddings_0]=sk-my-openai-2nd-key" \\`
}
}
const getMultiConfigCode = () => {
return `{
"overrideConfig": {
"openAIApiKey": {
"chatOpenAI_0": "sk-my-openai-1st-key",
"openAIEmbeddings_0": "sk-my-openai-2nd-key"
}
}
}`
}
const getLang = (codeLang) => {
if (codeLang === 'Python') {
return 'python'
@@ -515,6 +545,44 @@ query(formData).then((response) => {
showLineNumbers={false}
wrapLines
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
borderRadius: 10,
background: '#d8f3dc',
padding: 10,
marginTop: 10,
marginBottom: 10
}}
>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<IconBulb size={30} color='#2d6a4f' />
<span style={{ color: '#2d6a4f', marginLeft: 10, fontWeight: 500 }}>
You can also specify multiple values for a config parameter by
specifying the node id
</span>
</div>
<div style={{ padding: 10 }}>
<CopyBlock
theme={atomOneDark}
text={
isFormDataRequired
? getMultiConfigCodeWithFormData(codeLang)
: getMultiConfigCode()
}
language={getLang(codeLang)}
showLineNumbers={false}
wrapLines
/>
</div>
</div>
</TabPanel>
))}
</div>