Feature/agentflow v2 (#4298)
* agent flow v2 * chat message background * conditon agent flow * add sticky note * update human input dynamic prompt * add HTTP node * add default tool icon * fix export duplicate agentflow v2 * add agentflow v2 marketplaces * refractor memoization, add iteration nodes * add agentflow v2 templates * add agentflow generator * add migration scripts for mysql, mariadb, posrgres and fix date filters for executions * update agentflow chat history config * fix get all flows error after deletion and rename * add previous nodes from parent node * update generator prompt * update run time state when using iteration nodes * prevent looping connection, prevent duplication of start node, add executeflow node, add nodes agentflow, chat history variable * update embed * convert form input to string * bump openai version * add react rewards * add prompt generator to prediction queue * add array schema to overrideconfig * UI touchup * update embedded chat version * fix node info dialog * update start node and loop default iteration * update UI fixes for agentflow v2 * fix async drop down * add export import to agentflowsv2, executions, fix UI bugs * add default empty object to flowlisttable * add ability to share trace link publicly, allow MCP tool use for Agent and Assistant * add runtime message length to variable, display conditions on UI * fix array validation * add ability to add knowledge from vector store and embeddings for agent * add agent tool require human input * add ephemeral memory to start node * update agent flow node to show vs and embeddings icons * feat: add import chat data functionality for AgentFlowV2 * feat: set chatMessage.executionId to null if not found in import JSON file or database * fix: MariaDB execution migration script to utf8mb4_unicode_520_ci --------- Co-authored-by: Ong Chung Yau <33013947+chungyau97@users.noreply.github.com> Co-authored-by: chungyau97 <chungyau97@gmail.com>
@@ -2,7 +2,7 @@ import client from './client'
|
||||
|
||||
const getAllChatflows = () => client.get('/chatflows?type=CHATFLOW')
|
||||
|
||||
const getAllAgentflows = () => client.get('/chatflows?type=MULTIAGENT')
|
||||
const getAllAgentflows = (type) => client.get(`/chatflows?type=${type}`)
|
||||
|
||||
const getSpecificChatflow = (id) => client.get(`/chatflows/${id}`)
|
||||
|
||||
@@ -20,6 +20,8 @@ const getIsChatflowStreaming = (id) => client.get(`/chatflows-streaming/${id}`)
|
||||
|
||||
const getAllowChatflowUploads = (id) => client.get(`/chatflows-uploads/${id}`)
|
||||
|
||||
const generateAgentflow = (body) => client.post(`/agentflowv2-generator/generate`, body)
|
||||
|
||||
export default {
|
||||
getAllChatflows,
|
||||
getAllAgentflows,
|
||||
@@ -30,5 +32,6 @@ export default {
|
||||
updateChatflow,
|
||||
deleteChatflow,
|
||||
getIsChatflowStreaming,
|
||||
getAllowChatflowUploads
|
||||
getAllowChatflowUploads,
|
||||
generateAgentflow
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import client from './client'
|
||||
|
||||
const getAllExecutions = (params = {}) => client.get('/executions', { params })
|
||||
const deleteExecutions = (executionIds) => client.delete('/executions', { data: { executionIds } })
|
||||
const getExecutionById = (executionId) => client.get(`/executions/${executionId}`)
|
||||
const getExecutionByIdPublic = (executionId) => client.get(`/public-executions/${executionId}`)
|
||||
const updateExecution = (executionId, body) => client.put(`/executions/${executionId}`, body)
|
||||
|
||||
export default {
|
||||
getAllExecutions,
|
||||
deleteExecutions,
|
||||
getExecutionById,
|
||||
getExecutionByIdPublic,
|
||||
updateExecution
|
||||
}
|
||||
@@ -2,8 +2,10 @@ import client from './client'
|
||||
|
||||
const sendMessageAndGetPrediction = (id, input) => client.post(`/internal-prediction/${id}`, input)
|
||||
const sendMessageAndStreamPrediction = (id, input) => client.post(`/internal-prediction/stream/${id}`, input)
|
||||
const sendMessageAndGetPredictionPublic = (id, input) => client.post(`/prediction/${id}`, input)
|
||||
|
||||
export default {
|
||||
sendMessageAndGetPrediction,
|
||||
sendMessageAndStreamPrediction
|
||||
sendMessageAndStreamPrediction,
|
||||
sendMessageAndGetPredictionPublic
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import client from './client'
|
||||
|
||||
const checkValidation = (id) => client.get(`/validation/${id}`)
|
||||
|
||||
export default {
|
||||
checkValidation
|
||||
}
|
||||
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-key"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14.52 2c1.029 0 2.015 .409 2.742 1.136l3.602 3.602a3.877 3.877 0 0 1 0 5.483l-2.643 2.643a3.88 3.88 0 0 1 -4.941 .452l-.105 -.078l-5.882 5.883a3 3 0 0 1 -1.68 .843l-.22 .027l-.221 .009h-1.172c-1.014 0 -1.867 -.759 -1.991 -1.823l-.009 -.177v-1.172c0 -.704 .248 -1.386 .73 -1.96l.149 -.161l.414 -.414a1 1 0 0 1 .707 -.293h1v-1a1 1 0 0 1 .883 -.993l.117 -.007h1v-1a1 1 0 0 1 .206 -.608l.087 -.1l1.468 -1.469l-.076 -.103a3.9 3.9 0 0 1 -.678 -1.963l-.007 -.236c0 -1.029 .409 -2.015 1.136 -2.742l2.643 -2.643a3.88 3.88 0 0 1 2.741 -1.136m.495 5h-.02a2 2 0 1 0 0 4h.02a2 2 0 1 0 0 -4" /></svg>
|
||||
|
After Width: | Height: | Size: 817 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-tool"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5" /></svg>
|
||||
|
After Width: | Height: | Size: 407 B |
|
After Width: | Height: | Size: 10 KiB |
@@ -46,6 +46,7 @@ $grey50: #fafafa;
|
||||
$grey100: #f5f5f5;
|
||||
$grey200: #eeeeee;
|
||||
$grey300: #e0e0e0;
|
||||
$grey400: #c4c4c4;
|
||||
$grey500: #9e9e9e;
|
||||
$grey600: #757575;
|
||||
$grey700: #616161;
|
||||
@@ -134,6 +135,7 @@ $darkTextSecondary: #8492c4;
|
||||
grey100: $grey100;
|
||||
grey200: $grey200;
|
||||
grey300: $grey300;
|
||||
grey400: $grey400;
|
||||
grey500: $grey500;
|
||||
grey600: $grey600;
|
||||
grey700: $grey700;
|
||||
|
||||
@@ -120,3 +120,29 @@
|
||||
transform: translateX(10px);
|
||||
}
|
||||
}
|
||||
|
||||
.tiptap {
|
||||
.variable {
|
||||
background-color: #b3f0b8;
|
||||
border-radius: 0.4rem;
|
||||
box-decoration-break: clone;
|
||||
color: #0d7115;
|
||||
padding: 0.1rem 0.3rem;
|
||||
&::after {
|
||||
content: '\200B';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spin-animation {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ const config = {
|
||||
// basename: only at build time to set, and Don't add '/' at end off BASENAME for breadcrumbs, also Don't put only '/' use blank('') instead,
|
||||
basename: '',
|
||||
defaultPath: '/chatflows',
|
||||
fontFamily: `'Roboto', sans-serif`,
|
||||
// You can specify multiple fallback fonts
|
||||
fontFamily: `'Inter', 'Roboto', 'Arial', sans-serif`,
|
||||
borderRadius: 12
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
|
||||
const dataToExport = [
|
||||
'Agentflows',
|
||||
'Agentflows V2',
|
||||
'Assistants Custom',
|
||||
'Assistants OpenAI',
|
||||
'Assistants Azure',
|
||||
@@ -62,6 +63,7 @@ const dataToExport = [
|
||||
'Chat Feedbacks',
|
||||
'Custom Templates',
|
||||
'Document Stores',
|
||||
'Executions',
|
||||
'Tools',
|
||||
'Variables'
|
||||
]
|
||||
@@ -256,6 +258,7 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
const onExport = (data) => {
|
||||
const body = {}
|
||||
if (data.includes('Agentflows')) body.agentflow = true
|
||||
if (data.includes('Agentflows V2')) body.agentflowv2 = true
|
||||
if (data.includes('Assistants Custom')) body.assistantCustom = true
|
||||
if (data.includes('Assistants OpenAI')) body.assistantOpenAI = true
|
||||
if (data.includes('Assistants Azure')) body.assistantAzure = true
|
||||
@@ -264,6 +267,7 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
if (data.includes('Chat Feedbacks')) body.chat_feedback = true
|
||||
if (data.includes('Custom Templates')) body.custom_template = true
|
||||
if (data.includes('Document Stores')) body.document_store = true
|
||||
if (data.includes('Executions')) body.execution = true
|
||||
if (data.includes('Tools')) body.tool = true
|
||||
if (data.includes('Variables')) body.variable = true
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ const ViewHeader = ({
|
||||
<Box sx={{ display: 'flex', alignItems: 'start', flexDirection: 'column' }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '2rem',
|
||||
fontSize: '1.8rem',
|
||||
fontWeight: 600,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
|
||||
@@ -8,11 +8,23 @@ import {
|
||||
IconLock,
|
||||
IconRobot,
|
||||
IconVariable,
|
||||
IconFiles
|
||||
IconFiles,
|
||||
IconListCheck
|
||||
} from '@tabler/icons-react'
|
||||
|
||||
// constant
|
||||
const icons = { IconUsersGroup, IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable, IconFiles }
|
||||
const icons = {
|
||||
IconListCheck,
|
||||
IconUsersGroup,
|
||||
IconHierarchy,
|
||||
IconBuildingStore,
|
||||
IconKey,
|
||||
IconTool,
|
||||
IconLock,
|
||||
IconRobot,
|
||||
IconVariable,
|
||||
IconFiles
|
||||
}
|
||||
|
||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||
|
||||
@@ -35,8 +47,15 @@ const dashboard = {
|
||||
type: 'item',
|
||||
url: '/agentflows',
|
||||
icon: icons.IconUsersGroup,
|
||||
breadcrumbs: true,
|
||||
isBeta: true
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'executions',
|
||||
title: 'Executions',
|
||||
type: 'item',
|
||||
url: '/executions',
|
||||
icon: icons.IconListCheck,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'assistants',
|
||||
|
||||
@@ -7,6 +7,8 @@ import MinimalLayout from '@/layout/MinimalLayout'
|
||||
// canvas routing
|
||||
const Canvas = Loadable(lazy(() => import('@/views/canvas')))
|
||||
const MarketplaceCanvas = Loadable(lazy(() => import('@/views/marketplaces/MarketplaceCanvas')))
|
||||
const CanvasV2 = Loadable(lazy(() => import('@/views/agentflowsv2/Canvas')))
|
||||
const MarketplaceCanvasV2 = Loadable(lazy(() => import('@/views/agentflowsv2/MarketplaceCanvas')))
|
||||
|
||||
// ==============================|| CANVAS ROUTING ||============================== //
|
||||
|
||||
@@ -30,9 +32,21 @@ const CanvasRoutes = {
|
||||
path: '/agentcanvas/:id',
|
||||
element: <Canvas />
|
||||
},
|
||||
{
|
||||
path: '/v2/agentcanvas',
|
||||
element: <CanvasV2 />
|
||||
},
|
||||
{
|
||||
path: '/v2/agentcanvas/:id',
|
||||
element: <CanvasV2 />
|
||||
},
|
||||
{
|
||||
path: '/marketplace/:id',
|
||||
element: <MarketplaceCanvas />
|
||||
},
|
||||
{
|
||||
path: '/v2/marketplace/:id',
|
||||
element: <MarketplaceCanvasV2 />
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { lazy } from 'react'
|
||||
|
||||
// project imports
|
||||
import Loadable from '@/ui-component/loading/Loadable'
|
||||
import MinimalLayout from '@/layout/MinimalLayout'
|
||||
|
||||
// canvas routing
|
||||
const PublicExecutionDetails = Loadable(lazy(() => import('@/views/agentexecutions/PublicExecutionDetails')))
|
||||
|
||||
// ==============================|| CANVAS ROUTING ||============================== //
|
||||
|
||||
const ExecutionRoutes = {
|
||||
path: '/',
|
||||
element: <MinimalLayout />,
|
||||
children: [
|
||||
{
|
||||
path: '/execution/:id',
|
||||
element: <PublicExecutionDetails />
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default ExecutionRoutes
|
||||
@@ -39,6 +39,9 @@ const LoaderConfigPreviewChunks = Loadable(lazy(() => import('@/views/docstore/L
|
||||
const VectorStoreConfigure = Loadable(lazy(() => import('@/views/docstore/VectorStoreConfigure')))
|
||||
const VectorStoreQuery = Loadable(lazy(() => import('@/views/docstore/VectorStoreQuery')))
|
||||
|
||||
// execution routing
|
||||
const Executions = Loadable(lazy(() => import('@/views/agentexecutions')))
|
||||
|
||||
// ==============================|| MAIN ROUTING ||============================== //
|
||||
|
||||
const MainRoutes = {
|
||||
@@ -57,6 +60,10 @@ const MainRoutes = {
|
||||
path: '/agentflows',
|
||||
element: <Agentflows />
|
||||
},
|
||||
{
|
||||
path: '/executions',
|
||||
element: <Executions />
|
||||
},
|
||||
{
|
||||
path: '/marketplaces',
|
||||
element: <Marketplaces />
|
||||
|
||||
@@ -4,10 +4,11 @@ import { useRoutes } from 'react-router-dom'
|
||||
import MainRoutes from './MainRoutes'
|
||||
import CanvasRoutes from './CanvasRoutes'
|
||||
import ChatbotRoutes from './ChatbotRoutes'
|
||||
import ExecutionRoutes from './ExecutionRoutes'
|
||||
import config from '@/config'
|
||||
|
||||
// ==============================|| ROUTING RENDER ||============================== //
|
||||
|
||||
export default function ThemeRoutes() {
|
||||
return useRoutes([MainRoutes, CanvasRoutes, ChatbotRoutes], config.basename)
|
||||
return useRoutes([MainRoutes, CanvasRoutes, ChatbotRoutes, ExecutionRoutes], config.basename)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
// constant
|
||||
import {
|
||||
IconLibrary,
|
||||
IconTools,
|
||||
IconFunctionFilled,
|
||||
IconMessageCircleFilled,
|
||||
IconRobot,
|
||||
IconArrowsSplit,
|
||||
IconPlayerPlayFilled,
|
||||
IconSparkles,
|
||||
IconReplaceUser,
|
||||
IconRepeat,
|
||||
IconSubtask,
|
||||
IconNote,
|
||||
IconWorld,
|
||||
IconRelationOneToManyFilled,
|
||||
IconVectorBezier2
|
||||
} from '@tabler/icons-react'
|
||||
|
||||
export const gridSpacing = 3
|
||||
export const drawerWidth = 260
|
||||
export const appDrawerWidth = 320
|
||||
@@ -8,3 +26,80 @@ export const baseURL = import.meta.env.VITE_API_BASE_URL || window.location.orig
|
||||
export const uiBaseURL = import.meta.env.VITE_UI_BASE_URL || window.location.origin
|
||||
export const FLOWISE_CREDENTIAL_ID = 'FLOWISE_CREDENTIAL_ID'
|
||||
export const REDACTED_CREDENTIAL_VALUE = '_FLOWISE_BLANK_07167752-1a71-43b1-bf8f-4f32252165db'
|
||||
export const AGENTFLOW_ICONS = [
|
||||
{
|
||||
name: 'conditionAgentflow',
|
||||
icon: IconArrowsSplit,
|
||||
color: '#FFB938'
|
||||
},
|
||||
{
|
||||
name: 'startAgentflow',
|
||||
icon: IconPlayerPlayFilled,
|
||||
color: '#7EE787'
|
||||
},
|
||||
{
|
||||
name: 'llmAgentflow',
|
||||
icon: IconSparkles,
|
||||
color: '#64B5F6'
|
||||
},
|
||||
{
|
||||
name: 'agentAgentflow',
|
||||
icon: IconRobot,
|
||||
color: '#4DD0E1'
|
||||
},
|
||||
{
|
||||
name: 'humanInputAgentflow',
|
||||
icon: IconReplaceUser,
|
||||
color: '#6E6EFD'
|
||||
},
|
||||
{
|
||||
name: 'loopAgentflow',
|
||||
icon: IconRepeat,
|
||||
color: '#FFA07A'
|
||||
},
|
||||
{
|
||||
name: 'directReplyAgentflow',
|
||||
icon: IconMessageCircleFilled,
|
||||
color: '#4DDBBB'
|
||||
},
|
||||
{
|
||||
name: 'customFunctionAgentflow',
|
||||
icon: IconFunctionFilled,
|
||||
color: '#E4B7FF'
|
||||
},
|
||||
{
|
||||
name: 'toolAgentflow',
|
||||
icon: IconTools,
|
||||
color: '#d4a373'
|
||||
},
|
||||
{
|
||||
name: 'retrieverAgentflow',
|
||||
icon: IconLibrary,
|
||||
color: '#b8bedd'
|
||||
},
|
||||
{
|
||||
name: 'conditionAgentAgentflow',
|
||||
icon: IconSubtask,
|
||||
color: '#ff8fab'
|
||||
},
|
||||
{
|
||||
name: 'stickyNoteAgentflow',
|
||||
icon: IconNote,
|
||||
color: '#fee440'
|
||||
},
|
||||
{
|
||||
name: 'httpAgentflow',
|
||||
icon: IconWorld,
|
||||
color: '#FF7F7F'
|
||||
},
|
||||
{
|
||||
name: 'iterationAgentflow',
|
||||
icon: IconRelationOneToManyFilled,
|
||||
color: '#9C89B8'
|
||||
},
|
||||
{
|
||||
name: 'executeFlowAgentflow',
|
||||
icon: IconVectorBezier2,
|
||||
color: '#a3b18a'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createContext, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { getUniqueNodeId } from '@/utils/genericHelper'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { getUniqueNodeId, showHideInputParams } from '@/utils/genericHelper'
|
||||
import { cloneDeep, isEqual } from 'lodash'
|
||||
import { SET_DIRTY } from '@/store/actions'
|
||||
|
||||
const initialValue = {
|
||||
@@ -10,7 +10,8 @@ const initialValue = {
|
||||
setReactFlowInstance: () => {},
|
||||
duplicateNode: () => {},
|
||||
deleteNode: () => {},
|
||||
deleteEdge: () => {}
|
||||
deleteEdge: () => {},
|
||||
onNodeDataChange: () => {}
|
||||
}
|
||||
|
||||
export const flowContext = createContext(initialValue)
|
||||
@@ -19,10 +20,112 @@ export const ReactFlowContext = ({ children }) => {
|
||||
const dispatch = useDispatch()
|
||||
const [reactFlowInstance, setReactFlowInstance] = useState(null)
|
||||
|
||||
const onAgentflowNodeStatusUpdate = ({ nodeId, status, error }) => {
|
||||
reactFlowInstance.setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
status,
|
||||
error
|
||||
}
|
||||
}
|
||||
return node
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const clearAgentflowNodeStatus = () => {
|
||||
reactFlowInstance.setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
node.data = {
|
||||
...node.data,
|
||||
status: undefined,
|
||||
error: undefined
|
||||
}
|
||||
return node
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const onNodeDataChange = ({ nodeId, inputParam, newValue }) => {
|
||||
const updatedNodes = reactFlowInstance.getNodes().map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
const updatedInputs = { ...node.data.inputs }
|
||||
|
||||
updatedInputs[inputParam.name] = newValue
|
||||
|
||||
const updatedInputParams = showHideInputParams({
|
||||
...node.data,
|
||||
inputs: updatedInputs
|
||||
})
|
||||
|
||||
// Remove inputs with display set to false
|
||||
Object.keys(updatedInputs).forEach((key) => {
|
||||
const input = updatedInputParams.find((param) => param.name === key)
|
||||
if (input && input.display === false) {
|
||||
delete updatedInputs[key]
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
inputParams: updatedInputParams,
|
||||
inputs: updatedInputs
|
||||
}
|
||||
}
|
||||
}
|
||||
return node
|
||||
})
|
||||
|
||||
// Check if any node's inputParams have changed before updating
|
||||
const hasChanges = updatedNodes.some(
|
||||
(node, index) => !isEqual(node.data.inputParams, reactFlowInstance.getNodes()[index].data.inputParams)
|
||||
)
|
||||
|
||||
if (hasChanges) {
|
||||
reactFlowInstance.setNodes(updatedNodes)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteNode = (nodeid) => {
|
||||
deleteConnectedInput(nodeid, 'node')
|
||||
reactFlowInstance.setNodes(reactFlowInstance.getNodes().filter((n) => n.id !== nodeid))
|
||||
reactFlowInstance.setEdges(reactFlowInstance.getEdges().filter((ns) => ns.source !== nodeid && ns.target !== nodeid))
|
||||
|
||||
// Gather all nodes to be deleted (parent and all descendants)
|
||||
const nodesToDelete = new Set()
|
||||
|
||||
// Helper function to collect all descendant nodes recursively
|
||||
const collectDescendants = (parentId) => {
|
||||
const childNodes = reactFlowInstance.getNodes().filter((node) => node.parentNode === parentId)
|
||||
|
||||
childNodes.forEach((childNode) => {
|
||||
nodesToDelete.add(childNode.id)
|
||||
collectDescendants(childNode.id)
|
||||
})
|
||||
}
|
||||
|
||||
// Collect all descendants first
|
||||
collectDescendants(nodeid)
|
||||
|
||||
// Add the parent node itself last
|
||||
nodesToDelete.add(nodeid)
|
||||
|
||||
// Clean up inputs for all nodes to be deleted
|
||||
nodesToDelete.forEach((id) => {
|
||||
if (id !== nodeid) {
|
||||
// Skip parent node as it's already processed at the beginning
|
||||
deleteConnectedInput(id, 'node')
|
||||
}
|
||||
})
|
||||
|
||||
// Filter out all nodes and edges in a single operation
|
||||
reactFlowInstance.setNodes((nodes) => nodes.filter((node) => !nodesToDelete.has(node.id)))
|
||||
|
||||
// Remove all edges connected to any of the deleted nodes
|
||||
reactFlowInstance.setEdges((edges) => edges.filter((edge) => !nodesToDelete.has(edge.source) && !nodesToDelete.has(edge.target)))
|
||||
|
||||
dispatch({ type: SET_DIRTY })
|
||||
}
|
||||
|
||||
@@ -72,7 +175,7 @@ export const ReactFlowContext = ({ children }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const duplicateNode = (id) => {
|
||||
const duplicateNode = (id, distance = 50) => {
|
||||
const nodes = reactFlowInstance.getNodes()
|
||||
const originalNode = nodes.find((n) => n.id === id)
|
||||
if (originalNode) {
|
||||
@@ -83,16 +186,17 @@ export const ReactFlowContext = ({ children }) => {
|
||||
...clonedNode,
|
||||
id: newNodeId,
|
||||
position: {
|
||||
x: clonedNode.position.x + 400,
|
||||
x: clonedNode.position.x + clonedNode.width + distance,
|
||||
y: clonedNode.position.y
|
||||
},
|
||||
positionAbsolute: {
|
||||
x: clonedNode.positionAbsolute.x + 400,
|
||||
x: clonedNode.positionAbsolute.x + clonedNode.width + distance,
|
||||
y: clonedNode.positionAbsolute.y
|
||||
},
|
||||
data: {
|
||||
...clonedNode.data,
|
||||
id: newNodeId
|
||||
id: newNodeId,
|
||||
label: clonedNode.data.label + ` (${newNodeId.split('_').pop()})`
|
||||
},
|
||||
selected: false
|
||||
}
|
||||
@@ -147,7 +251,10 @@ export const ReactFlowContext = ({ children }) => {
|
||||
setReactFlowInstance,
|
||||
deleteNode,
|
||||
deleteEdge,
|
||||
duplicateNode
|
||||
duplicateNode,
|
||||
onAgentflowNodeStatusUpdate,
|
||||
clearAgentflowNodeStatus,
|
||||
onNodeDataChange
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -78,6 +78,10 @@ export default function themePalette(theme) {
|
||||
paper: theme.paper,
|
||||
default: theme.backgroundDefault
|
||||
},
|
||||
textBackground: {
|
||||
main: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.grey50,
|
||||
border: theme.customization.isDarkMode ? theme.colors?.transparent : theme.colors?.grey400
|
||||
},
|
||||
card: {
|
||||
main: theme.customization.isDarkMode ? theme.colors?.darkPrimaryMain : theme.colors?.paper,
|
||||
light: theme.customization.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.paper,
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Chip, Box, Button, IconButton } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { IconTrash, IconPlus } from '@tabler/icons-react'
|
||||
import NodeInputHandler from '@/views/canvas/NodeInputHandler'
|
||||
import { showHideInputs } from '@/utils/genericHelper'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
|
||||
export const ArrayRenderer = ({ inputParam, data, disabled }) => {
|
||||
const [arrayItems, setArrayItems] = useState([]) // these are the actual values. Ex: [{name: 'John', age: 30}, {name: 'Jane', age: 25}]
|
||||
const [itemParameters, setItemParameters] = useState([]) // these are the input parameters for each array item. Ex: [{label: 'Name', type: 'string', display: true}, {label: 'age', type: 'number', display: false}]
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const { reactFlowInstance } = useContext(flowContext)
|
||||
|
||||
// Handler for when input values change within array items
|
||||
const handleItemInputChange = ({ inputParam: changedParam, newValue }, itemIndex) => {
|
||||
// Create deep copy to avoid mutating state directly
|
||||
let clonedData = cloneDeep(data)
|
||||
|
||||
// Update the specific array item that changed
|
||||
const updatedArrayItems = [...arrayItems]
|
||||
const updatedItem = { ...updatedArrayItems[itemIndex] }
|
||||
|
||||
// Reset the value of fields which has show/hide rules, so the old values don't persist
|
||||
for (let i = 0; i < inputParam.array.length; i += 1) {
|
||||
const fieldDef = inputParam.array[i]
|
||||
if (fieldDef.show || fieldDef.hide) {
|
||||
updatedItem[fieldDef.name] = fieldDef.default || ''
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new value for the changed field
|
||||
updatedItem[changedParam.name] = newValue
|
||||
updatedArrayItems[itemIndex] = updatedItem
|
||||
|
||||
// Update local state and parent data
|
||||
setArrayItems(updatedArrayItems)
|
||||
data.inputs[inputParam.name] = updatedArrayItems
|
||||
clonedData.inputs[inputParam.name] = updatedArrayItems
|
||||
|
||||
// Recalculate display parameters based on new values
|
||||
const newItemParams = showHideInputs(clonedData, 'inputParams', cloneDeep(inputParam.array), itemIndex)
|
||||
|
||||
if (newItemParams.length) {
|
||||
const updatedItemParams = [...itemParameters]
|
||||
updatedItemParams[itemIndex] = newItemParams
|
||||
setItemParameters(updatedItemParams)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize array items and parameters when component mounts or data changes
|
||||
useEffect(() => {
|
||||
const initialArrayItems = data.inputs[inputParam.name] || []
|
||||
setArrayItems(initialArrayItems)
|
||||
|
||||
// Calculate initial display parameters for each array item
|
||||
const initialItemParameters = []
|
||||
for (let i = 0; i < initialArrayItems.length; i += 1) {
|
||||
const itemParams = showHideInputs(data, 'inputParams', cloneDeep(inputParam.array), i)
|
||||
if (itemParams.length) {
|
||||
initialItemParameters.push(itemParams)
|
||||
}
|
||||
}
|
||||
|
||||
setItemParameters(initialItemParameters)
|
||||
}, [data, inputParam])
|
||||
|
||||
const updateOutputAnchors = (items, type, indexToDelete) => {
|
||||
if (data.name !== 'conditionAgentflow' && data.name !== 'conditionAgentAgentflow') return
|
||||
|
||||
const updatedOutputs = items.map((_, i) => ({
|
||||
id: `${data.id}-output-${i}`,
|
||||
label: i,
|
||||
name: i,
|
||||
description: `Condition ${i}`
|
||||
}))
|
||||
|
||||
// always append additional output anchor for ELSE for condition
|
||||
if (data.name === 'conditionAgentflow') {
|
||||
updatedOutputs.push({
|
||||
id: `${data.id}-output-${items.length}`,
|
||||
label: items.length,
|
||||
name: items.length,
|
||||
description: 'Else'
|
||||
})
|
||||
}
|
||||
data.outputAnchors = updatedOutputs
|
||||
|
||||
const nodes = reactFlowInstance.getNodes()
|
||||
|
||||
// Update the current node with new output anchors
|
||||
const updatedNodes = nodes.map((node) => {
|
||||
if (node.id === data.id) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
outputAnchors: updatedOutputs
|
||||
}
|
||||
}
|
||||
}
|
||||
return node
|
||||
})
|
||||
|
||||
reactFlowInstance.setNodes(updatedNodes)
|
||||
|
||||
// Update edges if an item is deleted
|
||||
if (type === 'DELETE') {
|
||||
const edges = reactFlowInstance.getEdges()
|
||||
const updatedEdges = edges.filter((edge) => {
|
||||
if (edge.sourceHandle && edge.sourceHandle.includes(data.id)) {
|
||||
const sourceHandleIndex = edge.sourceHandle.split('-').pop()
|
||||
if (sourceHandleIndex === indexToDelete.toString()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
reactFlowInstance.setEdges(updatedEdges)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for adding new array items
|
||||
const handleAddItem = () => {
|
||||
// Initialize new item with default values
|
||||
let newItem = {}
|
||||
|
||||
for (const fieldDef of inputParam.array) {
|
||||
newItem[fieldDef.name] = fieldDef.default || ''
|
||||
}
|
||||
|
||||
/*if (inputParam.default?.length) {
|
||||
newItem = inputParam.default[0]
|
||||
}*/
|
||||
|
||||
// Update array items
|
||||
const updatedArrayItems = [...arrayItems, newItem]
|
||||
setArrayItems(updatedArrayItems)
|
||||
data.inputs[inputParam.name] = updatedArrayItems
|
||||
|
||||
// Calculate display parameters for all items including new one
|
||||
const updatedItemParameters = []
|
||||
for (let i = 0; i < updatedArrayItems.length; i += 1) {
|
||||
const itemParams = showHideInputs(data, 'inputParams', cloneDeep(inputParam.array), i)
|
||||
if (itemParams.length) {
|
||||
updatedItemParameters.push(itemParams)
|
||||
}
|
||||
}
|
||||
setItemParameters(updatedItemParameters)
|
||||
|
||||
updateOutputAnchors(updatedArrayItems, 'ADD')
|
||||
}
|
||||
|
||||
// Handler for deleting array items
|
||||
const handleDeleteItem = (indexToDelete) => {
|
||||
const updatedArrayItems = arrayItems.filter((_, i) => i !== indexToDelete)
|
||||
setArrayItems(updatedArrayItems)
|
||||
data.inputs[inputParam.name] = updatedArrayItems
|
||||
|
||||
const updatedItemParameters = itemParameters.filter((_, i) => i !== indexToDelete)
|
||||
setItemParameters(updatedItemParameters)
|
||||
|
||||
updateOutputAnchors(updatedArrayItems, 'DELETE', indexToDelete)
|
||||
}
|
||||
|
||||
const isDeleteButtonVisible = (data.name !== 'conditionAgentflow' && data.name !== 'conditionAgentAgentflow') || arrayItems.length > 1
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Render each array item */}
|
||||
{arrayItems.map((itemValues, index) => {
|
||||
// Create item data directly from parent data
|
||||
const itemData = {
|
||||
...data,
|
||||
inputs: itemValues,
|
||||
inputParams: itemParameters[index] || []
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
mt: 2,
|
||||
mb: 1,
|
||||
border: 1,
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
borderRadius: 2,
|
||||
position: 'relative'
|
||||
}}
|
||||
key={index}
|
||||
>
|
||||
{/* Delete button for array item */}
|
||||
{isDeleteButtonVisible && (
|
||||
<IconButton
|
||||
title='Delete'
|
||||
onClick={() => handleDeleteItem(index)}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
right: 10,
|
||||
top: 10,
|
||||
color: customization?.isDarkMode ? theme.palette.grey[300] : 'inherit',
|
||||
'&:hover': { color: 'red' }
|
||||
}}
|
||||
>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<Chip
|
||||
label={`${index}`}
|
||||
size='small'
|
||||
sx={{ position: 'absolute', right: isDeleteButtonVisible ? 45 : 10, top: 16 }}
|
||||
/>
|
||||
|
||||
{/* Render input fields for array item */}
|
||||
{itemParameters[index]
|
||||
.filter((param) => param.display !== false)
|
||||
.map((param, _index) => (
|
||||
<NodeInputHandler
|
||||
disabled={disabled}
|
||||
key={_index}
|
||||
inputParam={param}
|
||||
data={itemData}
|
||||
isAdditionalParams={true}
|
||||
parentParamForArray={inputParam}
|
||||
arrayIndex={index}
|
||||
onCustomDataChange={({ inputParam, newValue }) => {
|
||||
handleItemInputChange({ inputParam, newValue }, index)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Add new item button */}
|
||||
<Button
|
||||
fullWidth
|
||||
size='small'
|
||||
variant='outlined'
|
||||
sx={{ borderRadius: '16px', mt: 2 }}
|
||||
startIcon={<IconPlus />}
|
||||
onClick={handleAddItem}
|
||||
>
|
||||
Add {inputParam.label}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ArrayRenderer.propTypes = {
|
||||
inputParam: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
@@ -166,7 +166,11 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update
|
||||
}
|
||||
try {
|
||||
await updateChatflowApi.request(chatflow.id, updateBody)
|
||||
await updateFlowsApi.request()
|
||||
if (isAgentCanvas && localStorage.getItem('agentFlowVersion') === 'v2') {
|
||||
await updateFlowsApi.request('AGENTFLOW')
|
||||
} else {
|
||||
await updateFlowsApi.request(isAgentCanvas ? 'MULTIAGENT' : undefined)
|
||||
}
|
||||
} catch (error) {
|
||||
if (setError) setError(error)
|
||||
enqueueSnackbar({
|
||||
@@ -205,7 +209,7 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update
|
||||
}
|
||||
try {
|
||||
await updateChatflowApi.request(chatflow.id, updateBody)
|
||||
await updateFlowsApi.request()
|
||||
await updateFlowsApi.request(isAgentCanvas ? 'AGENTFLOW' : undefined)
|
||||
} catch (error) {
|
||||
if (setError) setError(error)
|
||||
enqueueSnackbar({
|
||||
@@ -237,7 +241,11 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
await chatflowsApi.deleteChatflow(chatflow.id)
|
||||
await updateFlowsApi.request()
|
||||
if (isAgentCanvas && localStorage.getItem('agentFlowVersion') === 'v2') {
|
||||
await updateFlowsApi.request('AGENTFLOW')
|
||||
} else {
|
||||
await updateFlowsApi.request(isAgentCanvas ? 'MULTIAGENT' : undefined)
|
||||
}
|
||||
} catch (error) {
|
||||
if (setError) setError(error)
|
||||
enqueueSnackbar({
|
||||
|
||||
@@ -29,7 +29,7 @@ const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
|
||||
// ===========================|| CONTRACT CARD ||=========================== //
|
||||
|
||||
const ItemCard = ({ data, images, onClick }) => {
|
||||
const ItemCard = ({ data, images, icons, onClick }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
@@ -106,7 +106,7 @@ const ItemCard = ({ data, images, onClick }) => {
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
{images && (
|
||||
{(images?.length > 0 || icons?.length > 0) && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -115,24 +115,48 @@ const ItemCard = ({ data, images, onClick }) => {
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
{images.slice(0, images.length > 3 ? 3 : images.length).map((img) => (
|
||||
<Box
|
||||
key={img}
|
||||
sx={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: customization.isDarkMode
|
||||
? theme.palette.common.white
|
||||
: theme.palette.grey[300] + 75
|
||||
}}
|
||||
>
|
||||
<img style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }} alt='' src={img} />
|
||||
</Box>
|
||||
))}
|
||||
{images.length > 3 && (
|
||||
{[
|
||||
...(images || []).map((img) => ({ type: 'image', src: img })),
|
||||
...(icons || []).map((ic) => ({ type: 'icon', icon: ic.icon, color: ic.color }))
|
||||
]
|
||||
.slice(0, 3)
|
||||
.map((item, index) =>
|
||||
item.type === 'image' ? (
|
||||
<Box
|
||||
key={item.src}
|
||||
sx={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: customization.isDarkMode
|
||||
? theme.palette.common.white
|
||||
: theme.palette.grey[300] + 75
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }}
|
||||
alt=''
|
||||
src={item.src}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<item.icon size={25} color={item.color} />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{images?.length + (icons?.length || 0) > 3 && (
|
||||
<Typography sx={{ alignItems: 'center', display: 'flex', fontSize: '.9rem', fontWeight: 200 }}>
|
||||
+ {images.length - 3} More
|
||||
+ {images?.length + (icons?.length || 0) - 3} More
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
@@ -146,6 +170,7 @@ const ItemCard = ({ data, images, onClick }) => {
|
||||
ItemCard.propTypes = {
|
||||
data: PropTypes.object,
|
||||
images: PropTypes.array,
|
||||
icons: PropTypes.array,
|
||||
onClick: PropTypes.func
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ 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,
|
||||
border: `1px solid ${theme.customization?.isDarkMode ? theme.palette.grey[900] + 25 : theme.palette.primary[200] + 75}`,
|
||||
width: '300px',
|
||||
height: 'auto',
|
||||
padding: '10px',
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
|
||||
boxShadow: `rgba(0, 0, 0, 0.05) 0px 0px 0px 1px`,
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.primary.main
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: `rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px`
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
@@ -47,15 +47,17 @@ const AdditionalParamsDialog = ({ show, dialogProps, onCancel }) => {
|
||||
overflowX: 'hidden'
|
||||
}}
|
||||
>
|
||||
{inputParams.map((inputParam, index) => (
|
||||
<NodeInputHandler
|
||||
disabled={dialogProps.disabled}
|
||||
key={index}
|
||||
inputParam={inputParam}
|
||||
data={data}
|
||||
isAdditionalParams={true}
|
||||
/>
|
||||
))}
|
||||
{inputParams
|
||||
.filter((inputParam) => inputParam.display !== false)
|
||||
.map((inputParam, index) => (
|
||||
<NodeInputHandler
|
||||
disabled={dialogProps.disabled}
|
||||
key={index}
|
||||
inputParam={inputParam}
|
||||
data={data}
|
||||
isAdditionalParams={true}
|
||||
/>
|
||||
))}
|
||||
</PerfectScrollbar>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Box, Typography, OutlinedInput, DialogActions, Button, Dialog, DialogContent, DialogTitle, LinearProgress } from '@mui/material'
|
||||
import chatflowsApi from '@/api/chatflows'
|
||||
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
|
||||
import { IconX, IconSparkles, IconArrowLeft } from '@tabler/icons-react'
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
import { LoadingButton } from '@mui/lab'
|
||||
import generatorGIF from '@/assets/images/agentflow-generator.gif'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import assistantsApi from '@/api/assistants'
|
||||
import { baseURL } from '@/store/constant'
|
||||
import { initNode } from '@/utils/genericHelper'
|
||||
import DocStoreInputHandler from '@/views/docstore/DocStoreInputHandler'
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
const defaultInstructions = [
|
||||
{
|
||||
text: 'An agent that can autonomously search the web and generate report'
|
||||
},
|
||||
{
|
||||
text: 'Summarize a document'
|
||||
},
|
||||
{
|
||||
text: 'Generate response to user queries and send it to Slack'
|
||||
},
|
||||
{
|
||||
text: 'A team of agents that can handle all customer queries'
|
||||
}
|
||||
]
|
||||
|
||||
const AgentflowGeneratorDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const [customAssistantInstruction, setCustomAssistantInstruction] = useState('')
|
||||
const [generatedInstruction, setGeneratedInstruction] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [chatModelsComponents, setChatModelsComponents] = useState([])
|
||||
const [chatModelsOptions, setChatModelsOptions] = useState([])
|
||||
const [selectedChatModel, setSelectedChatModel] = useState({})
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const getChatModelsApi = useApi(assistantsApi.getChatModels)
|
||||
const { reactFlowInstance } = useContext(flowContext)
|
||||
const theme = useTheme()
|
||||
|
||||
// ==============================|| Snackbar ||============================== //
|
||||
const dispatch = useDispatch()
|
||||
useNotifier()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
useEffect(() => {
|
||||
if (getChatModelsApi.data) {
|
||||
setChatModelsComponents(getChatModelsApi.data)
|
||||
|
||||
// Set options
|
||||
const options = getChatModelsApi.data.map((chatModel) => ({
|
||||
label: chatModel.label,
|
||||
name: chatModel.name,
|
||||
imageSrc: `${baseURL}/api/v1/node-icon/${chatModel.name}`
|
||||
}))
|
||||
setChatModelsOptions(options)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getChatModelsApi.data])
|
||||
|
||||
// Simulate progress for the fake progress bar
|
||||
useEffect(() => {
|
||||
let timer
|
||||
if (loading) {
|
||||
setProgress(0)
|
||||
timer = setInterval(() => {
|
||||
setProgress((prevProgress) => {
|
||||
// Slowly increase to 95% to give the impression of work happening
|
||||
// Last 5% will complete when the actual work is done
|
||||
if (prevProgress >= 95) {
|
||||
clearInterval(timer)
|
||||
return 95
|
||||
}
|
||||
// Speed up in the middle, slow at the beginning and end
|
||||
const increment = prevProgress < 30 ? 3 : prevProgress < 60 ? 5 : prevProgress < 80 ? 2 : 0.5
|
||||
return Math.min(prevProgress + increment, 95)
|
||||
})
|
||||
}, 500)
|
||||
} else {
|
||||
// When loading is done, immediately set to 100%
|
||||
setProgress(100)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}
|
||||
}, [loading])
|
||||
|
||||
const onGenerate = async () => {
|
||||
if (!customAssistantInstruction.trim()) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const response = await chatflowsApi.generateAgentflow({
|
||||
question: customAssistantInstruction.trim(),
|
||||
selectedChatModel: selectedChatModel
|
||||
})
|
||||
|
||||
if (response.data && response.data.nodes && response.data.edges) {
|
||||
reactFlowInstance.setNodes(response.data.nodes)
|
||||
reactFlowInstance.setEdges(response.data.edges)
|
||||
onConfirm()
|
||||
} else {
|
||||
enqueueSnackbar({
|
||||
message: response.error || 'Failed to generate agentflow',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
enqueueSnackbar({
|
||||
message: error.response?.data?.message || 'Failed to generate agentflow',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// clear the state when dialog is closed
|
||||
useEffect(() => {
|
||||
if (!show) {
|
||||
setCustomAssistantInstruction('')
|
||||
setGeneratedInstruction('')
|
||||
setProgress(0)
|
||||
} else {
|
||||
getChatModelsApi.request()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [show])
|
||||
|
||||
const component = show ? (
|
||||
<>
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth={loading ? 'sm' : 'md'}
|
||||
open={show}
|
||||
onClose={loading ? null : onCancel}
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
{dialogProps.title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column' }}>
|
||||
<img src={generatorGIF} alt='Generating Agentflow' style={{ maxWidth: '100%', height: 'auto' }} />
|
||||
<Typography variant='h5' sx={{ mt: 2 }}>
|
||||
Generating your Agentflow...
|
||||
</Typography>
|
||||
<Box sx={{ width: '100%', mt: 2 }}>
|
||||
<LinearProgress
|
||||
variant='determinate'
|
||||
value={progress}
|
||||
sx={{
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
'& .MuiLinearProgress-bar': {
|
||||
background: 'linear-gradient(45deg, #FF6B6B 30%, #FF8E53 90%)',
|
||||
borderRadius: 5
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Typography variant='body2' color='text.secondary' align='center' sx={{ mt: 1 }}>
|
||||
{`${Math.round(progress)}%`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span>{dialogProps.description}</span>
|
||||
<div
|
||||
style={{
|
||||
display: 'block',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
marginTop: '25px'
|
||||
}}
|
||||
>
|
||||
{defaultInstructions.map((instruction, index) => {
|
||||
return (
|
||||
<Button
|
||||
size='small'
|
||||
key={index}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
mr: 1,
|
||||
mb: 1,
|
||||
borderRadius: '16px',
|
||||
border: 'none',
|
||||
backgroundColor: customization.isDarkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
'&:hover': {
|
||||
backgroundColor: customization.isDarkMode
|
||||
? 'rgba(255,255,255,0.1)'
|
||||
: 'rgba(0,0,0,0.06)',
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.15)'
|
||||
}
|
||||
}}
|
||||
variant='contained'
|
||||
color='inherit'
|
||||
onClick={() => {
|
||||
setCustomAssistantInstruction(instruction.text)
|
||||
setGeneratedInstruction('')
|
||||
}}
|
||||
>
|
||||
{instruction.text}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{!generatedInstruction && (
|
||||
<OutlinedInput
|
||||
sx={{ mt: 2, width: '100%' }}
|
||||
type={'text'}
|
||||
multiline={true}
|
||||
rows={12}
|
||||
disabled={loading}
|
||||
value={customAssistantInstruction}
|
||||
placeholder={'Describe your agent here'}
|
||||
onChange={(event) => setCustomAssistantInstruction(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
{generatedInstruction && (
|
||||
<OutlinedInput
|
||||
sx={{ mt: 2, width: '100%' }}
|
||||
type={'text'}
|
||||
multiline={true}
|
||||
rows={12}
|
||||
value={generatedInstruction}
|
||||
onChange={(event) => setGeneratedInstruction(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
Select model to generate agentflow<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
</div>
|
||||
<Dropdown
|
||||
key={JSON.stringify(selectedChatModel)}
|
||||
name={'chatModel'}
|
||||
options={chatModelsOptions ?? []}
|
||||
onSelect={(newValue) => {
|
||||
if (!newValue) {
|
||||
setSelectedChatModel({})
|
||||
} else {
|
||||
const foundChatComponent = chatModelsComponents.find((chatModel) => chatModel.name === newValue)
|
||||
if (foundChatComponent) {
|
||||
const chatModelId = `${foundChatComponent.name}_0`
|
||||
const clonedComponent = cloneDeep(foundChatComponent)
|
||||
const initChatModelData = initNode(clonedComponent, chatModelId)
|
||||
setSelectedChatModel(initChatModelData)
|
||||
}
|
||||
}
|
||||
}}
|
||||
value={selectedChatModel ? selectedChatModel?.name : 'choose an option'}
|
||||
/>
|
||||
</Box>
|
||||
{selectedChatModel && Object.keys(selectedChatModel).length > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
p: 0,
|
||||
mt: 1,
|
||||
mb: 1,
|
||||
border: 1,
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
borderRadius: 2
|
||||
}}
|
||||
>
|
||||
{(selectedChatModel.inputParams ?? [])
|
||||
.filter((inputParam) => !inputParam.hidden)
|
||||
.map((inputParam, index) => (
|
||||
<DocStoreInputHandler key={index} inputParam={inputParam} data={selectedChatModel} />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ pb: 3, pr: 3 }}>
|
||||
{loading ? null : (
|
||||
<>
|
||||
{!generatedInstruction && (
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
variant='contained'
|
||||
onClick={() => {
|
||||
onGenerate()
|
||||
}}
|
||||
sx={{
|
||||
background: 'linear-gradient(45deg, #FF6B6B 30%, #FF8E53 90%)',
|
||||
'&:hover': { background: 'linear-gradient(45deg, #FF8E53 30%, #FF6B6B 90%)' }
|
||||
}}
|
||||
startIcon={<IconSparkles size={20} />}
|
||||
disabled={
|
||||
loading ||
|
||||
!customAssistantInstruction.trim() ||
|
||||
!selectedChatModel ||
|
||||
!Object.keys(selectedChatModel).length
|
||||
}
|
||||
>
|
||||
Generate
|
||||
</LoadingButton>
|
||||
)}
|
||||
{generatedInstruction && (
|
||||
<Button
|
||||
variant='outlined'
|
||||
startIcon={<IconArrowLeft size={20} />}
|
||||
onClick={() => {
|
||||
setGeneratedInstruction('')
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
AgentflowGeneratorDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onConfirm: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default AgentflowGeneratorDialog
|
||||
@@ -58,17 +58,19 @@ const ConditionDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{inputParam.tabs.map((inputChildParam, index) => (
|
||||
<TabPanel key={index} value={tabValue} index={index}>
|
||||
<NodeInputHandler
|
||||
disabled={inputChildParam.disabled}
|
||||
inputParam={inputChildParam}
|
||||
data={data}
|
||||
isAdditionalParams={true}
|
||||
disablePadding={true}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
{inputParam.tabs
|
||||
.filter((inputParam) => inputParam.display !== false)
|
||||
.map((inputChildParam, index) => (
|
||||
<TabPanel key={index} value={tabValue} index={index}>
|
||||
<NodeInputHandler
|
||||
disabled={inputChildParam.disabled}
|
||||
inputParam={inputChildParam}
|
||||
data={data}
|
||||
isAdditionalParams={true}
|
||||
disablePadding={true}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// MUI
|
||||
import { Button, Dialog, DialogActions, DialogContent, Typography, Box } from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
// Project Import
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
|
||||
// TipTap
|
||||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import { mergeAttributes } from '@tiptap/core'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Mention from '@tiptap/extension-mention'
|
||||
import { suggestionOptions } from '@/ui-component/input/suggestionOption'
|
||||
import { getAvailableNodesForVariable } from '@/utils/genericHelper'
|
||||
|
||||
// Store
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||
|
||||
// Add styled component for editor wrapper
|
||||
const StyledEditorContent = styled(EditorContent)(({ theme, rows }) => ({
|
||||
'& .ProseMirror': {
|
||||
padding: '0px 14px',
|
||||
height: rows ? `${rows * 1.4375}rem` : '2.4rem',
|
||||
overflowY: rows ? 'auto' : 'hidden',
|
||||
overflowX: rows ? 'auto' : 'hidden',
|
||||
lineHeight: rows ? '1.4375em' : '0.875em',
|
||||
fontWeight: 500,
|
||||
color: theme.palette.grey[900],
|
||||
border: `1px solid ${theme.palette.textBackground.border}`,
|
||||
borderRadius: '10px',
|
||||
backgroundColor: theme.palette.textBackground.main,
|
||||
boxSizing: 'border-box',
|
||||
whiteSpace: rows ? 'pre-wrap' : 'nowrap',
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.text.primary,
|
||||
cursor: 'text'
|
||||
},
|
||||
'&:focus': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: `0 0 0 0px ${theme.palette.primary.main}`,
|
||||
outline: 'none'
|
||||
},
|
||||
'&[disabled]': {
|
||||
backgroundColor: theme.palette.action.disabledBackground,
|
||||
color: theme.palette.action.disabled
|
||||
},
|
||||
// Placeholder for first paragraph when editor is empty
|
||||
'& p.is-editor-empty:first-of-type::before': {
|
||||
content: 'attr(data-placeholder)',
|
||||
float: 'left',
|
||||
color: theme.palette.text.primary,
|
||||
opacity: 0.4,
|
||||
pointerEvents: 'none',
|
||||
height: 0
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
// define your extension array
|
||||
const extensions = (availableNodesForVariable, availableState, acceptNodeOutputAsVariable, nodes, nodeData, isNodeInsideInteration) => [
|
||||
StarterKit,
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'variable'
|
||||
},
|
||||
renderHTML({ options, node }) {
|
||||
return [
|
||||
'span',
|
||||
mergeAttributes(this.HTMLAttributes, options.HTMLAttributes),
|
||||
`${options.suggestion.char} ${node.attrs.label ?? node.attrs.id} }}`
|
||||
]
|
||||
},
|
||||
suggestion: suggestionOptions(
|
||||
availableNodesForVariable,
|
||||
availableState,
|
||||
acceptNodeOutputAsVariable,
|
||||
nodes,
|
||||
nodeData,
|
||||
isNodeInsideInteration
|
||||
),
|
||||
deleteTriggerWithBackspace: true
|
||||
})
|
||||
]
|
||||
|
||||
const ExpandRichInputDialog = ({ show, dialogProps, onCancel, onInputHintDialogClicked, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [inputParam, setInputParam] = useState(null)
|
||||
const [availableNodesForVariable, setAvailableNodesForVariable] = useState([])
|
||||
const [availableState, setAvailableState] = useState([])
|
||||
const [nodeData, setNodeData] = useState({})
|
||||
const [isNodeInsideInteration, setIsNodeInsideInteration] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.value) {
|
||||
setInputValue(dialogProps.value)
|
||||
}
|
||||
if (dialogProps.inputParam) {
|
||||
setInputParam(dialogProps.inputParam)
|
||||
}
|
||||
|
||||
return () => {
|
||||
setInputValue('')
|
||||
setInputParam(null)
|
||||
}
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialogProps.disabled && dialogProps.nodes && dialogProps.edges && dialogProps.nodeId && inputParam) {
|
||||
const nodesForVariable = inputParam?.acceptVariable
|
||||
? getAvailableNodesForVariable(dialogProps.nodes, dialogProps.edges, dialogProps.nodeId, inputParam.id)
|
||||
: []
|
||||
setAvailableNodesForVariable(nodesForVariable)
|
||||
|
||||
const startAgentflowNode = dialogProps.nodes.find((node) => node.data.name === 'startAgentflow')
|
||||
const state = startAgentflowNode?.data?.inputs?.startState
|
||||
setAvailableState(state)
|
||||
|
||||
const agentflowNode = dialogProps.nodes.find((node) => node.data.id === dialogProps.nodeId)
|
||||
setNodeData(agentflowNode?.data)
|
||||
|
||||
setIsNodeInsideInteration(dialogProps.nodes.find((node) => node.data.id === dialogProps.nodeId)?.extent === 'parent')
|
||||
}
|
||||
}, [dialogProps.disabled, inputParam, dialogProps.nodes, dialogProps.edges, dialogProps.nodeId])
|
||||
|
||||
const editor = useEditor(
|
||||
{
|
||||
extensions: [
|
||||
...extensions(
|
||||
availableNodesForVariable,
|
||||
availableState,
|
||||
inputParam?.acceptNodeOutputAsVariable,
|
||||
dialogProps.nodes,
|
||||
nodeData,
|
||||
isNodeInsideInteration
|
||||
),
|
||||
Placeholder.configure({ placeholder: inputParam?.placeholder })
|
||||
],
|
||||
content: inputValue,
|
||||
onUpdate: ({ editor }) => {
|
||||
setInputValue(editor.getHTML())
|
||||
},
|
||||
editable: !dialogProps.disabled
|
||||
},
|
||||
[availableNodesForVariable]
|
||||
)
|
||||
|
||||
// Focus the editor when dialog opens
|
||||
useEffect(() => {
|
||||
if (show && editor) {
|
||||
setTimeout(() => {
|
||||
editor.commands.focus()
|
||||
}, 100)
|
||||
}
|
||||
}, [show, editor])
|
||||
|
||||
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 && (
|
||||
<div style={{ flex: 70, width: '100%' }}>
|
||||
<div style={{ marginBottom: '10px', display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography variant='h4'>{inputParam.label}</Typography>
|
||||
<div style={{ flex: 1 }} />
|
||||
{inputParam.hint && (
|
||||
<Button
|
||||
sx={{ p: 0, px: 2 }}
|
||||
color='secondary'
|
||||
variant='text'
|
||||
onClick={() => {
|
||||
onInputHintDialogClicked(inputParam.hint)
|
||||
}}
|
||||
>
|
||||
{inputParam.hint.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<PerfectScrollbar
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
height: '100%',
|
||||
maxHeight: 'calc(100vh - 220px)',
|
||||
overflowX: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ mt: 1, border: '' }}>
|
||||
<StyledEditorContent editor={editor} rows={15} />
|
||||
</Box>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
|
||||
<StyledButton disabled={dialogProps.disabled} variant='contained' onClick={() => onConfirm(inputValue, inputParam.name)}>
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
ExpandRichInputDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func,
|
||||
onInputHintDialogClicked: PropTypes.func
|
||||
}
|
||||
|
||||
export default ExpandRichInputDialog
|
||||
@@ -44,7 +44,13 @@ const ExportAsTemplateDialog = ({ show, dialogProps, onCancel }) => {
|
||||
useEffect(() => {
|
||||
if (dialogProps.chatflow) {
|
||||
setName(dialogProps.chatflow.name)
|
||||
setFlowType(dialogProps.chatflow.type === 'MULTIAGENT' ? 'Agentflow' : 'Chatflow')
|
||||
if (dialogProps.chatflow.type === 'AGENTFLOW') {
|
||||
setFlowType('AgentflowV2')
|
||||
} else if (dialogProps.chatflow.type === 'MULTIAGENT') {
|
||||
setFlowType('Agentflow')
|
||||
} else if (dialogProps.chatflow.type === 'CHATFLOW') {
|
||||
setFlowType('Chatflow')
|
||||
}
|
||||
}
|
||||
|
||||
if (dialogProps.tool) {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
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'
|
||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||
import { CodeBlock } from '@/ui-component/markdown/CodeBlock'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
|
||||
|
||||
const InputHintDialog = ({ show, dialogProps, onCancel }) => {
|
||||
@@ -24,29 +19,7 @@ const InputHintDialog = ({ show, dialogProps, onCancel }) => {
|
||||
{dialogProps.label}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<MemoizedReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeMathjax, rehypeRaw]}
|
||||
components={{
|
||||
code({ inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
return !inline ? (
|
||||
<CodeBlock
|
||||
isDialog={true}
|
||||
language={(match && match[1]) || ''}
|
||||
value={String(children).replace(/\n$/, '')}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{dialogProps?.value}
|
||||
</MemoizedReactMarkdown>
|
||||
<MemoizedReactMarkdown>{dialogProps?.value}</MemoizedReactMarkdown>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
@@ -7,10 +7,11 @@ import PropTypes from 'prop-types'
|
||||
import { Button, Dialog, DialogContent, DialogTitle } from '@mui/material'
|
||||
import { TableViewOnly } from '@/ui-component/table/Table'
|
||||
import { IconBook2 } from '@tabler/icons-react'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Store
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||
import { baseURL } from '@/store/constant'
|
||||
import { baseURL, AGENTFLOW_ICONS } from '@/store/constant'
|
||||
|
||||
// API
|
||||
import configApi from '@/api/config'
|
||||
@@ -19,9 +20,17 @@ import useApi from '@/hooks/useApi'
|
||||
const NodeInfoDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const dispatch = useDispatch()
|
||||
const theme = useTheme()
|
||||
|
||||
const getNodeConfigApi = useApi(configApi.getNodeConfig)
|
||||
|
||||
const renderIcon = (node) => {
|
||||
const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === node.name)
|
||||
|
||||
if (!foundIcon) return null
|
||||
return <foundIcon.icon size={24} color={'white'} />
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.data) {
|
||||
getNodeConfigApi.request(dialogProps.data)
|
||||
@@ -48,27 +57,46 @@ const NodeInfoDialog = ({ show, dialogProps, onCancel }) => {
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
{dialogProps.data && dialogProps.data.name && dialogProps.data.label && (
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
marginRight: 10,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
{dialogProps.data.color && !dialogProps.data.icon ? (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 7,
|
||||
borderRadius: '50%',
|
||||
objectFit: 'contain'
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.largeAvatar,
|
||||
borderRadius: '15px',
|
||||
backgroundColor: dialogProps.data.color,
|
||||
cursor: 'grab',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: dialogProps.data.color,
|
||||
marginRight: 10
|
||||
}}
|
||||
alt={dialogProps.data.name}
|
||||
src={`${baseURL}/api/v1/node-icon/${dialogProps.data.name}`}
|
||||
/>
|
||||
</div>
|
||||
>
|
||||
{renderIcon(dialogProps.data)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
marginRight: 10,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 7,
|
||||
borderRadius: '50%',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
alt={dialogProps.data.name}
|
||||
src={`${baseURL}/api/v1/node-icon/${dialogProps.data.name}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginLeft: 10 }}>
|
||||
{dialogProps.data.label}
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
|
||||
@@ -2,12 +2,6 @@ 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,
|
||||
@@ -44,7 +38,6 @@ 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'
|
||||
@@ -536,30 +529,7 @@ const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<MemoizedReactMarkdown>{selectedPrompt?.readme}</MemoizedReactMarkdown>
|
||||
</div>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
@@ -3,10 +3,6 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useState, useEffect, forwardRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import moment from 'moment'
|
||||
import rehypeMathjax from 'rehype-mathjax'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import axios from 'axios'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
@@ -42,7 +38,6 @@ import { IconTool, IconDeviceSdCard, IconFileExport, IconEraser, IconX, IconDown
|
||||
|
||||
// Project import
|
||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||
import { CodeBlock } from '@/ui-component/markdown/CodeBlock'
|
||||
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
|
||||
import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
@@ -806,33 +801,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<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={dialogProps.chatflow.id}
|
||||
isDialog={true}
|
||||
language={(match && match[1]) || ''}
|
||||
value={String(children).replace(/\n$/, '')}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.data}
|
||||
</MemoizedReactMarkdown>
|
||||
)
|
||||
return <MemoizedReactMarkdown chatflowid={dialogProps.chatflow.id}>{item.data}</MemoizedReactMarkdown>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1304,44 +1273,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
)}
|
||||
{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={
|
||||
dialogProps.chatflow.id
|
||||
}
|
||||
isDialog={true}
|
||||
language={
|
||||
(match && match[1]) ||
|
||||
''
|
||||
}
|
||||
value={String(
|
||||
children
|
||||
).replace(/\n$/, '')}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<code
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
}}
|
||||
chatflowid={dialogProps.chatflow.id}
|
||||
>
|
||||
{agent.messages.length > 1
|
||||
? agent.messages.join('\\n')
|
||||
@@ -1468,30 +1400,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
</div>
|
||||
)}
|
||||
<div className='markdownanswer'>
|
||||
{/* Messages are being rendered in Markdown format */}
|
||||
<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={dialogProps.chatflow.id}
|
||||
isDialog={true}
|
||||
language={(match && match[1]) || ''}
|
||||
value={String(children).replace(/\n$/, '')}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MemoizedReactMarkdown chatflowid={dialogProps.chatflow.id}>
|
||||
{message.message}
|
||||
</MemoizedReactMarkdown>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, Fragment } from 'react'
|
||||
import { useState, useEffect, useContext, Fragment } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import axios from 'axios'
|
||||
@@ -6,13 +6,15 @@ import axios from 'axios'
|
||||
// Material
|
||||
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
|
||||
import { Popper, CircularProgress, TextField, Box, Typography } from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import { useTheme, styled } from '@mui/material/styles'
|
||||
|
||||
// API
|
||||
import credentialsApi from '@/api/credentials'
|
||||
|
||||
// const
|
||||
import { baseURL } from '@/store/constant'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
import { getAvailableNodesForVariable } from '@/utils/genericHelper'
|
||||
|
||||
const StyledPopper = styled(Popper)({
|
||||
boxShadow: '0px 8px 10px -5px rgb(0 0 0 / 20%), 0px 16px 24px 2px rgb(0 0 0 / 14%), 0px 6px 30px 5px rgb(0 0 0 / 12%)',
|
||||
@@ -26,15 +28,16 @@ const StyledPopper = styled(Popper)({
|
||||
}
|
||||
})
|
||||
|
||||
const fetchList = async ({ name, nodeData }) => {
|
||||
const loadMethod = nodeData.inputParams.find((param) => param.name === name)?.loadMethod
|
||||
const fetchList = async ({ name, nodeData, previousNodes, currentNode }) => {
|
||||
const selectedParam = nodeData.inputParams.find((param) => param.name === name)
|
||||
const loadMethod = selectedParam?.loadMethod
|
||||
const username = localStorage.getItem('username')
|
||||
const password = localStorage.getItem('password')
|
||||
|
||||
let lists = await axios
|
||||
.post(
|
||||
`${baseURL}/api/v1/node-load-method/${nodeData.name}`,
|
||||
{ ...nodeData, loadMethod },
|
||||
{ ...nodeData, loadMethod, previousNodes, currentNode },
|
||||
{
|
||||
auth: username && password ? { username, password } : undefined,
|
||||
headers: { 'Content-type': 'application/json', 'x-request-from': 'internal' }
|
||||
@@ -63,6 +66,7 @@ export const AsyncDropdown = ({
|
||||
multiple = false
|
||||
}) => {
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const theme = useTheme()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [options, setOptions] = useState([])
|
||||
@@ -82,6 +86,7 @@ export const AsyncDropdown = ({
|
||||
const getDefaultOptionValue = () => (multiple ? [] : '')
|
||||
const addNewOption = [{ label: '- Create New -', name: '-create-' }]
|
||||
let [internalValue, setInternalValue] = useState(value ?? 'choose an option')
|
||||
const { reactFlowInstance } = useContext(flowContext)
|
||||
|
||||
const fetchCredentialList = async () => {
|
||||
try {
|
||||
@@ -112,7 +117,45 @@ export const AsyncDropdown = ({
|
||||
setLoading(true)
|
||||
;(async () => {
|
||||
const fetchData = async () => {
|
||||
let response = credentialNames.length ? await fetchCredentialList() : await fetchList({ name, nodeData })
|
||||
let response = []
|
||||
if (credentialNames.length) {
|
||||
response = await fetchCredentialList()
|
||||
} else {
|
||||
const body = {
|
||||
name,
|
||||
nodeData
|
||||
}
|
||||
if (reactFlowInstance) {
|
||||
const previousNodes = getAvailableNodesForVariable(
|
||||
reactFlowInstance.getNodes(),
|
||||
reactFlowInstance.getEdges(),
|
||||
nodeData.id,
|
||||
`${nodeData.id}-input-${name}-${nodeData.inputParams.find((param) => param.name === name)?.type || ''}`,
|
||||
true
|
||||
).map((node) => ({ id: node.id, name: node.data.name, label: node.data.label, inputs: node.data.inputs }))
|
||||
|
||||
let currentNode = reactFlowInstance.getNodes().find((node) => node.id === nodeData.id)
|
||||
if (currentNode) {
|
||||
currentNode = {
|
||||
id: currentNode.id,
|
||||
name: currentNode.data.name,
|
||||
label: currentNode.data.label,
|
||||
inputs: currentNode.data.inputs
|
||||
}
|
||||
body.currentNode = currentNode
|
||||
}
|
||||
|
||||
body.previousNodes = previousNodes
|
||||
}
|
||||
|
||||
response = await fetchList(body)
|
||||
}
|
||||
for (let j = 0; j < response.length; j += 1) {
|
||||
if (response[j].imageSrc) {
|
||||
const imageSrc = `${baseURL}/api/v1/node-icon/${response[j].name}`
|
||||
response[j].imageSrc = imageSrc
|
||||
}
|
||||
}
|
||||
if (isCreateNewOption) setOptions([...response, ...addNewOption])
|
||||
else setOptions([...response])
|
||||
setLoading(false)
|
||||
@@ -164,24 +207,70 @@ export const AsyncDropdown = ({
|
||||
}}
|
||||
PopperComponent={StyledPopper}
|
||||
loading={loading}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
value={internalValue}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<Fragment>
|
||||
{loading ? <CircularProgress color='inherit' size={20} /> : null}
|
||||
{params.InputProps.endAdornment}
|
||||
</Fragment>
|
||||
)
|
||||
}}
|
||||
sx={{ height: '100%', '& .MuiInputBase-root': { height: '100%' } }}
|
||||
/>
|
||||
)}
|
||||
renderInput={(params) => {
|
||||
const matchingOptions = multiple
|
||||
? findMatchingOptions(options, internalValue)
|
||||
: [findMatchingOptions(options, internalValue)].filter(Boolean)
|
||||
return (
|
||||
<TextField
|
||||
{...params}
|
||||
value={internalValue}
|
||||
sx={{
|
||||
height: '100%',
|
||||
'& .MuiInputBase-root': {
|
||||
height: '100%',
|
||||
'& fieldset': {
|
||||
borderColor: theme.palette.grey[900] + 25
|
||||
}
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<>
|
||||
{matchingOptions.map((option) =>
|
||||
option?.imageSrc ? (
|
||||
<Box
|
||||
key={option.name}
|
||||
component='img'
|
||||
src={option.imageSrc}
|
||||
alt={option.label || 'Selected Option'}
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
marginRight: 0.5
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
{params.InputProps.startAdornment}
|
||||
</>
|
||||
),
|
||||
endAdornment: (
|
||||
<Fragment>
|
||||
{loading ? <CircularProgress color='inherit' size={20} /> : null}
|
||||
{params.InputProps.endAdornment}
|
||||
</Fragment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
renderOption={(props, option) => (
|
||||
<Box component='li' {...props}>
|
||||
<Box component='li' {...props} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{option.imageSrc && (
|
||||
<img
|
||||
src={option.imageSrc}
|
||||
alt={option.description}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
padding: 1,
|
||||
borderRadius: '50%'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant='h5'>{option.label}</Typography>
|
||||
{option.description && (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux'
|
||||
|
||||
import { Popper, FormControl, TextField, Box, Typography } from '@mui/material'
|
||||
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import { useTheme, styled } from '@mui/material/styles'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const StyledPopper = styled(Popper)({
|
||||
@@ -23,6 +23,7 @@ export const Dropdown = ({ name, value, loading, options, onSelect, disabled = f
|
||||
const findMatchingOptions = (options = [], value) => options.find((option) => option.name === value)
|
||||
const getDefaultOptionValue = () => ''
|
||||
let [internalValue, setInternalValue] = useState(value ?? 'choose an option')
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
|
||||
@@ -49,7 +50,12 @@ export const Dropdown = ({ name, value, loading, options, onSelect, disabled = f
|
||||
value={internalValue}
|
||||
sx={{
|
||||
height: '100%',
|
||||
'& .MuiInputBase-root': { height: '100%' }
|
||||
'& .MuiInputBase-root': {
|
||||
height: '100%',
|
||||
'& fieldset': {
|
||||
borderColor: theme.palette.grey[900] + 25
|
||||
}
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux'
|
||||
|
||||
import { Popper, FormControl, TextField, Box, Typography } from '@mui/material'
|
||||
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import { useTheme, styled } from '@mui/material/styles'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const StyledPopper = styled(Popper)({
|
||||
@@ -28,6 +28,7 @@ export const MultiDropdown = ({ name, value, options, onSelect, formControlSx =
|
||||
}
|
||||
const getDefaultOptionValue = () => []
|
||||
let [internalValue, setInternalValue] = useState(value ?? [])
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<FormControl sx={{ mt: 1, width: '100%', ...formControlSx }} size='small'>
|
||||
@@ -54,7 +55,19 @@ export const MultiDropdown = ({ name, value, options, onSelect, formControlSx =
|
||||
}}
|
||||
PopperComponent={StyledPopper}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} value={internalValue} sx={{ height: '100%', '& .MuiInputBase-root': { height: '100%' } }} />
|
||||
<TextField
|
||||
{...params}
|
||||
value={internalValue}
|
||||
sx={{
|
||||
height: '100%',
|
||||
'& .MuiInputBase-root': {
|
||||
height: '100%',
|
||||
'& fieldset': {
|
||||
borderColor: theme.palette.grey[900] + 25
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
renderOption={(props, option) => (
|
||||
<Box component='li' {...props}>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const CodeEditor = ({
|
||||
'.cm-content':
|
||||
lang !== 'js'
|
||||
? {
|
||||
fontFamily: 'Roboto, sans-serif',
|
||||
fontFamily: `'Inter', 'Roboto', 'Arial', sans-serif`,
|
||||
fontSize: '0.95rem',
|
||||
letterSpacing: '0em',
|
||||
fontWeight: 400,
|
||||
@@ -47,11 +47,7 @@ export const CodeEditor = ({
|
||||
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]
|
||||
}
|
||||
extensions={[lang === 'js' ? javascript({ jsx: true }) : json(), EditorView.lineWrapping, customStyle]}
|
||||
onChange={onValueChange}
|
||||
readOnly={disabled}
|
||||
editable={!disabled}
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
||||
import { Box, Button, FormControl, ListItem, ListItemAvatar, ListItemText, MenuItem, Select, Typography } from '@mui/material'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Project Imports
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
@@ -310,6 +311,7 @@ const FollowUpPrompts = ({ dialogProps }) => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useNotifier()
|
||||
const theme = useTheme()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
@@ -473,7 +475,16 @@ const FollowUpPrompts = ({ dialogProps }) => {
|
||||
<>
|
||||
<Typography variant='h5'>Providers</Typography>
|
||||
<FormControl fullWidth>
|
||||
<Select size='small' value={selectedProvider} onChange={handleSelectedProviderChange}>
|
||||
<Select
|
||||
size='small'
|
||||
value={selectedProvider}
|
||||
onChange={handleSelectedProviderChange}
|
||||
sx={{
|
||||
'& .MuiSvgIcon-root': {
|
||||
color: theme?.customization?.isDarkMode ? '#fff' : 'inherit'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Object.values(followUpPromptsOptions).map((provider) => (
|
||||
<MenuItem key={provider.name} value={provider.name}>
|
||||
{provider.label}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logo from '@/assets/images/flowise_logo.png'
|
||||
import logoDark from '@/assets/images/flowise_logo_dark.png'
|
||||
import logo from '@/assets/images/flowise_white.svg'
|
||||
import logoDark from '@/assets/images/flowise_dark.svg'
|
||||
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
@@ -9,7 +9,7 @@ const Logo = () => {
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
return (
|
||||
<div style={{ alignItems: 'center', display: 'flex', flexDirection: 'row' }}>
|
||||
<div style={{ alignItems: 'center', display: 'flex', flexDirection: 'row', marginLeft: '10px' }}>
|
||||
<img
|
||||
style={{ objectFit: 'contain', height: 'auto', width: 150 }}
|
||||
src={customization.isDarkMode ? logoDark : logo}
|
||||
|
||||
@@ -43,6 +43,38 @@ const OverrideConfigTable = ({ columns, onToggle, rows, sx }) => {
|
||||
onToggle(row, enabled)
|
||||
}
|
||||
|
||||
const renderCellContent = (key, row) => {
|
||||
if (key === 'enabled') {
|
||||
return <SwitchInput onChange={(enabled) => handleChange(enabled, row)} value={row.enabled} />
|
||||
} else if (key === 'type' && row.schema) {
|
||||
// If there's schema information, add a tooltip
|
||||
const schemaContent =
|
||||
'[<br>' +
|
||||
row.schema
|
||||
.map(
|
||||
(item) =>
|
||||
` ${JSON.stringify(
|
||||
{
|
||||
[item.name]: item.type
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
.join(',<br>') +
|
||||
'<br>]'
|
||||
|
||||
return (
|
||||
<Stack direction='row' alignItems='center' spacing={1}>
|
||||
<Typography>{row[key]}</Typography>
|
||||
<TooltipWithParser title={`<div>Schema:<br/>${schemaContent}</div>`} />
|
||||
</Stack>
|
||||
)
|
||||
} else {
|
||||
return row[key]
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size='small' sx={{ minWidth: 650, ...sx }} aria-label='simple table'>
|
||||
@@ -57,16 +89,8 @@ const OverrideConfigTable = ({ columns, onToggle, rows, sx }) => {
|
||||
{rows.map((row, index) => (
|
||||
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
{Object.keys(row).map((key, index) => {
|
||||
if (key !== 'id') {
|
||||
return (
|
||||
<TableCell key={index}>
|
||||
{key === 'enabled' ? (
|
||||
<SwitchInput onChange={(enabled) => handleChange(enabled, row)} value={row.enabled} />
|
||||
) : (
|
||||
row[key]
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
if (key !== 'id' && key !== 'schema') {
|
||||
return <TableCell key={index}>{renderCellContent(key, row)}</TableCell>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
@@ -169,7 +193,7 @@ const OverrideConfig = ({ dialogProps }) => {
|
||||
const seenNodes = new Set()
|
||||
|
||||
nodes.forEach((item) => {
|
||||
const { node, nodeId, label, name, type } = item
|
||||
const { node, nodeId, label, name, type, schema } = item
|
||||
seenNodes.add(node)
|
||||
|
||||
if (!result[node]) {
|
||||
@@ -186,7 +210,7 @@ const OverrideConfig = ({ dialogProps }) => {
|
||||
|
||||
if (!result[node].nodeIds.includes(nodeId)) result[node].nodeIds.push(nodeId)
|
||||
|
||||
const param = { label, name, type }
|
||||
const param = { label, name, type, schema }
|
||||
|
||||
if (!result[node].params.some((existingParam) => JSON.stringify(existingParam) === JSON.stringify(param))) {
|
||||
result[node].params.push(param)
|
||||
@@ -395,7 +419,9 @@ const OverrideConfig = ({ dialogProps }) => {
|
||||
rows={nodeOverrides[nodeLabel]}
|
||||
columns={
|
||||
nodeOverrides[nodeLabel].length > 0
|
||||
? Object.keys(nodeOverrides[nodeLabel][0])
|
||||
? Object.keys(nodeOverrides[nodeLabel][0]).filter(
|
||||
(key) => key !== 'schema' && key !== 'id'
|
||||
)
|
||||
: []
|
||||
}
|
||||
onToggle={(property, status) =>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba
|
||||
// material-ui
|
||||
import { Typography, Box, Button, FormControl, ListItem, ListItemAvatar, ListItemText, MenuItem, Select } from '@mui/material'
|
||||
import { IconX } from '@tabler/icons-react'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Project import
|
||||
import CredentialInputHandler from '@/views/canvas/CredentialInputHandler'
|
||||
@@ -242,6 +243,7 @@ const SpeechToText = ({ dialogProps }) => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useNotifier()
|
||||
const theme = useTheme()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
@@ -349,7 +351,16 @@ const SpeechToText = ({ dialogProps }) => {
|
||||
<Box fullWidth sx={{ mb: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography>Providers</Typography>
|
||||
<FormControl fullWidth>
|
||||
<Select size='small' value={selectedProvider} onChange={handleProviderChange}>
|
||||
<Select
|
||||
size='small'
|
||||
value={selectedProvider}
|
||||
onChange={handleProviderChange}
|
||||
sx={{
|
||||
'& .MuiSvgIcon-root': {
|
||||
color: theme?.customization?.isDarkMode ? '#fff' : 'inherit'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value='none'>None</MenuItem>
|
||||
{Object.values(speechToTextProviders).map((provider) => (
|
||||
<MenuItem key={provider.name} value={provider.name}>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { FormControl, OutlinedInput, InputBase, Popover } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import SelectVariable from '@/ui-component/json/SelectVariable'
|
||||
import { getAvailableNodesForVariable } from '@/utils/genericHelper'
|
||||
|
||||
export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disabled = false }) => {
|
||||
const theme = useTheme()
|
||||
const [myValue, setMyValue] = useState(value ?? '')
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [availableNodesForVariable, setAvailableNodesForVariable] = useState([])
|
||||
@@ -71,7 +73,7 @@ export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disab
|
||||
style: {
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
color: '#212121'
|
||||
color: 'inherit'
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
@@ -108,6 +110,11 @@ export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disab
|
||||
height: inputParam.rows ? '90px' : 'inherit'
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: theme.palette.grey[900] + 25
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import { mergeAttributes } from '@tiptap/core'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import { Box } from '@mui/material'
|
||||
import Mention from '@tiptap/extension-mention'
|
||||
import { suggestionOptions } from './suggestionOption'
|
||||
import { getAvailableNodesForVariable } from '@/utils/genericHelper'
|
||||
|
||||
// define your extension array
|
||||
const extensions = (availableNodesForVariable, availableState, acceptNodeOutputAsVariable, nodes, nodeData, isNodeInsideInteration) => [
|
||||
StarterKit,
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'variable'
|
||||
},
|
||||
renderHTML({ options, node }) {
|
||||
return [
|
||||
'span',
|
||||
mergeAttributes(this.HTMLAttributes, options.HTMLAttributes),
|
||||
`${options.suggestion.char} ${node.attrs.label ?? node.attrs.id} }}`
|
||||
]
|
||||
},
|
||||
suggestion: suggestionOptions(
|
||||
availableNodesForVariable,
|
||||
availableState,
|
||||
acceptNodeOutputAsVariable,
|
||||
nodes,
|
||||
nodeData,
|
||||
isNodeInsideInteration
|
||||
),
|
||||
deleteTriggerWithBackspace: true
|
||||
})
|
||||
]
|
||||
|
||||
// Add styled component for editor wrapper
|
||||
const StyledEditorContent = styled(EditorContent)(({ theme, rows }) => ({
|
||||
'& .ProseMirror': {
|
||||
padding: '0px 14px',
|
||||
height: rows ? `${rows * 1.4375}rem` : '2.4rem',
|
||||
overflowY: rows ? 'auto' : 'hidden',
|
||||
overflowX: rows ? 'auto' : 'hidden',
|
||||
lineHeight: rows ? '1.4375em' : '0.875em',
|
||||
fontWeight: 500,
|
||||
color: theme.palette.grey[900],
|
||||
border: `1px solid ${theme.palette.grey[900] + 25}`,
|
||||
borderRadius: '10px',
|
||||
backgroundColor: theme.palette.textBackground.main,
|
||||
boxSizing: 'border-box',
|
||||
whiteSpace: rows ? 'pre-wrap' : 'nowrap',
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.text.primary,
|
||||
cursor: 'text'
|
||||
},
|
||||
'&:focus': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
outline: 'none'
|
||||
},
|
||||
'&[disabled]': {
|
||||
backgroundColor: theme.palette.action.disabledBackground,
|
||||
color: theme.palette.action.disabled
|
||||
},
|
||||
// Placeholder for first paragraph when editor is empty
|
||||
'& p.is-editor-empty:first-of-type::before': {
|
||||
content: 'attr(data-placeholder)',
|
||||
float: 'left',
|
||||
color: theme.palette.text.primary,
|
||||
opacity: 0.4,
|
||||
pointerEvents: 'none',
|
||||
height: 0
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
export const RichInput = ({ inputParam, value, nodes, edges, nodeId, onChange, disabled = false }) => {
|
||||
const [availableNodesForVariable, setAvailableNodesForVariable] = useState([])
|
||||
const [availableState, setAvailableState] = useState([])
|
||||
const [nodeData, setNodeData] = useState({})
|
||||
const [isNodeInsideInteration, setIsNodeInsideInteration] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!disabled && nodes && edges && nodeId && inputParam) {
|
||||
const nodesForVariable = inputParam?.acceptVariable ? getAvailableNodesForVariable(nodes, edges, nodeId, inputParam.id) : []
|
||||
setAvailableNodesForVariable(nodesForVariable)
|
||||
|
||||
const startAgentflowNode = nodes.find((node) => node.data.name === 'startAgentflow')
|
||||
const state = startAgentflowNode?.data?.inputs?.startState
|
||||
setAvailableState(state)
|
||||
|
||||
const agentflowNode = nodes.find((node) => node.data.id === nodeId)
|
||||
setNodeData(agentflowNode?.data)
|
||||
|
||||
setIsNodeInsideInteration(nodes.find((node) => node.data.id === nodeId)?.extent === 'parent')
|
||||
}
|
||||
}, [disabled, inputParam, nodes, edges, nodeId])
|
||||
|
||||
const editor = useEditor(
|
||||
{
|
||||
extensions: [
|
||||
...extensions(
|
||||
availableNodesForVariable,
|
||||
availableState,
|
||||
inputParam?.acceptNodeOutputAsVariable,
|
||||
nodes,
|
||||
nodeData,
|
||||
isNodeInsideInteration
|
||||
),
|
||||
Placeholder.configure({ placeholder: inputParam?.placeholder })
|
||||
],
|
||||
content: value,
|
||||
onUpdate: ({ editor }) => {
|
||||
onChange(editor.getHTML())
|
||||
},
|
||||
editable: !disabled
|
||||
},
|
||||
[availableNodesForVariable]
|
||||
)
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 1, border: '' }}>
|
||||
<StyledEditorContent editor={editor} rows={inputParam?.rows} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
RichInput.propTypes = {
|
||||
inputParam: PropTypes.object,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
nodes: PropTypes.array,
|
||||
edges: PropTypes.array,
|
||||
nodeId: PropTypes.string
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { List, ListItem, ListItemButton, Paper, Typography, Divider } from '@mui/material'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const SuggestionList = forwardRef((props, ref) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
// Configure tippy to auto-adjust placement
|
||||
const tippyOptions = {
|
||||
placement: 'bottom-start',
|
||||
flip: true,
|
||||
flipOnUpdate: true,
|
||||
// Optional: you can add an offset to give some spacing
|
||||
offset: [0, 8]
|
||||
}
|
||||
|
||||
// Update tippy instance with new options
|
||||
if (props.tippyInstance) {
|
||||
Object.assign(props.tippyInstance, tippyOptions)
|
||||
}
|
||||
}, [props.tippyInstance])
|
||||
|
||||
const selectItem = (index) => {
|
||||
if (index >= props.items.length) {
|
||||
// Make sure we actually have enough items to select the given index. For
|
||||
// instance, if a user presses "Enter" when there are no options, the index will
|
||||
// be 0 but there won't be any items, so just ignore the callback here
|
||||
return
|
||||
}
|
||||
|
||||
const suggestion = props.items[index]
|
||||
|
||||
// Set all of the attributes of our Mention node based on the suggestion
|
||||
// data. The fields of `suggestion` will depend on whatever data you
|
||||
// return from your `items` function in your "suggestion" options handler.
|
||||
// Our suggestion handler returns `MentionSuggestion`s (which we've
|
||||
// indicated via SuggestionProps<MentionSuggestion>). We are passing an
|
||||
// object of the `MentionNodeAttrs` shape when calling `command` (utilized
|
||||
// by the Mention extension to create a Mention Node).
|
||||
const mentionItem = {
|
||||
id: suggestion.id,
|
||||
label: suggestion.mentionLabel
|
||||
}
|
||||
// @ts-expect-error there is currently a bug in the Tiptap SuggestionProps
|
||||
// type where if you specify the suggestion type (like
|
||||
// `SuggestionProps<MentionSuggestion>`), it will incorrectly require that
|
||||
// type variable for `command`'s argument as well (whereas instead the
|
||||
// type of that argument should be the Mention Node attributes). This
|
||||
// should be fixed once https://github.com/ueberdosis/tiptap/pull/4136 is
|
||||
// merged and we can add a separate type arg to `SuggestionProps` to
|
||||
// specify the type of the commanded selected item.
|
||||
props.command(mentionItem)
|
||||
}
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length)
|
||||
}
|
||||
|
||||
const downHandler = () => {
|
||||
setSelectedIndex((selectedIndex + 1) % props.items.length)
|
||||
}
|
||||
|
||||
const enterHandler = () => {
|
||||
selectItem(selectedIndex)
|
||||
}
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [props.items])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({ event }) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
upHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
downHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
enterHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}))
|
||||
|
||||
// Group items by category
|
||||
const groupedItems = props.items.reduce((acc, item) => {
|
||||
const category = item.category || 'Other'
|
||||
if (!acc[category]) {
|
||||
acc[category] = []
|
||||
}
|
||||
acc[category].push(item)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return props.items.length > 0 ? (
|
||||
<Paper
|
||||
elevation={5}
|
||||
sx={{
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
}}
|
||||
>
|
||||
<List
|
||||
dense
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
maxWidth: '300px'
|
||||
}}
|
||||
>
|
||||
{Object.entries(groupedItems).map(([category, items], categoryIndex) => (
|
||||
<div key={category}>
|
||||
{/* Add divider before each category except the first one */}
|
||||
{categoryIndex > 0 && <Divider />}
|
||||
|
||||
{/* Category header */}
|
||||
<ListItem sx={{ py: 0.5, bgcolor: customization.isDarkMode ? theme.palette.common.black : theme.palette.grey[50] }}>
|
||||
<Typography variant='overline' color='text.secondary'>
|
||||
{category}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
{/* Category items */}
|
||||
{items.map((item) => {
|
||||
const itemIndex = props.items.findIndex((i) => i.id === item.id)
|
||||
return (
|
||||
<ListItem key={item.id} disablePadding>
|
||||
<ListItemButton
|
||||
selected={itemIndex === selectedIndex}
|
||||
onClick={() => selectItem(itemIndex)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<Typography variant='body1' sx={{ fontWeight: 500 }}>
|
||||
{item.label || item.mentionLabel}
|
||||
</Typography>
|
||||
{item.description && (
|
||||
<Typography
|
||||
variant='caption'
|
||||
color='text.secondary'
|
||||
sx={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}
|
||||
>
|
||||
{item.description}
|
||||
</Typography>
|
||||
)}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
) : null
|
||||
})
|
||||
|
||||
SuggestionList.displayName = 'SuggestionList'
|
||||
|
||||
// Add PropTypes validation
|
||||
SuggestionList.propTypes = {
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
mentionLabel: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
category: PropTypes.string
|
||||
})
|
||||
).isRequired,
|
||||
command: PropTypes.func.isRequired,
|
||||
tippyInstance: PropTypes.object
|
||||
}
|
||||
|
||||
export default SuggestionList
|
||||
@@ -0,0 +1,223 @@
|
||||
import { ReactRenderer } from '@tiptap/react'
|
||||
import tippy from 'tippy.js'
|
||||
import SuggestionList from './SuggestionList'
|
||||
import variablesApi from '@/api/variables'
|
||||
|
||||
/**
|
||||
* Workaround for the current typing incompatibility between Tippy.js and Tiptap
|
||||
* Suggestion utility.
|
||||
*
|
||||
* @see https://github.com/ueberdosis/tiptap/issues/2795#issuecomment-1160623792
|
||||
*
|
||||
* Adopted from
|
||||
* https://github.com/Doist/typist/blob/a1726a6be089e3e1452def641dfcfc622ac3e942/stories/typist-editor/constants/suggestions.ts#L169-L186
|
||||
*/
|
||||
const DOM_RECT_FALLBACK = {
|
||||
bottom: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache for storing variables
|
||||
let cachedVariables = []
|
||||
|
||||
// Function to fetch variables
|
||||
const fetchVariables = async () => {
|
||||
try {
|
||||
const response = await variablesApi.getAllVariables()
|
||||
cachedVariables = response.data || []
|
||||
return cachedVariables
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch variables:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const suggestionOptions = (
|
||||
availableNodesForVariable,
|
||||
availableState,
|
||||
acceptNodeOutputAsVariable,
|
||||
nodes,
|
||||
nodeData,
|
||||
isNodeInsideInteration
|
||||
) => ({
|
||||
char: '{{',
|
||||
items: async ({ query }) => {
|
||||
const defaultItems = [
|
||||
{ id: 'question', mentionLabel: 'question', description: "User's question from chatbox", category: 'Chat Context' },
|
||||
{
|
||||
id: 'chat_history',
|
||||
mentionLabel: 'chat_history',
|
||||
description: 'Past conversation history between user and AI',
|
||||
category: 'Chat Context'
|
||||
},
|
||||
{
|
||||
id: 'runtime_messages_length',
|
||||
mentionLabel: 'runtime_messages_length',
|
||||
description: 'Total messsages between LLM and Agent',
|
||||
category: 'Chat Context'
|
||||
},
|
||||
{
|
||||
id: 'file_attachment',
|
||||
mentionLabel: 'file_attachment',
|
||||
description: 'Files uploaded from the chat',
|
||||
category: 'Chat Context'
|
||||
},
|
||||
{ id: '$flow.sessionId', mentionLabel: '$flow.sessionId', description: 'Current session ID', category: 'Flow Variables' },
|
||||
{ id: '$flow.chatId', mentionLabel: '$flow.chatId', description: 'Current chat ID', category: 'Flow Variables' },
|
||||
{ id: '$flow.chatflowId', mentionLabel: '$flow.chatflowId', description: 'Current chatflow ID', category: 'Flow Variables' }
|
||||
]
|
||||
|
||||
const stateItems = (availableState || []).map((state) => ({
|
||||
id: `$flow.state.${state.key}`,
|
||||
mentionLabel: `$flow.state.${state.key}`,
|
||||
category: 'Flow State'
|
||||
}))
|
||||
|
||||
if (isNodeInsideInteration) {
|
||||
defaultItems.unshift({
|
||||
id: '$iteration',
|
||||
mentionLabel: '$iteration',
|
||||
description: 'Iteration item. For JSON, use dot notation: $iteration.name',
|
||||
category: 'Iteration'
|
||||
})
|
||||
}
|
||||
|
||||
// Add output option if acceptNodeOutputAsVariable is true
|
||||
if (acceptNodeOutputAsVariable) {
|
||||
defaultItems.unshift({
|
||||
id: 'output',
|
||||
mentionLabel: 'output',
|
||||
description: 'Output from the current node',
|
||||
category: 'Node Outputs'
|
||||
})
|
||||
|
||||
const structuredOutputs = nodeData?.inputs?.llmStructuredOutput ?? []
|
||||
if (structuredOutputs && structuredOutputs.length > 0) {
|
||||
structuredOutputs.forEach((item) => {
|
||||
defaultItems.unshift({
|
||||
id: `output.${item.key}`,
|
||||
mentionLabel: `output.${item.key}`,
|
||||
description: `${item.description}`,
|
||||
category: 'Node Outputs'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch variables if cache is empty
|
||||
if (cachedVariables.length === 0) {
|
||||
await fetchVariables()
|
||||
}
|
||||
|
||||
const variableItems = cachedVariables.map((variable) => ({
|
||||
id: `$vars.${variable.name}`,
|
||||
mentionLabel: `$vars.${variable.name}`,
|
||||
description: `Variable: ${variable.value} (${variable.type})`,
|
||||
category: 'Custom Variables'
|
||||
}))
|
||||
|
||||
const startAgentflowNode = nodes.find((node) => node.data.name === 'startAgentflow')
|
||||
const formInputTypes = startAgentflowNode?.data?.inputs?.formInputTypes
|
||||
|
||||
let formItems = []
|
||||
if (formInputTypes) {
|
||||
formItems = (formInputTypes || []).map((input) => ({
|
||||
id: `$form.${input.name}`,
|
||||
mentionLabel: `$form.${input.name}`,
|
||||
description: `Form Input: ${input.label}`,
|
||||
category: 'Form Inputs'
|
||||
}))
|
||||
}
|
||||
|
||||
const nodeItems = (availableNodesForVariable || []).map((node) => {
|
||||
const selectedOutputAnchor = node.data.outputAnchors?.[0]?.options?.find((ancr) => ancr.name === node.data.outputs['output'])
|
||||
|
||||
return {
|
||||
id: `${node.id}`,
|
||||
mentionLabel: node.data.inputs.chainName ?? node.data.inputs.functionName ?? node.data.inputs.variableName ?? node.data.id,
|
||||
description:
|
||||
node.data.name === 'ifElseFunction'
|
||||
? node.data.description
|
||||
: `${selectedOutputAnchor?.label ?? 'Output'} from ${node.data.label}`,
|
||||
category: 'Node Outputs'
|
||||
}
|
||||
})
|
||||
|
||||
const allItems = [...defaultItems, ...formItems, ...nodeItems, ...stateItems, ...variableItems]
|
||||
|
||||
return allItems.filter(
|
||||
(item) => item.mentionLabel.toLowerCase().includes(query.toLowerCase()) || item.id.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
},
|
||||
render: () => {
|
||||
let component
|
||||
let popup
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
component = new ReactRenderer(SuggestionList, {
|
||||
props,
|
||||
editor: props.editor
|
||||
})
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: () => props.clientRect?.() ?? DOM_RECT_FALLBACK,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start'
|
||||
})[0]
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
component?.updateProps(props)
|
||||
|
||||
popup?.setProps({
|
||||
getReferenceClientRect: () => props.clientRect?.() ?? DOM_RECT_FALLBACK
|
||||
})
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup?.hide()
|
||||
return true
|
||||
}
|
||||
|
||||
if (!component?.ref) {
|
||||
return false
|
||||
}
|
||||
|
||||
return component.ref.onKeyDown(props)
|
||||
},
|
||||
|
||||
onExit() {
|
||||
popup?.destroy()
|
||||
component?.destroy()
|
||||
|
||||
// Remove references to the old popup and component upon destruction/exit.
|
||||
// (This should prevent redundant calls to `popup.destroy()`, which Tippy
|
||||
// warns in the console is a sign of a memory leak, as the `suggestion`
|
||||
// plugin seems to call `onExit` both when a suggestion menu is closed after
|
||||
// a user chooses an option, *and* when the editor itself is destroyed.)
|
||||
popup = undefined
|
||||
component = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Export function to refresh variables cache
|
||||
export const refreshVariablesCache = () => {
|
||||
return fetchVariables()
|
||||
}
|
||||
@@ -69,7 +69,18 @@ export const JsonEditorInput = ({
|
||||
/>
|
||||
)}
|
||||
{!disabled && (
|
||||
<div key={JSON.stringify(myValue)}>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}}
|
||||
role='button'
|
||||
aria-label='JSON Editor'
|
||||
tabIndex={0}
|
||||
key={JSON.stringify(myValue)}
|
||||
>
|
||||
<ReactJson
|
||||
theme={isDarkMode ? 'ocean' : 'rjv-default'}
|
||||
style={{ padding: 10, borderRadius: 10 }}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Box } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// Syntax highlighting function for JSON
|
||||
function syntaxHighlight(json) {
|
||||
if (!json) return '' // No JSON from response
|
||||
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
|
||||
return json.replace(
|
||||
// eslint-disable-next-line
|
||||
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
||||
function (match) {
|
||||
let cls = 'number'
|
||||
if (/^"/.test(match)) {
|
||||
if (/:$/.test(match)) {
|
||||
cls = 'key'
|
||||
} else {
|
||||
cls = 'string'
|
||||
}
|
||||
} else if (/true|false/.test(match)) {
|
||||
cls = 'boolean'
|
||||
} else if (/null/.test(match)) {
|
||||
cls = 'null'
|
||||
}
|
||||
return '<span class="' + cls + '">' + match + '</span>'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const JSONViewer = ({ data, maxHeight = '400px' }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const isDarkMode = customization.isDarkMode
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
p: 2,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
width: '100%',
|
||||
overflow: 'auto',
|
||||
maxHeight: maxHeight
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
pre .string {
|
||||
color: ${isDarkMode ? '#9cdcfe' : 'green'};
|
||||
}
|
||||
pre .number {
|
||||
color: ${isDarkMode ? '#b5cea8' : 'darkorange'};
|
||||
}
|
||||
pre .boolean {
|
||||
color: ${isDarkMode ? '#569cd6' : 'blue'};
|
||||
}
|
||||
pre .null {
|
||||
color: ${isDarkMode ? '#d4d4d4' : 'magenta'};
|
||||
}
|
||||
pre .key {
|
||||
color: ${isDarkMode ? '#ff5733' : '#ff5733'};
|
||||
}
|
||||
`}</style>
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
fontFamily: `'Inter', 'Roboto', 'Arial', sans-serif`,
|
||||
fontSize: '0.875rem',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: syntaxHighlight(JSON.stringify(data, null, 2), isDarkMode)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
JSONViewer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
maxHeight: PropTypes.string
|
||||
}
|
||||
@@ -32,7 +32,7 @@ const programmingLanguages = {
|
||||
css: '.css'
|
||||
}
|
||||
|
||||
export const CodeBlock = memo(({ language, chatflowid, isDialog, value }) => {
|
||||
export const CodeBlock = memo(({ language, chatflowid, isFullWidth, value }) => {
|
||||
const theme = useTheme()
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const openPopOver = Boolean(anchorEl)
|
||||
@@ -76,7 +76,7 @@ export const CodeBlock = memo(({ language, chatflowid, isDialog, value }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: isDialog ? '' : 300 }}>
|
||||
<div style={{ width: isFullWidth ? '' : 300 }}>
|
||||
<Box sx={{ color: 'white', background: theme.palette?.common.dark, p: 1, borderTopLeftRadius: 10, borderTopRightRadius: 10 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
{language}
|
||||
@@ -118,6 +118,6 @@ CodeBlock.displayName = 'CodeBlock'
|
||||
CodeBlock.propTypes = {
|
||||
language: PropTypes.string,
|
||||
chatflowid: PropTypes.string,
|
||||
isDialog: PropTypes.bool,
|
||||
isFullWidth: PropTypes.bool,
|
||||
value: PropTypes.string
|
||||
}
|
||||
|
||||
@@ -1,19 +1,165 @@
|
||||
import { memo } from 'react'
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import './Markdown.css'
|
||||
import { CodeBlock } from '../markdown/CodeBlock'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import rehypeMathjax from 'rehype-mathjax'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
|
||||
/**
|
||||
* Checks if text likely contains LaTeX math notation
|
||||
* @param {string} text - Text to check for LaTeX math
|
||||
* @param {Object[]} customPatterns - Additional regex patterns to check
|
||||
* @returns {boolean} - Whether LaTeX math is likely present
|
||||
*/
|
||||
const containsLaTeX = (text, customPatterns = []) => {
|
||||
if (!text || typeof text !== 'string') return false
|
||||
|
||||
// Common LaTeX patterns - more permissive to catch edge cases
|
||||
const defaultPatterns = [
|
||||
{ regex: /\$\$.+?\$\$/s, name: 'Block math: $$...$$' },
|
||||
{ regex: /\\\(.+?\\\)/s, name: 'Inline math: \\(...\\)' },
|
||||
{ regex: /\\\[[\s\S]*?\\\]/, name: 'Display math: \\[...\\]' },
|
||||
{ regex: /\\begin{(equation|align|gather|math|matrix|bmatrix|pmatrix|vmatrix|cases)}.+?\\end{\1}/s, name: 'Environment math' },
|
||||
{ regex: /\$(.*?[\\{}_^].*?)\$/, name: 'Inline math with $' },
|
||||
{ regex: /\\frac/, name: 'LaTeX command: \\frac' },
|
||||
{ regex: /\\sqrt/, name: 'LaTeX command: \\sqrt' },
|
||||
{ regex: /\\pm/, name: 'LaTeX command: \\pm' },
|
||||
{ regex: /\\cdot/, name: 'LaTeX command: \\cdot' },
|
||||
{ regex: /\\text/, name: 'LaTeX command: \\text' },
|
||||
{ regex: /\\sum/, name: 'LaTeX command: \\sum' },
|
||||
{ regex: /\\prod/, name: 'LaTeX command: \\prod' },
|
||||
{ regex: /\\int/, name: 'LaTeX command: \\int' }
|
||||
]
|
||||
|
||||
// Combine default and custom patterns
|
||||
const patterns = [...defaultPatterns, ...customPatterns]
|
||||
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.regex.test(text)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocesses text to make LaTeX syntax more compatible with Markdown
|
||||
* @param {string} text - Original text with potentially problematic LaTeX syntax
|
||||
* @returns {string} - Text with LaTeX syntax adjusted for better compatibility
|
||||
*/
|
||||
const preprocessLatex = (text) => {
|
||||
if (!text || typeof text !== 'string') return text
|
||||
|
||||
// Replace problematic LaTeX patterns with more compatible alternatives
|
||||
const processedText = text
|
||||
// Convert display math with indentation to dollar-dollar format
|
||||
.replace(/(\n\s*)\\\[([\s\S]*?)\\\](\s*\n|$)/g, (match, before, content, after) => {
|
||||
// Preserve indentation but use $$ format which is more reliably handled
|
||||
return `${before}$$${content}$$${after}`
|
||||
})
|
||||
// Convert inline math to dollar format with spaces to avoid conflicts
|
||||
.replace(/\\\(([\s\S]*?)\\\)/g, '$ $1 $')
|
||||
|
||||
return processedText
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced Markdown component with memoization for better performance
|
||||
* Supports various plugins and custom rendering components
|
||||
*/
|
||||
export const MemoizedReactMarkdown = memo(
|
||||
({ children, ...props }) => (
|
||||
<div className='react-markdown'>
|
||||
<ReactMarkdown {...props}>{children}</ReactMarkdown>
|
||||
</div>
|
||||
),
|
||||
(prevProps, nextProps) => prevProps.children === nextProps.children
|
||||
({ children, ...props }) => {
|
||||
// Preprocess text to improve LaTeX compatibility
|
||||
const processedChildren = useMemo(() => (typeof children === 'string' ? preprocessLatex(children) : children), [children])
|
||||
|
||||
// Enable math by default unless explicitly disabled
|
||||
const shouldEnableMath = useMemo(() => {
|
||||
const hasLatex = processedChildren && containsLaTeX(processedChildren, props.mathPatterns || [])
|
||||
|
||||
return props.disableMath === true ? false : props.forceMath || hasLatex
|
||||
}, [processedChildren, props.forceMath, props.disableMath, props.mathPatterns])
|
||||
|
||||
// Configure plugins based on content
|
||||
const remarkPlugins = useMemo(() => {
|
||||
if (props.remarkPlugins) return props.remarkPlugins
|
||||
return shouldEnableMath ? [remarkGfm, remarkMath] : [remarkGfm]
|
||||
}, [props.remarkPlugins, shouldEnableMath])
|
||||
|
||||
const rehypePlugins = useMemo(() => {
|
||||
if (props.rehypePlugins) return props.rehypePlugins
|
||||
return shouldEnableMath ? [rehypeMathjax, rehypeRaw] : [rehypeRaw]
|
||||
}, [props.rehypePlugins, shouldEnableMath])
|
||||
|
||||
return (
|
||||
<div className='react-markdown'>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={{
|
||||
code({ inline, className, children, ...codeProps }) {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
return !inline ? (
|
||||
<CodeBlock
|
||||
key={Math.random()}
|
||||
chatflowid={props.chatflowid}
|
||||
isFullWidth={props.isFullWidth !== undefined ? props.isFullWidth : true}
|
||||
language={(match && match[1]) || ''}
|
||||
value={String(children).replace(/\n$/, '')}
|
||||
{...codeProps}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...codeProps}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
p({ children }) {
|
||||
return <p style={{ whiteSpace: 'pre-line' }}>{children}</p>
|
||||
},
|
||||
...props.components
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{processedChildren}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
// More detailed comparison for better memoization
|
||||
if (prevProps.children !== nextProps.children) return false
|
||||
|
||||
// Check if other props have changed
|
||||
const prevEntries = Object.entries(prevProps).filter(([key]) => key !== 'children')
|
||||
const nextEntries = Object.entries(nextProps).filter(([key]) => key !== 'children')
|
||||
|
||||
if (prevEntries.length !== nextEntries.length) return false
|
||||
|
||||
// Simple shallow comparison of remaining props
|
||||
for (const [key, value] of prevEntries) {
|
||||
if (key === 'components' || key === 'remarkPlugins' || key === 'rehypePlugins') continue // Skip complex objects
|
||||
|
||||
if (nextProps[key] !== value) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
MemoizedReactMarkdown.displayName = 'MemoizedReactMarkdown'
|
||||
|
||||
MemoizedReactMarkdown.propTypes = {
|
||||
children: PropTypes.any
|
||||
children: PropTypes.any,
|
||||
chatflowid: PropTypes.string,
|
||||
isFullWidth: PropTypes.bool,
|
||||
remarkPlugins: PropTypes.array,
|
||||
rehypePlugins: PropTypes.array,
|
||||
components: PropTypes.object,
|
||||
forceMath: PropTypes.bool,
|
||||
disableMath: PropTypes.bool,
|
||||
mathPatterns: PropTypes.array
|
||||
}
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
import { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useSelector } from 'react-redux'
|
||||
import moment from 'moment'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
useTheme,
|
||||
Checkbox
|
||||
} from '@mui/material'
|
||||
import { tableCellClasses } from '@mui/material/TableCell'
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||
import StopCircleIcon from '@mui/icons-material/StopCircle'
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import { IconLoader, IconCircleXFilled } from '@tabler/icons-react'
|
||||
|
||||
const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
|
||||
[`&.${tableCellClasses.head}`]: {
|
||||
color: theme.palette.grey[900]
|
||||
},
|
||||
[`&.${tableCellClasses.body}`]: {
|
||||
fontSize: 14,
|
||||
height: 64
|
||||
}
|
||||
}))
|
||||
|
||||
const StyledTableRow = styled(TableRow)(() => ({
|
||||
// hide last border
|
||||
'&:last-child td, &:last-child th': {
|
||||
border: 0
|
||||
}
|
||||
}))
|
||||
|
||||
const getIconFromStatus = (state, theme) => {
|
||||
switch (state) {
|
||||
case 'FINISHED':
|
||||
return CheckCircleIcon
|
||||
case 'ERROR':
|
||||
case 'TIMEOUT':
|
||||
return ErrorIcon
|
||||
case 'TERMINATED':
|
||||
// eslint-disable-next-line react/display-name
|
||||
return (props) => {
|
||||
const IconWrapper = (props) => <IconCircleXFilled {...props} color={theme.palette.error.main} />
|
||||
IconWrapper.displayName = 'TerminatedIcon'
|
||||
return <IconWrapper {...props} />
|
||||
}
|
||||
case 'STOPPED':
|
||||
return StopCircleIcon
|
||||
case 'INPROGRESS':
|
||||
// eslint-disable-next-line react/display-name
|
||||
return (props) => {
|
||||
const IconWrapper = (props) => (
|
||||
// eslint-disable-next-line
|
||||
<IconLoader {...props} color={theme.palette.warning.dark} className={`spin-animation ${props.className || ''}`} />
|
||||
)
|
||||
IconWrapper.displayName = 'InProgressIcon'
|
||||
return <IconWrapper {...props} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getIconColor = (state) => {
|
||||
switch (state) {
|
||||
case 'FINISHED':
|
||||
return 'success.dark'
|
||||
case 'ERROR':
|
||||
case 'TIMEOUT':
|
||||
return 'error.main'
|
||||
case 'TERMINATED':
|
||||
case 'STOPPED':
|
||||
return 'error.main'
|
||||
case 'INPROGRESS':
|
||||
return 'warning.main'
|
||||
}
|
||||
}
|
||||
|
||||
export const ExecutionsListTable = ({ data, isLoading, onExecutionRowClick, onSelectionChange }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const localStorageKeyOrder = 'executions_order'
|
||||
const localStorageKeyOrderBy = 'executions_orderBy'
|
||||
|
||||
const [order, setOrder] = useState(localStorage.getItem(localStorageKeyOrder) || 'desc')
|
||||
const [orderBy, setOrderBy] = useState(localStorage.getItem(localStorageKeyOrderBy) || 'updatedDate')
|
||||
const [selected, setSelected] = useState([])
|
||||
|
||||
const handleRequestSort = (property) => {
|
||||
const isAsc = orderBy === property && order === 'asc'
|
||||
const newOrder = isAsc ? 'desc' : 'asc'
|
||||
setOrder(newOrder)
|
||||
setOrderBy(property)
|
||||
localStorage.setItem(localStorageKeyOrder, newOrder)
|
||||
localStorage.setItem(localStorageKeyOrderBy, property)
|
||||
}
|
||||
|
||||
const handleSelectAllClick = (event) => {
|
||||
if (event.target.checked) {
|
||||
const newSelected = data.map((n) => n.id)
|
||||
setSelected(newSelected)
|
||||
onSelectionChange && onSelectionChange(newSelected)
|
||||
} else {
|
||||
setSelected([])
|
||||
onSelectionChange && onSelectionChange([])
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = (event, id) => {
|
||||
event.stopPropagation()
|
||||
const selectedIndex = selected.indexOf(id)
|
||||
let newSelected = []
|
||||
|
||||
if (selectedIndex === -1) {
|
||||
newSelected = newSelected.concat(selected, id)
|
||||
} else if (selectedIndex === 0) {
|
||||
newSelected = newSelected.concat(selected.slice(1))
|
||||
} else if (selectedIndex === selected.length - 1) {
|
||||
newSelected = newSelected.concat(selected.slice(0, -1))
|
||||
} else if (selectedIndex > 0) {
|
||||
newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1))
|
||||
}
|
||||
|
||||
setSelected(newSelected)
|
||||
onSelectionChange && onSelectionChange(newSelected)
|
||||
}
|
||||
|
||||
const isSelected = (id) => selected.indexOf(id) !== -1
|
||||
|
||||
const sortedData = data
|
||||
? [...data].sort((a, b) => {
|
||||
if (orderBy === 'name') {
|
||||
return order === 'asc' ? (a.name || '').localeCompare(b.name || '') : (b.name || '').localeCompare(a.name || '')
|
||||
} else if (orderBy === 'updatedDate') {
|
||||
return order === 'asc'
|
||||
? new Date(a.updatedDate) - new Date(b.updatedDate)
|
||||
: new Date(b.updatedDate) - new Date(a.updatedDate)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
: []
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} size='small' aria-label='a dense table'>
|
||||
<TableHead
|
||||
sx={{
|
||||
backgroundColor: customization.isDarkMode ? theme.palette.common.black : theme.palette.grey[100],
|
||||
height: 56
|
||||
}}
|
||||
>
|
||||
<TableRow>
|
||||
<StyledTableCell padding='checkbox'>
|
||||
<Checkbox
|
||||
color='primary'
|
||||
indeterminate={selected.length > 0 && selected.length < data.length}
|
||||
checked={data.length > 0 && selected.length === data.length}
|
||||
onChange={handleSelectAllClick}
|
||||
inputProps={{
|
||||
'aria-label': 'select all executions'
|
||||
}}
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>Status</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<TableSortLabel
|
||||
active={orderBy === 'updatedDate'}
|
||||
direction={order}
|
||||
onClick={() => handleRequestSort('updatedDate')}
|
||||
>
|
||||
Last Updated
|
||||
</TableSortLabel>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell component='th' scope='row'>
|
||||
<TableSortLabel active={orderBy === 'name'} direction={order} onClick={() => handleRequestSort('name')}>
|
||||
Agentflow
|
||||
</TableSortLabel>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>Session</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<TableSortLabel
|
||||
active={orderBy === 'createdDate'}
|
||||
direction={order}
|
||||
onClick={() => handleRequestSort('createdDate')}
|
||||
>
|
||||
Created
|
||||
</TableSortLabel>
|
||||
</StyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<StyledTableRow>
|
||||
<StyledTableCell padding='checkbox'>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
<StyledTableRow>
|
||||
<StyledTableCell padding='checkbox'>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{sortedData.map((row, index) => {
|
||||
const isItemSelected = isSelected(row.id)
|
||||
const labelId = `enhanced-table-checkbox-${index}`
|
||||
|
||||
return (
|
||||
<StyledTableRow
|
||||
hover
|
||||
key={index}
|
||||
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<StyledTableCell padding='checkbox'>
|
||||
<Checkbox
|
||||
color='primary'
|
||||
checked={isItemSelected}
|
||||
onClick={(event) => handleClick(event, row.id)}
|
||||
inputProps={{
|
||||
'aria-labelledby': labelId
|
||||
}}
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell onClick={() => onExecutionRowClick(row)}>
|
||||
<Box
|
||||
component={getIconFromStatus(row.state, theme)}
|
||||
className='labelIcon'
|
||||
color={getIconColor(row.state)}
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell onClick={() => onExecutionRowClick(row)}>
|
||||
{moment(row.updatedDate).format('MMM D, YYYY h:mm A')}
|
||||
</StyledTableCell>
|
||||
<StyledTableCell onClick={() => onExecutionRowClick(row)}>
|
||||
{row.agentflow?.name}
|
||||
</StyledTableCell>
|
||||
<StyledTableCell onClick={() => onExecutionRowClick(row)}>{row.sessionId}</StyledTableCell>
|
||||
<StyledTableCell onClick={() => onExecutionRowClick(row)}>
|
||||
{moment(row.createdDate).format('MMM D, YYYY h:mm A')}
|
||||
</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ExecutionsListTable.propTypes = {
|
||||
data: PropTypes.array,
|
||||
isLoading: PropTypes.bool,
|
||||
onExecutionRowClick: PropTypes.func,
|
||||
onSelectionChange: PropTypes.func,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
ExecutionsListTable.displayName = 'ExecutionsListTable'
|
||||
@@ -47,7 +47,7 @@ const getLocalStorageKeyName = (name, isAgentCanvas) => {
|
||||
return (isAgentCanvas ? 'agentcanvas' : 'chatflowcanvas') + '_' + name
|
||||
}
|
||||
|
||||
export const FlowListTable = ({ data, images, isLoading, filterFunction, updateFlowsApi, setError, isAgentCanvas }) => {
|
||||
export const FlowListTable = ({ data, images = {}, icons = {}, isLoading, filterFunction, updateFlowsApi, setError, isAgentCanvas }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
@@ -66,6 +66,14 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF
|
||||
localStorage.setItem(localStorageKeyOrderBy, property)
|
||||
}
|
||||
|
||||
const onFlowClick = (row) => {
|
||||
if (!isAgentCanvas) {
|
||||
return `/canvas/${row.id}`
|
||||
} else {
|
||||
return localStorage.getItem('agentFlowVersion') === 'v2' ? `/v2/agentcanvas/${row.id}` : `/agentcanvas/${row.id}`
|
||||
}
|
||||
}
|
||||
|
||||
const sortedData = data
|
||||
? [...data].sort((a, b) => {
|
||||
if (orderBy === 'name') {
|
||||
@@ -170,10 +178,7 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to={`/${isAgentCanvas ? 'agentcanvas' : 'canvas'}/${row.id}`}
|
||||
style={{ color: '#2196f3', textDecoration: 'none' }}
|
||||
>
|
||||
<Link to={onFlowClick(row)} style={{ color: '#2196f3', textDecoration: 'none' }}>
|
||||
{row.templateName || row.name}
|
||||
</Link>
|
||||
</Typography>
|
||||
@@ -198,7 +203,7 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF
|
||||
</div>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell key='2'>
|
||||
{images[row.id] && (
|
||||
{(images[row.id] || icons[row.id]) && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -207,33 +212,55 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
{images[row.id]
|
||||
.slice(0, images[row.id].length > 5 ? 5 : images[row.id].length)
|
||||
.map((img) => (
|
||||
<Box
|
||||
key={img}
|
||||
sx={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: customization.isDarkMode
|
||||
? theme.palette.common.white
|
||||
: theme.palette.grey[300] + 75
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 5,
|
||||
objectFit: 'contain'
|
||||
{[
|
||||
...(images[row.id] || []).map((img) => ({ type: 'image', src: img })),
|
||||
...(icons[row.id] || []).map((ic) => ({
|
||||
type: 'icon',
|
||||
icon: ic.icon,
|
||||
color: ic.color
|
||||
}))
|
||||
]
|
||||
.slice(0, 5)
|
||||
.map((item, index) =>
|
||||
item.type === 'image' ? (
|
||||
<Box
|
||||
key={item.src}
|
||||
sx={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: customization.isDarkMode
|
||||
? theme.palette.common.white
|
||||
: theme.palette.grey[300] + 75
|
||||
}}
|
||||
alt=''
|
||||
src={img}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
{images[row.id].length > 5 && (
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 5,
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
alt=''
|
||||
src={item.src}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<item.icon size={25} color={item.color} />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{(images[row.id]?.length || 0) + (icons[row.id]?.length || 0) > 5 && (
|
||||
<Typography
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
@@ -242,7 +269,7 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF
|
||||
fontWeight: 200
|
||||
}}
|
||||
>
|
||||
+ {images[row.id].length - 5} More
|
||||
+ {(images[row.id]?.length || 0) + (icons[row.id]?.length || 0) - 5} More
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
@@ -280,6 +307,7 @@ export const FlowListTable = ({ data, images, isLoading, filterFunction, updateF
|
||||
FlowListTable.propTypes = {
|
||||
data: PropTypes.array,
|
||||
images: PropTypes.object,
|
||||
icons: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
filterFunction: PropTypes.func,
|
||||
updateFlowsApi: PropTypes.object,
|
||||
|
||||
@@ -1,8 +1,46 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { TableContainer, Table, TableHead, TableCell, TableRow, TableBody, Paper, Chip } from '@mui/material'
|
||||
import { TableContainer, Table, TableHead, TableCell, TableRow, TableBody, Paper, Chip, Stack, Typography } from '@mui/material'
|
||||
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
|
||||
|
||||
export const TableViewOnly = ({ columns, rows, sx }) => {
|
||||
// Helper function to safely render cell content
|
||||
const renderCellContent = (key, row) => {
|
||||
if (row[key] === null || row[key] === undefined) {
|
||||
return ''
|
||||
} else if (key === 'enabled') {
|
||||
return row[key] ? <Chip label='Enabled' color='primary' /> : <Chip label='Disabled' />
|
||||
} else if (key === 'type' && row.schema) {
|
||||
// If there's schema information, add a tooltip
|
||||
const schemaContent =
|
||||
'[<br>' +
|
||||
row.schema
|
||||
.map(
|
||||
(item) =>
|
||||
` ${JSON.stringify(
|
||||
{
|
||||
[item.name]: item.type
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
.join(',<br>') +
|
||||
'<br>]'
|
||||
|
||||
return (
|
||||
<Stack direction='row' alignItems='center' spacing={1}>
|
||||
<Typography>{row[key]}</Typography>
|
||||
<TooltipWithParser title={`<div>Schema:<br/>${schemaContent}</div>`} />
|
||||
</Stack>
|
||||
)
|
||||
} else if (typeof row[key] === 'object') {
|
||||
// For other objects (that are not handled by special cases above)
|
||||
return JSON.stringify(row[key])
|
||||
} else {
|
||||
return row[key]
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
@@ -32,20 +70,8 @@ export const TableViewOnly = ({ columns, rows, sx }) => {
|
||||
{rows.map((row, index) => (
|
||||
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
{Object.keys(row).map((key, index) => {
|
||||
if (key !== 'id') {
|
||||
return (
|
||||
<TableCell key={index}>
|
||||
{key === 'enabled' ? (
|
||||
row[key] ? (
|
||||
<Chip label='Enabled' color='primary' />
|
||||
) : (
|
||||
<Chip label='Disabled' />
|
||||
)
|
||||
) : (
|
||||
row[key]
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
if (key !== 'id' && key !== 'schema') {
|
||||
return <TableCell key={index}>{renderCellContent(key, row)}</TableCell>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
|
||||
@@ -78,6 +78,7 @@ export const exportData = (exportAllData) => {
|
||||
try {
|
||||
return {
|
||||
AgentFlow: sanitizeChatflow(exportAllData.AgentFlow),
|
||||
AgentFlowV2: sanitizeChatflow(exportAllData.AgentFlowV2),
|
||||
AssistantFlow: sanitizeChatflow(exportAllData.AssistantFlow),
|
||||
AssistantCustom: sanitizeAssistant(exportAllData.AssistantCustom),
|
||||
AssistantOpenAI: sanitizeAssistant(exportAllData.AssistantOpenAI),
|
||||
@@ -88,6 +89,7 @@ export const exportData = (exportAllData) => {
|
||||
CustomTemplate: exportAllData.CustomTemplate,
|
||||
DocumentStore: exportAllData.DocumentStore,
|
||||
DocumentStoreFileChunk: exportAllData.DocumentStoreFileChunk,
|
||||
Execution: exportAllData.Execution,
|
||||
Tool: sanitizeTool(exportAllData.Tool),
|
||||
Variable: sanitizeVariable(exportAllData.Variable)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { uniq } from 'lodash'
|
||||
import { uniq, get, isEqual } from 'lodash'
|
||||
import moment from 'moment'
|
||||
|
||||
export const getUniqueNodeId = (nodeData, nodes) => {
|
||||
@@ -16,6 +16,94 @@ export const getUniqueNodeId = (nodeData, nodes) => {
|
||||
return baseId
|
||||
}
|
||||
|
||||
export const getUniqueNodeLabel = (nodeData, nodes) => {
|
||||
if (nodeData.type === 'StickyNote') return nodeData.label
|
||||
if (nodeData.name === 'startAgentflow') return nodeData.label
|
||||
|
||||
let suffix = 0
|
||||
|
||||
// Construct base ID
|
||||
let baseId = `${nodeData.name}_${suffix}`
|
||||
|
||||
// Increment suffix until a unique ID is found
|
||||
while (nodes.some((node) => node.id === baseId)) {
|
||||
suffix += 1
|
||||
baseId = `${nodeData.name}_${suffix}`
|
||||
}
|
||||
|
||||
return `${nodeData.label} ${suffix}`
|
||||
}
|
||||
|
||||
const createAgentFlowOutputs = (nodeData, newNodeId) => {
|
||||
if (nodeData.hideOutput) return []
|
||||
|
||||
if (nodeData.outputs?.length) {
|
||||
return nodeData.outputs.map((_, index) => ({
|
||||
id: `${newNodeId}-output-${index}`,
|
||||
label: nodeData.label,
|
||||
name: nodeData.name
|
||||
}))
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: `${newNodeId}-output-${nodeData.name}`,
|
||||
label: nodeData.label,
|
||||
name: nodeData.name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const createOutputOption = (output, newNodeId) => {
|
||||
const outputBaseClasses = output.baseClasses ?? []
|
||||
const baseClasses = outputBaseClasses.length > 1 ? outputBaseClasses.join('|') : outputBaseClasses[0] || ''
|
||||
|
||||
const type = outputBaseClasses.length > 1 ? outputBaseClasses.join(' | ') : outputBaseClasses[0] || ''
|
||||
|
||||
return {
|
||||
id: `${newNodeId}-output-${output.name}-${baseClasses}`,
|
||||
name: output.name,
|
||||
label: output.label,
|
||||
description: output.description ?? '',
|
||||
type,
|
||||
isAnchor: output?.isAnchor,
|
||||
hidden: output?.hidden
|
||||
}
|
||||
}
|
||||
|
||||
const createStandardOutputs = (nodeData, newNodeId) => {
|
||||
if (nodeData.hideOutput) return []
|
||||
|
||||
if (nodeData.outputs?.length) {
|
||||
const outputOptions = nodeData.outputs.map((output) => createOutputOption(output, newNodeId))
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'output',
|
||||
label: 'Output',
|
||||
type: 'options',
|
||||
description: nodeData.outputs[0].description ?? '',
|
||||
options: outputOptions,
|
||||
default: nodeData.outputs[0].name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: `${newNodeId}-output-${nodeData.name}-${nodeData.baseClasses.join('|')}`,
|
||||
name: nodeData.name,
|
||||
label: nodeData.type,
|
||||
description: nodeData.description ?? '',
|
||||
type: nodeData.baseClasses.join(' | ')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const initializeOutputAnchors = (nodeData, newNodeId, isAgentflow) => {
|
||||
return isAgentflow ? createAgentFlowOutputs(nodeData, newNodeId) : createStandardOutputs(nodeData, newNodeId)
|
||||
}
|
||||
|
||||
export const initializeDefaultNodeData = (nodeParams) => {
|
||||
const initialValues = {}
|
||||
|
||||
@@ -27,17 +115,17 @@ export const initializeDefaultNodeData = (nodeParams) => {
|
||||
return initialValues
|
||||
}
|
||||
|
||||
export const initNode = (nodeData, newNodeId) => {
|
||||
export const initNode = (nodeData, newNodeId, isAgentflow) => {
|
||||
const inputAnchors = []
|
||||
const inputParams = []
|
||||
const incoming = nodeData.inputs ? nodeData.inputs.length : 0
|
||||
const outgoing = 1
|
||||
|
||||
const whitelistTypes = [
|
||||
'asyncOptions',
|
||||
'asyncMultiOptions',
|
||||
'options',
|
||||
'multiOptions',
|
||||
'array',
|
||||
'datagrid',
|
||||
'string',
|
||||
'number',
|
||||
@@ -75,55 +163,7 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
}
|
||||
|
||||
// Outputs
|
||||
const outputAnchors = []
|
||||
for (let i = 0; i < outgoing; i += 1) {
|
||||
if (nodeData.hideOutput) continue
|
||||
if (nodeData.outputs && nodeData.outputs.length) {
|
||||
const options = []
|
||||
for (let j = 0; j < nodeData.outputs.length; j += 1) {
|
||||
let baseClasses = ''
|
||||
let type = ''
|
||||
|
||||
const outputBaseClasses = nodeData.outputs[j].baseClasses ?? []
|
||||
if (outputBaseClasses.length > 1) {
|
||||
baseClasses = outputBaseClasses.join('|')
|
||||
type = outputBaseClasses.join(' | ')
|
||||
} else if (outputBaseClasses.length === 1) {
|
||||
baseClasses = outputBaseClasses[0]
|
||||
type = outputBaseClasses[0]
|
||||
}
|
||||
|
||||
const newOutputOption = {
|
||||
id: `${newNodeId}-output-${nodeData.outputs[j].name}-${baseClasses}`,
|
||||
name: nodeData.outputs[j].name,
|
||||
label: nodeData.outputs[j].label,
|
||||
description: nodeData.outputs[j].description ?? '',
|
||||
type,
|
||||
isAnchor: nodeData.outputs[j]?.isAnchor,
|
||||
hidden: nodeData.outputs[j]?.hidden
|
||||
}
|
||||
options.push(newOutputOption)
|
||||
}
|
||||
const newOutput = {
|
||||
name: 'output',
|
||||
label: 'Output',
|
||||
type: 'options',
|
||||
description: nodeData.outputs[0].description ?? '',
|
||||
options,
|
||||
default: nodeData.outputs[0].name
|
||||
}
|
||||
outputAnchors.push(newOutput)
|
||||
} else {
|
||||
const newOutput = {
|
||||
id: `${newNodeId}-output-${nodeData.name}-${nodeData.baseClasses.join('|')}`,
|
||||
name: nodeData.name,
|
||||
label: nodeData.type,
|
||||
description: nodeData.description ?? '',
|
||||
type: nodeData.baseClasses.join(' | ')
|
||||
}
|
||||
outputAnchors.push(newOutput)
|
||||
}
|
||||
}
|
||||
let outputAnchors = initializeOutputAnchors(nodeData, newNodeId, isAgentflow)
|
||||
|
||||
/* Initial
|
||||
inputs = [
|
||||
@@ -160,9 +200,10 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
|
||||
// Inputs
|
||||
if (nodeData.inputs) {
|
||||
nodeData.inputAnchors = inputAnchors
|
||||
nodeData.inputParams = inputParams
|
||||
nodeData.inputs = initializeDefaultNodeData(nodeData.inputs)
|
||||
const defaultInputs = initializeDefaultNodeData(nodeData.inputs)
|
||||
nodeData.inputAnchors = showHideInputAnchors({ ...nodeData, inputAnchors, inputs: defaultInputs })
|
||||
nodeData.inputParams = showHideInputParams({ ...nodeData, inputParams, inputs: defaultInputs })
|
||||
nodeData.inputs = defaultInputs
|
||||
} else {
|
||||
nodeData.inputAnchors = []
|
||||
nodeData.inputParams = []
|
||||
@@ -185,8 +226,10 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
return nodeData
|
||||
}
|
||||
|
||||
export const updateOutdatedNodeData = (newComponentNodeData, existingComponentNodeData) => {
|
||||
const initNewComponentNodeData = initNode(newComponentNodeData, existingComponentNodeData.id)
|
||||
export const updateOutdatedNodeData = (newComponentNodeData, existingComponentNodeData, isAgentflow) => {
|
||||
const initNewComponentNodeData = initNode(newComponentNodeData, existingComponentNodeData.id, isAgentflow)
|
||||
|
||||
const isAgentFlowV2 = newComponentNodeData.category === 'Agent Flows' || existingComponentNodeData.category === 'Agent Flows'
|
||||
|
||||
// Update credentials with existing credentials
|
||||
if (existingComponentNodeData.credential) {
|
||||
@@ -220,6 +263,11 @@ export const updateOutdatedNodeData = (newComponentNodeData, existingComponentNo
|
||||
}
|
||||
}
|
||||
|
||||
if (isAgentFlowV2) {
|
||||
// persists the label from the existing node
|
||||
initNewComponentNodeData.label = existingComponentNodeData.label
|
||||
}
|
||||
|
||||
// Special case for Condition node to update outputAnchors
|
||||
if (initNewComponentNodeData.name.includes('seqCondition')) {
|
||||
const options = existingComponentNodeData.outputAnchors[0].options || []
|
||||
@@ -243,22 +291,34 @@ export const updateOutdatedNodeData = (newComponentNodeData, existingComponentNo
|
||||
|
||||
export const updateOutdatedNodeEdge = (newComponentNodeData, edges) => {
|
||||
const removedEdges = []
|
||||
|
||||
const isAgentFlowV2 = newComponentNodeData.category === 'Agent Flows'
|
||||
|
||||
for (const edge of edges) {
|
||||
const targetNodeId = edge.targetHandle.split('-')[0]
|
||||
const sourceNodeId = edge.sourceHandle.split('-')[0]
|
||||
|
||||
if (targetNodeId === newComponentNodeData.id) {
|
||||
// Check if targetHandle is in inputParams or inputAnchors
|
||||
const inputParam = newComponentNodeData.inputParams.find((param) => param.id === edge.targetHandle)
|
||||
const inputAnchor = newComponentNodeData.inputAnchors.find((param) => param.id === edge.targetHandle)
|
||||
if (isAgentFlowV2) {
|
||||
if (edge.targetHandle !== newComponentNodeData.id) {
|
||||
removedEdges.push(edge)
|
||||
}
|
||||
} else {
|
||||
// Check if targetHandle is in inputParams or inputAnchors
|
||||
const inputParam = newComponentNodeData.inputParams.find((param) => param.id === edge.targetHandle)
|
||||
const inputAnchor = newComponentNodeData.inputAnchors.find((param) => param.id === edge.targetHandle)
|
||||
|
||||
if (!inputParam && !inputAnchor) {
|
||||
removedEdges.push(edge)
|
||||
if (!inputParam && !inputAnchor) {
|
||||
removedEdges.push(edge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceNodeId === newComponentNodeData.id) {
|
||||
if (newComponentNodeData.outputAnchors?.length) {
|
||||
if (isAgentFlowV2) {
|
||||
// AgentFlow v2 doesn't have specific output anchors, connections are directly from node
|
||||
// No need to remove edges for AgentFlow v2 outputs
|
||||
} else if (newComponentNodeData.outputAnchors?.length) {
|
||||
for (const outputAnchor of newComponentNodeData.outputAnchors) {
|
||||
const outputAnchorType = outputAnchor.type
|
||||
if (outputAnchorType === 'options') {
|
||||
@@ -315,6 +375,63 @@ export const isValidConnection = (connection, reactFlowInstance) => {
|
||||
return false
|
||||
}
|
||||
|
||||
export const isValidConnectionAgentflowV2 = (connection, reactFlowInstance) => {
|
||||
const source = connection.source
|
||||
const target = connection.target
|
||||
|
||||
// Prevent self connections
|
||||
if (source === target) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if this connection would create a cycle in the graph
|
||||
if (wouldCreateCycle(source, target, reactFlowInstance)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Function to check if a new connection would create a cycle
|
||||
const wouldCreateCycle = (sourceId, targetId, reactFlowInstance) => {
|
||||
// The most direct cycle check: if target connects back to source
|
||||
if (sourceId === targetId) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Build directed graph from existing edges
|
||||
const graph = {}
|
||||
const edges = reactFlowInstance.getEdges()
|
||||
|
||||
// Initialize graph
|
||||
edges.forEach((edge) => {
|
||||
if (!graph[edge.source]) graph[edge.source] = []
|
||||
graph[edge.source].push(edge.target)
|
||||
})
|
||||
|
||||
// Check if there's a path from target to source (which would create a cycle when we add source → target)
|
||||
const visited = new Set()
|
||||
|
||||
function hasPath(current, destination) {
|
||||
if (current === destination) return true
|
||||
if (visited.has(current)) return false
|
||||
|
||||
visited.add(current)
|
||||
|
||||
const neighbors = graph[current] || []
|
||||
for (const neighbor of neighbors) {
|
||||
if (hasPath(neighbor, destination)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// If there's a path from target to source, adding an edge from source to target will create a cycle
|
||||
return hasPath(targetId, sourceId)
|
||||
}
|
||||
|
||||
export const convertDateStringToDateObject = (dateString) => {
|
||||
if (dateString === undefined || !dateString) return undefined
|
||||
|
||||
@@ -367,6 +484,21 @@ export const getFolderName = (base64ArrayStr) => {
|
||||
}
|
||||
}
|
||||
|
||||
const _removeCredentialId = (obj) => {
|
||||
if (!obj || typeof obj !== 'object') return obj
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => _removeCredentialId(item))
|
||||
}
|
||||
|
||||
const newObj = {}
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (key === 'FLOWISE_CREDENTIAL_ID') continue
|
||||
newObj[key] = _removeCredentialId(value)
|
||||
}
|
||||
return newObj
|
||||
}
|
||||
|
||||
export const generateExportFlowData = (flowData) => {
|
||||
const nodes = flowData.nodes
|
||||
const edges = flowData.edges
|
||||
@@ -381,6 +513,9 @@ export const generateExportFlowData = (flowData) => {
|
||||
version: node.data.version,
|
||||
name: node.data.name,
|
||||
type: node.data.type,
|
||||
color: node.data.color,
|
||||
hideOutput: node.data.hideOutput,
|
||||
hideInput: node.data.hideInput,
|
||||
baseClasses: node.data.baseClasses,
|
||||
tags: node.data.tags,
|
||||
category: node.data.category,
|
||||
@@ -406,7 +541,7 @@ export const generateExportFlowData = (flowData) => {
|
||||
newNodeData.inputs = nodeDataInputs
|
||||
}
|
||||
|
||||
nodes[i].data = newNodeData
|
||||
nodes[i].data = _removeCredentialId(newNodeData)
|
||||
}
|
||||
const exportJson = {
|
||||
nodes,
|
||||
@@ -415,11 +550,13 @@ export const generateExportFlowData = (flowData) => {
|
||||
return exportJson
|
||||
}
|
||||
|
||||
export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle) => {
|
||||
export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle, includesStart = false) => {
|
||||
// example edge id = "llmChain_0-llmChain_0-output-outputPrediction-string|json-llmChain_1-llmChain_1-input-promptValues-string"
|
||||
// {source} -{sourceHandle} -{target} -{targetHandle}
|
||||
const parentNodes = []
|
||||
|
||||
const isAgentFlowV2 = nodes.find((nd) => nd.id === target)?.data?.category === 'Agent Flows'
|
||||
|
||||
const isSeqAgent = nodes.find((nd) => nd.id === target)?.data?.category === 'Sequential Agents'
|
||||
|
||||
function collectParentNodes(targetNodeId, nodes, edges) {
|
||||
@@ -442,10 +579,35 @@ export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle)
|
||||
}
|
||||
})
|
||||
}
|
||||
function collectAgentFlowV2ParentNodes(targetNodeId, nodes, edges) {
|
||||
const inputEdges = edges.filter((edg) => edg.target === targetNodeId && edg.targetHandle === targetNodeId)
|
||||
|
||||
// Traverse each edge found
|
||||
inputEdges.forEach((edge) => {
|
||||
const parentNode = nodes.find((nd) => nd.id === edge.source)
|
||||
if (!parentNode) return
|
||||
|
||||
// Recursive call to explore further up the tree
|
||||
collectAgentFlowV2ParentNodes(parentNode.id, nodes, edges)
|
||||
|
||||
// Check and add the parent node to the list if it does not include specific names
|
||||
const excludeNodeNames = ['startAgentflow']
|
||||
if (!excludeNodeNames.includes(parentNode.data.name) || includesStart) {
|
||||
parentNodes.push(parentNode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (isSeqAgent) {
|
||||
collectParentNodes(target, nodes, edges)
|
||||
return uniq(parentNodes)
|
||||
} else if (isAgentFlowV2) {
|
||||
collectAgentFlowV2ParentNodes(target, nodes, edges)
|
||||
const parentNodeId = nodes.find((nd) => nd.id === target)?.parentNode
|
||||
if (parentNodeId) {
|
||||
collectAgentFlowV2ParentNodes(parentNodeId, nodes, edges)
|
||||
}
|
||||
return uniq(parentNodes)
|
||||
} else {
|
||||
const inputEdges = edges.filter((edg) => edg.target === target && edg.targetHandle === targetHandle)
|
||||
if (inputEdges && inputEdges.length) {
|
||||
@@ -931,3 +1093,84 @@ export const getCustomConditionOutputs = (value, nodeId, existingEdges, isDataGr
|
||||
|
||||
return { outputAnchors, toBeRemovedEdgeIds }
|
||||
}
|
||||
|
||||
const _showHideOperation = (nodeData, inputParam, displayType, index) => {
|
||||
const displayOptions = inputParam[displayType]
|
||||
/* For example:
|
||||
show: {
|
||||
enableMemory: true
|
||||
}
|
||||
*/
|
||||
Object.keys(displayOptions).forEach((path) => {
|
||||
const comparisonValue = displayOptions[path]
|
||||
if (path.includes('$index')) {
|
||||
path = path.replace('$index', index)
|
||||
}
|
||||
const groundValue = get(nodeData.inputs, path, '')
|
||||
|
||||
if (Array.isArray(comparisonValue)) {
|
||||
if (displayType === 'show' && !comparisonValue.includes(groundValue)) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && comparisonValue.includes(groundValue)) {
|
||||
inputParam.display = false
|
||||
}
|
||||
} else if (typeof comparisonValue === 'string') {
|
||||
if (displayType === 'show' && !(comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && (comparisonValue === groundValue || new RegExp(comparisonValue).test(groundValue))) {
|
||||
inputParam.display = false
|
||||
}
|
||||
} else if (typeof comparisonValue === 'boolean') {
|
||||
if (displayType === 'show' && comparisonValue !== groundValue) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && comparisonValue === groundValue) {
|
||||
inputParam.display = false
|
||||
}
|
||||
} else if (typeof comparisonValue === 'object') {
|
||||
if (displayType === 'show' && !isEqual(comparisonValue, groundValue)) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && isEqual(comparisonValue, groundValue)) {
|
||||
inputParam.display = false
|
||||
}
|
||||
} else if (typeof comparisonValue === 'number') {
|
||||
if (displayType === 'show' && comparisonValue !== groundValue) {
|
||||
inputParam.display = false
|
||||
}
|
||||
if (displayType === 'hide' && comparisonValue === groundValue) {
|
||||
inputParam.display = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const showHideInputs = (nodeData, inputType, overrideParams, arrayIndex) => {
|
||||
const params = overrideParams ?? nodeData[inputType] ?? []
|
||||
|
||||
for (let i = 0; i < params.length; i += 1) {
|
||||
const inputParam = params[i]
|
||||
|
||||
// Reset display flag to false for each inputParam
|
||||
inputParam.display = true
|
||||
|
||||
if (inputParam.show) {
|
||||
_showHideOperation(nodeData, inputParam, 'show', arrayIndex)
|
||||
}
|
||||
if (inputParam.hide) {
|
||||
_showHideOperation(nodeData, inputParam, 'hide', arrayIndex)
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
export const showHideInputParams = (nodeData) => {
|
||||
return showHideInputs(nodeData, 'inputParams')
|
||||
}
|
||||
|
||||
export const showHideInputAnchors = (nodeData) => {
|
||||
return showHideInputs(nodeData, 'inputAnchors')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,983 @@
|
||||
import { useEffect, useState, useCallback, forwardRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import moment from 'moment'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
|
||||
// MUI
|
||||
import { RichTreeView } from '@mui/x-tree-view/RichTreeView'
|
||||
import { Typography, Box, Drawer, Chip, Button, Tooltip } from '@mui/material'
|
||||
import { styled, alpha } from '@mui/material/styles'
|
||||
import { useTreeItem2 } from '@mui/x-tree-view/useTreeItem2'
|
||||
import {
|
||||
TreeItem2Content,
|
||||
TreeItem2IconContainer,
|
||||
TreeItem2GroupTransition,
|
||||
TreeItem2Label,
|
||||
TreeItem2Root,
|
||||
TreeItem2Checkbox
|
||||
} from '@mui/x-tree-view/TreeItem2'
|
||||
import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'
|
||||
import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'
|
||||
import { TreeItem2DragAndDropOverlay } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle'
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||
import StopCircleIcon from '@mui/icons-material/StopCircle'
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import { IconButton } from '@mui/material'
|
||||
import {
|
||||
IconRefresh,
|
||||
IconExternalLink,
|
||||
IconCopy,
|
||||
IconLoader,
|
||||
IconCircleXFilled,
|
||||
IconRelationOneToManyFilled,
|
||||
IconShare,
|
||||
IconWorld,
|
||||
IconX
|
||||
} from '@tabler/icons-react'
|
||||
|
||||
// Project imports
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { FLOWISE_CREDENTIAL_ID, AGENTFLOW_ICONS } from '@/store/constant'
|
||||
import { NodeExecutionDetails } from '@/views/agentexecutions/NodeExecutionDetails'
|
||||
import ShareExecutionDialog from './ShareExecutionDialog'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
|
||||
|
||||
// API
|
||||
import executionsApi from '@/api/executions'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
const getIconColor = (status) => {
|
||||
switch (status) {
|
||||
case 'FINISHED':
|
||||
return 'success.dark'
|
||||
case 'ERROR':
|
||||
case 'TIMEOUT':
|
||||
return 'error.main'
|
||||
case 'TERMINATED':
|
||||
case 'STOPPED':
|
||||
return 'error.main'
|
||||
case 'INPROGRESS':
|
||||
return 'warning.dark'
|
||||
}
|
||||
}
|
||||
|
||||
const StyledTreeItemRoot = styled(TreeItem2Root)(({ theme }) => ({
|
||||
color: theme.palette.grey[400]
|
||||
}))
|
||||
|
||||
const CustomTreeItemContent = styled(TreeItem2Content)(({ theme }) => ({
|
||||
flexDirection: 'row-reverse',
|
||||
borderRadius: theme.spacing(0.7),
|
||||
marginBottom: theme.spacing(0.5),
|
||||
marginTop: theme.spacing(0.5),
|
||||
padding: theme.spacing(0.5),
|
||||
paddingRight: theme.spacing(1),
|
||||
fontWeight: 500,
|
||||
[`&.Mui-expanded `]: {
|
||||
'&:not(.Mui-focused, .Mui-selected, .Mui-selected.Mui-focused) .labelIcon': {
|
||||
color: theme.palette.primary.dark,
|
||||
...theme.applyStyles('light', {
|
||||
color: theme.palette.primary.main
|
||||
})
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
left: '16px',
|
||||
top: '44px',
|
||||
height: 'calc(100% - 48px)',
|
||||
width: '1.5px',
|
||||
backgroundColor: theme.palette.grey[700],
|
||||
...theme.applyStyles('light', {
|
||||
backgroundColor: theme.palette.grey[300]
|
||||
})
|
||||
}
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.primary.main, 0.1),
|
||||
color: 'white',
|
||||
...theme.applyStyles('light', {
|
||||
color: theme.palette.primary.main
|
||||
})
|
||||
},
|
||||
[`&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused`]: {
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
color: theme.palette.primary.contrastText,
|
||||
...theme.applyStyles('light', {
|
||||
backgroundColor: theme.palette.primary.main
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
const StyledTreeItemLabelText = styled(Typography)(({ theme }) => ({
|
||||
color: theme.palette.text.primary
|
||||
}))
|
||||
|
||||
function CustomLabel({ icon: Icon, itemStatus, children, name, ...other }) {
|
||||
// Check if this is an iteration node
|
||||
const isIterationNode = name === 'iterationAgentflow'
|
||||
|
||||
return (
|
||||
<TreeItem2Label
|
||||
{...other}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
// Display iteration icon for iteration nodes
|
||||
if (isIterationNode) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
mr: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<IconRelationOneToManyFilled size={20} color={'#9C89B8'} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Otherwise display the node icon
|
||||
const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === name)
|
||||
if (foundIcon) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
mr: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<foundIcon.icon size={20} color={foundIcon.color} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
|
||||
<StyledTreeItemLabelText sx={{ flex: 1 }}>{children}</StyledTreeItemLabelText>
|
||||
|
||||
{Icon && <Box component={Icon} className='labelIcon' color={getIconColor(itemStatus)} sx={{ ml: 1, fontSize: '1.2rem' }} />}
|
||||
</TreeItem2Label>
|
||||
)
|
||||
}
|
||||
|
||||
CustomLabel.propTypes = {
|
||||
icon: PropTypes.func,
|
||||
itemStatus: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
name: PropTypes.string
|
||||
}
|
||||
|
||||
CustomLabel.displayName = 'CustomLabel'
|
||||
|
||||
const isExpandable = (reactChildren) => {
|
||||
if (Array.isArray(reactChildren)) {
|
||||
return reactChildren.length > 0 && reactChildren.some(isExpandable)
|
||||
}
|
||||
return Boolean(reactChildren)
|
||||
}
|
||||
|
||||
const getIconFromStatus = (status, theme) => {
|
||||
switch (status) {
|
||||
case 'FINISHED':
|
||||
return CheckCircleIcon
|
||||
case 'ERROR':
|
||||
case 'TIMEOUT':
|
||||
return ErrorIcon
|
||||
case 'TERMINATED':
|
||||
// eslint-disable-next-line react/display-name
|
||||
return (props) => {
|
||||
const IconWrapper = (props) => <IconCircleXFilled {...props} color={theme.palette.error.main} />
|
||||
IconWrapper.displayName = 'TerminatedIcon'
|
||||
return <IconWrapper {...props} />
|
||||
}
|
||||
case 'STOPPED':
|
||||
return StopCircleIcon
|
||||
case 'INPROGRESS':
|
||||
// eslint-disable-next-line react/display-name
|
||||
return (props) => {
|
||||
const IconWrapper = (props) => (
|
||||
// eslint-disable-next-line
|
||||
<IconLoader {...props} color={theme.palette.warning.dark} className={`spin-animation ${props.className || ''}`} />
|
||||
)
|
||||
IconWrapper.displayName = 'InProgressIcon'
|
||||
return <IconWrapper {...props} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CustomTreeItem = forwardRef(function CustomTreeItem(props, ref) {
|
||||
const { id, itemId, label, disabled, children, ...other } = props
|
||||
const theme = useTheme()
|
||||
|
||||
const {
|
||||
getRootProps,
|
||||
getContentProps,
|
||||
getIconContainerProps,
|
||||
getCheckboxProps,
|
||||
getLabelProps,
|
||||
getGroupTransitionProps,
|
||||
getDragAndDropOverlayProps,
|
||||
status,
|
||||
publicAPI
|
||||
} = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref })
|
||||
|
||||
const item = publicAPI.getItem(itemId)
|
||||
const expandable = isExpandable(children)
|
||||
let icon
|
||||
if (item.status) {
|
||||
icon = getIconFromStatus(item.status, theme)
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeItem2Provider itemId={itemId}>
|
||||
<StyledTreeItemRoot {...getRootProps(other)}>
|
||||
<CustomTreeItemContent {...getContentProps()}>
|
||||
<TreeItem2IconContainer {...getIconContainerProps()}>
|
||||
<TreeItem2Icon status={status} />
|
||||
</TreeItem2IconContainer>
|
||||
<TreeItem2Checkbox {...getCheckboxProps()} />
|
||||
<CustomLabel
|
||||
{...getLabelProps({
|
||||
icon,
|
||||
itemStatus: item.status,
|
||||
expandable: expandable && status.expanded,
|
||||
name: item.name || item.id?.split('_')[0]
|
||||
})}
|
||||
/>
|
||||
<TreeItem2DragAndDropOverlay {...getDragAndDropOverlayProps()} />
|
||||
</CustomTreeItemContent>
|
||||
{children && (
|
||||
<TreeItem2GroupTransition
|
||||
{...getGroupTransitionProps()}
|
||||
style={{
|
||||
borderLeft: `${status.selected ? '3px solid' : '1px dashed'} ${(() => {
|
||||
const nodeName = item.name || item.id?.split('_')[0]
|
||||
const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === nodeName)
|
||||
return foundIcon ? foundIcon.color : theme.palette.primary.main
|
||||
})()}`,
|
||||
marginLeft: '13px',
|
||||
paddingLeft: '8px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</StyledTreeItemRoot>
|
||||
</TreeItem2Provider>
|
||||
)
|
||||
})
|
||||
|
||||
CustomTreeItem.propTypes = {
|
||||
id: PropTypes.string,
|
||||
itemId: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
const MIN_DRAWER_WIDTH = 400
|
||||
const DEFAULT_DRAWER_WIDTH = window.innerWidth - 400
|
||||
const MAX_DRAWER_WIDTH = window.innerWidth
|
||||
|
||||
export const ExecutionDetails = ({ open, isPublic, execution, metadata, onClose, onProceedSuccess, onUpdateSharing, onRefresh }) => {
|
||||
const [drawerWidth, setDrawerWidth] = useState(Math.min(DEFAULT_DRAWER_WIDTH, MAX_DRAWER_WIDTH))
|
||||
const [executionTree, setExecution] = useState([])
|
||||
const [expandedItems, setExpandedItems] = useState([])
|
||||
const [selectedItem, setSelectedItem] = useState(null)
|
||||
const [showShareDialog, setShowShareDialog] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [localMetadata, setLocalMetadata] = useState({})
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const updateExecutionApi = useApi(executionsApi.updateExecution)
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// useEffect to initialize localMetadata when metadata changes
|
||||
useEffect(() => {
|
||||
if (metadata) {
|
||||
setLocalMetadata(metadata)
|
||||
}
|
||||
}, [metadata])
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(localMetadata?.id)
|
||||
setCopied(true)
|
||||
|
||||
// Show success message
|
||||
dispatch(
|
||||
enqueueSnackbarAction({
|
||||
message: 'ID copied to clipboard',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => dispatch(closeSnackbarAction(key))}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Reset copied state after 2 seconds
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const handleMouseDown = () => {
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
const handleMouseMove = useCallback((e) => {
|
||||
const newWidth = document.body.offsetWidth - e.clientX
|
||||
if (newWidth >= MIN_DRAWER_WIDTH && newWidth <= MAX_DRAWER_WIDTH) {
|
||||
setDrawerWidth(newWidth)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
const getAllNodeIds = (nodes) => {
|
||||
let ids = []
|
||||
nodes.forEach((node) => {
|
||||
ids.push(node.id)
|
||||
if (node.children && node.children.length > 0) {
|
||||
ids = [...ids, ...getAllNodeIds(node.children)]
|
||||
}
|
||||
})
|
||||
return ids
|
||||
}
|
||||
|
||||
// Transform the execution data into a tree structure
|
||||
const buildTreeData = (nodes) => {
|
||||
// for each node, loop through each and every nested key of node.data, and remove the key if it is equal to FLOWISE_CREDENTIAL_ID
|
||||
nodes.forEach((node) => {
|
||||
const removeFlowiseCredentialId = (data) => {
|
||||
for (const key in data) {
|
||||
if (key === FLOWISE_CREDENTIAL_ID) {
|
||||
delete data[key]
|
||||
}
|
||||
if (typeof data[key] === 'object') {
|
||||
removeFlowiseCredentialId(data[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
removeFlowiseCredentialId(node.data)
|
||||
})
|
||||
|
||||
// Create a map for quick node lookup
|
||||
// Use execution index to make each node instance unique
|
||||
const nodeMap = new Map()
|
||||
nodes.forEach((node, index) => {
|
||||
const uniqueNodeId = `${node.nodeId}_${index}`
|
||||
nodeMap.set(uniqueNodeId, { ...node, uniqueNodeId, children: [], executionIndex: index })
|
||||
})
|
||||
|
||||
// Identify iteration nodes and their children
|
||||
const iterationGroups = new Map() // parentId -> Map of iterationIndex -> nodes
|
||||
|
||||
// Group iteration child nodes by their parent and iteration index
|
||||
nodes.forEach((node, index) => {
|
||||
if (node.data?.parentNodeId && node.data?.iterationIndex !== undefined) {
|
||||
const parentId = node.data.parentNodeId
|
||||
const iterationIndex = node.data.iterationIndex
|
||||
|
||||
if (!iterationGroups.has(parentId)) {
|
||||
iterationGroups.set(parentId, new Map())
|
||||
}
|
||||
|
||||
const iterationMap = iterationGroups.get(parentId)
|
||||
if (!iterationMap.has(iterationIndex)) {
|
||||
iterationMap.set(iterationIndex, [])
|
||||
}
|
||||
|
||||
iterationMap.get(iterationIndex).push(`${node.nodeId}_${index}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Create virtual iteration container nodes
|
||||
iterationGroups.forEach((iterationMap, parentId) => {
|
||||
iterationMap.forEach((nodeIds, iterationIndex) => {
|
||||
// Find the parent iteration node
|
||||
let parentNode = null
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].nodeId === parentId) {
|
||||
parentNode = nodes[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!parentNode) return
|
||||
|
||||
// Get iteration context from first child node
|
||||
const firstChildId = nodeIds[0]
|
||||
const firstChild = nodeMap.get(firstChildId)
|
||||
const iterationContext = firstChild?.data?.iterationContext || { index: iterationIndex }
|
||||
|
||||
// Create a virtual node for this iteration
|
||||
const iterationNodeId = `${parentId}_${iterationIndex}`
|
||||
const iterationLabel = `Iteration #${iterationIndex}`
|
||||
|
||||
// Determine status based on child nodes
|
||||
const childNodes = nodeIds.map((id) => nodeMap.get(id))
|
||||
const iterationStatus = childNodes.some((n) => n.status === 'ERROR')
|
||||
? 'ERROR'
|
||||
: childNodes.some((n) => n.status === 'INPROGRESS')
|
||||
? 'INPROGRESS'
|
||||
: childNodes.every((n) => n.status === 'FINISHED')
|
||||
? 'FINISHED'
|
||||
: 'UNKNOWN'
|
||||
|
||||
// Create the virtual node and add to nodeMap
|
||||
const virtualNode = {
|
||||
nodeId: iterationNodeId,
|
||||
nodeLabel: iterationLabel,
|
||||
data: {
|
||||
name: 'iterationAgentflow',
|
||||
iterationIndex,
|
||||
iterationContext,
|
||||
isVirtualNode: true,
|
||||
parentIterationId: parentId
|
||||
},
|
||||
previousNodeIds: [], // Will be handled in the main tree building
|
||||
status: iterationStatus,
|
||||
uniqueNodeId: iterationNodeId,
|
||||
children: [],
|
||||
executionIndex: -1 // Flag as a virtual node
|
||||
}
|
||||
|
||||
nodeMap.set(iterationNodeId, virtualNode)
|
||||
|
||||
// Set this virtual node as the parent for all nodes in this iteration
|
||||
nodeIds.forEach((childId) => {
|
||||
const childNode = nodeMap.get(childId)
|
||||
if (childNode) {
|
||||
childNode.virtualParentId = iterationNodeId
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Root nodes have no previous nodes
|
||||
const rootNodes = []
|
||||
const processedNodes = new Set()
|
||||
|
||||
// First pass: Build the main tree structure (excluding iteration children)
|
||||
nodes.forEach((node, index) => {
|
||||
const uniqueNodeId = `${node.nodeId}_${index}`
|
||||
const treeNode = nodeMap.get(uniqueNodeId)
|
||||
|
||||
// Skip nodes that belong to an iteration (they'll be added to their virtual parent)
|
||||
if (node.data?.parentNodeId && node.data?.iterationIndex !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (node.previousNodeIds.length === 0) {
|
||||
rootNodes.push(treeNode)
|
||||
} else {
|
||||
// Find the most recent (latest) parent node among all previous nodes
|
||||
let mostRecentParentIndex = -1
|
||||
let mostRecentParentId = null
|
||||
|
||||
node.previousNodeIds.forEach((parentId) => {
|
||||
// Find the most recent instance of this parent node
|
||||
for (let i = 0; i < index; i++) {
|
||||
if (nodes[i].nodeId === parentId && i > mostRecentParentIndex) {
|
||||
mostRecentParentIndex = i
|
||||
mostRecentParentId = parentId
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Only add to the most recent parent
|
||||
if (mostRecentParentIndex !== -1) {
|
||||
const parentUniqueId = `${mostRecentParentId}_${mostRecentParentIndex}`
|
||||
const parentNode = nodeMap.get(parentUniqueId)
|
||||
if (parentNode) {
|
||||
parentNode.children.push(treeNode)
|
||||
processedNodes.add(uniqueNodeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Second pass: Build the iteration sub-trees
|
||||
iterationGroups.forEach((iterationMap, parentId) => {
|
||||
// Find all instances of the parent node
|
||||
const parentInstances = []
|
||||
nodes.forEach((node, index) => {
|
||||
if (node.nodeId === parentId) {
|
||||
parentInstances.push(`${node.nodeId}_${index}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Find the latest instance of the parent node that exists in the tree
|
||||
let latestParent = null
|
||||
for (let i = parentInstances.length - 1; i >= 0; i--) {
|
||||
const parentId = parentInstances[i]
|
||||
const parent = nodeMap.get(parentId)
|
||||
if (parent) {
|
||||
latestParent = parent
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!latestParent) return
|
||||
|
||||
// Add all virtual iteration nodes to the parent
|
||||
iterationMap.forEach((nodeIds, iterationIndex) => {
|
||||
const iterationNodeId = `${parentId}_${iterationIndex}`
|
||||
const virtualNode = nodeMap.get(iterationNodeId)
|
||||
if (virtualNode) {
|
||||
latestParent.children.push(virtualNode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Third pass: Build the structure inside each virtual iteration node
|
||||
nodeMap.forEach((node) => {
|
||||
if (node.virtualParentId) {
|
||||
const virtualParent = nodeMap.get(node.virtualParentId)
|
||||
if (virtualParent) {
|
||||
if (node.previousNodeIds.length === 0) {
|
||||
// This is a root node within the iteration
|
||||
virtualParent.children.push(node)
|
||||
} else {
|
||||
// Find its parent within the same iteration
|
||||
let parentFound = false
|
||||
for (const prevNodeId of node.previousNodeIds) {
|
||||
// Look for nodes with the same previous node ID in the same iteration
|
||||
nodeMap.forEach((potentialParent) => {
|
||||
if (
|
||||
potentialParent.nodeId === prevNodeId &&
|
||||
potentialParent.data?.iterationIndex === node.data?.iterationIndex &&
|
||||
potentialParent.data?.parentNodeId === node.data?.parentNodeId &&
|
||||
!parentFound
|
||||
) {
|
||||
potentialParent.children.push(node)
|
||||
parentFound = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// If no parent was found within the iteration, add directly to virtual parent
|
||||
if (!parentFound) {
|
||||
virtualParent.children.push(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Final pass: Sort all children arrays to ensure iteration nodes appear first
|
||||
const sortChildrenNodes = (node) => {
|
||||
if (node.children && node.children.length > 0) {
|
||||
// Sort children: iteration nodes first, then others by their original execution order
|
||||
node.children.sort((a, b) => {
|
||||
// Check if a is an iteration node
|
||||
const aIsIteration = a.data?.name === 'iterationAgentflow' || a.data?.isVirtualNode
|
||||
// Check if b is an iteration node
|
||||
const bIsIteration = b.data?.name === 'iterationAgentflow' || b.data?.isVirtualNode
|
||||
|
||||
// If both are iterations or both are not iterations, preserve original order
|
||||
if (aIsIteration === bIsIteration) {
|
||||
return a.executionIndex - b.executionIndex
|
||||
}
|
||||
|
||||
// Otherwise, put iterations first
|
||||
return aIsIteration ? -1 : 1
|
||||
})
|
||||
|
||||
// Recursively sort children's children
|
||||
node.children.forEach(sortChildrenNodes)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply sorting to all root nodes and their children
|
||||
rootNodes.forEach(sortChildrenNodes)
|
||||
|
||||
// Transform to the required format
|
||||
const transformNode = (node) => ({
|
||||
id: node.uniqueNodeId,
|
||||
label: node.nodeLabel,
|
||||
name: node.data?.name,
|
||||
status: node.status,
|
||||
data: node.data,
|
||||
children: node.children.map(transformNode)
|
||||
})
|
||||
|
||||
return rootNodes.map(transformNode)
|
||||
}
|
||||
|
||||
const handleExpandedItemsChange = (event, itemIds) => {
|
||||
setExpandedItems(itemIds)
|
||||
}
|
||||
|
||||
const onSharePublicly = () => {
|
||||
const newIsPublic = !localMetadata.isPublic
|
||||
updateExecutionApi.request(localMetadata.id, { isPublic: newIsPublic }).then(() => {
|
||||
// Update local metadata to reflect the change
|
||||
setLocalMetadata((prev) => ({
|
||||
...prev,
|
||||
isPublic: newIsPublic
|
||||
}))
|
||||
|
||||
// Show success message
|
||||
dispatch(
|
||||
enqueueSnackbarAction({
|
||||
message: newIsPublic ? 'Execution shared publicly' : 'Execution is no longer public',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => dispatch(closeSnackbarAction(key))}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Notify parent component to refresh data
|
||||
if (onUpdateSharing) {
|
||||
onUpdateSharing()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (execution) {
|
||||
const newTree = buildTreeData(execution)
|
||||
|
||||
// Find first stopped item if metadata state is STOPPED
|
||||
if (metadata?.state === 'STOPPED') {
|
||||
const findFirstStoppedNode = (nodes) => {
|
||||
for (const node of nodes) {
|
||||
if (node.status === 'STOPPED') return node
|
||||
if (node.children) {
|
||||
const found = findFirstStoppedNode(node.children)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
const stoppedNode = findFirstStoppedNode(newTree)
|
||||
|
||||
if (stoppedNode) {
|
||||
setExpandedItems(getAllNodeIds(newTree))
|
||||
setSelectedItem(stoppedNode)
|
||||
} else {
|
||||
setExpandedItems(getAllNodeIds(newTree))
|
||||
// Set the first item as default selected item
|
||||
if (newTree.length > 0) {
|
||||
setSelectedItem(newTree[0])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setExpandedItems(getAllNodeIds(newTree))
|
||||
// Set the first item as default selected item
|
||||
if (newTree.length > 0) {
|
||||
setSelectedItem(newTree[0])
|
||||
}
|
||||
}
|
||||
setExecution(newTree)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [execution, metadata])
|
||||
|
||||
const handleNodeSelect = (event, itemId) => {
|
||||
const findNode = (nodes, id) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node
|
||||
if (node.children) {
|
||||
const found = findNode(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
const selectedNode = findNode(executionTree, itemId)
|
||||
setSelectedItem(selectedNode)
|
||||
}
|
||||
|
||||
// Content to be rendered in both drawer and full page modes
|
||||
const contentComponent = (
|
||||
<Box sx={{ display: 'flex', height: '100%', flexDirection: 'row' }}>
|
||||
<Box
|
||||
sx={{
|
||||
flex: '1 1 35%',
|
||||
padding: 2,
|
||||
borderRight: 1,
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
pb: 1,
|
||||
mb: 2,
|
||||
backgroundColor: (theme) => theme.palette.background.paper,
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
{!isPublic && (
|
||||
<Chip
|
||||
sx={{ pl: 1 }}
|
||||
icon={<IconExternalLink size={15} />}
|
||||
variant='outlined'
|
||||
label={metadata?.agentflow?.name || metadata?.agentflow?.id || 'Go to AgentFlow'}
|
||||
className={'button'}
|
||||
onClick={() => window.open(`/v2/agentcanvas/${metadata?.agentflow?.id}`, '_blank')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isPublic && (
|
||||
<Tooltip
|
||||
title={`Execution ID: ${localMetadata?.id || ''}`}
|
||||
placement='top'
|
||||
disableHoverListener={!localMetadata?.id}
|
||||
>
|
||||
<Chip
|
||||
sx={{ ml: 1, pl: 1 }}
|
||||
icon={<IconCopy size={15} />}
|
||||
variant='outlined'
|
||||
label={copied ? 'Copied!' : 'Copy ID'}
|
||||
className={'button'}
|
||||
onClick={copyToClipboard}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!isPublic && !localMetadata.isPublic && (
|
||||
<Chip
|
||||
sx={{ ml: 1, pl: 1 }}
|
||||
icon={
|
||||
updateExecutionApi.loading ? (
|
||||
<IconLoader size={15} className='spin-animation' />
|
||||
) : (
|
||||
<IconShare size={15} />
|
||||
)
|
||||
}
|
||||
variant='outlined'
|
||||
label={updateExecutionApi.loading ? 'Updating...' : 'Share'}
|
||||
className={'button'}
|
||||
onClick={() => onSharePublicly()}
|
||||
disabled={updateExecutionApi.loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isPublic && localMetadata.isPublic && (
|
||||
<Chip
|
||||
sx={{ ml: 1, pl: 1 }}
|
||||
icon={
|
||||
updateExecutionApi.loading ? (
|
||||
<IconLoader size={15} className='spin-animation' />
|
||||
) : (
|
||||
<IconWorld size={15} />
|
||||
)
|
||||
}
|
||||
variant='outlined'
|
||||
label={updateExecutionApi.loading ? 'Updating...' : 'Public'}
|
||||
className={'button'}
|
||||
onClick={() => setShowShareDialog(true)}
|
||||
disabled={updateExecutionApi.loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', alignContent: 'center' }}>
|
||||
<Typography sx={{ flex: 1, mt: 1 }} color='text.primary'>
|
||||
{metadata?.updatedDate ? moment(metadata.updatedDate).format('MMM D, YYYY h:mm A') : 'N/A'}
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={() => onRefresh(localMetadata?.id)}
|
||||
size='small'
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: (theme) => theme.palette.primary.main + '20'
|
||||
}
|
||||
}}
|
||||
title='Refresh execution data'
|
||||
>
|
||||
<IconRefresh size={20} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<RichTreeView
|
||||
expandedItems={expandedItems}
|
||||
onExpandedItemsChange={handleExpandedItemsChange}
|
||||
selectedItems={selectedItem ? [selectedItem.id] : []}
|
||||
onSelectedItemsChange={handleNodeSelect}
|
||||
items={executionTree}
|
||||
slots={{
|
||||
item: CustomTreeItem
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
flex: '1 1 65%',
|
||||
padding: 2,
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
{selectedItem && selectedItem.data ? (
|
||||
<NodeExecutionDetails
|
||||
data={selectedItem.data}
|
||||
label={selectedItem.label}
|
||||
status={selectedItem.status}
|
||||
metadata={metadata}
|
||||
isPublic={isPublic}
|
||||
onProceedSuccess={onProceedSuccess}
|
||||
/>
|
||||
) : (
|
||||
<Typography color='text.secondary'>No data available for this item</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
// Resize handle component (shared between modes)
|
||||
const resizeHandle = (
|
||||
<button
|
||||
aria-label='Resize drawer'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '8px',
|
||||
cursor: 'ew-resize',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
'&:hover': {
|
||||
background: 'rgba(0, 0, 0, 0.1)'
|
||||
}
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
// Start resize mode
|
||||
handleMouseDown()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DragHandleIcon
|
||||
sx={{
|
||||
transform: 'rotate(90deg)',
|
||||
fontSize: '20px',
|
||||
color: customization.isDarkMode ? 'white' : 'action.disabled'
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
|
||||
// Render as full page component if isPublic is true
|
||||
if (isPublic) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 1300,
|
||||
backgroundColor: (theme) => theme.palette.background.paper
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{contentComponent}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Render as drawer component (original behavior)
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
variant='temporary'
|
||||
anchor='right'
|
||||
sx={{
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
'& .MuiDrawer-paper': {
|
||||
width: drawerWidth,
|
||||
height: '100%'
|
||||
}
|
||||
}}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
>
|
||||
{resizeHandle}
|
||||
{contentComponent}
|
||||
</Drawer>
|
||||
<ShareExecutionDialog
|
||||
show={showShareDialog}
|
||||
executionId={localMetadata?.id}
|
||||
onClose={() => setShowShareDialog(false)}
|
||||
onUnshare={() => {
|
||||
updateExecutionApi.request(localMetadata.id, { isPublic: false }).then(() => {
|
||||
// Update local metadata to reflect the change
|
||||
setLocalMetadata((prev) => ({
|
||||
...prev,
|
||||
isPublic: false
|
||||
}))
|
||||
setShowShareDialog(false)
|
||||
|
||||
// Notify parent component to refresh data
|
||||
if (onUpdateSharing) {
|
||||
onUpdateSharing()
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ExecutionDetails.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
isPublic: PropTypes.bool,
|
||||
execution: PropTypes.array,
|
||||
metadata: PropTypes.object,
|
||||
onClose: PropTypes.func,
|
||||
onProceedSuccess: PropTypes.func,
|
||||
onUpdateSharing: PropTypes.func,
|
||||
onRefresh: PropTypes.func
|
||||
}
|
||||
|
||||
ExecutionDetails.displayName = 'ExecutionDetails'
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { ExecutionDetails } from './ExecutionDetails'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
// API
|
||||
import executionsApi from '@/api/executions'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
// MUI
|
||||
import { Box, Card, Stack, Typography, useTheme } from '@mui/material'
|
||||
import { IconCircleXFilled } from '@tabler/icons-react'
|
||||
import { alpha } from '@mui/material/styles'
|
||||
|
||||
// ==============================|| PublicExecutionDetails ||============================== //
|
||||
|
||||
const PublicExecutionDetails = () => {
|
||||
const { id: executionId } = useParams()
|
||||
const theme = useTheme()
|
||||
|
||||
const [execution, setExecution] = useState(null)
|
||||
const [selectedMetadata, setSelectedMetadata] = useState({})
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
|
||||
const getExecutionByIdPublicApi = useApi(executionsApi.getExecutionByIdPublic)
|
||||
|
||||
useEffect(() => {
|
||||
getExecutionByIdPublicApi.request(executionId)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (getExecutionByIdPublicApi.data) {
|
||||
const execution = getExecutionByIdPublicApi.data
|
||||
const executionDetails =
|
||||
typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData
|
||||
setExecution(executionDetails)
|
||||
setSelectedMetadata(omit(execution, ['executionData']))
|
||||
}
|
||||
}, [getExecutionByIdPublicApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(getExecutionByIdPublicApi.loading)
|
||||
}, [getExecutionByIdPublicApi.loading])
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLoading ? (
|
||||
<>
|
||||
{!execution || getExecutionByIdPublicApi.error ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80vh' }}>
|
||||
<Box sx={{ maxWidth: '500px', width: '100%' }}>
|
||||
<Card
|
||||
variant='outlined'
|
||||
sx={{
|
||||
border: `1px solid ${theme.palette.error.main}`,
|
||||
borderRadius: 2,
|
||||
padding: '20px',
|
||||
boxShadow: `0 4px 8px ${alpha(theme.palette.error.main, 0.15)}`
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2} alignItems='center'>
|
||||
<IconCircleXFilled size={50} color={theme.palette.error.main} />
|
||||
<Typography variant='h3' color='error.main' align='center'>
|
||||
Invalid Execution
|
||||
</Typography>
|
||||
<Typography variant='body1' color='text.secondary' align='center'>
|
||||
{`The execution you're looking for doesn't exist or you don't have permission to view it.`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<ExecutionDetails
|
||||
isPublic={true}
|
||||
execution={execution}
|
||||
metadata={selectedMetadata}
|
||||
onProceedSuccess={() => {
|
||||
getExecutionByIdPublicApi.request(executionId)
|
||||
}}
|
||||
onRefresh={(executionId) => {
|
||||
getExecutionByIdPublicApi.request(executionId)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PublicExecutionDetails
|
||||
@@ -0,0 +1,126 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useState } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
|
||||
// Material
|
||||
import { Typography, Box, Dialog, DialogContent, DialogTitle, Button, Tooltip } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { IconCopy, IconX, IconLink } from '@tabler/icons-react'
|
||||
|
||||
// Constants
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
|
||||
|
||||
// API
|
||||
import executionsApi from '@/api/executions'
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
const ShareExecutionDialog = ({ show, executionId, onClose, onUnshare }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const updateExecutionApi = useApi(executionsApi.updateExecution)
|
||||
|
||||
// Create shareable link
|
||||
const origin = window.location.origin
|
||||
const shareableLink = `${origin}/execution/${executionId}`
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(shareableLink)
|
||||
setCopied(true)
|
||||
|
||||
// Show success message
|
||||
dispatch(
|
||||
enqueueSnackbarAction({
|
||||
message: 'Link copied to clipboard',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => dispatch(closeSnackbarAction(key))}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Reset copied state after 2 seconds
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const handleUnshare = () => {
|
||||
updateExecutionApi.request(executionId, { isPublic: false })
|
||||
if (onUnshare) onUnshare()
|
||||
onClose()
|
||||
}
|
||||
|
||||
const component = show ? (
|
||||
<Dialog open={show} onClose={onClose} maxWidth='sm' fullWidth aria-labelledby='share-dialog-title'>
|
||||
<DialogTitle id='share-dialog-title' sx={{ fontSize: '1.1rem', fontWeight: 600 }}>
|
||||
Public Trace Link
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant='body2' color='text.secondary' sx={{ mb: 2 }}>
|
||||
Anyone with the link below can view this execution trace.
|
||||
</Typography>
|
||||
|
||||
{/* Link Display Box */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
mb: 3,
|
||||
p: 1,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: '8px',
|
||||
backgroundColor: customization.isDarkMode ? theme.palette.background.paper : theme.palette.grey[100]
|
||||
}}
|
||||
>
|
||||
<IconLink size={20} style={{ marginRight: '8px', color: theme.palette.text.secondary }} />
|
||||
<Typography
|
||||
variant='body2'
|
||||
sx={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
color: theme.palette.primary.main,
|
||||
mr: 1
|
||||
}}
|
||||
>
|
||||
{shareableLink}
|
||||
</Typography>
|
||||
<Tooltip title={copied ? 'Copied!' : 'Copy link'}>
|
||||
<Button variant='text' color='primary' onClick={copyToClipboard} startIcon={<IconCopy size={18} />}>
|
||||
Copy
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Actions */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button color='error' onClick={handleUnshare} sx={{ mr: 1 }}>
|
||||
Unshare
|
||||
</Button>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
ShareExecutionDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
executionId: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
onUnshare: PropTypes.func
|
||||
}
|
||||
|
||||
export default ShareExecutionDialog
|
||||
@@ -0,0 +1,464 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import DatePicker from 'react-datepicker'
|
||||
import 'react-datepicker/dist/react-datepicker.css'
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
Pagination,
|
||||
Box,
|
||||
Stack,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Button,
|
||||
Grid,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import ErrorBoundary from '@/ErrorBoundary'
|
||||
import ViewHeader from '@/layout/MainLayout/ViewHeader'
|
||||
|
||||
// API
|
||||
import useApi from '@/hooks/useApi'
|
||||
import executionsApi from '@/api/executions'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
// icons
|
||||
import execution_empty from '@/assets/images/executions_empty.svg'
|
||||
import { IconTrash } from '@tabler/icons-react'
|
||||
|
||||
// const
|
||||
import { ExecutionsListTable } from '@/ui-component/table/ExecutionsListTable'
|
||||
import { ExecutionDetails } from './ExecutionDetails'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
// ==============================|| AGENT EXECUTIONS ||============================== //
|
||||
|
||||
const AgentExecutions = () => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const borderColor = theme.palette.grey[900] + 25
|
||||
|
||||
const getAllExecutions = useApi(executionsApi.getAllExecutions)
|
||||
const deleteExecutionsApi = useApi(executionsApi.deleteExecutions)
|
||||
const getExecutionByIdApi = useApi(executionsApi.getExecutionById)
|
||||
|
||||
const [error, setError] = useState(null)
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [executions, setExecutions] = useState([])
|
||||
const [openDrawer, setOpenDrawer] = useState(false)
|
||||
const [selectedExecutionData, setSelectedExecutionData] = useState([])
|
||||
const [selectedMetadata, setSelectedMetadata] = useState({})
|
||||
const [selectedExecutionIds, setSelectedExecutionIds] = useState([])
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false)
|
||||
const [filters, setFilters] = useState({
|
||||
state: '',
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
agentflowId: '',
|
||||
sessionId: ''
|
||||
})
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const handleFilterChange = (field, value) => {
|
||||
setFilters({
|
||||
...filters,
|
||||
[field]: value
|
||||
})
|
||||
}
|
||||
|
||||
const onDateChange = (field, date) => {
|
||||
const updatedDate = new Date(date)
|
||||
updatedDate.setHours(0, 0, 0, 0)
|
||||
|
||||
setFilters({
|
||||
...filters,
|
||||
[field]: updatedDate
|
||||
})
|
||||
}
|
||||
|
||||
const handlePageChange = (event, newPage) => {
|
||||
setPagination({
|
||||
...pagination,
|
||||
page: newPage
|
||||
})
|
||||
}
|
||||
|
||||
const handleLimitChange = (event) => {
|
||||
setPagination({
|
||||
...pagination,
|
||||
page: 1, // Reset to first page when changing items per page
|
||||
limit: parseInt(event.target.value, 10)
|
||||
})
|
||||
}
|
||||
|
||||
const applyFilters = () => {
|
||||
setLoading(true)
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit
|
||||
}
|
||||
|
||||
if (filters.state) params.state = filters.state
|
||||
|
||||
// Create date strings that preserve the exact date values
|
||||
if (filters.startDate) {
|
||||
const date = new Date(filters.startDate)
|
||||
// Format date as YYYY-MM-DD and set to start of day in UTC
|
||||
// This ensures the server sees the same date we've selected regardless of timezone
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
params.startDate = `${year}-${month}-${day}T00:00:00.000Z`
|
||||
}
|
||||
|
||||
if (filters.endDate) {
|
||||
const date = new Date(filters.endDate)
|
||||
// Format date as YYYY-MM-DD and set to end of day in UTC
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
params.endDate = `${year}-${month}-${day}T23:59:59.999Z`
|
||||
}
|
||||
|
||||
if (filters.agentflowId) params.agentflowId = filters.agentflowId
|
||||
if (filters.sessionId) params.sessionId = filters.sessionId
|
||||
|
||||
getAllExecutions.request(params)
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
setFilters({
|
||||
state: '',
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
agentflowId: '',
|
||||
sessionId: ''
|
||||
})
|
||||
getAllExecutions.request()
|
||||
}
|
||||
|
||||
const handleExecutionSelectionChange = (selectedIds) => {
|
||||
setSelectedExecutionIds(selectedIds)
|
||||
}
|
||||
|
||||
const handleDeleteDialogOpen = () => {
|
||||
if (selectedExecutionIds.length > 0) {
|
||||
setOpenDeleteDialog(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteDialogClose = () => {
|
||||
setOpenDeleteDialog(false)
|
||||
}
|
||||
|
||||
const handleDeleteExecutions = () => {
|
||||
deleteExecutionsApi.request(selectedExecutionIds)
|
||||
setOpenDeleteDialog(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAllExecutions.request()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllExecutions.data) {
|
||||
try {
|
||||
const { data, total } = getAllExecutions.data
|
||||
if (!Array.isArray(data)) return
|
||||
setExecutions(data)
|
||||
setPagination((prev) => ({ ...prev, total }))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}, [getAllExecutions.data])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(getAllExecutions.loading)
|
||||
}, [getAllExecutions.loading])
|
||||
|
||||
useEffect(() => {
|
||||
setError(getAllExecutions.error)
|
||||
}, [getAllExecutions.error])
|
||||
|
||||
useEffect(() => {
|
||||
applyFilters()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pagination.page, pagination.limit])
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteExecutionsApi.data) {
|
||||
// Refresh the executions list
|
||||
getAllExecutions.request({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit
|
||||
})
|
||||
setSelectedExecutionIds([])
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [deleteExecutionsApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getExecutionByIdApi.data) {
|
||||
const execution = getExecutionByIdApi.data
|
||||
const executionDetails =
|
||||
typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData
|
||||
setSelectedExecutionData(executionDetails)
|
||||
setSelectedMetadata(omit(execution, ['executionData']))
|
||||
}
|
||||
}, [getExecutionByIdApi.data])
|
||||
|
||||
return (
|
||||
<MainCard>
|
||||
{error ? (
|
||||
<ErrorBoundary error={error} />
|
||||
) : (
|
||||
<Stack flexDirection='column' sx={{ gap: 3 }}>
|
||||
<ViewHeader title='Agent Executions' description='Monitor and manage agentflows executions' />
|
||||
|
||||
{/* Filter Section */}
|
||||
<Box sx={{ mb: 2, width: '100%' }}>
|
||||
<Grid container spacing={2} alignItems='center'>
|
||||
<Grid item xs={12} md={2}>
|
||||
<FormControl fullWidth size='small'>
|
||||
<InputLabel id='state-select-label'>State</InputLabel>
|
||||
<Select
|
||||
labelId='state-select-label'
|
||||
value={filters.state}
|
||||
label='State'
|
||||
onChange={(e) => handleFilterChange('state', e.target.value)}
|
||||
size='small'
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: borderColor
|
||||
},
|
||||
'& .MuiSvgIcon-root': {
|
||||
color: customization.isDarkMode ? '#fff' : 'inherit'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value=''>All</MenuItem>
|
||||
<MenuItem value='INPROGRESS'>In Progress</MenuItem>
|
||||
<MenuItem value='FINISHED'>Finished</MenuItem>
|
||||
<MenuItem value='ERROR'>Error</MenuItem>
|
||||
<MenuItem value='TERMINATED'>Terminated</MenuItem>
|
||||
<MenuItem value='TIMEOUT'>Timeout</MenuItem>
|
||||
<MenuItem value='STOPPED'>Stopped</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={2}>
|
||||
<DatePicker
|
||||
selected={filters.startDate}
|
||||
onChange={(date) => onDateChange('startDate', date)}
|
||||
selectsStart
|
||||
startDate={filters.startDate}
|
||||
className='form-control'
|
||||
wrapperClassName='datePicker'
|
||||
maxDate={new Date()}
|
||||
customInput={
|
||||
<TextField
|
||||
size='small'
|
||||
label='Start date'
|
||||
fullWidth
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: borderColor
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid sx={{ ml: -1 }} item xs={12} md={2}>
|
||||
<DatePicker
|
||||
selected={filters.endDate}
|
||||
onChange={(date) => onDateChange('endDate', date)}
|
||||
selectsEnd
|
||||
endDate={filters.endDate}
|
||||
className='form-control'
|
||||
wrapperClassName='datePicker'
|
||||
minDate={filters.startDate}
|
||||
maxDate={new Date()}
|
||||
customInput={
|
||||
<TextField
|
||||
size='small'
|
||||
label='End date'
|
||||
fullWidth
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: borderColor
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid sx={{ ml: -1 }} item xs={12} md={2}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label='Session ID'
|
||||
value={filters.sessionId}
|
||||
onChange={(e) => handleFilterChange('sessionId', e.target.value)}
|
||||
size='small'
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: borderColor
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Stack direction='row' spacing={1}>
|
||||
<Button variant='contained' color='primary' onClick={applyFilters} size='small'>
|
||||
Apply
|
||||
</Button>
|
||||
<Button variant='outlined' onClick={resetFilters} size='small'>
|
||||
Reset
|
||||
</Button>
|
||||
<Tooltip title='Delete selected executions'>
|
||||
<span>
|
||||
<IconButton
|
||||
sx={{ height: 30, width: 30 }}
|
||||
size='small'
|
||||
color='error'
|
||||
onClick={handleDeleteDialogOpen}
|
||||
edge='end'
|
||||
disabled={selectedExecutionIds.length === 0}
|
||||
>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<ExecutionsListTable
|
||||
data={executions}
|
||||
isLoading={isLoading}
|
||||
onSelectionChange={handleExecutionSelectionChange}
|
||||
onExecutionRowClick={(execution) => {
|
||||
setOpenDrawer(true)
|
||||
const executionDetails =
|
||||
typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData
|
||||
setSelectedExecutionData(executionDetails)
|
||||
setSelectedMetadata(omit(execution, ['executionData']))
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Pagination and Page Size Controls */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Typography variant='body2'>Items per page:</Typography>
|
||||
<FormControl
|
||||
variant='outlined'
|
||||
size='small'
|
||||
sx={{
|
||||
minWidth: 80,
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: borderColor
|
||||
},
|
||||
'& .MuiSvgIcon-root': {
|
||||
color: customization.isDarkMode ? '#fff' : 'inherit'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select value={pagination.limit} onChange={handleLimitChange} displayEmpty>
|
||||
<MenuItem value={10}>10</MenuItem>
|
||||
<MenuItem value={50}>50</MenuItem>
|
||||
<MenuItem value={100}>100</MenuItem>
|
||||
<MenuItem value={1000}>1000</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Pagination
|
||||
count={Math.ceil(pagination.total / pagination.limit)}
|
||||
page={pagination.page}
|
||||
onChange={handlePageChange}
|
||||
color='primary'
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<ExecutionDetails
|
||||
open={openDrawer}
|
||||
execution={selectedExecutionData}
|
||||
metadata={selectedMetadata}
|
||||
onClose={() => setOpenDrawer(false)}
|
||||
onProceedSuccess={() => {
|
||||
setOpenDrawer(false)
|
||||
getAllExecutions.request()
|
||||
}}
|
||||
onUpdateSharing={() => {
|
||||
getAllExecutions.request()
|
||||
}}
|
||||
onRefresh={(executionId) => {
|
||||
getAllExecutions.request()
|
||||
getExecutionByIdApi.request(executionId)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog
|
||||
open={openDeleteDialog}
|
||||
onClose={handleDeleteDialogClose}
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle id='alert-dialog-title'>Confirm Deletion</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id='alert-dialog-description'>
|
||||
Are you sure you want to delete {selectedExecutionIds.length} execution
|
||||
{selectedExecutionIds.length !== 1 ? 's' : ''}? This action cannot be undone.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDeleteDialogClose} color='primary'>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleDeleteExecutions} color='error'>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{!isLoading && (!executions || executions.length === 0) && (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
||||
<Box sx={{ p: 2, height: 'auto' }}>
|
||||
<img
|
||||
style={{ objectFit: 'cover', height: '20vh', width: 'auto' }}
|
||||
src={execution_empty}
|
||||
alt='execution_empty'
|
||||
/>
|
||||
</Box>
|
||||
<div>No Executions Yet</div>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</MainCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentExecutions
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { Box, Skeleton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'
|
||||
import { Chip, Box, Skeleton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
@@ -24,7 +24,7 @@ import chatflowsApi from '@/api/chatflows'
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
// const
|
||||
import { baseURL } from '@/store/constant'
|
||||
import { baseURL, AGENTFLOW_ICONS } from '@/store/constant'
|
||||
|
||||
// icons
|
||||
import { IconPlus, IconLayoutGrid, IconList } from '@tabler/icons-react'
|
||||
@@ -38,12 +38,14 @@ const Agentflows = () => {
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [images, setImages] = useState({})
|
||||
const [icons, setIcons] = 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 [agentflowVersion, setAgentflowVersion] = useState(localStorage.getItem('agentFlowVersion') || 'v2')
|
||||
|
||||
const handleChange = (event, nextView) => {
|
||||
if (nextView === null) return
|
||||
@@ -51,6 +53,13 @@ const Agentflows = () => {
|
||||
setView(nextView)
|
||||
}
|
||||
|
||||
const handleVersionChange = (event, nextView) => {
|
||||
if (nextView === null) return
|
||||
localStorage.setItem('agentFlowVersion', nextView)
|
||||
setAgentflowVersion(nextView)
|
||||
getAllAgentflows.request(nextView === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT')
|
||||
}
|
||||
|
||||
const onSearchChange = (event) => {
|
||||
setSearch(event.target.value)
|
||||
}
|
||||
@@ -70,15 +79,23 @@ const Agentflows = () => {
|
||||
}
|
||||
|
||||
const addNew = () => {
|
||||
navigate('/agentcanvas')
|
||||
if (agentflowVersion === 'v2') {
|
||||
navigate('/v2/agentcanvas')
|
||||
} else {
|
||||
navigate('/agentcanvas')
|
||||
}
|
||||
}
|
||||
|
||||
const goToCanvas = (selectedAgentflow) => {
|
||||
navigate(`/agentcanvas/${selectedAgentflow.id}`)
|
||||
if (selectedAgentflow.type === 'AGENTFLOW') {
|
||||
navigate(`/v2/agentcanvas/${selectedAgentflow.id}`)
|
||||
} else {
|
||||
navigate(`/agentcanvas/${selectedAgentflow.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAllAgentflows.request()
|
||||
getAllAgentflows.request(agentflowVersion === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT')
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
@@ -106,19 +123,27 @@ const Agentflows = () => {
|
||||
try {
|
||||
const agentflows = getAllAgentflows.data
|
||||
const images = {}
|
||||
const icons = {}
|
||||
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] = []
|
||||
icons[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)
|
||||
const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === nodes[j].data.name)
|
||||
if (foundIcon) {
|
||||
icons[agentflows[i].id].push(foundIcon)
|
||||
} else {
|
||||
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)
|
||||
setIcons(icons)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
@@ -131,7 +156,46 @@ const Agentflows = () => {
|
||||
<ErrorBoundary error={error} />
|
||||
) : (
|
||||
<Stack flexDirection='column' sx={{ gap: 3 }}>
|
||||
<ViewHeader onSearchChange={onSearchChange} search={true} searchPlaceholder='Search Name or Category' title='Agents'>
|
||||
<ViewHeader
|
||||
onSearchChange={onSearchChange}
|
||||
search={true}
|
||||
searchPlaceholder='Search Name or Category'
|
||||
title='Agentflows'
|
||||
description='Multi-agent systems, workflow orchestration'
|
||||
>
|
||||
<ToggleButtonGroup
|
||||
sx={{ borderRadius: 2, maxHeight: 40 }}
|
||||
value={agentflowVersion}
|
||||
color='primary'
|
||||
exclusive
|
||||
onChange={handleVersionChange}
|
||||
>
|
||||
<ToggleButton
|
||||
sx={{
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
borderRadius: 2,
|
||||
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
|
||||
}}
|
||||
variant='contained'
|
||||
value='v2'
|
||||
title='V2'
|
||||
>
|
||||
<Chip sx={{ mr: 1 }} label='NEW' size='small' color='primary' />
|
||||
V2
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
sx={{
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
borderRadius: 2,
|
||||
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
|
||||
}}
|
||||
variant='contained'
|
||||
value='v1'
|
||||
title='V1'
|
||||
>
|
||||
V1
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<ToggleButtonGroup
|
||||
sx={{ borderRadius: 2, maxHeight: 40 }}
|
||||
value={view}
|
||||
@@ -179,7 +243,13 @@ const Agentflows = () => {
|
||||
) : (
|
||||
<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]} />
|
||||
<ItemCard
|
||||
key={index}
|
||||
onClick={() => goToCanvas(data)}
|
||||
data={data}
|
||||
images={images[data.id]}
|
||||
icons={icons[data.id]}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
@@ -189,6 +259,7 @@ const Agentflows = () => {
|
||||
isAgentCanvas={true}
|
||||
data={getAllAgentflows.data}
|
||||
images={images}
|
||||
icons={icons}
|
||||
isLoading={isLoading}
|
||||
filterFunction={filterFlows}
|
||||
updateFlowsApi={getAllAgentflows}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { EdgeLabelRenderer, getBezierPath } from 'reactflow'
|
||||
import { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
function EdgeLabel({ transform, isHumanInput, label, color }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
background: 'transparent',
|
||||
left: isHumanInput ? 10 : 0,
|
||||
paddingTop: 1,
|
||||
color: color,
|
||||
fontSize: '0.5rem',
|
||||
fontWeight: 700,
|
||||
transform,
|
||||
zIndex: 1000
|
||||
}}
|
||||
className='nodrag nopan'
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
EdgeLabel.propTypes = {
|
||||
transform: PropTypes.string,
|
||||
isHumanInput: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
color: PropTypes.string
|
||||
}
|
||||
|
||||
const AgentFlowEdge = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data, markerEnd, selected }) => {
|
||||
const xEqual = sourceX === targetX
|
||||
const yEqual = sourceY === targetY
|
||||
|
||||
const [edgePath] = getBezierPath({
|
||||
// we need this little hack in order to display the gradient for a straight line
|
||||
sourceX: xEqual ? sourceX + 0.0001 : sourceX,
|
||||
sourceY: yEqual ? sourceY + 0.0001 : sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition
|
||||
})
|
||||
|
||||
const gradientId = `edge-gradient-${id}`
|
||||
return (
|
||||
<>
|
||||
<defs>
|
||||
<linearGradient id={gradientId}>
|
||||
<stop offset='0%' stopColor={data?.sourceColor || '#ae53ba'} />
|
||||
<stop offset='100%' stopColor={data?.targetColor || '#2a8af6'} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
id={`${id}-selector`}
|
||||
className='agent-flow-edge-selector'
|
||||
style={{
|
||||
stroke: 'transparent',
|
||||
strokeWidth: 15,
|
||||
fill: 'none',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
d={edgePath}
|
||||
/>
|
||||
<path
|
||||
id={id}
|
||||
className='agent-flow-edge'
|
||||
style={{
|
||||
strokeWidth: selected ? 3 : 2,
|
||||
stroke: `url(#${gradientId})`,
|
||||
filter: selected ? 'drop-shadow(0 0 3px rgba(0,0,0,0.3))' : 'none',
|
||||
cursor: 'pointer',
|
||||
opacity: selected ? 1 : 0.75,
|
||||
fill: 'none'
|
||||
}}
|
||||
d={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
/>
|
||||
{data?.edgeLabel && (
|
||||
<EdgeLabelRenderer>
|
||||
<EdgeLabel
|
||||
isHumanInput={data?.isHumanInput}
|
||||
color={data?.sourceColor || '#ae53ba'}
|
||||
label={data.edgeLabel}
|
||||
transform={`translate(-50%, 0%) translate(${sourceX}px,${sourceY}px)`}
|
||||
/>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
AgentFlowEdge.propTypes = {
|
||||
id: PropTypes.string,
|
||||
sourceX: PropTypes.number,
|
||||
sourceY: PropTypes.number,
|
||||
targetX: PropTypes.number,
|
||||
targetY: PropTypes.number,
|
||||
sourcePosition: PropTypes.any,
|
||||
targetPosition: PropTypes.any,
|
||||
style: PropTypes.object,
|
||||
data: PropTypes.object,
|
||||
markerEnd: PropTypes.any,
|
||||
selected: PropTypes.bool
|
||||
}
|
||||
|
||||
export default memo(AgentFlowEdge)
|
||||
@@ -0,0 +1,484 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useContext, memo, useRef, useState, useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Handle, Position, useUpdateNodeInternals, NodeToolbar } from 'reactflow'
|
||||
|
||||
// material-ui
|
||||
import { styled, useTheme, alpha, darken, lighten } from '@mui/material/styles'
|
||||
import { ButtonGroup, Avatar, Box, Typography, IconButton, Tooltip } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
import NodeInfoDialog from '@/ui-component/dialog/NodeInfoDialog'
|
||||
|
||||
// icons
|
||||
import {
|
||||
IconCheck,
|
||||
IconExclamationMark,
|
||||
IconCircleChevronRightFilled,
|
||||
IconCopy,
|
||||
IconTrash,
|
||||
IconInfoCircle,
|
||||
IconLoader
|
||||
} from '@tabler/icons-react'
|
||||
import StopCircleIcon from '@mui/icons-material/StopCircle'
|
||||
import CancelIcon from '@mui/icons-material/Cancel'
|
||||
|
||||
// const
|
||||
import { baseURL, AGENTFLOW_ICONS } from '@/store/constant'
|
||||
|
||||
const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
background: theme.palette.card.main,
|
||||
color: theme.darkTextPrimary,
|
||||
border: 'solid 1px',
|
||||
width: 'max-content',
|
||||
height: 'auto',
|
||||
padding: '10px',
|
||||
boxShadow: 'none'
|
||||
}))
|
||||
|
||||
const StyledNodeToolbar = styled(NodeToolbar)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.card.main,
|
||||
color: theme.darkTextPrimary,
|
||||
padding: '5px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)'
|
||||
}))
|
||||
|
||||
// ===========================|| CANVAS NODE ||=========================== //
|
||||
|
||||
const AgentFlowNode = ({ data }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const ref = useRef(null)
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
// eslint-disable-next-line
|
||||
const [position, setPosition] = useState(0)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const { deleteNode, duplicateNode } = useContext(flowContext)
|
||||
const [showInfoDialog, setShowInfoDialog] = useState(false)
|
||||
const [infoDialogProps, setInfoDialogProps] = useState({})
|
||||
|
||||
const defaultColor = '#666666' // fallback color if data.color is not present
|
||||
const nodeColor = data.color || defaultColor
|
||||
|
||||
// Get different shades of the color based on state
|
||||
const getStateColor = () => {
|
||||
if (data.selected) return nodeColor
|
||||
if (isHovered) return alpha(nodeColor, 0.8)
|
||||
return alpha(nodeColor, 0.5)
|
||||
}
|
||||
|
||||
const getOutputAnchors = () => {
|
||||
return data.outputAnchors ?? []
|
||||
}
|
||||
|
||||
const getAnchorPosition = (index) => {
|
||||
const currentHeight = ref.current?.clientHeight || 0
|
||||
const spacing = currentHeight / (getOutputAnchors().length + 1)
|
||||
const position = spacing * (index + 1)
|
||||
|
||||
// Update node internals when we get a non-zero position
|
||||
if (position > 0) {
|
||||
updateNodeInternals(data.id)
|
||||
}
|
||||
|
||||
return position
|
||||
}
|
||||
|
||||
const getMinimumHeight = () => {
|
||||
const outputCount = getOutputAnchors().length
|
||||
// Use exactly 60px as minimum height
|
||||
return Math.max(60, outputCount * 20 + 40)
|
||||
}
|
||||
|
||||
const getBackgroundColor = () => {
|
||||
if (customization.isDarkMode) {
|
||||
return isHovered ? darken(nodeColor, 0.7) : darken(nodeColor, 0.8)
|
||||
}
|
||||
return isHovered ? lighten(nodeColor, 0.8) : lighten(nodeColor, 0.9)
|
||||
}
|
||||
|
||||
const getStatusBackgroundColor = (status) => {
|
||||
switch (status) {
|
||||
case 'ERROR':
|
||||
return theme.palette.error.dark
|
||||
case 'INPROGRESS':
|
||||
return theme.palette.warning.dark
|
||||
case 'STOPPED':
|
||||
case 'TERMINATED':
|
||||
return theme.palette.error.main
|
||||
case 'FINISHED':
|
||||
return theme.palette.success.dark
|
||||
default:
|
||||
return theme.palette.primary.dark
|
||||
}
|
||||
}
|
||||
|
||||
const renderIcon = (node) => {
|
||||
const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === node.name)
|
||||
|
||||
if (!foundIcon) return null
|
||||
return <foundIcon.icon size={24} color={'white'} />
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
setTimeout(() => {
|
||||
setPosition(ref.current?.offsetTop + ref.current?.clientHeight / 2)
|
||||
updateNodeInternals(data.id)
|
||||
}, 10)
|
||||
}
|
||||
}, [data, ref, updateNodeInternals])
|
||||
|
||||
return (
|
||||
<div ref={ref} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
|
||||
<StyledNodeToolbar>
|
||||
<ButtonGroup sx={{ gap: 1 }} variant='outlined' aria-label='Basic button group'>
|
||||
{data.name !== 'startAgentflow' && (
|
||||
<IconButton
|
||||
size={'small'}
|
||||
title='Duplicate'
|
||||
onClick={() => {
|
||||
duplicateNode(data.id)
|
||||
}}
|
||||
sx={{
|
||||
color: customization.isDarkMode ? 'white' : 'inherit',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconCopy size={20} />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
size={'small'}
|
||||
title='Delete'
|
||||
onClick={() => {
|
||||
deleteNode(data.id)
|
||||
}}
|
||||
sx={{
|
||||
color: customization.isDarkMode ? 'white' : 'inherit',
|
||||
'&:hover': {
|
||||
color: theme.palette.error.main
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size={'small'}
|
||||
title='Info'
|
||||
onClick={() => {
|
||||
setInfoDialogProps({ data })
|
||||
setShowInfoDialog(true)
|
||||
}}
|
||||
sx={{
|
||||
color: customization.isDarkMode ? 'white' : 'inherit',
|
||||
'&:hover': {
|
||||
color: theme.palette.info.main
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconInfoCircle size={20} />
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
</StyledNodeToolbar>
|
||||
<CardWrapper
|
||||
content={false}
|
||||
sx={{
|
||||
borderColor: getStateColor(),
|
||||
borderWidth: '1px',
|
||||
boxShadow: data.selected ? `0 0 0 1px ${getStateColor()} !important` : 'none',
|
||||
minHeight: getMinimumHeight(),
|
||||
height: 'auto',
|
||||
backgroundColor: getBackgroundColor(),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'&:hover': {
|
||||
boxShadow: data.selected ? `0 0 0 1px ${getStateColor()} !important` : 'none'
|
||||
}
|
||||
}}
|
||||
border={false}
|
||||
>
|
||||
{data && data.status && (
|
||||
<Tooltip title={data.status === 'ERROR' ? data.error || 'Error' : ''}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.smallAvatar,
|
||||
borderRadius: '50%',
|
||||
background:
|
||||
data.status === 'STOPPED' || data.status === 'TERMINATED'
|
||||
? 'white'
|
||||
: getStatusBackgroundColor(data.status),
|
||||
color: 'white',
|
||||
ml: 2,
|
||||
position: 'absolute',
|
||||
top: -10,
|
||||
right: -10
|
||||
}}
|
||||
>
|
||||
{data.status === 'INPROGRESS' ? (
|
||||
<IconLoader className='spin-animation' />
|
||||
) : data.status === 'ERROR' ? (
|
||||
<IconExclamationMark />
|
||||
) : data.status === 'TERMINATED' ? (
|
||||
<CancelIcon sx={{ color: getStatusBackgroundColor(data.status) }} />
|
||||
) : data.status === 'STOPPED' ? (
|
||||
<StopCircleIcon sx={{ color: getStatusBackgroundColor(data.status) }} />
|
||||
) : (
|
||||
<IconCheck />
|
||||
)}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{!data.hideInput && (
|
||||
<Handle
|
||||
type='target'
|
||||
position={Position.Left}
|
||||
id={data.id}
|
||||
style={{
|
||||
width: 5,
|
||||
height: 20,
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
position: 'absolute',
|
||||
left: -2
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 5,
|
||||
height: 20,
|
||||
backgroundColor: nodeColor,
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
/>
|
||||
</Handle>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Box item style={{ width: 50 }}>
|
||||
{data.color && !data.icon ? (
|
||||
<div
|
||||
style={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.largeAvatar,
|
||||
borderRadius: '15px',
|
||||
backgroundColor: data.color,
|
||||
cursor: 'grab',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: data.color
|
||||
}}
|
||||
>
|
||||
{renderIcon(data)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.largeAvatar,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'grab'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }}
|
||||
src={`${baseURL}/api/v1/node-icon/${data.name}`}
|
||||
alt={data.name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 500
|
||||
}}
|
||||
>
|
||||
{data.label}
|
||||
</Typography>
|
||||
|
||||
{(() => {
|
||||
// Array of model configs to check and render
|
||||
const modelConfigs = [
|
||||
{ model: data.inputs?.llmModel, config: data.inputs?.llmModelConfig },
|
||||
{ model: data.inputs?.agentModel, config: data.inputs?.agentModelConfig },
|
||||
{ model: data.inputs?.conditionAgentModel, config: data.inputs?.conditionAgentModelConfig }
|
||||
]
|
||||
|
||||
// Filter out undefined models and render each valid one
|
||||
return modelConfigs
|
||||
.filter((item) => item.model && item.config)
|
||||
.map((item, index) => (
|
||||
<Box key={`model-${index}`} sx={{ display: 'flex', gap: 1, mt: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: customization.isDarkMode
|
||||
? 'rgba(255, 255, 255, 0.2)'
|
||||
: 'rgba(255, 255, 255, 0.9)',
|
||||
borderRadius: '16px',
|
||||
width: 'max-content',
|
||||
height: 24,
|
||||
pl: 1,
|
||||
pr: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ width: 20, height: 20, objectFit: 'contain' }}
|
||||
src={`${baseURL}/api/v1/node-icon/${item.model}`}
|
||||
alt={item.model}
|
||||
/>
|
||||
<Typography sx={{ fontSize: '0.7rem', ml: 0.5 }}>
|
||||
{item.config.modelName || item.config.model}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
})()}
|
||||
|
||||
{(() => {
|
||||
// Array of tool configurations to check and render
|
||||
const toolConfigs = [
|
||||
{ tools: data.inputs?.llmTools, toolProperty: 'llmSelectedTool' },
|
||||
{ tools: data.inputs?.agentTools, toolProperty: 'agentSelectedTool' },
|
||||
{
|
||||
tools: data.inputs?.selectedTool ? [{ selectedTool: data.inputs?.selectedTool }] : [],
|
||||
toolProperty: 'selectedTool'
|
||||
},
|
||||
{ tools: data.inputs?.agentKnowledgeVSEmbeddings, toolProperty: ['vectorStore', 'embeddingModel'] }
|
||||
]
|
||||
|
||||
// Filter out undefined tools and render each valid collection
|
||||
return toolConfigs
|
||||
.filter((config) => config.tools && config.tools.length > 0)
|
||||
.map((config, configIndex) => (
|
||||
<Box key={`tools-${configIndex}`} sx={{ display: 'flex', gap: 1, mt: 1 }}>
|
||||
{config.tools.flatMap((tool, toolIndex) => {
|
||||
if (Array.isArray(config.toolProperty)) {
|
||||
return config.toolProperty
|
||||
.filter((prop) => tool[prop])
|
||||
.map((prop, propIndex) => {
|
||||
const toolName = tool[prop]
|
||||
return (
|
||||
<Box
|
||||
key={`tool-${configIndex}-${toolIndex}-${propIndex}`}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
borderRadius: '50%',
|
||||
width: 24,
|
||||
height: 24,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '4px'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
src={`${baseURL}/api/v1/node-icon/${toolName}`}
|
||||
alt={toolName}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
} else {
|
||||
const toolName = tool[config.toolProperty]
|
||||
if (!toolName) return []
|
||||
|
||||
return [
|
||||
<Box
|
||||
key={`tool-${configIndex}-${toolIndex}`}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
borderRadius: '50%',
|
||||
width: 24,
|
||||
height: 24,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '4px'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
src={`${baseURL}/api/v1/node-icon/${toolName}`}
|
||||
alt={toolName}
|
||||
/>
|
||||
</Box>
|
||||
]
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
))
|
||||
})()}
|
||||
</Box>
|
||||
</div>
|
||||
{getOutputAnchors().map((outputAnchor, index) => {
|
||||
return (
|
||||
<Handle
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
key={outputAnchor.id}
|
||||
id={outputAnchor.id}
|
||||
style={{
|
||||
height: 20,
|
||||
width: 20,
|
||||
top: getAnchorPosition(index),
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
position: 'absolute',
|
||||
right: -10,
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: 'opacity 0.2s'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: theme.palette.background.paper, // or 'white'
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
<IconCircleChevronRightFilled
|
||||
size={20}
|
||||
color={nodeColor}
|
||||
style={{
|
||||
pointerEvents: 'none',
|
||||
position: 'relative',
|
||||
zIndex: 1
|
||||
}}
|
||||
/>
|
||||
</Handle>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</CardWrapper>
|
||||
<NodeInfoDialog show={showInfoDialog} dialogProps={infoDialogProps} onCancel={() => setShowInfoDialog(false)}></NodeInfoDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
AgentFlowNode.propTypes = {
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
export default memo(AgentFlowNode)
|
||||
@@ -0,0 +1,786 @@
|
||||
import { useEffect, useRef, useState, useCallback, useContext } from 'react'
|
||||
import ReactFlow, { addEdge, Controls, MiniMap, Background, useNodesState, useEdgesState } from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import './index.css'
|
||||
import { useReward } from 'react-rewards'
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import {
|
||||
REMOVE_DIRTY,
|
||||
SET_DIRTY,
|
||||
SET_CHATFLOW,
|
||||
enqueueSnackbar as enqueueSnackbarAction,
|
||||
closeSnackbar as closeSnackbarAction
|
||||
} from '@/store/actions'
|
||||
import { omit, cloneDeep } from 'lodash'
|
||||
|
||||
// material-ui
|
||||
import { Toolbar, Box, AppBar, Button, Fab } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
import CanvasNode from './AgentFlowNode'
|
||||
import IterationNode from './IterationNode'
|
||||
import AgentFlowEdge from './AgentFlowEdge'
|
||||
import ConnectionLine from './ConnectionLine'
|
||||
import StickyNote from './StickyNote'
|
||||
import CanvasHeader from '@/views/canvas/CanvasHeader'
|
||||
import AddNodes from '@/views/canvas/AddNodes'
|
||||
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
|
||||
import EditNodeDialog from '@/views/agentflowsv2/EditNodeDialog'
|
||||
import ChatPopUp from '@/views/chatmessage/ChatPopUp'
|
||||
import ValidationPopUp from '@/views/chatmessage/ValidationPopUp'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
|
||||
// API
|
||||
import nodesApi from '@/api/nodes'
|
||||
import chatflowsApi from '@/api/chatflows'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
|
||||
// icons
|
||||
import { IconX, IconRefreshAlert } from '@tabler/icons-react'
|
||||
|
||||
// utils
|
||||
import {
|
||||
getUniqueNodeLabel,
|
||||
getUniqueNodeId,
|
||||
initNode,
|
||||
updateOutdatedNodeData,
|
||||
updateOutdatedNodeEdge,
|
||||
isValidConnectionAgentflowV2
|
||||
} from '@/utils/genericHelper'
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
import { usePrompt } from '@/utils/usePrompt'
|
||||
|
||||
// const
|
||||
import { FLOWISE_CREDENTIAL_ID, AGENTFLOW_ICONS } from '@/store/constant'
|
||||
|
||||
const nodeTypes = { agentFlow: CanvasNode, stickyNote: StickyNote, iteration: IterationNode }
|
||||
const edgeTypes = { agentFlow: AgentFlowEdge }
|
||||
|
||||
// ==============================|| CANVAS ||============================== //
|
||||
|
||||
const AgentflowCanvas = () => {
|
||||
const theme = useTheme()
|
||||
const navigate = useNavigate()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const { state } = useLocation()
|
||||
const templateFlowData = state ? state.templateFlowData : ''
|
||||
|
||||
const URLpath = document.location.pathname.toString().split('/')
|
||||
const chatflowId =
|
||||
URLpath[URLpath.length - 1] === 'canvas' || URLpath[URLpath.length - 1] === 'agentcanvas' ? '' : URLpath[URLpath.length - 1]
|
||||
const canvasTitle = URLpath.includes('agentcanvas') ? 'Agent' : 'Chatflow'
|
||||
|
||||
const { confirm } = useConfirm()
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const canvas = useSelector((state) => state.canvas)
|
||||
const [canvasDataStore, setCanvasDataStore] = useState(canvas)
|
||||
const [chatflow, setChatflow] = useState(null)
|
||||
const { reactFlowInstance, setReactFlowInstance } = useContext(flowContext)
|
||||
|
||||
// ==============================|| Snackbar ||============================== //
|
||||
|
||||
useNotifier()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
// ==============================|| ReactFlow ||============================== //
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState()
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState()
|
||||
|
||||
const [selectedNode, setSelectedNode] = useState(null)
|
||||
const [isSyncNodesButtonEnabled, setIsSyncNodesButtonEnabled] = useState(false)
|
||||
const [editNodeDialogOpen, setEditNodeDialogOpen] = useState(false)
|
||||
const [editNodeDialogProps, setEditNodeDialogProps] = useState({})
|
||||
|
||||
const reactFlowWrapper = useRef(null)
|
||||
|
||||
// ==============================|| Chatflow API ||============================== //
|
||||
|
||||
const getNodesApi = useApi(nodesApi.getAllNodes)
|
||||
const createNewChatflowApi = useApi(chatflowsApi.createNewChatflow)
|
||||
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
|
||||
const getSpecificChatflowApi = useApi(chatflowsApi.getSpecificChatflow)
|
||||
|
||||
// ==============================|| Events & Actions ||============================== //
|
||||
|
||||
const onConnect = (params) => {
|
||||
if (!isValidConnectionAgentflowV2(params, reactFlowInstance)) {
|
||||
return
|
||||
}
|
||||
|
||||
const nodeName = params.sourceHandle.split('_')[0]
|
||||
const targetNodeName = params.targetHandle.split('_')[0]
|
||||
|
||||
const targetColor = AGENTFLOW_ICONS.find((icon) => icon.name === targetNodeName)?.color ?? theme.palette.primary.main
|
||||
const sourceColor = AGENTFLOW_ICONS.find((icon) => icon.name === nodeName)?.color ?? theme.palette.primary.main
|
||||
|
||||
let edgeLabel = undefined
|
||||
if (nodeName === 'conditionAgentflow' || nodeName === 'conditionAgentAgentflow') {
|
||||
const _edgeLabel = params.sourceHandle.split('-').pop()
|
||||
edgeLabel = (isNaN(_edgeLabel) ? 0 : _edgeLabel).toString()
|
||||
}
|
||||
|
||||
if (nodeName === 'humanInputAgentflow') {
|
||||
edgeLabel = params.sourceHandle.split('-').pop()
|
||||
edgeLabel = edgeLabel === '0' ? 'proceed' : 'reject'
|
||||
}
|
||||
|
||||
// Check if both source and target nodes are within the same iteration node
|
||||
const sourceNode = reactFlowInstance.getNodes().find((node) => node.id === params.source)
|
||||
const targetNode = reactFlowInstance.getNodes().find((node) => node.id === params.target)
|
||||
const isWithinIterationNode = sourceNode?.parentNode && targetNode?.parentNode && sourceNode.parentNode === targetNode.parentNode
|
||||
|
||||
const newEdge = {
|
||||
...params,
|
||||
data: {
|
||||
...params.data,
|
||||
sourceColor,
|
||||
targetColor,
|
||||
edgeLabel,
|
||||
isHumanInput: nodeName === 'humanInputAgentflow'
|
||||
},
|
||||
...(isWithinIterationNode && { zIndex: 9999 }),
|
||||
type: 'agentFlow',
|
||||
id: `${params.source}-${params.sourceHandle}-${params.target}-${params.targetHandle}`
|
||||
}
|
||||
setEdges((eds) => addEdge(newEdge, eds))
|
||||
}
|
||||
|
||||
const handleLoadFlow = (file) => {
|
||||
try {
|
||||
const flowData = JSON.parse(file)
|
||||
const nodes = flowData.nodes || []
|
||||
|
||||
setNodes(nodes)
|
||||
setEdges(flowData.edges || [])
|
||||
setTimeout(() => setDirty(), 0)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteFlow = async () => {
|
||||
const confirmPayload = {
|
||||
title: `Delete`,
|
||||
description: `Delete ${canvasTitle} ${chatflow.name}?`,
|
||||
confirmButtonName: 'Delete',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
const isConfirmed = await confirm(confirmPayload)
|
||||
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
await chatflowsApi.deleteChatflow(chatflow.id)
|
||||
localStorage.removeItem(`${chatflow.id}_INTERNAL`)
|
||||
navigate('/agentflows')
|
||||
} catch (error) {
|
||||
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 handleSaveFlow = (chatflowName) => {
|
||||
if (reactFlowInstance) {
|
||||
const nodes = reactFlowInstance.getNodes().map((node) => {
|
||||
const nodeData = cloneDeep(node.data)
|
||||
if (Object.prototype.hasOwnProperty.call(nodeData.inputs, FLOWISE_CREDENTIAL_ID)) {
|
||||
nodeData.credential = nodeData.inputs[FLOWISE_CREDENTIAL_ID]
|
||||
nodeData.inputs = omit(nodeData.inputs, [FLOWISE_CREDENTIAL_ID])
|
||||
}
|
||||
node.data = {
|
||||
...nodeData,
|
||||
selected: false,
|
||||
status: undefined
|
||||
}
|
||||
return node
|
||||
})
|
||||
|
||||
const rfInstanceObject = reactFlowInstance.toObject()
|
||||
rfInstanceObject.nodes = nodes
|
||||
const flowData = JSON.stringify(rfInstanceObject)
|
||||
|
||||
if (!chatflow.id) {
|
||||
const newChatflowBody = {
|
||||
name: chatflowName,
|
||||
deployed: false,
|
||||
isPublic: false,
|
||||
flowData,
|
||||
type: 'AGENTFLOW'
|
||||
}
|
||||
createNewChatflowApi.request(newChatflowBody)
|
||||
} else {
|
||||
const updateBody = {
|
||||
name: chatflowName,
|
||||
flowData
|
||||
}
|
||||
updateChatflowApi.request(chatflow.id, updateBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
const onNodeClick = useCallback((event, clickedNode) => {
|
||||
setSelectedNode(clickedNode)
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === clickedNode.id) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
selected: true
|
||||
}
|
||||
} else {
|
||||
node.data = {
|
||||
...node.data,
|
||||
selected: false
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// eslint-disable-next-line
|
||||
const onNodeDoubleClick = useCallback((event, node) => {
|
||||
if (!node || !node.data) return
|
||||
if (node.data.name === 'stickyNoteAgentflow') {
|
||||
// dont show dialog
|
||||
} else {
|
||||
const dialogProps = {
|
||||
data: node.data,
|
||||
inputParams: node.data.inputParams.filter((inputParam) => !inputParam.hidden)
|
||||
}
|
||||
|
||||
setEditNodeDialogProps(dialogProps)
|
||||
setEditNodeDialogOpen(true)
|
||||
}
|
||||
})
|
||||
|
||||
const onDragOver = useCallback((event) => {
|
||||
event.preventDefault()
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
}, [])
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault()
|
||||
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect()
|
||||
let nodeData = event.dataTransfer.getData('application/reactflow')
|
||||
|
||||
// check if the dropped element is valid
|
||||
if (typeof nodeData === 'undefined' || !nodeData) {
|
||||
return
|
||||
}
|
||||
|
||||
nodeData = JSON.parse(nodeData)
|
||||
|
||||
const position = reactFlowInstance.project({
|
||||
x: event.clientX - reactFlowBounds.left - 100,
|
||||
y: event.clientY - reactFlowBounds.top - 50
|
||||
})
|
||||
const nodes = reactFlowInstance.getNodes()
|
||||
|
||||
if (nodeData.name === 'startAgentflow' && nodes.find((node) => node.data.name === 'startAgentflow')) {
|
||||
enqueueSnackbar({
|
||||
message: 'Only one start node is allowed',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const newNodeId = getUniqueNodeId(nodeData, reactFlowInstance.getNodes())
|
||||
const newNodeLabel = getUniqueNodeLabel(nodeData, nodes)
|
||||
|
||||
const newNode = {
|
||||
id: newNodeId,
|
||||
position,
|
||||
data: { ...initNode(nodeData, newNodeId, true), label: newNodeLabel }
|
||||
}
|
||||
|
||||
if (nodeData.type === 'Iteration') {
|
||||
newNode.type = 'iteration'
|
||||
} else if (nodeData.type === 'StickyNote') {
|
||||
newNode.type = 'stickyNote'
|
||||
} else {
|
||||
newNode.type = 'agentFlow'
|
||||
}
|
||||
|
||||
// Check if the dropped node is within any Iteration node's flowContainerSize
|
||||
const iterationNodes = nodes.filter((node) => node.type === 'iteration')
|
||||
let parentNode = null
|
||||
|
||||
for (const iterationNode of iterationNodes) {
|
||||
// Get the iteration node's position and dimensions
|
||||
const nodeWidth = iterationNode.width || 300
|
||||
const nodeHeight = iterationNode.height || 250
|
||||
|
||||
// Calculate the boundaries of the iteration node
|
||||
const nodeLeft = iterationNode.position.x
|
||||
const nodeRight = nodeLeft + nodeWidth
|
||||
const nodeTop = iterationNode.position.y
|
||||
const nodeBottom = nodeTop + nodeHeight
|
||||
|
||||
// Check if the dropped position is within these boundaries
|
||||
if (position.x >= nodeLeft && position.x <= nodeRight && position.y >= nodeTop && position.y <= nodeBottom) {
|
||||
parentNode = iterationNode
|
||||
|
||||
// We can't have nested iteration nodes
|
||||
if (nodeData.name === 'iterationAgentflow') {
|
||||
enqueueSnackbar({
|
||||
message: 'Nested iteration node is not supported yet',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// We can't have human input node inside iteration node
|
||||
if (nodeData.name === 'humanInputAgentflow') {
|
||||
enqueueSnackbar({
|
||||
message: 'Human input node is not supported inside Iteration node',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the node is dropped inside an iteration node, set its parent
|
||||
if (parentNode) {
|
||||
newNode.parentNode = parentNode.id
|
||||
newNode.extent = 'parent'
|
||||
// Adjust position to be relative to the parent
|
||||
newNode.position = {
|
||||
x: position.x - parentNode.position.x,
|
||||
y: position.y - parentNode.position.y
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedNode(newNode)
|
||||
setNodes((nds) => {
|
||||
return (nds ?? []).concat(newNode).map((node) => {
|
||||
if (node.id === newNode.id) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
selected: true
|
||||
}
|
||||
} else {
|
||||
node.data = {
|
||||
...node.data,
|
||||
selected: false
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
})
|
||||
})
|
||||
setTimeout(() => setDirty(), 0)
|
||||
},
|
||||
|
||||
// eslint-disable-next-line
|
||||
[reactFlowInstance]
|
||||
)
|
||||
|
||||
const syncNodes = () => {
|
||||
const componentNodes = canvas.componentNodes
|
||||
|
||||
const cloneNodes = cloneDeep(nodes)
|
||||
const cloneEdges = cloneDeep(edges)
|
||||
let toBeRemovedEdges = []
|
||||
|
||||
for (let i = 0; i < cloneNodes.length; i++) {
|
||||
const node = cloneNodes[i]
|
||||
const componentNode = componentNodes.find((cn) => cn.name === node.data.name)
|
||||
if (componentNode && componentNode.version > node.data.version) {
|
||||
const clonedComponentNode = cloneDeep(componentNode)
|
||||
cloneNodes[i].data = updateOutdatedNodeData(clonedComponentNode, node.data, true)
|
||||
toBeRemovedEdges.push(...updateOutdatedNodeEdge(cloneNodes[i].data, cloneEdges))
|
||||
}
|
||||
}
|
||||
|
||||
setNodes(cloneNodes)
|
||||
setEdges(cloneEdges.filter((edge) => !toBeRemovedEdges.includes(edge)))
|
||||
setDirty()
|
||||
setIsSyncNodesButtonEnabled(false)
|
||||
}
|
||||
|
||||
const { reward: confettiReward } = useReward('canvasConfetti', 'confetti', {
|
||||
elementCount: 150,
|
||||
spread: 80,
|
||||
lifetime: 300,
|
||||
startVelocity: 40,
|
||||
zIndex: 10000,
|
||||
decay: 0.92,
|
||||
position: 'fixed'
|
||||
})
|
||||
|
||||
const triggerConfetti = () => {
|
||||
setTimeout(() => {
|
||||
confettiReward()
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const saveChatflowSuccess = () => {
|
||||
dispatch({ type: REMOVE_DIRTY })
|
||||
enqueueSnackbar({
|
||||
message: `${canvasTitle} saved`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const errorFailed = (message) => {
|
||||
enqueueSnackbar({
|
||||
message,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setDirty = () => {
|
||||
dispatch({ type: SET_DIRTY })
|
||||
}
|
||||
|
||||
const checkIfSyncNodesAvailable = (nodes) => {
|
||||
const componentNodes = canvas.componentNodes
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
const componentNode = componentNodes.find((cn) => cn.name === node.data.name)
|
||||
if (componentNode && componentNode.version > node.data.version) {
|
||||
setIsSyncNodesButtonEnabled(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setIsSyncNodesButtonEnabled(false)
|
||||
}
|
||||
|
||||
// ==============================|| useEffect ||============================== //
|
||||
|
||||
// Get specific chatflow successful
|
||||
useEffect(() => {
|
||||
if (getSpecificChatflowApi.data) {
|
||||
const chatflow = getSpecificChatflowApi.data
|
||||
const initialFlow = chatflow.flowData ? JSON.parse(chatflow.flowData) : []
|
||||
setNodes(initialFlow.nodes || [])
|
||||
setEdges(initialFlow.edges || [])
|
||||
dispatch({ type: SET_CHATFLOW, chatflow })
|
||||
} else if (getSpecificChatflowApi.error) {
|
||||
errorFailed(`Failed to retrieve ${canvasTitle}: ${getSpecificChatflowApi.error.response.data.message}`)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getSpecificChatflowApi.data, getSpecificChatflowApi.error])
|
||||
|
||||
// Create new chatflow successful
|
||||
useEffect(() => {
|
||||
if (createNewChatflowApi.data) {
|
||||
const chatflow = createNewChatflowApi.data
|
||||
dispatch({ type: SET_CHATFLOW, chatflow })
|
||||
saveChatflowSuccess()
|
||||
window.history.replaceState(state, null, `/v2/agentcanvas/${chatflow.id}`)
|
||||
} else if (createNewChatflowApi.error) {
|
||||
errorFailed(`Failed to save ${canvasTitle}: ${createNewChatflowApi.error.response.data.message}`)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [createNewChatflowApi.data, createNewChatflowApi.error])
|
||||
|
||||
// Update chatflow successful
|
||||
useEffect(() => {
|
||||
if (updateChatflowApi.data) {
|
||||
dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data })
|
||||
saveChatflowSuccess()
|
||||
} else if (updateChatflowApi.error) {
|
||||
errorFailed(`Failed to save ${canvasTitle}: ${updateChatflowApi.error.response.data.message}`)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updateChatflowApi.data, updateChatflowApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
setChatflow(canvasDataStore.chatflow)
|
||||
if (canvasDataStore.chatflow) {
|
||||
const flowData = canvasDataStore.chatflow.flowData ? JSON.parse(canvasDataStore.chatflow.flowData) : []
|
||||
checkIfSyncNodesAvailable(flowData.nodes || [])
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [canvasDataStore.chatflow])
|
||||
|
||||
// Initialization
|
||||
useEffect(() => {
|
||||
setIsSyncNodesButtonEnabled(false)
|
||||
if (chatflowId) {
|
||||
getSpecificChatflowApi.request(chatflowId)
|
||||
} else {
|
||||
if (localStorage.getItem('duplicatedFlowData')) {
|
||||
handleLoadFlow(localStorage.getItem('duplicatedFlowData'))
|
||||
setTimeout(() => localStorage.removeItem('duplicatedFlowData'), 0)
|
||||
} else {
|
||||
setNodes([])
|
||||
setEdges([])
|
||||
}
|
||||
dispatch({
|
||||
type: SET_CHATFLOW,
|
||||
chatflow: {
|
||||
name: `Untitled ${canvasTitle}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getNodesApi.request()
|
||||
|
||||
// Clear dirty state before leaving and remove any ongoing test triggers and webhooks
|
||||
return () => {
|
||||
setTimeout(() => dispatch({ type: REMOVE_DIRTY }), 0)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setCanvasDataStore(canvas)
|
||||
}, [canvas])
|
||||
|
||||
useEffect(() => {
|
||||
function handlePaste(e) {
|
||||
const pasteData = e.clipboardData.getData('text')
|
||||
//TODO: prevent paste event when input focused, temporary fix: catch chatflow syntax
|
||||
if (pasteData.includes('{"nodes":[') && pasteData.includes('],"edges":[')) {
|
||||
handleLoadFlow(pasteData)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('paste', handlePaste)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('paste', handlePaste)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (templateFlowData && templateFlowData.includes('"nodes":[') && templateFlowData.includes('],"edges":[')) {
|
||||
handleLoadFlow(templateFlowData)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [templateFlowData])
|
||||
|
||||
usePrompt('You have unsaved changes! Do you want to navigate away?', canvasDataStore.isDirty)
|
||||
|
||||
const [chatPopupOpen, setChatPopupOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatflowId && !localStorage.getItem('duplicatedFlowData') && getNodesApi.data && nodes.length === 0) {
|
||||
const startNodeData = getNodesApi.data.find((node) => node.name === 'startAgentflow')
|
||||
if (startNodeData) {
|
||||
const clonedStartNodeData = cloneDeep(startNodeData)
|
||||
clonedStartNodeData.position = { x: 100, y: 100 }
|
||||
const startNode = {
|
||||
id: 'startAgentflow_0',
|
||||
type: 'agentFlow',
|
||||
position: { x: 100, y: 100 },
|
||||
data: {
|
||||
...initNode(clonedStartNodeData, 'startAgentflow_0', true),
|
||||
label: 'Start'
|
||||
}
|
||||
}
|
||||
setNodes([startNode])
|
||||
setEdges([])
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getNodesApi.data, chatflowId])
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
id='canvasConfetti'
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '0',
|
||||
height: '0',
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none',
|
||||
background: 'transparent'
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<AppBar
|
||||
enableColorOnDark
|
||||
position='fixed'
|
||||
color='inherit'
|
||||
elevation={1}
|
||||
sx={{
|
||||
bgcolor: theme.palette.background.default
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<CanvasHeader
|
||||
chatflow={chatflow}
|
||||
handleSaveFlow={handleSaveFlow}
|
||||
handleDeleteFlow={handleDeleteFlow}
|
||||
handleLoadFlow={handleLoadFlow}
|
||||
isAgentCanvas={true}
|
||||
isAgentflowV2={true}
|
||||
/>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Box sx={{ pt: '70px', height: '100vh', width: '100%' }}>
|
||||
<div className='reactflow-parent-wrapper'>
|
||||
<div className='reactflow-wrapper' ref={reactFlowWrapper}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onNodeDragStop={setDirty}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onConnect={onConnect}
|
||||
onInit={setReactFlowInstance}
|
||||
fitView
|
||||
deleteKeyCode={canvas.canvasDialogShow ? null : ['Delete']}
|
||||
minZoom={0.5}
|
||||
connectionLineComponent={ConnectionLine}
|
||||
>
|
||||
<Controls
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
backgroundColor: customization.isDarkMode ? theme.palette.background.default : '#fff'
|
||||
}}
|
||||
/>
|
||||
<MiniMap
|
||||
nodeStrokeWidth={3}
|
||||
nodeColor={customization.isDarkMode ? '#2d2d2d' : '#e2e2e2'}
|
||||
nodeStrokeColor={customization.isDarkMode ? '#525252' : '#fff'}
|
||||
maskColor={customization.isDarkMode ? 'rgb(45, 45, 45, 0.6)' : 'rgb(240, 240, 240, 0.6)'}
|
||||
style={{
|
||||
backgroundColor: customization.isDarkMode ? theme.palette.background.default : '#fff'
|
||||
}}
|
||||
/>
|
||||
<Background color='#aaa' gap={16} />
|
||||
<AddNodes
|
||||
isAgentCanvas={true}
|
||||
isAgentflowv2={true}
|
||||
nodesData={getNodesApi.data}
|
||||
node={selectedNode}
|
||||
onFlowGenerated={triggerConfetti}
|
||||
/>
|
||||
<EditNodeDialog
|
||||
show={editNodeDialogOpen}
|
||||
dialogProps={editNodeDialogProps}
|
||||
onCancel={() => setEditNodeDialogOpen(false)}
|
||||
/>
|
||||
{isSyncNodesButtonEnabled && (
|
||||
<Fab
|
||||
sx={{
|
||||
left: 60,
|
||||
top: 20,
|
||||
color: 'white',
|
||||
background: 'orange',
|
||||
'&:hover': {
|
||||
background: 'orange',
|
||||
backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)`
|
||||
}
|
||||
}}
|
||||
size='small'
|
||||
aria-label='sync'
|
||||
title='Sync Nodes'
|
||||
onClick={() => syncNodes()}
|
||||
>
|
||||
<IconRefreshAlert />
|
||||
</Fab>
|
||||
)}
|
||||
<ChatPopUp isAgentCanvas={true} chatflowid={chatflowId} onOpenChange={setChatPopupOpen} />
|
||||
{!chatPopupOpen && <ValidationPopUp isAgentCanvas={true} chatflowid={chatflowId} />}
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
<ConfirmDialog />
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentflowCanvas
|
||||
@@ -0,0 +1,219 @@
|
||||
import { useContext, useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
// Material
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Box, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
import { IconSettings } from '@tabler/icons-react'
|
||||
|
||||
// Project imports
|
||||
import NodeInputHandler from '../canvas/NodeInputHandler'
|
||||
|
||||
// API
|
||||
import nodesApi from '@/api/nodes'
|
||||
|
||||
// const
|
||||
import { initNode, showHideInputParams, initializeDefaultNodeData } from '@/utils/genericHelper'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
import { FLOWISE_CREDENTIAL_ID } from '@/store/constant'
|
||||
|
||||
export const ConfigInput = ({ data, inputParam, disabled = false, arrayIndex = null, parentParamForArray = null }) => {
|
||||
const theme = useTheme()
|
||||
const { reactFlowInstance } = useContext(flowContext)
|
||||
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [selectedComponentNodeData, setSelectedComponentNodeData] = useState({})
|
||||
|
||||
const handleAccordionChange = (event, isExpanded) => {
|
||||
setExpanded(isExpanded)
|
||||
}
|
||||
|
||||
const onCustomDataChange = ({ inputParam, newValue }) => {
|
||||
let nodeData = cloneDeep(selectedComponentNodeData)
|
||||
|
||||
const updatedInputs = { ...nodeData.inputs }
|
||||
updatedInputs[inputParam.name] = newValue
|
||||
|
||||
const updatedInputParams = showHideInputParams({
|
||||
...nodeData,
|
||||
inputs: updatedInputs
|
||||
})
|
||||
|
||||
// Remove inputs with display set to false
|
||||
Object.keys(updatedInputs).forEach((key) => {
|
||||
const input = updatedInputParams.find((param) => param.name === key)
|
||||
if (input && input.display === false) {
|
||||
delete updatedInputs[key]
|
||||
}
|
||||
})
|
||||
|
||||
const credential = updatedInputs.credential || updatedInputs[FLOWISE_CREDENTIAL_ID]
|
||||
|
||||
nodeData = {
|
||||
...nodeData,
|
||||
inputParams: updatedInputParams,
|
||||
inputs: updatedInputs,
|
||||
credential: credential ? credential : undefined
|
||||
}
|
||||
|
||||
setSelectedComponentNodeData(nodeData)
|
||||
}
|
||||
|
||||
// Load initial component data when the component mounts
|
||||
useEffect(() => {
|
||||
const loadComponentData = async () => {
|
||||
// Get the node name from inputs
|
||||
const nodeName = data.inputs[inputParam.name]
|
||||
const node = await nodesApi.getSpecificNode(nodeName)
|
||||
|
||||
if (!node.data) return
|
||||
|
||||
// Initialize component node with basic data
|
||||
const componentNodeData = cloneDeep(initNode(node.data, `${node.data.nodeName}_0`))
|
||||
|
||||
// Helper function to check if array-based configuration exists
|
||||
const isArray = () => {
|
||||
return parentParamForArray && data.inputs[parentParamForArray.name]
|
||||
}
|
||||
|
||||
const hasArrayConfig = () => {
|
||||
return (
|
||||
parentParamForArray &&
|
||||
data.inputs[parentParamForArray.name] &&
|
||||
Array.isArray(data.inputs[parentParamForArray.name]) &&
|
||||
data.inputs[parentParamForArray.name][arrayIndex] &&
|
||||
data.inputs[parentParamForArray.name][arrayIndex][`${inputParam.name}Config`]
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to get current input value
|
||||
const getCurrentInputValue = () => {
|
||||
return hasArrayConfig() ? data.inputs[parentParamForArray.name][arrayIndex][inputParam.name] : data.inputs[inputParam.name]
|
||||
}
|
||||
|
||||
// Helper function to get config data
|
||||
const getConfigData = () => {
|
||||
return hasArrayConfig()
|
||||
? data.inputs[parentParamForArray.name][arrayIndex][`${inputParam.name}Config`]
|
||||
: data.inputs[`${inputParam.name}Config`]
|
||||
}
|
||||
|
||||
// Update component inputs based on configuration
|
||||
if (hasArrayConfig() || data.inputs[`${inputParam.name}Config`]) {
|
||||
const configData = getConfigData()
|
||||
const currentValue = getCurrentInputValue()
|
||||
|
||||
// If stored config value doesn't match current input, reset to defaults
|
||||
if (configData[inputParam.name] !== currentValue) {
|
||||
const defaultInput = initializeDefaultNodeData(componentNodeData.inputParams)
|
||||
componentNodeData.inputs = { ...defaultInput, [inputParam.name]: currentValue }
|
||||
} else {
|
||||
// Use existing config with current input value
|
||||
componentNodeData.inputs = { ...configData, [inputParam.name]: currentValue }
|
||||
}
|
||||
} else {
|
||||
const currentValue = isArray()
|
||||
? data.inputs[parentParamForArray.name][arrayIndex][inputParam.name]
|
||||
: data.inputs[inputParam.name]
|
||||
componentNodeData.inputs = {
|
||||
...componentNodeData.inputs,
|
||||
[inputParam.name]: currentValue
|
||||
}
|
||||
}
|
||||
|
||||
// Update input parameters visibility based on current inputs
|
||||
componentNodeData.inputParams = showHideInputParams({
|
||||
...componentNodeData,
|
||||
inputs: componentNodeData.inputs
|
||||
})
|
||||
|
||||
const credential = componentNodeData.inputs.credential || componentNodeData.inputs[FLOWISE_CREDENTIAL_ID]
|
||||
componentNodeData.credential = credential ? credential : undefined
|
||||
|
||||
setSelectedComponentNodeData(componentNodeData)
|
||||
}
|
||||
|
||||
loadComponentData()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// Update node configuration when selected component data changes
|
||||
useEffect(() => {
|
||||
if (!selectedComponentNodeData.inputs) return
|
||||
|
||||
reactFlowInstance.setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id !== data.id) return node
|
||||
|
||||
// Handle array-based configuration
|
||||
if (arrayIndex !== null && parentParamForArray) {
|
||||
// Initialize array if it doesn't exist
|
||||
if (!node.data.inputs[parentParamForArray.name]) {
|
||||
node.data.inputs[parentParamForArray.name] = []
|
||||
}
|
||||
// Initialize array element if it doesn't exist
|
||||
if (!node.data.inputs[parentParamForArray.name][arrayIndex]) {
|
||||
node.data.inputs[parentParamForArray.name][arrayIndex] = {}
|
||||
}
|
||||
// Store config in array
|
||||
node.data.inputs[parentParamForArray.name][arrayIndex][`${inputParam.name}Config`] = selectedComponentNodeData.inputs
|
||||
} else {
|
||||
// Store config directly
|
||||
node.data.inputs[`${inputParam.name}Config`] = selectedComponentNodeData.inputs
|
||||
}
|
||||
return node
|
||||
})
|
||||
)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data.inputs, arrayIndex, parentParamForArray, selectedComponentNodeData])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
p: 0,
|
||||
mt: 1,
|
||||
mb: 1,
|
||||
border: 1,
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
borderRadius: 2
|
||||
}}
|
||||
>
|
||||
<Accordion sx={{ background: 'transparent' }} expanded={expanded} onChange={handleAccordionChange}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ background: 'transparent' }}>
|
||||
<IconSettings stroke={1.5} size='1.3rem' />
|
||||
<Typography sx={{ ml: 1 }}>{selectedComponentNodeData?.label} Parameters</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{(selectedComponentNodeData.inputParams ?? [])
|
||||
.filter((inputParam) => !inputParam.hidden)
|
||||
.filter((inputParam) => inputParam.display !== false)
|
||||
.map((inputParam, index) => (
|
||||
<NodeInputHandler
|
||||
disabled={disabled}
|
||||
key={index}
|
||||
inputParam={inputParam}
|
||||
data={selectedComponentNodeData}
|
||||
isAdditionalParams={true}
|
||||
onCustomDataChange={onCustomDataChange}
|
||||
/>
|
||||
))}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ConfigInput.propTypes = {
|
||||
name: PropTypes.string,
|
||||
inputParam: PropTypes.object,
|
||||
data: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
arrayIndex: PropTypes.number,
|
||||
parentParamForArray: PropTypes.object
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { memo } from 'react'
|
||||
import { EdgeLabelRenderer, useStore, getBezierPath } from 'reactflow'
|
||||
import PropTypes from 'prop-types'
|
||||
import { AGENTFLOW_ICONS } from '@/store/constant'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
function EdgeLabel({ transform, isHumanInput, label, color }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
background: 'transparent',
|
||||
left: isHumanInput ? 20 : 10,
|
||||
paddingTop: 1,
|
||||
color: color,
|
||||
fontSize: '0.5rem',
|
||||
fontWeight: 700,
|
||||
transform,
|
||||
zIndex: 1000
|
||||
}}
|
||||
className='nodrag nopan'
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
EdgeLabel.propTypes = {
|
||||
transform: PropTypes.string,
|
||||
isHumanInput: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
color: PropTypes.string
|
||||
}
|
||||
|
||||
const ConnectionLine = ({ fromX, fromY, toX, toY, fromPosition, toPosition }) => {
|
||||
const [edgePath] = getBezierPath({
|
||||
// we need this little hack in order to display the gradient for a straight line
|
||||
sourceX: fromX,
|
||||
sourceY: fromY,
|
||||
sourcePosition: fromPosition,
|
||||
targetX: toX,
|
||||
targetY: toY,
|
||||
targetPosition: toPosition
|
||||
})
|
||||
|
||||
const { connectionHandleId } = useStore()
|
||||
const theme = useTheme()
|
||||
const nodeName = (connectionHandleId || '').split('_')[0] || ''
|
||||
|
||||
const isLabelVisible = nodeName === 'humanInputAgentflow' || nodeName === 'conditionAgentflow' || nodeName === 'conditionAgentAgentflow'
|
||||
|
||||
const getEdgeLabel = () => {
|
||||
let edgeLabel = undefined
|
||||
if (nodeName === 'conditionAgentflow' || nodeName === 'conditionAgentAgentflow') {
|
||||
const _edgeLabel = connectionHandleId.split('-').pop()
|
||||
edgeLabel = (isNaN(_edgeLabel) ? 0 : _edgeLabel).toString()
|
||||
}
|
||||
if (nodeName === 'humanInputAgentflow') {
|
||||
const _edgeLabel = connectionHandleId.split('-').pop()
|
||||
edgeLabel = (isNaN(_edgeLabel) ? 0 : _edgeLabel).toString()
|
||||
edgeLabel = edgeLabel === '0' ? 'proceed' : 'reject'
|
||||
}
|
||||
return edgeLabel
|
||||
}
|
||||
|
||||
const color =
|
||||
AGENTFLOW_ICONS.find((icon) => icon.name === (connectionHandleId || '').split('_')[0] || '')?.color ?? theme.palette.primary.main
|
||||
|
||||
return (
|
||||
<g>
|
||||
<path fill='none' stroke={color} strokeWidth={1.5} className='animated' d={edgePath} />
|
||||
<g transform={`translate(${toX - 10}, ${toY - 10}) scale(0.8)`}>
|
||||
<path stroke='none' d='M0 0h24v24H0z' fill='none' />
|
||||
<path
|
||||
d='M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -20 0c0 -5.523 4.477 -10 10 -10m-.293 6.293a1 1 0 0 0 -1.414 0l-.083 .094a1 1 0 0 0 .083 1.32l2.292 2.293l-2.292 2.293a1 1 0 0 0 1.414 1.414l3 -3a1 1 0 0 0 0 -1.414z'
|
||||
fill={color}
|
||||
/>
|
||||
</g>
|
||||
{isLabelVisible && (
|
||||
<EdgeLabelRenderer>
|
||||
<EdgeLabel
|
||||
color={color}
|
||||
isHumanInput={nodeName === 'humanInputAgentflow'}
|
||||
label={getEdgeLabel()}
|
||||
transform={`translate(-50%, 0%) translate(${fromX}px,${fromY}px)`}
|
||||
/>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
ConnectionLine.propTypes = {
|
||||
fromX: PropTypes.number,
|
||||
fromY: PropTypes.number,
|
||||
toX: PropTypes.number,
|
||||
toY: PropTypes.number,
|
||||
fromPosition: PropTypes.any,
|
||||
toPosition: PropTypes.any
|
||||
}
|
||||
|
||||
export default memo(ConnectionLine)
|
||||
@@ -0,0 +1,279 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useState, useEffect, useRef, useContext, memo } from 'react'
|
||||
import { useUpdateNodeInternals } from 'reactflow'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Stack, Box, Typography, TextField, Dialog, DialogContent, ButtonBase, Avatar } from '@mui/material'
|
||||
import NodeInputHandler from '@/views/canvas/NodeInputHandler'
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||
import { IconPencil, IconX, IconCheck, IconInfoCircle } from '@tabler/icons-react'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
import { showHideInputParams } from '@/utils/genericHelper'
|
||||
|
||||
const EditNodeDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const dispatch = useDispatch()
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const nodeNameRef = useRef()
|
||||
const { reactFlowInstance } = useContext(flowContext)
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
|
||||
const [inputParams, setInputParams] = useState([])
|
||||
const [data, setData] = useState({})
|
||||
const [isEditingNodeName, setEditingNodeName] = useState(null)
|
||||
const [nodeName, setNodeName] = useState('')
|
||||
|
||||
const onNodeLabelChange = () => {
|
||||
reactFlowInstance.setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === data.id) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
label: nodeNameRef.current.value
|
||||
}
|
||||
setData(node.data)
|
||||
}
|
||||
return node
|
||||
})
|
||||
)
|
||||
updateNodeInternals(data.id)
|
||||
}
|
||||
|
||||
const onCustomDataChange = ({ nodeId, inputParam, newValue }) => {
|
||||
reactFlowInstance.setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
const updatedInputs = {
|
||||
...node.data.inputs,
|
||||
[inputParam.name]: newValue
|
||||
}
|
||||
|
||||
const updatedInputParams = showHideInputParams({
|
||||
...node.data,
|
||||
inputs: updatedInputs
|
||||
})
|
||||
|
||||
// Remove inputs with display set to false
|
||||
Object.keys(updatedInputs).forEach((key) => {
|
||||
const input = updatedInputParams.find((param) => param.name === key)
|
||||
if (input && input.display === false) {
|
||||
delete updatedInputs[key]
|
||||
}
|
||||
})
|
||||
|
||||
node.data = {
|
||||
...node.data,
|
||||
inputParams: updatedInputParams,
|
||||
inputs: updatedInputs
|
||||
}
|
||||
|
||||
setInputParams(updatedInputParams)
|
||||
setData(node.data)
|
||||
}
|
||||
return node
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.inputParams) {
|
||||
setInputParams(dialogProps.inputParams)
|
||||
}
|
||||
if (dialogProps.data) {
|
||||
setData(dialogProps.data)
|
||||
if (dialogProps.data.label) setNodeName(dialogProps.data.label)
|
||||
}
|
||||
|
||||
return () => {
|
||||
setInputParams([])
|
||||
setData({})
|
||||
}
|
||||
}, [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'
|
||||
>
|
||||
<DialogContent>
|
||||
{data && data.name && (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{!isEditingNodeName ? (
|
||||
<Stack flexDirection='row' sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
ml: 2,
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
variant='h4'
|
||||
>
|
||||
{nodeName}
|
||||
</Typography>
|
||||
|
||||
{data?.id && (
|
||||
<ButtonBase title='Edit Name' sx={{ borderRadius: '50%' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
ml: 1,
|
||||
background: theme.palette.secondary.light,
|
||||
color: theme.palette.secondary.dark,
|
||||
'&:hover': {
|
||||
background: theme.palette.secondary.dark,
|
||||
color: theme.palette.secondary.light
|
||||
}
|
||||
}}
|
||||
color='inherit'
|
||||
onClick={() => setEditingNodeName(true)}
|
||||
>
|
||||
<IconPencil stroke={1.5} size='1rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
)}
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack flexDirection='row' sx={{ width: '100%' }}>
|
||||
<TextField
|
||||
//eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
size='small'
|
||||
sx={{
|
||||
width: '100%',
|
||||
ml: 2
|
||||
}}
|
||||
inputRef={nodeNameRef}
|
||||
defaultValue={nodeName}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
data.label = nodeNameRef.current.value
|
||||
setNodeName(nodeNameRef.current.value)
|
||||
onNodeLabelChange()
|
||||
setEditingNodeName(false)
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditingNodeName(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ButtonBase title='Save Name' sx={{ borderRadius: '50%' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.success.light,
|
||||
color: theme.palette.success.dark,
|
||||
ml: 1,
|
||||
'&:hover': {
|
||||
background: theme.palette.success.dark,
|
||||
color: theme.palette.success.light
|
||||
}
|
||||
}}
|
||||
color='inherit'
|
||||
onClick={() => {
|
||||
data.label = nodeNameRef.current.value
|
||||
setNodeName(nodeNameRef.current.value)
|
||||
onNodeLabelChange()
|
||||
setEditingNodeName(false)
|
||||
}}
|
||||
>
|
||||
<IconCheck stroke={1.5} size='1rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
<ButtonBase title='Cancel' sx={{ borderRadius: '50%' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.error.light,
|
||||
color: theme.palette.error.dark,
|
||||
ml: 1,
|
||||
'&:hover': {
|
||||
background: theme.palette.error.dark,
|
||||
color: theme.palette.error.light
|
||||
}
|
||||
}}
|
||||
color='inherit'
|
||||
onClick={() => setEditingNodeName(false)}
|
||||
>
|
||||
<IconX stroke={1.5} size='1rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{data?.hint && (
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems='center'
|
||||
sx={{
|
||||
ml: 2,
|
||||
backgroundColor: customization.isDarkMode ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.03)',
|
||||
borderRadius: '8px',
|
||||
mr: 2,
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
mt: 1,
|
||||
mb: 1,
|
||||
border: `1px solid ${customization.isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.08)'}`
|
||||
}}
|
||||
>
|
||||
<IconInfoCircle size='1rem' stroke={1.5} color={theme.palette.info.main} style={{ marginRight: '6px' }} />
|
||||
<Typography
|
||||
variant='caption'
|
||||
color='text.secondary'
|
||||
sx={{
|
||||
fontStyle: 'italic',
|
||||
lineHeight: 1.2
|
||||
}}
|
||||
>
|
||||
{data.hint}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
{inputParams
|
||||
.filter((inputParam) => inputParam.display !== false)
|
||||
.map((inputParam, index) => (
|
||||
<NodeInputHandler
|
||||
disabled={dialogProps.disabled}
|
||||
key={index}
|
||||
inputParam={inputParam}
|
||||
data={data}
|
||||
isAdditionalParams={true}
|
||||
onCustomDataChange={onCustomDataChange}
|
||||
/>
|
||||
))}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
EditNodeDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default memo(EditNodeDialog)
|
||||
@@ -0,0 +1,425 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useContext, memo, useRef, useState, useEffect, useCallback } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Background, Handle, Position, useUpdateNodeInternals, NodeToolbar, NodeResizer } from 'reactflow'
|
||||
|
||||
// material-ui
|
||||
import { styled, useTheme, alpha, darken, lighten } from '@mui/material/styles'
|
||||
import { ButtonGroup, Avatar, Box, Typography, IconButton, Tooltip } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
import NodeInfoDialog from '@/ui-component/dialog/NodeInfoDialog'
|
||||
|
||||
// icons
|
||||
import {
|
||||
IconCheck,
|
||||
IconExclamationMark,
|
||||
IconCircleChevronRightFilled,
|
||||
IconCopy,
|
||||
IconTrash,
|
||||
IconInfoCircle,
|
||||
IconLoader
|
||||
} from '@tabler/icons-react'
|
||||
import StopCircleIcon from '@mui/icons-material/StopCircle'
|
||||
import CancelIcon from '@mui/icons-material/Cancel'
|
||||
|
||||
// const
|
||||
import { baseURL, AGENTFLOW_ICONS } from '@/store/constant'
|
||||
|
||||
const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
background: theme.palette.card.main,
|
||||
color: theme.darkTextPrimary,
|
||||
border: 'solid 1px',
|
||||
width: 'max-content',
|
||||
height: 'auto',
|
||||
padding: '10px',
|
||||
boxShadow: 'none'
|
||||
}))
|
||||
|
||||
const StyledNodeToolbar = styled(NodeToolbar)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.card.main,
|
||||
color: theme.darkTextPrimary,
|
||||
padding: '5px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)'
|
||||
}))
|
||||
|
||||
// ===========================|| ITERATION NODE ||=========================== //
|
||||
|
||||
const IterationNode = ({ data }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const ref = useRef(null)
|
||||
const reactFlowWrapper = useRef(null)
|
||||
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
// eslint-disable-next-line
|
||||
const [position, setPosition] = useState(0)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const { deleteNode, duplicateNode, reactFlowInstance } = useContext(flowContext)
|
||||
const [showInfoDialog, setShowInfoDialog] = useState(false)
|
||||
const [infoDialogProps, setInfoDialogProps] = useState({})
|
||||
|
||||
const [cardDimensions, setCardDimensions] = useState({
|
||||
width: '300px',
|
||||
height: '250px'
|
||||
})
|
||||
|
||||
// Add useEffect to update dimensions when reactFlowInstance becomes available
|
||||
useEffect(() => {
|
||||
if (reactFlowInstance) {
|
||||
const node = reactFlowInstance.getNodes().find((node) => node.id === data.id)
|
||||
if (node && node.width && node.height) {
|
||||
setCardDimensions({
|
||||
width: `${node.width}px`,
|
||||
height: `${node.height}px`
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [reactFlowInstance, data.id])
|
||||
|
||||
const defaultColor = '#666666' // fallback color if data.color is not present
|
||||
const nodeColor = data.color || defaultColor
|
||||
|
||||
// Get different shades of the color based on state
|
||||
const getStateColor = () => {
|
||||
if (data.selected) return nodeColor
|
||||
if (isHovered) return alpha(nodeColor, 0.8)
|
||||
return alpha(nodeColor, 0.5)
|
||||
}
|
||||
|
||||
const getOutputAnchors = () => {
|
||||
return data.outputAnchors ?? []
|
||||
}
|
||||
|
||||
const getAnchorPosition = (index) => {
|
||||
const currentHeight = ref.current?.clientHeight || 0
|
||||
const spacing = currentHeight / (getOutputAnchors().length + 1)
|
||||
const position = spacing * (index + 1)
|
||||
|
||||
// Update node internals when we get a non-zero position
|
||||
if (position > 0) {
|
||||
updateNodeInternals(data.id)
|
||||
}
|
||||
|
||||
return position
|
||||
}
|
||||
|
||||
const getMinimumHeight = () => {
|
||||
const outputCount = getOutputAnchors().length
|
||||
// Use exactly 60px as minimum height
|
||||
return Math.max(60, outputCount * 20 + 40)
|
||||
}
|
||||
|
||||
const getBackgroundColor = () => {
|
||||
if (customization.isDarkMode) {
|
||||
return isHovered ? darken(nodeColor, 0.7) : darken(nodeColor, 0.8)
|
||||
}
|
||||
return isHovered ? lighten(nodeColor, 0.8) : lighten(nodeColor, 0.9)
|
||||
}
|
||||
|
||||
const getStatusBackgroundColor = (status) => {
|
||||
switch (status) {
|
||||
case 'ERROR':
|
||||
return theme.palette.error.dark
|
||||
case 'INPROGRESS':
|
||||
return theme.palette.warning.dark
|
||||
case 'STOPPED':
|
||||
case 'TERMINATED':
|
||||
return theme.palette.error.main
|
||||
case 'FINISHED':
|
||||
return theme.palette.success.dark
|
||||
default:
|
||||
return theme.palette.primary.dark
|
||||
}
|
||||
}
|
||||
|
||||
const renderIcon = (node) => {
|
||||
const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === node.name)
|
||||
|
||||
if (!foundIcon) return null
|
||||
return <foundIcon.icon size={24} color={'white'} />
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
setTimeout(() => {
|
||||
setPosition(ref.current?.offsetTop + ref.current?.clientHeight / 2)
|
||||
updateNodeInternals(data.id)
|
||||
}, 10)
|
||||
}
|
||||
}, [data, ref, updateNodeInternals])
|
||||
|
||||
const onResizeEnd = useCallback(
|
||||
(e, params) => {
|
||||
if (!ref.current) return
|
||||
|
||||
// Set the card dimensions directly from resize params
|
||||
setCardDimensions({
|
||||
width: `${params.width}px`,
|
||||
height: `${params.height}px`
|
||||
})
|
||||
},
|
||||
[ref, setCardDimensions]
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={ref} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
|
||||
<NodeToolbar align='start' isVisible={true}>
|
||||
<Box style={{ display: 'flex', alignItems: 'center', flexDirection: 'row' }}>
|
||||
{data.color && !data.icon ? (
|
||||
<div
|
||||
style={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.largeAvatar,
|
||||
borderRadius: '15px',
|
||||
backgroundColor: data.color,
|
||||
cursor: 'grab',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: data.color
|
||||
}}
|
||||
>
|
||||
{renderIcon(data)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.largeAvatar,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'grab'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }}
|
||||
src={`${baseURL}/api/v1/node-icon/${data.name}`}
|
||||
alt={data.name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 500,
|
||||
ml: 1
|
||||
}}
|
||||
>
|
||||
{data.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
</NodeToolbar>
|
||||
<StyledNodeToolbar align='end'>
|
||||
<ButtonGroup sx={{ gap: 1 }} variant='outlined' aria-label='Basic button group'>
|
||||
<IconButton
|
||||
size={'small'}
|
||||
title='Duplicate'
|
||||
onClick={() => {
|
||||
duplicateNode(data.id)
|
||||
}}
|
||||
sx={{
|
||||
color: customization.isDarkMode ? 'white' : 'inherit',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconCopy size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size={'small'}
|
||||
title='Delete'
|
||||
onClick={() => {
|
||||
deleteNode(data.id)
|
||||
}}
|
||||
sx={{
|
||||
color: customization.isDarkMode ? 'white' : 'inherit',
|
||||
'&:hover': {
|
||||
color: theme.palette.error.main
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size={'small'}
|
||||
title='Info'
|
||||
onClick={() => {
|
||||
setInfoDialogProps({ data })
|
||||
setShowInfoDialog(true)
|
||||
}}
|
||||
sx={{
|
||||
color: customization.isDarkMode ? 'white' : 'inherit',
|
||||
'&:hover': {
|
||||
color: theme.palette.info.main
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconInfoCircle size={20} />
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
</StyledNodeToolbar>
|
||||
<NodeResizer minWidth={300} minHeight={Math.max(getMinimumHeight(), 250)} onResizeEnd={onResizeEnd} />
|
||||
<CardWrapper
|
||||
content={false}
|
||||
sx={{
|
||||
borderColor: getStateColor(),
|
||||
borderWidth: '1px',
|
||||
boxShadow: data.selected ? `0 0 0 1px ${getStateColor()} !important` : 'none',
|
||||
minHeight: Math.max(getMinimumHeight(), 250),
|
||||
minWidth: 300,
|
||||
width: cardDimensions.width,
|
||||
height: cardDimensions.height,
|
||||
backgroundColor: getBackgroundColor(),
|
||||
display: 'flex',
|
||||
'&:hover': {
|
||||
boxShadow: data.selected ? `0 0 0 1px ${getStateColor()} !important` : 'none'
|
||||
}
|
||||
}}
|
||||
border={false}
|
||||
>
|
||||
{data && data.status && (
|
||||
<Tooltip title={data.status === 'ERROR' ? data.error || 'Error' : ''}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.smallAvatar,
|
||||
borderRadius: '50%',
|
||||
background:
|
||||
data.status === 'STOPPED' || data.status === 'TERMINATED'
|
||||
? 'white'
|
||||
: getStatusBackgroundColor(data.status),
|
||||
color: 'white',
|
||||
ml: 2,
|
||||
position: 'absolute',
|
||||
top: -10,
|
||||
right: -10
|
||||
}}
|
||||
>
|
||||
{data.status === 'INPROGRESS' ? (
|
||||
<IconLoader className='spin-animation' />
|
||||
) : data.status === 'ERROR' ? (
|
||||
<IconExclamationMark />
|
||||
) : data.status === 'TERMINATED' ? (
|
||||
<CancelIcon sx={{ color: getStatusBackgroundColor(data.status) }} />
|
||||
) : data.status === 'STOPPED' ? (
|
||||
<StopCircleIcon sx={{ color: getStatusBackgroundColor(data.status) }} />
|
||||
) : (
|
||||
<IconCheck />
|
||||
)}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{!data.hideInput && (
|
||||
<Handle
|
||||
type='target'
|
||||
position={Position.Left}
|
||||
id={data.id}
|
||||
style={{
|
||||
width: 5,
|
||||
height: 20,
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
position: 'absolute',
|
||||
left: -2
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 5,
|
||||
height: 20,
|
||||
backgroundColor: nodeColor,
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
/>
|
||||
</Handle>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Box
|
||||
sx={{
|
||||
height: `calc(${cardDimensions.height} - 20px)`,
|
||||
width: `${cardDimensions.width}`,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
borderRadius: '10px'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={reactFlowWrapper}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: theme.palette.background.default
|
||||
}}
|
||||
>
|
||||
<Background color='#aaa' gap={16} />
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
{getOutputAnchors().map((outputAnchor, index) => {
|
||||
return (
|
||||
<Handle
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
key={outputAnchor.id}
|
||||
id={outputAnchor.id}
|
||||
style={{
|
||||
height: 20,
|
||||
width: 20,
|
||||
top: getAnchorPosition(index),
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
position: 'absolute',
|
||||
right: -10,
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: 'opacity 0.2s'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: theme.palette.background.paper, // or 'white'
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
<IconCircleChevronRightFilled
|
||||
size={20}
|
||||
color={nodeColor}
|
||||
style={{
|
||||
pointerEvents: 'none',
|
||||
position: 'relative',
|
||||
zIndex: 1
|
||||
}}
|
||||
/>
|
||||
</Handle>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</CardWrapper>
|
||||
<NodeInfoDialog show={showInfoDialog} dialogProps={infoDialogProps} onCancel={() => setShowInfoDialog(false)}></NodeInfoDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
IterationNode.propTypes = {
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
export default memo(IterationNode)
|
||||
@@ -0,0 +1,135 @@
|
||||
import { useEffect, useState, useCallback, useRef, useContext } from 'react'
|
||||
import ReactFlow, { Controls, Background, useNodesState, useEdgesState } from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import '@/views/canvas/index.css'
|
||||
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { Toolbar, Box, AppBar } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
import AgentFlowNode from './AgentFlowNode'
|
||||
import AgentFlowEdge from './AgentFlowEdge'
|
||||
import IterationNode from './IterationNode'
|
||||
import MarketplaceCanvasHeader from '@/views/marketplaces/MarketplaceCanvasHeader'
|
||||
import StickyNote from './StickyNote'
|
||||
import EditNodeDialog from '@/views/agentflowsv2/EditNodeDialog'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
|
||||
const nodeTypes = { agentFlow: AgentFlowNode, stickyNote: StickyNote, iteration: IterationNode }
|
||||
const edgeTypes = { agentFlow: AgentFlowEdge }
|
||||
|
||||
// ==============================|| CANVAS ||============================== //
|
||||
|
||||
const MarketplaceCanvasV2 = () => {
|
||||
const theme = useTheme()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { state } = useLocation()
|
||||
const { flowData, name } = state
|
||||
|
||||
// ==============================|| ReactFlow ||============================== //
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState()
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState()
|
||||
const [editNodeDialogOpen, setEditNodeDialogOpen] = useState(false)
|
||||
const [editNodeDialogProps, setEditNodeDialogProps] = useState({})
|
||||
|
||||
const reactFlowWrapper = useRef(null)
|
||||
const { setReactFlowInstance } = useContext(flowContext)
|
||||
|
||||
// ==============================|| useEffect ||============================== //
|
||||
|
||||
useEffect(() => {
|
||||
if (flowData) {
|
||||
const initialFlow = JSON.parse(flowData)
|
||||
setNodes(initialFlow.nodes || [])
|
||||
setEdges(initialFlow.edges || [])
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [flowData])
|
||||
|
||||
const onChatflowCopy = (flowData) => {
|
||||
const templateFlowData = JSON.stringify(flowData)
|
||||
navigate('/v2/agentcanvas', { state: { templateFlowData } })
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
const onNodeDoubleClick = useCallback((event, node) => {
|
||||
if (!node || !node.data) return
|
||||
if (node.data.name === 'stickyNoteAgentflow') {
|
||||
// dont show dialog
|
||||
} else {
|
||||
const dialogProps = {
|
||||
data: node.data,
|
||||
inputParams: node.data.inputParams.filter((inputParam) => !inputParam.hidden),
|
||||
disabled: true
|
||||
}
|
||||
|
||||
setEditNodeDialogProps(dialogProps)
|
||||
setEditNodeDialogOpen(true)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<AppBar
|
||||
enableColorOnDark
|
||||
position='fixed'
|
||||
color='inherit'
|
||||
elevation={1}
|
||||
sx={{
|
||||
bgcolor: theme.palette.background.default
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<MarketplaceCanvasHeader
|
||||
flowName={name}
|
||||
flowData={JSON.parse(flowData)}
|
||||
onChatflowCopy={(flowData) => onChatflowCopy(flowData)}
|
||||
/>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Box sx={{ pt: '70px', height: '100vh', width: '100%' }}>
|
||||
<div className='reactflow-parent-wrapper'>
|
||||
<div className='reactflow-wrapper' ref={reactFlowWrapper}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onInit={setReactFlowInstance}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
fitView
|
||||
minZoom={0.1}
|
||||
>
|
||||
<Controls
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
/>
|
||||
<Background color='#aaa' gap={16} />
|
||||
<EditNodeDialog
|
||||
show={editNodeDialogOpen}
|
||||
dialogProps={editNodeDialogProps}
|
||||
onCancel={() => setEditNodeDialogOpen(false)}
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarketplaceCanvasV2
|
||||
@@ -0,0 +1,136 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useRef, useContext, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { NodeToolbar } from 'reactflow'
|
||||
|
||||
// material-ui
|
||||
import { styled, useTheme, alpha, darken, lighten } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
import { ButtonGroup, IconButton, Box } from '@mui/material'
|
||||
import { IconCopy, IconTrash } from '@tabler/icons-react'
|
||||
import { Input } from '@/ui-component/input/Input'
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
|
||||
// const
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
|
||||
const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
background: theme.palette.card.main,
|
||||
color: theme.darkTextPrimary,
|
||||
border: 'solid 1px',
|
||||
width: 'max-content',
|
||||
height: 'auto',
|
||||
padding: '10px',
|
||||
boxShadow: 'none'
|
||||
}))
|
||||
|
||||
const StyledNodeToolbar = styled(NodeToolbar)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.card.main,
|
||||
color: theme.darkTextPrimary,
|
||||
padding: '5px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)'
|
||||
}))
|
||||
|
||||
const StickyNote = ({ data }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const ref = useRef(null)
|
||||
|
||||
const { reactFlowInstance, deleteNode, duplicateNode } = useContext(flowContext)
|
||||
const [inputParam] = data.inputParams
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
const defaultColor = '#666666' // fallback color if data.color is not present
|
||||
const nodeColor = data.color || defaultColor
|
||||
|
||||
// Get different shades of the color based on state
|
||||
const getStateColor = () => {
|
||||
if (data.selected) return nodeColor
|
||||
if (isHovered) return alpha(nodeColor, 0.8)
|
||||
return alpha(nodeColor, 0.5)
|
||||
}
|
||||
|
||||
const getBackgroundColor = () => {
|
||||
if (customization.isDarkMode) {
|
||||
return isHovered ? darken(nodeColor, 0.7) : darken(nodeColor, 0.8)
|
||||
}
|
||||
return isHovered ? lighten(nodeColor, 0.8) : lighten(nodeColor, 0.9)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
|
||||
<StyledNodeToolbar>
|
||||
<ButtonGroup sx={{ gap: 1 }} variant='outlined' aria-label='Basic button group'>
|
||||
<IconButton
|
||||
size={'small'}
|
||||
title='Duplicate'
|
||||
onClick={() => {
|
||||
duplicateNode(data.id)
|
||||
}}
|
||||
sx={{
|
||||
color: customization.isDarkMode ? 'white' : 'inherit',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.main
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconCopy size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size={'small'}
|
||||
title='Delete'
|
||||
onClick={() => {
|
||||
deleteNode(data.id)
|
||||
}}
|
||||
sx={{
|
||||
color: customization.isDarkMode ? 'white' : 'inherit',
|
||||
'&:hover': {
|
||||
color: theme.palette.error.main
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
</StyledNodeToolbar>
|
||||
<CardWrapper
|
||||
content={false}
|
||||
sx={{
|
||||
borderColor: getStateColor(),
|
||||
borderWidth: '1px',
|
||||
boxShadow: data.selected ? `0 0 0 1px ${getStateColor()} !important` : 'none',
|
||||
minHeight: 60,
|
||||
height: 'auto',
|
||||
backgroundColor: getBackgroundColor(),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'&:hover': {
|
||||
boxShadow: data.selected ? `0 0 0 1px ${getStateColor()} !important` : 'none'
|
||||
}
|
||||
}}
|
||||
border={false}
|
||||
>
|
||||
<Box>
|
||||
<Input
|
||||
key={data.id}
|
||||
placeholder={inputParam.placeholder}
|
||||
inputParam={inputParam}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
|
||||
nodes={reactFlowInstance ? reactFlowInstance.getNodes() : []}
|
||||
edges={reactFlowInstance ? reactFlowInstance.getEdges() : []}
|
||||
nodeId={data.id}
|
||||
/>
|
||||
</Box>
|
||||
</CardWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
StickyNote.propTypes = {
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
export default StickyNote
|
||||
@@ -0,0 +1,56 @@
|
||||
.edgebutton {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #eee;
|
||||
border: 1px solid #fff;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.edgebutton:hover {
|
||||
background: #5e35b1;
|
||||
color: #eee;
|
||||
box-shadow: 0 0 6px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.edgebutton-foreignobject div {
|
||||
background: transparent;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.reactflow-parent-wrapper {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.reactflow-parent-wrapper .reactflow-wrapper {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chatflow-canvas .react-flow__handle-connecting {
|
||||
cursor: not-allowed;
|
||||
background: #db4e4e !important;
|
||||
}
|
||||
|
||||
.chatflow-canvas .react-flow__handle-valid {
|
||||
cursor: crosshair;
|
||||
background: #5dba62 !important;
|
||||
}
|
||||
|
||||
.agent-flow-edge-selector:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.agent-flow-edge-selector:hover + .agent-flow-edge {
|
||||
stroke-width: 3 !important;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -367,7 +367,13 @@ const APIKey = () => {
|
||||
<ErrorBoundary error={error} />
|
||||
) : (
|
||||
<Stack flexDirection='column' sx={{ gap: 3 }}>
|
||||
<ViewHeader onSearchChange={onSearchChange} search={true} searchPlaceholder='Search API Keys' title='API Keys'>
|
||||
<ViewHeader
|
||||
onSearchChange={onSearchChange}
|
||||
search={true}
|
||||
searchPlaceholder='Search API Keys'
|
||||
title='API Keys'
|
||||
description='Flowise API & SDK authentication keys'
|
||||
>
|
||||
<Button
|
||||
variant='outlined'
|
||||
sx={{ borderRadius: 2, height: '100%' }}
|
||||
|
||||
@@ -72,9 +72,10 @@ const AddCustomAssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) =>
|
||||
onConfirm(createResp.data.id)
|
||||
}
|
||||
} catch (err) {
|
||||
const errorData = typeof err === 'string' ? err : err.response?.data || `${err.response.data.message}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to add new Custom Assistant: ${errorData}`,
|
||||
message: `Failed to add new Custom Assistant: ${
|
||||
typeof err.response.data === 'object' ? err.response.data.message : err.response.data
|
||||
}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
|
||||
@@ -1000,7 +1000,7 @@ const CustomAssistantConfigurePreview = () => {
|
||||
sx={{ borderRadius: 20 }}
|
||||
size='small'
|
||||
variant='text'
|
||||
onClick={() => generateInstruction(customAssistantInstruction)}
|
||||
onClick={() => generateInstruction()}
|
||||
startIcon={<IconWand size={20} />}
|
||||
>
|
||||
Generate
|
||||
|
||||
@@ -98,6 +98,7 @@ const CustomAssistantLayout = () => {
|
||||
search={true}
|
||||
searchPlaceholder='Search Assistants'
|
||||
title='Custom Assistant'
|
||||
description='Create custom assistants with your choice of LLMs'
|
||||
onBack={() => navigate(-1)}
|
||||
>
|
||||
<StyledButton
|
||||
|
||||
@@ -122,7 +122,10 @@ const Assistants = () => {
|
||||
<>
|
||||
<MainCard>
|
||||
<Stack flexDirection='column' sx={{ gap: 3 }}>
|
||||
<ViewHeader title='Assistants' />
|
||||
<ViewHeader
|
||||
title='Assistants'
|
||||
description='Chat assistants with instructions, tools, and files to respond to user queries'
|
||||
/>
|
||||
<FeatureCards />
|
||||
</Stack>
|
||||
</MainCard>
|
||||
|
||||
@@ -120,6 +120,7 @@ const OpenAIAssistantLayout = () => {
|
||||
search={true}
|
||||
searchPlaceholder='Search Assistants'
|
||||
title='OpenAI Assistant'
|
||||
description='Create assistants using OpenAI Assistant API'
|
||||
onBack={() => navigate(-1)}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useState, useRef, useEffect, memo } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
@@ -35,15 +35,16 @@ import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import Transitions from '@/ui-component/extended/Transitions'
|
||||
import { StyledFab } from '@/ui-component/button/StyledFab'
|
||||
import AgentflowGeneratorDialog from '@/ui-component/dialog/AgentflowGeneratorDialog'
|
||||
|
||||
// icons
|
||||
import { IconPlus, IconSearch, IconMinus, IconX } from '@tabler/icons-react'
|
||||
import { IconPlus, IconSearch, IconMinus, IconX, IconSparkles } from '@tabler/icons-react'
|
||||
import LlamaindexPNG from '@/assets/images/llamaindex.png'
|
||||
import LangChainPNG from '@/assets/images/langchain.png'
|
||||
import utilNodesPNG from '@/assets/images/utilNodes.png'
|
||||
|
||||
// const
|
||||
import { baseURL } from '@/store/constant'
|
||||
import { baseURL, AGENTFLOW_ICONS } from '@/store/constant'
|
||||
import { SET_COMPONENT_NODES } from '@/store/actions'
|
||||
|
||||
// ==============================|| ADD NODES||============================== //
|
||||
@@ -69,7 +70,7 @@ const blacklistForChatflowCanvas = {
|
||||
Memory: agentMemoryNodes
|
||||
}
|
||||
|
||||
const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
const AddNodes = ({ nodesData, node, isAgentCanvas, isAgentflowv2, onFlowGenerated }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const dispatch = useDispatch()
|
||||
@@ -80,6 +81,11 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
const [categoryExpanded, setCategoryExpanded] = useState({})
|
||||
const [tabValue, setTabValue] = useState(0)
|
||||
|
||||
const [openDialog, setOpenDialog] = useState(false)
|
||||
const [dialogProps, setDialogProps] = useState({})
|
||||
|
||||
const isAgentCanvasV2 = window.location.pathname.includes('/v2/agentcanvas')
|
||||
|
||||
const anchorRef = useRef(null)
|
||||
const prevOpen = useRef(open)
|
||||
const ps = useRef()
|
||||
@@ -177,6 +183,15 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
|
||||
const filteredResult = {}
|
||||
for (const category in result) {
|
||||
if (isAgentCanvasV2) {
|
||||
if (category !== 'Agent Flows') {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if (category === 'Agent Flows') {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Filter out blacklisted categories
|
||||
if (!blacklistCategoriesForAgentCanvas.includes(category)) {
|
||||
// Filter out LlamaIndex nodes
|
||||
@@ -195,6 +210,7 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
accordianCategories['Multi Agents'] = true
|
||||
accordianCategories['Sequential Agents'] = true
|
||||
accordianCategories['Memory'] = true
|
||||
accordianCategories['Agent Flows'] = true
|
||||
setCategoryExpanded(accordianCategories)
|
||||
} else {
|
||||
const taggedNodes = groupByTags(nodes, newTabValue)
|
||||
@@ -208,7 +224,7 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
|
||||
const filteredResult = {}
|
||||
for (const category in result) {
|
||||
if (category === 'Multi Agents' || category === 'Sequential Agents') {
|
||||
if (category === 'Agent Flows' || category === 'Multi Agents' || category === 'Sequential Agents') {
|
||||
continue
|
||||
}
|
||||
if (Object.keys(blacklistForChatflowCanvas).includes(category)) {
|
||||
@@ -255,6 +271,13 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const renderIcon = (node) => {
|
||||
const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === node.name)
|
||||
|
||||
if (!foundIcon) return null
|
||||
return <foundIcon.icon size={30} color={node.color} />
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
anchorRef.current.focus()
|
||||
@@ -276,6 +299,25 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodesData, dispatch])
|
||||
|
||||
// Handle dialog open/close
|
||||
const handleOpenDialog = () => {
|
||||
setOpenDialog(true)
|
||||
setDialogProps({
|
||||
title: 'What would you like to build?',
|
||||
description:
|
||||
'Enter your prompt to generate an agentflow. Performance may vary with different models. Only nodes and edges are generated, you will need to fill in the input fields for each node.'
|
||||
})
|
||||
}
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setOpenDialog(false)
|
||||
}
|
||||
|
||||
const handleConfirmDialog = () => {
|
||||
setOpenDialog(false)
|
||||
onFlowGenerated()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledFab
|
||||
@@ -289,6 +331,33 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
>
|
||||
{open ? <IconMinus /> : <IconPlus />}
|
||||
</StyledFab>
|
||||
{isAgentflowv2 && (
|
||||
<StyledFab
|
||||
sx={{
|
||||
left: 40,
|
||||
top: 20,
|
||||
background: 'linear-gradient(45deg, #FF6B6B 30%, #FF8E53 90%)',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(45deg, #FF8E53 30%, #FF6B6B 90%)'
|
||||
}
|
||||
}}
|
||||
onClick={handleOpenDialog}
|
||||
size='small'
|
||||
color='primary'
|
||||
aria-label='generate'
|
||||
title='Generate Agentflow'
|
||||
>
|
||||
<IconSparkles />
|
||||
</StyledFab>
|
||||
)}
|
||||
|
||||
<AgentflowGeneratorDialog
|
||||
show={openDialog}
|
||||
dialogProps={dialogProps}
|
||||
onCancel={handleCloseDialog}
|
||||
onConfirm={handleConfirmDialog}
|
||||
/>
|
||||
|
||||
<Popper
|
||||
placement='bottom-end'
|
||||
open={open}
|
||||
@@ -489,27 +558,43 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
}}
|
||||
>
|
||||
<ListItem alignItems='center'>
|
||||
<ListItemAvatar>
|
||||
<div
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
{node.color && !node.icon ? (
|
||||
<ListItemAvatar>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 10,
|
||||
objectFit: 'contain'
|
||||
width: 50,
|
||||
height: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
alt={node.name}
|
||||
src={`${baseURL}/api/v1/node-icon/${node.name}`}
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
>
|
||||
{renderIcon(node)}
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
) : (
|
||||
<ListItemAvatar>
|
||||
<div
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 10,
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
alt={node.name}
|
||||
src={`${baseURL}/api/v1/node-icon/${node.name}`}
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
)}
|
||||
<ListItemText
|
||||
sx={{ ml: 1 }}
|
||||
primary={
|
||||
@@ -583,7 +668,9 @@ const AddNodes = ({ nodesData, node, isAgentCanvas }) => {
|
||||
AddNodes.propTypes = {
|
||||
nodesData: PropTypes.array,
|
||||
node: PropTypes.object,
|
||||
isAgentCanvas: PropTypes.bool
|
||||
onFlowGenerated: PropTypes.func,
|
||||
isAgentCanvas: PropTypes.bool,
|
||||
isAgentflowv2: PropTypes.bool
|
||||
}
|
||||
|
||||
export default AddNodes
|
||||
export default memo(AddNodes)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getBezierPath, EdgeText } from 'reactflow'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useContext } from 'react'
|
||||
import { useContext, memo } from 'react'
|
||||
import { SET_DIRTY } from '@/store/actions'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
import { IconX } from '@tabler/icons-react'
|
||||
@@ -75,4 +75,4 @@ ButtonEdge.propTypes = {
|
||||
markerEnd: PropTypes.any
|
||||
}
|
||||
|
||||
export default ButtonEdge
|
||||
export default memo(ButtonEdge)
|
||||
|
||||
@@ -33,7 +33,7 @@ import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackba
|
||||
|
||||
// ==============================|| CANVAS HEADER ||============================== //
|
||||
|
||||
const CanvasHeader = ({ chatflow, isAgentCanvas, handleSaveFlow, handleDeleteFlow, handleLoadFlow }) => {
|
||||
const CanvasHeader = ({ chatflow, isAgentCanvas, isAgentflowV2, handleSaveFlow, handleDeleteFlow, handleLoadFlow }) => {
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
@@ -122,7 +122,13 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, handleSaveFlow, handleDeleteFlo
|
||||
const parsedFlowData = JSON.parse(flowData)
|
||||
flowData = JSON.stringify(parsedFlowData)
|
||||
localStorage.setItem('duplicatedFlowData', flowData)
|
||||
window.open(`${uiBaseURL}/${isAgentCanvas ? 'agentcanvas' : 'canvas'}`, '_blank')
|
||||
if (isAgentflowV2) {
|
||||
window.open(`${uiBaseURL}/v2/agentcanvas`, '_blank')
|
||||
} else if (isAgentCanvas) {
|
||||
window.open(`${uiBaseURL}/agentcanvas`, '_blank')
|
||||
} else {
|
||||
window.open(`${uiBaseURL}/canvas`, '_blank')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
@@ -255,9 +261,13 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, handleSaveFlow, handleDeleteFlo
|
||||
}
|
||||
}}
|
||||
color='inherit'
|
||||
onClick={() =>
|
||||
window.history.state && window.history.state.idx > 0 ? navigate(-1) : navigate('/', { replace: true })
|
||||
}
|
||||
onClick={() => {
|
||||
if (window.history.state && window.history.state.idx > 0) {
|
||||
navigate(-1)
|
||||
} else {
|
||||
navigate('/', { replace: true })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconChevronLeft stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
@@ -486,7 +496,8 @@ CanvasHeader.propTypes = {
|
||||
handleSaveFlow: PropTypes.func,
|
||||
handleDeleteFlow: PropTypes.func,
|
||||
handleLoadFlow: PropTypes.func,
|
||||
isAgentCanvas: PropTypes.bool
|
||||
isAgentCanvas: PropTypes.bool,
|
||||
isAgentflowV2: PropTypes.bool
|
||||
}
|
||||
|
||||
export default CanvasHeader
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useContext, useState, useEffect } from 'react'
|
||||
import { useContext, useState, useEffect, memo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
@@ -64,6 +64,12 @@ const CanvasNode = ({ data }) => {
|
||||
setShowDialog(true)
|
||||
}
|
||||
|
||||
const getBorderColor = () => {
|
||||
if (data.selected) return theme.palette.primary.main
|
||||
else if (theme?.customization?.isDarkMode) return theme.palette.grey[900] + 25
|
||||
else return theme.palette.grey[900] + 50
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const componentNode = canvas.componentNodes.find((nd) => nd.name === data.name)
|
||||
if (componentNode) {
|
||||
@@ -88,7 +94,7 @@ const CanvasNode = ({ data }) => {
|
||||
content={false}
|
||||
sx={{
|
||||
padding: 0,
|
||||
borderColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary
|
||||
borderColor: getBorderColor()
|
||||
}}
|
||||
border={false}
|
||||
>
|
||||
@@ -142,14 +148,16 @@ const CanvasNode = ({ data }) => {
|
||||
>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Box style={{ width: 50, marginRight: 10, padding: 5 }}>
|
||||
<Box style={{ width: 50, marginRight: 10, padding: 10 }}>
|
||||
<div
|
||||
style={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.largeAvatar,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'grab'
|
||||
cursor: 'grab',
|
||||
width: '40px',
|
||||
height: '40px'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
@@ -218,6 +226,7 @@ const CanvasNode = ({ data }) => {
|
||||
))}
|
||||
{data.inputParams
|
||||
.filter((inputParam) => !inputParam.hidden)
|
||||
.filter((inputParam) => inputParam.display !== false)
|
||||
.map((inputParam, index) => (
|
||||
<NodeInputHandler
|
||||
key={index}
|
||||
@@ -283,4 +292,4 @@ CanvasNode.propTypes = {
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
export default CanvasNode
|
||||
export default memo(CanvasNode)
|
||||
|
||||
@@ -12,12 +12,13 @@ import CredentialListDialog from '@/views/credentials/CredentialListDialog'
|
||||
|
||||
// API
|
||||
import credentialsApi from '@/api/credentials'
|
||||
import { FLOWISE_CREDENTIAL_ID } from '@/store/constant'
|
||||
|
||||
// ===========================|| CredentialInputHandler ||=========================== //
|
||||
|
||||
const CredentialInputHandler = ({ inputParam, data, onSelect, disabled = false }) => {
|
||||
const ref = useRef(null)
|
||||
const [credentialId, setCredentialId] = useState(data?.credential ?? '')
|
||||
const [credentialId, setCredentialId] = useState(data?.credential || (data?.inputs && data.inputs[FLOWISE_CREDENTIAL_ID]) || '')
|
||||
const [showCredentialListDialog, setShowCredentialListDialog] = useState(false)
|
||||
const [credentialListDialogProps, setCredentialListDialogProps] = useState({})
|
||||
const [showSpecificCredentialDialog, setShowSpecificCredentialDialog] = useState(false)
|
||||
@@ -89,7 +90,7 @@ const CredentialInputHandler = ({ inputParam, data, onSelect, disabled = false }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCredentialId(data?.credential ?? '')
|
||||
setCredentialId(data?.credential || (data?.inputs && data.inputs[FLOWISE_CREDENTIAL_ID]) || '')
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,49 +1,82 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Handle, Position, useUpdateNodeInternals } from 'reactflow'
|
||||
import { useEffect, useRef, useState, useContext } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
// material-ui
|
||||
import { Tabs } from '@mui/base/Tabs'
|
||||
import IconAutoFixHigh from '@mui/icons-material/AutoFixHigh'
|
||||
import { Box, Button, IconButton, Popper, TextField, Tooltip, Typography } from '@mui/material'
|
||||
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
|
||||
import { styled, useTheme } from '@mui/material/styles'
|
||||
import { tooltipClasses } from '@mui/material/Tooltip'
|
||||
import { useTheme, styled } from '@mui/material/styles'
|
||||
import {
|
||||
Popper,
|
||||
Box,
|
||||
Typography,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Button,
|
||||
TextField,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions
|
||||
} from '@mui/material'
|
||||
import { useGridApiContext } from '@mui/x-data-grid'
|
||||
import { IconAlertTriangle, IconArrowsMaximize, IconBulb, IconEdit, IconRefresh } from '@tabler/icons-react'
|
||||
import IconAutoFixHigh from '@mui/icons-material/AutoFixHigh'
|
||||
import { tooltipClasses } from '@mui/material/Tooltip'
|
||||
import { IconWand, IconVariable, IconArrowsMaximize, IconEdit, IconAlertTriangle, IconBulb, IconRefresh, IconX } from '@tabler/icons-react'
|
||||
import { Tabs } from '@mui/base/Tabs'
|
||||
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
|
||||
|
||||
// project import
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
import ConditionDialog from '@/ui-component/dialog/ConditionDialog'
|
||||
import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog'
|
||||
import FormatPromptValuesDialog from '@/ui-component/dialog/FormatPromptValuesDialog'
|
||||
import InputHintDialog from '@/ui-component/dialog/InputHintDialog'
|
||||
import ManageScrapedLinksDialog from '@/ui-component/dialog/ManageScrapedLinksDialog'
|
||||
import NvidiaNIMDialog from '@/ui-component/dialog/NvidiaNIMDialog'
|
||||
import PromptLangsmithHubDialog from '@/ui-component/dialog/PromptLangsmithHubDialog'
|
||||
import { AsyncDropdown } from '@/ui-component/dropdown/AsyncDropdown'
|
||||
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
|
||||
import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown'
|
||||
import { CodeEditor } from '@/ui-component/editor/CodeEditor'
|
||||
import { File } from '@/ui-component/file/File'
|
||||
import { DataGrid } from '@/ui-component/grid/DataGrid'
|
||||
import { AsyncDropdown } from '@/ui-component/dropdown/AsyncDropdown'
|
||||
import { Input } from '@/ui-component/input/Input'
|
||||
import { JsonEditorInput } from '@/ui-component/json/JsonEditor'
|
||||
import { RichInput } from '@/ui-component/input/RichInput'
|
||||
import { DataGrid } from '@/ui-component/grid/DataGrid'
|
||||
import { File } from '@/ui-component/file/File'
|
||||
import { SwitchInput } from '@/ui-component/switch/Switch'
|
||||
import { Tab } from '@/ui-component/tabs/Tab'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
import { JsonEditorInput } from '@/ui-component/json/JsonEditor'
|
||||
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
|
||||
import { CodeEditor } from '@/ui-component/editor/CodeEditor'
|
||||
import { TabPanel } from '@/ui-component/tabs/TabPanel'
|
||||
import { TabsList } from '@/ui-component/tabs/TabsList'
|
||||
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
|
||||
import AssistantDialog from '@/views/assistants/openai/AssistantDialog'
|
||||
import { ArrayRenderer } from '@/ui-component/array/ArrayRenderer'
|
||||
import { Tab } from '@/ui-component/tabs/Tab'
|
||||
import { ConfigInput } from '@/views/agentflowsv2/ConfigInput'
|
||||
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
|
||||
import DocStoreInputHandler from '@/views/docstore/DocStoreInputHandler'
|
||||
|
||||
import ToolDialog from '@/views/tools/ToolDialog'
|
||||
import AssistantDialog from '@/views/assistants/openai/AssistantDialog'
|
||||
import FormatPromptValuesDialog from '@/ui-component/dialog/FormatPromptValuesDialog'
|
||||
import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog'
|
||||
import ExpandRichInputDialog from '@/ui-component/dialog/ExpandRichInputDialog'
|
||||
import ConditionDialog from '@/ui-component/dialog/ConditionDialog'
|
||||
import PromptLangsmithHubDialog from '@/ui-component/dialog/PromptLangsmithHubDialog'
|
||||
import ManageScrapedLinksDialog from '@/ui-component/dialog/ManageScrapedLinksDialog'
|
||||
import CredentialInputHandler from './CredentialInputHandler'
|
||||
import InputHintDialog from '@/ui-component/dialog/InputHintDialog'
|
||||
import NvidiaNIMDialog from '@/ui-component/dialog/NvidiaNIMDialog'
|
||||
import PromptGeneratorDialog from '@/ui-component/dialog/PromptGeneratorDialog'
|
||||
|
||||
// API
|
||||
import assistantsApi from '@/api/assistants'
|
||||
import documentstoreApi from '@/api/documentstore'
|
||||
|
||||
// utils
|
||||
import { getAvailableNodesForVariable, getCustomConditionOutputs, getInputVariables, isValidConnection } from '@/utils/genericHelper'
|
||||
import {
|
||||
initNode,
|
||||
getInputVariables,
|
||||
getCustomConditionOutputs,
|
||||
isValidConnection,
|
||||
getAvailableNodesForVariable
|
||||
} from '@/utils/genericHelper'
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
|
||||
// const
|
||||
import { FLOWISE_CREDENTIAL_ID } from '@/store/constant'
|
||||
import { baseURL, FLOWISE_CREDENTIAL_ID } from '@/store/constant'
|
||||
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
|
||||
|
||||
const EDITABLE_OPTIONS = ['selectedTool', 'selectedAssistant']
|
||||
|
||||
@@ -74,16 +107,27 @@ const NodeInputHandler = ({
|
||||
disabled = false,
|
||||
isAdditionalParams = false,
|
||||
disablePadding = false,
|
||||
onHideNodeInfoDialog
|
||||
parentParamForArray = null,
|
||||
arrayIndex = null,
|
||||
onHideNodeInfoDialog,
|
||||
onCustomDataChange
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const ref = useRef(null)
|
||||
const { reactFlowInstance, deleteEdge } = useContext(flowContext)
|
||||
const { reactFlowInstance, deleteEdge, onNodeDataChange } = useContext(flowContext)
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
|
||||
useNotifier()
|
||||
const dispatch = useDispatch()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const [position, setPosition] = useState(0)
|
||||
const [showExpandDialog, setShowExpandDialog] = useState(false)
|
||||
const [expandDialogProps, setExpandDialogProps] = useState({})
|
||||
const [showExpandRichDialog, setShowExpandRichDialog] = useState(false)
|
||||
const [expandRichDialogProps, setExpandRichDialogProps] = useState({})
|
||||
const [showAsyncOptionDialog, setAsyncOptionEditDialog] = useState('')
|
||||
const [asyncOptionEditDialogProps, setAsyncOptionEditDialogProps] = useState({})
|
||||
const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString())
|
||||
@@ -99,6 +143,28 @@ const NodeInputHandler = ({
|
||||
const [isNvidiaNIMDialogOpen, setIsNvidiaNIMDialogOpen] = useState(false)
|
||||
const [tabValue, setTabValue] = useState(0)
|
||||
|
||||
const [modelSelectionDialogOpen, setModelSelectionDialogOpen] = useState(false)
|
||||
const [availableChatModels, setAvailableChatModels] = useState([])
|
||||
const [availableChatModelsOptions, setAvailableChatModelsOptions] = useState([])
|
||||
const [selectedTempChatModel, setSelectedTempChatModel] = useState({})
|
||||
const [modelSelectionCallback, setModelSelectionCallback] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [promptGeneratorDialogOpen, setPromptGeneratorDialogOpen] = useState(false)
|
||||
const [promptGeneratorDialogProps, setPromptGeneratorDialogProps] = useState({})
|
||||
|
||||
const handleDataChange = ({ inputParam, newValue }) => {
|
||||
data.inputs[inputParam.name] = newValue
|
||||
const allowedShowHideInputTypes = ['boolean', 'asyncOptions', 'asyncMultiOptions', 'options', 'multiOptions']
|
||||
if (allowedShowHideInputTypes.includes(inputParam.type)) {
|
||||
if (onCustomDataChange) {
|
||||
onCustomDataChange({ nodeId: data.id, inputParam, newValue })
|
||||
} else {
|
||||
onNodeDataChange({ nodeId: data.id, inputParam, newValue })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onInputHintDialogClicked = (hint) => {
|
||||
const dialogProps = {
|
||||
...hint
|
||||
@@ -113,11 +179,19 @@ const NodeInputHandler = ({
|
||||
inputParam,
|
||||
disabled,
|
||||
languageType,
|
||||
nodes: reactFlowInstance?.getNodes() || [],
|
||||
edges: reactFlowInstance?.getEdges() || [],
|
||||
nodeId: data.id,
|
||||
confirmButtonName: 'Save',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
setExpandDialogProps(dialogProps)
|
||||
setShowExpandDialog(true)
|
||||
if (inputParam.acceptVariable) {
|
||||
setExpandRichDialogProps(dialogProps)
|
||||
setShowExpandRichDialog(true)
|
||||
} else {
|
||||
setExpandDialogProps(dialogProps)
|
||||
setShowExpandDialog(true)
|
||||
}
|
||||
}
|
||||
|
||||
const onConditionDialogClicked = (inputParam) => {
|
||||
@@ -375,6 +449,11 @@ const NodeInputHandler = ({
|
||||
setShowExpandDialog(false)
|
||||
}
|
||||
|
||||
const onExpandRichDialogSave = (newValue, inputParamName) => {
|
||||
data.inputs[inputParamName] = newValue
|
||||
setShowExpandRichDialog(false)
|
||||
}
|
||||
|
||||
const onConditionDialogSave = (newData, inputParam, tabValue) => {
|
||||
data.inputs[`${inputParam.tabIdentifier}_${data.id}`] = inputParam.tabs[tabValue].name
|
||||
|
||||
@@ -437,8 +516,10 @@ const NodeInputHandler = ({
|
||||
const onConfirmAsyncOption = (selectedOptionId = '') => {
|
||||
if (!selectedOptionId) {
|
||||
data.inputs[showAsyncOptionDialog] = ''
|
||||
handleDataChange({ inputParam: { name: showAsyncOptionDialog }, newValue: '' })
|
||||
} else {
|
||||
data.inputs[showAsyncOptionDialog] = selectedOptionId
|
||||
handleDataChange({ inputParam: { name: showAsyncOptionDialog }, newValue: selectedOptionId })
|
||||
setReloadTimestamp(Date.now().toString())
|
||||
}
|
||||
setAsyncOptionEditDialogProps({})
|
||||
@@ -452,6 +533,230 @@ const NodeInputHandler = ({
|
||||
}
|
||||
}
|
||||
|
||||
const loadChatModels = async () => {
|
||||
try {
|
||||
const resp = await assistantsApi.getChatModels()
|
||||
if (resp.data) {
|
||||
const chatModels = resp.data ?? []
|
||||
const chatModelsOptions = chatModels.map((model) => ({
|
||||
name: model.name,
|
||||
label: model.label,
|
||||
description: model.description,
|
||||
imageSrc: `${baseURL}/api/v1/node-icon/${model.name}`
|
||||
}))
|
||||
setAvailableChatModels(chatModels)
|
||||
setAvailableChatModelsOptions(chatModelsOptions)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading chat models:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const checkInputParamsMandatory = () => {
|
||||
let canSubmit = true
|
||||
|
||||
if (selectedTempChatModel && Object.keys(selectedTempChatModel).length > 0) {
|
||||
const inputParams = (selectedTempChatModel.inputParams ?? []).filter((inputParam) => !inputParam.hidden)
|
||||
for (const inputParam of inputParams) {
|
||||
if (!inputParam.optional && (!selectedTempChatModel.inputs[inputParam.name] || !selectedTempChatModel.credential)) {
|
||||
if (inputParam.type === 'credential' && !selectedTempChatModel.credential) {
|
||||
canSubmit = false
|
||||
break
|
||||
} else if (inputParam.type !== 'credential' && !selectedTempChatModel.inputs[inputParam.name]) {
|
||||
canSubmit = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return canSubmit
|
||||
}
|
||||
|
||||
const displayWarning = () => {
|
||||
enqueueSnackbar({
|
||||
message: 'Please fill in all mandatory fields.',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'warning',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const generateDocStoreToolDesc = async (storeId) => {
|
||||
if (!storeId) {
|
||||
enqueueSnackbar({
|
||||
message: 'Please select a knowledge base',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
storeId = storeId.split(':')[0]
|
||||
const isValid = checkInputParamsMandatory()
|
||||
if (!isValid) {
|
||||
displayWarning()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if model is already selected in the node
|
||||
const currentNode = reactFlowInstance?.getNodes().find((node) => node.id === data.id)
|
||||
const currentNodeInputs = currentNode?.data?.inputs
|
||||
|
||||
const existingModel = currentNodeInputs?.llmModel || currentNodeInputs?.agentModel || currentNodeInputs?.humanInputModel
|
||||
if (existingModel) {
|
||||
try {
|
||||
setLoading(true)
|
||||
const selectedChatModelObj = {
|
||||
name: existingModel,
|
||||
inputs:
|
||||
currentNodeInputs?.llmModelConfig || currentNodeInputs?.agentModelConfig || currentNodeInputs?.humanInputModelConfig
|
||||
}
|
||||
const resp = await documentstoreApi.generateDocStoreToolDesc(storeId, { selectedChatModel: selectedChatModelObj })
|
||||
if (resp.data) {
|
||||
setLoading(false)
|
||||
const content = resp.data?.content || resp.data.kwargs?.content
|
||||
// Update the input value directly
|
||||
data.inputs[inputParam.name] = content
|
||||
enqueueSnackbar({
|
||||
message: 'Document Store Tool Description generated successfully',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating doc store tool desc', error)
|
||||
setLoading(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>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If no model selected, load chat models and open model selection dialog
|
||||
await loadChatModels()
|
||||
setModelSelectionCallback(() => async (selectedModel) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const selectedChatModelObj = {
|
||||
name: selectedModel.name,
|
||||
inputs: selectedModel.inputs
|
||||
}
|
||||
const resp = await documentstoreApi.generateDocStoreToolDesc(storeId, { selectedChatModel: selectedChatModelObj })
|
||||
if (resp.data) {
|
||||
setLoading(false)
|
||||
const content = resp.data?.content || resp.data.kwargs?.content
|
||||
// Update the input value directly
|
||||
data.inputs[inputParam.name] = content
|
||||
enqueueSnackbar({
|
||||
message: 'Document Store Tool Description generated successfully',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating doc store tool desc', error)
|
||||
setLoading(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>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
setModelSelectionDialogOpen(true)
|
||||
}
|
||||
|
||||
const generateInstruction = async () => {
|
||||
const isValid = checkInputParamsMandatory()
|
||||
if (!isValid) {
|
||||
displayWarning()
|
||||
return
|
||||
}
|
||||
|
||||
const currentNode = reactFlowInstance?.getNodes().find((node) => node.id === data.id)
|
||||
const currentNodeInputs = currentNode?.data?.inputs
|
||||
|
||||
// Check if model is already selected in the node
|
||||
const existingModel = currentNodeInputs?.llmModel || currentNodeInputs?.agentModel || currentNodeInputs?.humanInputModel
|
||||
if (existingModel) {
|
||||
// Open prompt generator dialog directly with existing model
|
||||
setPromptGeneratorDialogProps({
|
||||
title: 'Generate Instructions',
|
||||
description: 'You can generate a prompt template by sharing basic details about your task.',
|
||||
data: {
|
||||
selectedChatModel: {
|
||||
name: existingModel,
|
||||
inputs:
|
||||
currentNodeInputs?.llmModelConfig ||
|
||||
currentNodeInputs?.agentModelConfig ||
|
||||
currentNodeInputs?.humanInputModelConfig
|
||||
}
|
||||
}
|
||||
})
|
||||
setPromptGeneratorDialogOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
// If no model selected, load chat models and open model selection dialog
|
||||
await loadChatModels()
|
||||
setModelSelectionCallback(() => async (selectedModel) => {
|
||||
// After model selection, open prompt generator dialog
|
||||
setPromptGeneratorDialogProps({
|
||||
title: 'Generate Instructions',
|
||||
description: 'You can generate a prompt template by sharing basic details about your task.',
|
||||
data: { selectedChatModel: selectedModel }
|
||||
})
|
||||
setPromptGeneratorDialogOpen(true)
|
||||
})
|
||||
setModelSelectionDialogOpen(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
||||
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2)
|
||||
@@ -537,7 +842,7 @@ const NodeInputHandler = ({
|
||||
></PromptLangsmithHubDialog>
|
||||
</>
|
||||
)}
|
||||
{data.name === 'Chat NVIDIA NIM' && inputParam.name === 'modelName' && (
|
||||
{data.name === 'chatNvidiaNIM' && inputParam.name === 'modelName' && (
|
||||
<>
|
||||
<Button
|
||||
style={{
|
||||
@@ -553,7 +858,7 @@ const NodeInputHandler = ({
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Typography>
|
||||
{inputParam.label}
|
||||
{!inputParam.optional && <span style={{ color: 'red' }}> *</span>}
|
||||
@@ -587,12 +892,47 @@ const NodeInputHandler = ({
|
||||
{inputParam.hint.label}
|
||||
</Button>
|
||||
)}
|
||||
{inputParam.acceptVariable && inputParam.type === 'string' && (
|
||||
<Tooltip title='Type {{ to select variables'>
|
||||
<IconVariable size={20} style={{ color: 'teal' }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{inputParam.generateDocStoreDescription && (
|
||||
<IconButton
|
||||
title='Generate knowledge base description'
|
||||
sx={{
|
||||
height: 25,
|
||||
width: 25
|
||||
}}
|
||||
size='small'
|
||||
color='secondary'
|
||||
onClick={() => generateDocStoreToolDesc(data.inputs['documentStore'])}
|
||||
>
|
||||
<IconWand />
|
||||
</IconButton>
|
||||
)}
|
||||
{inputParam.generateInstruction && (
|
||||
<IconButton
|
||||
title='Generate instructions'
|
||||
sx={{
|
||||
height: 25,
|
||||
width: 25,
|
||||
ml: 0.5
|
||||
}}
|
||||
size='small'
|
||||
color='secondary'
|
||||
onClick={() => generateInstruction()}
|
||||
>
|
||||
<IconWand />
|
||||
</IconButton>
|
||||
)}
|
||||
{((inputParam.type === 'string' && inputParam.rows) || inputParam.type === 'code') && (
|
||||
<IconButton
|
||||
size='small'
|
||||
sx={{
|
||||
height: 25,
|
||||
width: 25
|
||||
width: 25,
|
||||
ml: 0.5
|
||||
}}
|
||||
title='Expand'
|
||||
color='primary'
|
||||
@@ -650,17 +990,19 @@ const NodeInputHandler = ({
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{inputParam.tabs.map((inputChildParam, index) => (
|
||||
<TabPanel key={index} value={getTabValue(inputParam)} index={index}>
|
||||
<NodeInputHandler
|
||||
disabled={inputChildParam.disabled}
|
||||
inputParam={inputChildParam}
|
||||
data={data}
|
||||
isAdditionalParams={true}
|
||||
disablePadding={true}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
{inputParam.tabs
|
||||
.filter((inputParam) => inputParam.display !== false)
|
||||
.map((inputChildParam, index) => (
|
||||
<TabPanel key={index} value={getTabValue(inputParam)} index={index}>
|
||||
<NodeInputHandler
|
||||
disabled={inputChildParam.disabled}
|
||||
inputParam={inputChildParam}
|
||||
data={data}
|
||||
isAdditionalParams={true}
|
||||
disablePadding={true}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{inputParam.type === 'file' && (
|
||||
@@ -674,7 +1016,7 @@ const NodeInputHandler = ({
|
||||
{inputParam.type === 'boolean' && (
|
||||
<SwitchInput
|
||||
disabled={disabled}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
onChange={(newValue) => handleDataChange({ inputParam, newValue })}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? false}
|
||||
/>
|
||||
)}
|
||||
@@ -723,18 +1065,33 @@ const NodeInputHandler = ({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
|
||||
<Input
|
||||
key={data.inputs[inputParam.name]}
|
||||
disabled={disabled}
|
||||
inputParam={inputParam}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
|
||||
nodes={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getNodes() : []}
|
||||
edges={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getEdges() : []}
|
||||
nodeId={data.id}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') &&
|
||||
(inputParam?.acceptVariable ? (
|
||||
<RichInput
|
||||
key={data.inputs[inputParam.name]}
|
||||
placeholder={inputParam.placeholder}
|
||||
disabled={disabled}
|
||||
inputParam={inputParam}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
|
||||
nodes={reactFlowInstance ? reactFlowInstance.getNodes() : []}
|
||||
edges={reactFlowInstance ? reactFlowInstance.getEdges() : []}
|
||||
nodeId={data.id}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
key={data.inputs[inputParam.name]}
|
||||
placeholder={inputParam.placeholder}
|
||||
disabled={disabled}
|
||||
inputParam={inputParam}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
|
||||
nodes={[]}
|
||||
edges={[]}
|
||||
nodeId={data.id}
|
||||
/>
|
||||
))}
|
||||
{inputParam.type === 'json' && (
|
||||
<>
|
||||
{!inputParam?.acceptVariable && (
|
||||
@@ -777,28 +1134,35 @@ const NodeInputHandler = ({
|
||||
</>
|
||||
)}
|
||||
{inputParam.type === 'options' && (
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
name={inputParam.name}
|
||||
options={getDropdownOptions(inputParam)}
|
||||
freeSolo={inputParam.freeSolo}
|
||||
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
|
||||
/>
|
||||
<div key={`${data.id}_${JSON.stringify(data.inputs[inputParam.name])}`}>
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
name={inputParam.name}
|
||||
options={getDropdownOptions(inputParam)}
|
||||
freeSolo={inputParam.freeSolo}
|
||||
onSelect={(newValue) => handleDataChange({ inputParam, newValue })}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{inputParam.type === 'multiOptions' && (
|
||||
<MultiDropdown
|
||||
disabled={disabled}
|
||||
name={inputParam.name}
|
||||
options={getDropdownOptions(inputParam)}
|
||||
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
|
||||
/>
|
||||
<div key={`${data.id}_${JSON.stringify(data.inputs[inputParam.name])}`}>
|
||||
<MultiDropdown
|
||||
disabled={disabled}
|
||||
name={inputParam.name}
|
||||
options={getDropdownOptions(inputParam)}
|
||||
onSelect={(newValue) => handleDataChange({ inputParam, newValue })}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(inputParam.type === 'asyncOptions' || inputParam.type === 'asyncMultiOptions') && (
|
||||
<>
|
||||
{data.inputParams.length === 1 && <div style={{ marginTop: 10 }} />}
|
||||
<div key={reloadTimestamp} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 1 }}>
|
||||
<div
|
||||
key={`${reloadTimestamp}_${data.id}_${JSON.stringify(data.inputs[inputParam.name])}`}
|
||||
style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 1 }}
|
||||
>
|
||||
<AsyncDropdown
|
||||
disabled={disabled}
|
||||
name={inputParam.name}
|
||||
@@ -807,7 +1171,10 @@ const NodeInputHandler = ({
|
||||
freeSolo={inputParam.freeSolo}
|
||||
multiple={inputParam.type === 'asyncMultiOptions'}
|
||||
isCreateNewOption={EDITABLE_OPTIONS.includes(inputParam.name)}
|
||||
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
onSelect={(newValue) => {
|
||||
if (inputParam.loadConfig) setReloadTimestamp(Date.now().toString())
|
||||
handleDataChange({ inputParam, newValue })
|
||||
}}
|
||||
onCreateNew={() => addAsyncOption(inputParam.name)}
|
||||
/>
|
||||
{EDITABLE_OPTIONS.includes(inputParam.name) && data.inputs[inputParam.name] && (
|
||||
@@ -833,6 +1200,7 @@ const NodeInputHandler = ({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{inputParam.type === 'array' && <ArrayRenderer inputParam={inputParam} data={data} disabled={disabled} />}
|
||||
{/* CUSTOM INPUT LOGIC */}
|
||||
{inputParam.type.includes('conditionFunction') && (
|
||||
<>
|
||||
@@ -883,6 +1251,20 @@ const NodeInputHandler = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{inputParam.loadConfig && data && data.inputs && data.inputs[inputParam.name] && (
|
||||
<>
|
||||
<ConfigInput
|
||||
key={`${data.id}_${JSON.stringify(data.inputs[inputParam.name])}_${arrayIndex}_${
|
||||
parentParamForArray?.name
|
||||
}`}
|
||||
data={data}
|
||||
inputParam={inputParam}
|
||||
disabled={disabled}
|
||||
arrayIndex={arrayIndex}
|
||||
parentParamForArray={parentParamForArray}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
@@ -905,6 +1287,13 @@ const NodeInputHandler = ({
|
||||
onConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)}
|
||||
onInputHintDialogClicked={onInputHintDialogClicked}
|
||||
></ExpandTextDialog>
|
||||
<ExpandRichInputDialog
|
||||
show={showExpandRichDialog}
|
||||
dialogProps={expandRichDialogProps}
|
||||
onCancel={() => setShowExpandRichDialog(false)}
|
||||
onConfirm={(newValue, inputParamName) => onExpandRichDialogSave(newValue, inputParamName)}
|
||||
onInputHintDialogClicked={onInputHintDialogClicked}
|
||||
></ExpandRichInputDialog>
|
||||
<ConditionDialog
|
||||
show={showConditionDialog}
|
||||
dialogProps={conditionDialogProps}
|
||||
@@ -924,6 +1313,100 @@ const NodeInputHandler = ({
|
||||
onClose={() => setIsNvidiaNIMDialogOpen(false)}
|
||||
onComplete={handleNvidiaNIMDialogComplete}
|
||||
></NvidiaNIMDialog>
|
||||
<Dialog
|
||||
open={modelSelectionDialogOpen}
|
||||
onClose={() => {
|
||||
setModelSelectionDialogOpen(false)
|
||||
setSelectedTempChatModel({})
|
||||
}}
|
||||
aria-labelledby='model-selection-dialog-title'
|
||||
maxWidth='sm'
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle id='model-selection-dialog-title'>Select Model</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Box sx={{ px: 2 }}>
|
||||
<Dropdown
|
||||
name={'chatModel'}
|
||||
options={availableChatModelsOptions ?? []}
|
||||
onSelect={(newValue) => {
|
||||
if (!newValue) {
|
||||
setSelectedTempChatModel({})
|
||||
} else {
|
||||
const foundChatComponent = availableChatModels.find((chatModel) => chatModel.name === newValue)
|
||||
if (foundChatComponent) {
|
||||
const chatModelId = `${foundChatComponent.name}_0`
|
||||
const clonedComponent = cloneDeep(foundChatComponent)
|
||||
const initChatModelData = initNode(clonedComponent, chatModelId)
|
||||
setSelectedTempChatModel(initChatModelData)
|
||||
}
|
||||
}
|
||||
}}
|
||||
value={selectedTempChatModel?.name ?? 'choose an option'}
|
||||
/>
|
||||
</Box>
|
||||
{selectedTempChatModel && Object.keys(selectedTempChatModel).length > 0 && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
{(selectedTempChatModel.inputParams ?? [])
|
||||
.filter((inputParam) => !inputParam.hidden)
|
||||
.map((inputParam, index) => (
|
||||
<DocStoreInputHandler key={index} inputParam={inputParam} data={selectedTempChatModel} />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setModelSelectionDialogOpen(false)
|
||||
setSelectedTempChatModel({})
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!selectedTempChatModel || Object.keys(selectedTempChatModel).length === 0}
|
||||
onClick={async () => {
|
||||
setModelSelectionDialogOpen(false)
|
||||
if (modelSelectionCallback) {
|
||||
await modelSelectionCallback(selectedTempChatModel)
|
||||
}
|
||||
setSelectedTempChatModel({})
|
||||
}}
|
||||
variant='contained'
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<PromptGeneratorDialog
|
||||
show={promptGeneratorDialogOpen}
|
||||
dialogProps={promptGeneratorDialogProps}
|
||||
onCancel={() => setPromptGeneratorDialogOpen(false)}
|
||||
onConfirm={(generatedInstruction) => {
|
||||
try {
|
||||
data.inputs[inputParam.name] = generatedInstruction
|
||||
setPromptGeneratorDialogOpen(false)
|
||||
} catch (error) {
|
||||
enqueueSnackbar({
|
||||
message: 'Error setting generated instruction',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{loading && <BackdropLoader open={loading} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -935,6 +1418,9 @@ NodeInputHandler.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
isAdditionalParams: PropTypes.bool,
|
||||
disablePadding: PropTypes.bool,
|
||||
parentParamForArray: PropTypes.object,
|
||||
arrayIndex: PropTypes.number,
|
||||
onCustomDataChange: PropTypes.func,
|
||||
onHideNodeInfoDialog: PropTypes.func
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useContext, useState } from 'react'
|
||||
import { useContext, useState, memo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
@@ -31,13 +31,19 @@ const StickyNote = ({ data }) => {
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const getBorderColor = () => {
|
||||
if (data.selected) return theme.palette.primary.main
|
||||
else if (theme?.customization?.isDarkMode) return theme.palette.grey[900] + 25
|
||||
else return theme.palette.grey[900] + 50
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeCardWrapper
|
||||
content={false}
|
||||
sx={{
|
||||
padding: 0,
|
||||
borderColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
borderColor: getBorderColor(),
|
||||
backgroundColor: data.selected ? '#FFDC00' : '#FFE770'
|
||||
}}
|
||||
border={false}
|
||||
@@ -100,4 +106,4 @@ StickyNote.propTypes = {
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
export default StickyNote
|
||||
export default memo(StickyNote)
|
||||
|
||||
@@ -24,8 +24,8 @@ import StickyNote from './StickyNote'
|
||||
import CanvasHeader from './CanvasHeader'
|
||||
import AddNodes from './AddNodes'
|
||||
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
|
||||
import { ChatPopUp } from '@/views/chatmessage/ChatPopUp'
|
||||
import { VectorStorePopUp } from '@/views/vectorstore/VectorStorePopUp'
|
||||
import ChatPopUp from '@/views/chatmessage/ChatPopUp'
|
||||
import VectorStorePopUp from '@/views/vectorstore/VectorStorePopUp'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
|
||||
// API
|
||||
|
||||
@@ -11,6 +11,11 @@ import chatflowsApi from '@/api/chatflows'
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
// MUI
|
||||
import { Box, Card, Stack, Typography, useTheme } from '@mui/material'
|
||||
import { IconCircleXFilled } from '@tabler/icons-react'
|
||||
import { alpha } from '@mui/material/styles'
|
||||
|
||||
//Const
|
||||
import { baseURL } from '@/store/constant'
|
||||
|
||||
@@ -20,6 +25,7 @@ const ChatbotFull = () => {
|
||||
const URLpath = document.location.pathname.toString().split('/')
|
||||
const chatflowId = URLpath[URLpath.length - 1] === 'chatbot' ? '' : URLpath[URLpath.length - 1]
|
||||
const navigate = useNavigate()
|
||||
const theme = useTheme()
|
||||
|
||||
const [chatflow, setChatflow] = useState(null)
|
||||
const [chatbotTheme, setChatbotTheme] = useState({})
|
||||
@@ -80,7 +86,7 @@ const ChatbotFull = () => {
|
||||
const chatflowType = chatflowData.type
|
||||
if (chatflowData.chatbotConfig) {
|
||||
let parsedConfig = {}
|
||||
if (chatflowType === 'MULTIAGENT') {
|
||||
if (chatflowType === 'MULTIAGENT' || chatflowType === 'AGENTFLOW') {
|
||||
parsedConfig.showAgentMessages = true
|
||||
}
|
||||
|
||||
@@ -99,7 +105,7 @@ const ChatbotFull = () => {
|
||||
setChatbotTheme(parsedConfig)
|
||||
setChatbotOverrideConfig({})
|
||||
}
|
||||
} else if (chatflowType === 'MULTIAGENT') {
|
||||
} else if (chatflowType === 'MULTIAGENT' || chatflowType === 'AGENTFLOW') {
|
||||
setChatbotTheme({ showAgentMessages: true })
|
||||
}
|
||||
}
|
||||
@@ -114,7 +120,29 @@ const ChatbotFull = () => {
|
||||
{!isLoading ? (
|
||||
<>
|
||||
{!chatflow || chatflow.apikeyid ? (
|
||||
<p>Invalid Chatbot</p>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80vh' }}>
|
||||
<Box sx={{ maxWidth: '500px', width: '100%' }}>
|
||||
<Card
|
||||
variant='outlined'
|
||||
sx={{
|
||||
border: `1px solid ${theme.palette.error.main}`,
|
||||
borderRadius: 2,
|
||||
padding: '20px',
|
||||
boxShadow: `0 4px 8px ${alpha(theme.palette.error.main, 0.15)}`
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2} alignItems='center'>
|
||||
<IconCircleXFilled size={50} color={theme.palette.error.main} />
|
||||
<Typography variant='h3' color='error.main' align='center'>
|
||||
Invalid Chatbot
|
||||
</Typography>
|
||||
<Typography variant='body1' color='text.secondary' align='center'>
|
||||
{`The chatbot you're looking for doesn't exist or requires API key authentication.`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<FullPageChat
|
||||
chatflowid={chatflow.id}
|
||||
|
||||
@@ -131,7 +131,13 @@ const Chatflows = () => {
|
||||
<ErrorBoundary error={error} />
|
||||
) : (
|
||||
<Stack flexDirection='column' sx={{ gap: 3 }}>
|
||||
<ViewHeader onSearchChange={onSearchChange} search={true} searchPlaceholder='Search Name or Category' title='Chatflows'>
|
||||
<ViewHeader
|
||||
onSearchChange={onSearchChange}
|
||||
search={true}
|
||||
searchPlaceholder='Search Name or Category'
|
||||
title='Chatflows'
|
||||
description='Build single-agent systems, chatbots and simple LLM flows'
|
||||
>
|
||||
<ToggleButtonGroup
|
||||
sx={{ borderRadius: 2, maxHeight: 40 }}
|
||||
value={view}
|
||||
|
||||
@@ -0,0 +1,711 @@
|
||||
import { useEffect, useState, useCallback, forwardRef, memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// MUI
|
||||
import { RichTreeView } from '@mui/x-tree-view/RichTreeView'
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Divider,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions
|
||||
} from '@mui/material'
|
||||
import { styled, alpha } from '@mui/material/styles'
|
||||
import { useTreeItem2 } from '@mui/x-tree-view/useTreeItem2'
|
||||
import {
|
||||
TreeItem2Content,
|
||||
TreeItem2IconContainer,
|
||||
TreeItem2GroupTransition,
|
||||
TreeItem2Label,
|
||||
TreeItem2Root,
|
||||
TreeItem2Checkbox
|
||||
} from '@mui/x-tree-view/TreeItem2'
|
||||
import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'
|
||||
import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'
|
||||
import { TreeItem2DragAndDropOverlay } from '@mui/x-tree-view/TreeItem2DragAndDropOverlay'
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||
import StopCircleIcon from '@mui/icons-material/StopCircle'
|
||||
import ErrorIcon from '@mui/icons-material/Error'
|
||||
import { IconButton } from '@mui/material'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
import { IconArrowsMaximize, IconLoader, IconCircleXFilled, IconRelationOneToManyFilled } from '@tabler/icons-react'
|
||||
|
||||
// Project imports
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { FLOWISE_CREDENTIAL_ID, AGENTFLOW_ICONS } from '@/store/constant'
|
||||
import { NodeExecutionDetails } from '@/views/agentexecutions/NodeExecutionDetails'
|
||||
|
||||
const getIconColor = (status) => {
|
||||
switch (status) {
|
||||
case 'FINISHED':
|
||||
return 'success.dark'
|
||||
case 'ERROR':
|
||||
case 'TIMEOUT':
|
||||
return 'error.main'
|
||||
case 'TERMINATED':
|
||||
case 'STOPPED':
|
||||
return 'error.main'
|
||||
case 'INPROGRESS':
|
||||
return 'warning.dark'
|
||||
}
|
||||
}
|
||||
|
||||
const StyledTreeItemRoot = styled(TreeItem2Root)(({ theme }) => ({
|
||||
color: theme.palette.grey[400]
|
||||
}))
|
||||
|
||||
const CustomTreeItemContent = styled(TreeItem2Content)(({ theme }) => ({
|
||||
flexDirection: 'row-reverse',
|
||||
borderRadius: theme.spacing(0.7),
|
||||
marginBottom: theme.spacing(0.5),
|
||||
marginTop: theme.spacing(0.5),
|
||||
padding: theme.spacing(0.5),
|
||||
paddingRight: theme.spacing(1),
|
||||
fontWeight: 500,
|
||||
[`&.Mui-expanded `]: {
|
||||
'&:not(.Mui-focused, .Mui-selected, .Mui-selected.Mui-focused) .labelIcon': {
|
||||
color: theme.palette.primary.dark,
|
||||
...theme.applyStyles('light', {
|
||||
color: theme.palette.primary.main
|
||||
})
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
left: '16px',
|
||||
top: '44px',
|
||||
height: 'calc(100% - 48px)',
|
||||
width: '1.5px',
|
||||
backgroundColor: theme.palette.grey[700],
|
||||
...theme.applyStyles('light', {
|
||||
backgroundColor: theme.palette.grey[300]
|
||||
})
|
||||
}
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.primary.main, 0.1),
|
||||
color: 'white',
|
||||
...theme.applyStyles('light', {
|
||||
color: theme.palette.primary.main
|
||||
})
|
||||
},
|
||||
[`&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused`]: {
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
color: theme.palette.primary.contrastText,
|
||||
...theme.applyStyles('light', {
|
||||
backgroundColor: theme.palette.primary.main
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
const StyledTreeItemLabelText = styled(Typography)(({ theme }) => ({
|
||||
color: theme.palette.text.primary
|
||||
}))
|
||||
|
||||
function CustomLabel({ icon: Icon, itemStatus, children, name, label, data, metadata, ...other }) {
|
||||
const [openDialog, setOpenDialog] = useState(false)
|
||||
|
||||
const handleOpenDialog = (event) => {
|
||||
// Stop propagation to prevent parent elements from capturing the click
|
||||
event.stopPropagation()
|
||||
setOpenDialog(true)
|
||||
}
|
||||
|
||||
const handleCloseDialog = () => setOpenDialog(false)
|
||||
|
||||
// Check if this is an iteration node
|
||||
const isIterationNode = name === 'iterationAgentflow'
|
||||
|
||||
return (
|
||||
<TreeItem2Label
|
||||
{...other}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||
{(() => {
|
||||
// Display iteration icon for iteration nodes
|
||||
if (isIterationNode) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
mr: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<IconRelationOneToManyFilled size={20} color={'#9C89B8'} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Otherwise display the node icon
|
||||
const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === name)
|
||||
if (foundIcon) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
mr: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<foundIcon.icon size={20} color={foundIcon.color} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
|
||||
<StyledTreeItemLabelText sx={{ flex: 1 }}>{children}</StyledTreeItemLabelText>
|
||||
<IconButton
|
||||
onClick={handleOpenDialog}
|
||||
size='small'
|
||||
title='View Details'
|
||||
sx={{
|
||||
ml: 2,
|
||||
zIndex: 10 // Increase z-index to ensure the button is clickable
|
||||
}}
|
||||
>
|
||||
<IconArrowsMaximize size={15} color={'teal'} />
|
||||
</IconButton>
|
||||
{Icon && <Box component={Icon} className='labelIcon' color={getIconColor(itemStatus)} sx={{ ml: 1, fontSize: '1.2rem' }} />}
|
||||
</Box>
|
||||
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth='md' fullWidth disableBackdropClick={true}>
|
||||
<DialogContent onClick={(e) => e.stopPropagation()}>
|
||||
{data ? (
|
||||
<NodeExecutionDetails data={data} label={label} metadata={metadata} />
|
||||
) : (
|
||||
<Typography color='text.secondary'>No data available for this item</Typography>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDialog}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</TreeItem2Label>
|
||||
)
|
||||
}
|
||||
|
||||
CustomLabel.propTypes = {
|
||||
icon: PropTypes.elementType,
|
||||
itemStatus: PropTypes.string,
|
||||
expandable: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
name: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
status: PropTypes.object,
|
||||
data: PropTypes.object,
|
||||
metadata: PropTypes.object
|
||||
}
|
||||
|
||||
CustomLabel.displayName = 'CustomLabel'
|
||||
|
||||
const isExpandable = (reactChildren) => {
|
||||
if (Array.isArray(reactChildren)) {
|
||||
return reactChildren.length > 0 && reactChildren.some(isExpandable)
|
||||
}
|
||||
return Boolean(reactChildren)
|
||||
}
|
||||
|
||||
const getIconFromStatus = (status, theme) => {
|
||||
switch (status) {
|
||||
case 'FINISHED':
|
||||
return CheckCircleIcon
|
||||
case 'ERROR':
|
||||
case 'TIMEOUT':
|
||||
return ErrorIcon
|
||||
case 'TERMINATED':
|
||||
// eslint-disable-next-line react/display-name
|
||||
return (props) => <IconCircleXFilled {...props} color={theme.palette.error.main} />
|
||||
case 'STOPPED':
|
||||
return StopCircleIcon
|
||||
case 'INPROGRESS':
|
||||
// eslint-disable-next-line react/display-name
|
||||
return (props) => (
|
||||
// eslint-disable-next-line
|
||||
<IconLoader {...props} color={theme.palette.warning.dark} className={`spin-animation ${props.className || ''}`} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const CustomTreeItem = forwardRef(function CustomTreeItem(props, ref) {
|
||||
const { id, itemId, label, disabled, children, agentflowId, sessionId, ...other } = props
|
||||
const theme = useTheme()
|
||||
|
||||
const {
|
||||
getRootProps,
|
||||
getContentProps,
|
||||
getIconContainerProps,
|
||||
getCheckboxProps,
|
||||
getLabelProps,
|
||||
getGroupTransitionProps,
|
||||
getDragAndDropOverlayProps,
|
||||
status,
|
||||
publicAPI
|
||||
} = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref })
|
||||
|
||||
const item = publicAPI.getItem(itemId)
|
||||
const expandable = isExpandable(children)
|
||||
let icon
|
||||
if (item.status) {
|
||||
icon = getIconFromStatus(item.status, theme)
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeItem2Provider itemId={itemId}>
|
||||
<StyledTreeItemRoot {...getRootProps(other)}>
|
||||
<CustomTreeItemContent {...getContentProps()}>
|
||||
<TreeItem2IconContainer {...getIconContainerProps()}>
|
||||
<TreeItem2Icon status={status} />
|
||||
</TreeItem2IconContainer>
|
||||
<TreeItem2Checkbox {...getCheckboxProps()} />
|
||||
<CustomLabel
|
||||
{...getLabelProps({
|
||||
icon,
|
||||
itemStatus: item.status,
|
||||
expandable: expandable && status.expanded,
|
||||
name: item.name || item.id?.split('_')[0],
|
||||
label: item.label,
|
||||
status,
|
||||
data: item.data,
|
||||
metadata: { agentflowId, sessionId }
|
||||
})}
|
||||
/>
|
||||
<TreeItem2DragAndDropOverlay {...getDragAndDropOverlayProps()} />
|
||||
</CustomTreeItemContent>
|
||||
{children && (
|
||||
<TreeItem2GroupTransition
|
||||
{...getGroupTransitionProps()}
|
||||
style={{
|
||||
borderLeft: `${status.selected ? '3px solid' : '1px dashed'} ${(() => {
|
||||
const nodeName = item.name || item.id?.split('_')[0]
|
||||
const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === nodeName)
|
||||
return foundIcon ? foundIcon.color : theme.palette.primary.main
|
||||
})()}`,
|
||||
marginLeft: '13px',
|
||||
paddingLeft: '8px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</StyledTreeItemRoot>
|
||||
</TreeItem2Provider>
|
||||
)
|
||||
})
|
||||
|
||||
CustomTreeItem.propTypes = {
|
||||
id: PropTypes.string,
|
||||
itemId: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
agentflowId: PropTypes.string,
|
||||
sessionId: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
CustomTreeItem.displayName = 'CustomTreeItem'
|
||||
|
||||
const AgentExecutedDataCard = ({ status, execution, agentflowId, sessionId }) => {
|
||||
const [executionTree, setExecution] = useState([])
|
||||
const [expandedItems, setExpandedItems] = useState([])
|
||||
const [selectedItem, setSelectedItem] = useState(null)
|
||||
const theme = useTheme()
|
||||
|
||||
const getAllNodeIds = (nodes) => {
|
||||
let ids = []
|
||||
nodes.forEach((node) => {
|
||||
ids.push(node.id)
|
||||
if (node.children && node.children.length > 0) {
|
||||
ids = [...ids, ...getAllNodeIds(node.children)]
|
||||
}
|
||||
})
|
||||
return ids
|
||||
}
|
||||
|
||||
// Transform the execution data into a tree structure
|
||||
const buildTreeData = (nodes) => {
|
||||
// for each node, loop through each and every nested key of node.data, and remove the key if it is equal to FLOWISE_CREDENTIAL_ID
|
||||
nodes.forEach((node) => {
|
||||
const removeFlowiseCredentialId = (data) => {
|
||||
for (const key in data) {
|
||||
if (key === FLOWISE_CREDENTIAL_ID) {
|
||||
delete data[key]
|
||||
}
|
||||
if (typeof data[key] === 'object') {
|
||||
removeFlowiseCredentialId(data[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
removeFlowiseCredentialId(node.data)
|
||||
})
|
||||
|
||||
// Create a map for quick node lookup
|
||||
// Use execution index to make each node instance unique
|
||||
const nodeMap = new Map()
|
||||
nodes.forEach((node, index) => {
|
||||
const uniqueNodeId = `${node.nodeId}_${index}`
|
||||
nodeMap.set(uniqueNodeId, { ...node, uniqueNodeId, children: [], executionIndex: index })
|
||||
})
|
||||
|
||||
// Identify iteration nodes and their children
|
||||
const iterationGroups = new Map() // parentId -> Map of iterationIndex -> nodes
|
||||
|
||||
// Group iteration child nodes by their parent and iteration index
|
||||
nodes.forEach((node, index) => {
|
||||
if (node.data?.parentNodeId && node.data?.iterationIndex !== undefined) {
|
||||
const parentId = node.data.parentNodeId
|
||||
const iterationIndex = node.data.iterationIndex
|
||||
|
||||
if (!iterationGroups.has(parentId)) {
|
||||
iterationGroups.set(parentId, new Map())
|
||||
}
|
||||
|
||||
const iterationMap = iterationGroups.get(parentId)
|
||||
if (!iterationMap.has(iterationIndex)) {
|
||||
iterationMap.set(iterationIndex, [])
|
||||
}
|
||||
|
||||
iterationMap.get(iterationIndex).push(`${node.nodeId}_${index}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Create virtual iteration container nodes
|
||||
iterationGroups.forEach((iterationMap, parentId) => {
|
||||
iterationMap.forEach((nodeIds, iterationIndex) => {
|
||||
// Find the parent iteration node
|
||||
let parentNode = null
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].nodeId === parentId) {
|
||||
parentNode = nodes[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!parentNode) return
|
||||
|
||||
// Get iteration context from first child node
|
||||
const firstChildId = nodeIds[0]
|
||||
const firstChild = nodeMap.get(firstChildId)
|
||||
const iterationContext = firstChild?.data?.iterationContext || { index: iterationIndex }
|
||||
|
||||
// Create a virtual node for this iteration
|
||||
const iterationNodeId = `${parentId}_${iterationIndex}`
|
||||
const iterationLabel = `Iteration #${iterationIndex}`
|
||||
|
||||
// Determine status based on child nodes
|
||||
const childNodes = nodeIds.map((id) => nodeMap.get(id))
|
||||
const iterationStatus = childNodes.some((n) => n.status === 'ERROR')
|
||||
? 'ERROR'
|
||||
: childNodes.some((n) => n.status === 'INPROGRESS')
|
||||
? 'INPROGRESS'
|
||||
: childNodes.every((n) => n.status === 'FINISHED')
|
||||
? 'FINISHED'
|
||||
: 'UNKNOWN'
|
||||
|
||||
// Create the virtual node and add to nodeMap
|
||||
const virtualNode = {
|
||||
nodeId: iterationNodeId,
|
||||
nodeLabel: iterationLabel,
|
||||
data: {
|
||||
name: 'iterationAgentflow',
|
||||
iterationIndex,
|
||||
iterationContext,
|
||||
isVirtualNode: true,
|
||||
parentIterationId: parentId
|
||||
},
|
||||
previousNodeIds: [], // Will be handled in the main tree building
|
||||
status: iterationStatus,
|
||||
uniqueNodeId: iterationNodeId,
|
||||
children: [],
|
||||
executionIndex: -1 // Flag as a virtual node
|
||||
}
|
||||
|
||||
nodeMap.set(iterationNodeId, virtualNode)
|
||||
|
||||
// Set this virtual node as the parent for all nodes in this iteration
|
||||
nodeIds.forEach((childId) => {
|
||||
const childNode = nodeMap.get(childId)
|
||||
if (childNode) {
|
||||
childNode.virtualParentId = iterationNodeId
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Root nodes have no previous nodes
|
||||
const rootNodes = []
|
||||
const processedNodes = new Set()
|
||||
|
||||
// First pass: Build the main tree structure (excluding iteration children)
|
||||
nodes.forEach((node, index) => {
|
||||
const uniqueNodeId = `${node.nodeId}_${index}`
|
||||
const treeNode = nodeMap.get(uniqueNodeId)
|
||||
|
||||
// Skip nodes that belong to an iteration (they'll be added to their virtual parent)
|
||||
if (node.data?.parentNodeId && node.data?.iterationIndex !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (node.previousNodeIds.length === 0) {
|
||||
rootNodes.push(treeNode)
|
||||
} else {
|
||||
// Find the most recent (latest) parent node among all previous nodes
|
||||
let mostRecentParentIndex = -1
|
||||
let mostRecentParentId = null
|
||||
|
||||
node.previousNodeIds.forEach((parentId) => {
|
||||
// Find the most recent instance of this parent node
|
||||
for (let i = 0; i < index; i++) {
|
||||
if (nodes[i].nodeId === parentId && i > mostRecentParentIndex) {
|
||||
mostRecentParentIndex = i
|
||||
mostRecentParentId = parentId
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Only add to the most recent parent
|
||||
if (mostRecentParentIndex !== -1) {
|
||||
const parentUniqueId = `${mostRecentParentId}_${mostRecentParentIndex}`
|
||||
const parentNode = nodeMap.get(parentUniqueId)
|
||||
if (parentNode) {
|
||||
parentNode.children.push(treeNode)
|
||||
processedNodes.add(uniqueNodeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Second pass: Build the iteration sub-trees
|
||||
iterationGroups.forEach((iterationMap, parentId) => {
|
||||
// Find all instances of the parent node
|
||||
const parentInstances = []
|
||||
nodes.forEach((node, index) => {
|
||||
if (node.nodeId === parentId) {
|
||||
parentInstances.push(`${node.nodeId}_${index}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Find the latest instance of the parent node that exists in the tree
|
||||
let latestParent = null
|
||||
for (let i = parentInstances.length - 1; i >= 0; i--) {
|
||||
const parentId = parentInstances[i]
|
||||
const parent = nodeMap.get(parentId)
|
||||
if (parent) {
|
||||
latestParent = parent
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!latestParent) return
|
||||
|
||||
// Add all virtual iteration nodes to the parent
|
||||
iterationMap.forEach((nodeIds, iterationIndex) => {
|
||||
const iterationNodeId = `${parentId}_${iterationIndex}`
|
||||
const virtualNode = nodeMap.get(iterationNodeId)
|
||||
if (virtualNode) {
|
||||
latestParent.children.push(virtualNode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Third pass: Build the structure inside each virtual iteration node
|
||||
nodeMap.forEach((node) => {
|
||||
if (node.virtualParentId) {
|
||||
const virtualParent = nodeMap.get(node.virtualParentId)
|
||||
if (virtualParent) {
|
||||
if (node.previousNodeIds.length === 0) {
|
||||
// This is a root node within the iteration
|
||||
virtualParent.children.push(node)
|
||||
} else {
|
||||
// Find its parent within the same iteration
|
||||
let parentFound = false
|
||||
for (const prevNodeId of node.previousNodeIds) {
|
||||
// Look for nodes with the same previous node ID in the same iteration
|
||||
nodeMap.forEach((potentialParent) => {
|
||||
if (
|
||||
potentialParent.nodeId === prevNodeId &&
|
||||
potentialParent.data?.iterationIndex === node.data?.iterationIndex &&
|
||||
potentialParent.data?.parentNodeId === node.data?.parentNodeId &&
|
||||
!parentFound
|
||||
) {
|
||||
potentialParent.children.push(node)
|
||||
parentFound = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// If no parent was found within the iteration, add directly to virtual parent
|
||||
if (!parentFound) {
|
||||
virtualParent.children.push(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Final pass: Sort all children arrays to ensure iteration nodes appear first
|
||||
const sortChildrenNodes = (node) => {
|
||||
if (node.children && node.children.length > 0) {
|
||||
// Sort children: iteration nodes first, then others by their original execution order
|
||||
node.children.sort((a, b) => {
|
||||
// Check if a is an iteration node
|
||||
const aIsIteration = a.data?.name === 'iterationAgentflow' || a.data?.isVirtualNode
|
||||
// Check if b is an iteration node
|
||||
const bIsIteration = b.data?.name === 'iterationAgentflow' || b.data?.isVirtualNode
|
||||
|
||||
// If both are iterations or both are not iterations, preserve original order
|
||||
if (aIsIteration === bIsIteration) {
|
||||
return a.executionIndex - b.executionIndex
|
||||
}
|
||||
|
||||
// Otherwise, put iterations first
|
||||
return aIsIteration ? -1 : 1
|
||||
})
|
||||
|
||||
// Recursively sort children's children
|
||||
node.children.forEach(sortChildrenNodes)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply sorting to all root nodes and their children
|
||||
rootNodes.forEach(sortChildrenNodes)
|
||||
|
||||
// Transform to the required format
|
||||
const transformNode = (node) => ({
|
||||
id: node.uniqueNodeId,
|
||||
label: node.nodeLabel,
|
||||
name: node.data?.name,
|
||||
status: node.status,
|
||||
data: node.data,
|
||||
children: node.children.map(transformNode)
|
||||
})
|
||||
|
||||
return rootNodes.map(transformNode)
|
||||
}
|
||||
|
||||
const handleExpandedItemsChange = (event, itemIds) => {
|
||||
setExpandedItems(itemIds)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (execution) {
|
||||
const newTree = buildTreeData(execution)
|
||||
|
||||
setExecution(newTree)
|
||||
setExpandedItems(getAllNodeIds(newTree))
|
||||
// Set the first item as default selected item
|
||||
if (newTree.length > 0) {
|
||||
setSelectedItem(newTree[0])
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [execution])
|
||||
|
||||
const handleNodeSelect = (event, itemId) => {
|
||||
const findNode = (nodes, id) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node
|
||||
if (node.children) {
|
||||
const found = findNode(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const selectedNode = findNode(executionTree, itemId)
|
||||
setSelectedItem(selectedNode)
|
||||
}
|
||||
|
||||
const getExecutionStatus = useCallback((executionTree) => {
|
||||
const getAllStatuses = (nodes) => {
|
||||
let statuses = []
|
||||
nodes.forEach((node) => {
|
||||
if (node.status) statuses.push(node.status)
|
||||
if (node.children && node.children.length > 0) {
|
||||
statuses = [...statuses, ...getAllStatuses(node.children)]
|
||||
}
|
||||
})
|
||||
return statuses
|
||||
}
|
||||
|
||||
const statuses = getAllStatuses(executionTree)
|
||||
if (statuses.includes('ERROR')) return 'ERROR'
|
||||
if (statuses.includes('INPROGRESS')) return 'INPROGRESS'
|
||||
if (statuses.includes('STOPPED')) return 'STOPPED'
|
||||
if (statuses.every((status) => status === 'FINISHED')) return 'FINISHED'
|
||||
return null
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', height: '100%', width: '100%', mt: 2 }}>
|
||||
<Accordion
|
||||
sx={{
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
sx={{
|
||||
'& .MuiAccordionSummary-content': {
|
||||
alignItems: 'center'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{executionTree.length > 0 &&
|
||||
(() => {
|
||||
const execStatus = status ?? getExecutionStatus(executionTree)
|
||||
return (
|
||||
<Box sx={{ mr: 1, fontSize: '1.2rem' }}>
|
||||
<Box
|
||||
component={getIconFromStatus(execStatus, theme)}
|
||||
sx={{
|
||||
mr: 1,
|
||||
fontSize: '1.2rem',
|
||||
color: getIconColor(execStatus)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
})()}
|
||||
<Typography>Process Flow</Typography>
|
||||
</AccordionSummary>
|
||||
<Divider />
|
||||
<AccordionDetails>
|
||||
<RichTreeView
|
||||
expandedItems={expandedItems}
|
||||
onExpandedItemsChange={handleExpandedItemsChange}
|
||||
selectedItems={selectedItem ? [selectedItem.id] : []}
|
||||
onSelectedItemsChange={handleNodeSelect}
|
||||
items={executionTree}
|
||||
slots={{
|
||||
item: (treeItemProps) => <CustomTreeItem {...treeItemProps} agentflowId={agentflowId} sessionId={sessionId} />
|
||||
}}
|
||||
sx={{ width: '100%' }}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
AgentExecutedDataCard.propTypes = {
|
||||
status: PropTypes.string,
|
||||
execution: PropTypes.array,
|
||||
agentflowId: PropTypes.string,
|
||||
sessionId: PropTypes.string
|
||||
}
|
||||
|
||||
export default memo(AgentExecutedDataCard)
|
||||
@@ -0,0 +1,183 @@
|
||||
import { Box, Card, CardContent, Chip, Stack } from '@mui/material'
|
||||
import { IconTool, IconDeviceSdCard } from '@tabler/icons-react'
|
||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||
import nextAgentGIF from '@/assets/images/next-agent.gif'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const AgentReasoningCard = ({
|
||||
agent,
|
||||
index,
|
||||
customization,
|
||||
chatflowid,
|
||||
isDialog,
|
||||
onSourceDialogClick,
|
||||
renderArtifacts,
|
||||
agentReasoningArtifacts,
|
||||
getAgentIcon,
|
||||
removeDuplicateURL,
|
||||
isValidURL,
|
||||
onURLClick,
|
||||
getLabel
|
||||
}) => {
|
||||
if (agent.nextAgent) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card key={index} sx={{ 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={getAgentIcon(agent.nodeName, agent.instructions)}
|
||||
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.state && Object.keys(agent.state).length > 0 && (
|
||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||
<Chip
|
||||
size='small'
|
||||
label={'State'}
|
||||
component='a'
|
||||
sx={{ mr: 1, mt: 1 }}
|
||||
variant='outlined'
|
||||
clickable
|
||||
icon={<IconDeviceSdCard size={15} />}
|
||||
onClick={() => onSourceDialogClick(agent.state, 'State')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{agent.artifacts && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
{agentReasoningArtifacts(agent.artifacts).map((item, index) => {
|
||||
return item !== null ? <>{renderArtifacts(item, index, true)}</> : null
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{agent.messages.length > 0 && (
|
||||
<MemoizedReactMarkdown chatflowid={chatflowid} isFullWidth={isDialog}>
|
||||
{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={getLabel(URL, source) || ''}
|
||||
component='a'
|
||||
sx={{ mr: 1, mb: 1 }}
|
||||
variant='outlined'
|
||||
clickable
|
||||
onClick={() => (URL ? onURLClick(source.metadata.source) : onSourceDialogClick(source))}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
AgentReasoningCard.propTypes = {
|
||||
agent: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
customization: PropTypes.object.isRequired,
|
||||
chatflowid: PropTypes.string,
|
||||
isDialog: PropTypes.bool,
|
||||
onSourceDialogClick: PropTypes.func.isRequired,
|
||||
renderArtifacts: PropTypes.func.isRequired,
|
||||
agentReasoningArtifacts: PropTypes.func.isRequired,
|
||||
getAgentIcon: PropTypes.func.isRequired,
|
||||
removeDuplicateURL: PropTypes.func.isRequired,
|
||||
isValidURL: PropTypes.func.isRequired,
|
||||
onURLClick: PropTypes.func.isRequired,
|
||||
getLabel: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
AgentReasoningCard.displayName = 'AgentReasoningCard'
|
||||
|
||||
export default AgentReasoningCard
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { Dialog, DialogContent, DialogTitle, Button } from '@mui/material'
|
||||
import { ChatMessage } from './ChatMessage'
|
||||
import ChatMessage from './ChatMessage'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import { IconEraser } from '@tabler/icons-react'
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { useState, useRef, useEffect, useCallback, Fragment } from 'react'
|
||||
import { useState, useRef, useEffect, useCallback, Fragment, useContext, memo } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import rehypeMathjax from 'rehype-mathjax'
|
||||
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 { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'
|
||||
@@ -22,10 +18,14 @@ import {
|
||||
InputAdornment,
|
||||
OutlinedInput,
|
||||
Typography,
|
||||
CardContent,
|
||||
Stack
|
||||
Stack,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField
|
||||
} from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { darken, useTheme } from '@mui/material/styles'
|
||||
import {
|
||||
IconCircleDot,
|
||||
IconDownload,
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
IconX,
|
||||
IconTool,
|
||||
IconSquareFilled,
|
||||
IconDeviceSdCard,
|
||||
IconCheck,
|
||||
IconPaperclip,
|
||||
IconSparkles
|
||||
@@ -46,14 +45,15 @@ 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'
|
||||
import NodeInputHandler from '@/views/canvas/NodeInputHandler'
|
||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
|
||||
import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog'
|
||||
import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'
|
||||
import AgentReasoningCard from './AgentReasoningCard'
|
||||
import AgentExecutedDataCard from './AgentExecutedDataCard'
|
||||
import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-component/button/ImageButton'
|
||||
import CopyToClipboardButton from '@/ui-component/button/CopyToClipboardButton'
|
||||
import ThumbsUpButton from '@/ui-component/button/ThumbsUpButton'
|
||||
@@ -70,9 +70,11 @@ import vectorstoreApi from '@/api/vectorstore'
|
||||
import attachmentsApi from '@/api/attachments'
|
||||
import chatmessagefeedbackApi from '@/api/chatmessagefeedback'
|
||||
import leadsApi from '@/api/lead'
|
||||
import executionsApi from '@/api/executions'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
|
||||
// Const
|
||||
import { baseURL, maxScroll } from '@/store/constant'
|
||||
@@ -159,13 +161,14 @@ CardWithDeleteOverlay.propTypes = {
|
||||
onDelete: PropTypes.func
|
||||
}
|
||||
|
||||
export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setPreviews }) => {
|
||||
const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setPreviews }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const ps = useRef()
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const { onAgentflowNodeStatusUpdate, clearAgentflowNodeStatus } = useContext(flowContext)
|
||||
|
||||
useNotifier()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
@@ -192,6 +195,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
|
||||
const inputRef = useRef(null)
|
||||
const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow)
|
||||
const getAllExecutionsApi = useApi(executionsApi.getAllExecutions)
|
||||
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming)
|
||||
const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads)
|
||||
const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow)
|
||||
@@ -232,6 +236,20 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
const [recordingNotSupported, setRecordingNotSupported] = useState(false)
|
||||
const [isLoadingRecording, setIsLoadingRecording] = useState(false)
|
||||
|
||||
const [openFeedbackDialog, setOpenFeedbackDialog] = useState(false)
|
||||
const [feedback, setFeedback] = useState('')
|
||||
const [pendingActionData, setPendingActionData] = useState(null)
|
||||
const [feedbackType, setFeedbackType] = useState('')
|
||||
|
||||
// start input type
|
||||
const [startInputType, setStartInputType] = useState('')
|
||||
const [formTitle, setFormTitle] = useState('')
|
||||
const [formDescription, setFormDescription] = useState('')
|
||||
const [formInputsData, setFormInputsData] = useState({})
|
||||
const [formInputParams, setFormInputParams] = useState([])
|
||||
|
||||
const [isConfigLoading, setIsConfigLoading] = useState(true)
|
||||
|
||||
const isFileAllowedForUpload = (file) => {
|
||||
const constraints = getAllowChatFlowUploads.data
|
||||
/**
|
||||
@@ -555,6 +573,28 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
})
|
||||
}
|
||||
|
||||
const updateAgentFlowEvent = (event) => {
|
||||
if (event === 'INPROGRESS') {
|
||||
setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage', agentFlowEventStatus: event }])
|
||||
} else {
|
||||
setMessages((prevMessages) => {
|
||||
let allMessages = [...cloneDeep(prevMessages)]
|
||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||
allMessages[allMessages.length - 1].agentFlowEventStatus = event
|
||||
return allMessages
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateAgentFlowExecutedData = (agentFlowExecutedData) => {
|
||||
setMessages((prevMessages) => {
|
||||
let allMessages = [...cloneDeep(prevMessages)]
|
||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||
allMessages[allMessages.length - 1].agentFlowExecutedData = agentFlowExecutedData
|
||||
return allMessages
|
||||
})
|
||||
}
|
||||
|
||||
const updateLastMessageAction = (action) => {
|
||||
setMessages((prevMessages) => {
|
||||
let allMessages = [...cloneDeep(prevMessages)]
|
||||
@@ -594,6 +634,28 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
})
|
||||
}
|
||||
|
||||
const updateLastMessageNextAgentFlow = (nextAgentFlow) => {
|
||||
onAgentflowNodeStatusUpdate(nextAgentFlow)
|
||||
}
|
||||
|
||||
const updateLastMessageUsedTools = (usedTools) => {
|
||||
setMessages((prevMessages) => {
|
||||
let allMessages = [...cloneDeep(prevMessages)]
|
||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||
allMessages[allMessages.length - 1].usedTools = usedTools
|
||||
return allMessages
|
||||
})
|
||||
}
|
||||
|
||||
const updateLastMessageFileAnnotations = (fileAnnotations) => {
|
||||
setMessages((prevMessages) => {
|
||||
let allMessages = [...cloneDeep(prevMessages)]
|
||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||
allMessages[allMessages.length - 1].fileAnnotations = fileAnnotations
|
||||
return allMessages
|
||||
})
|
||||
}
|
||||
|
||||
const abortMessage = () => {
|
||||
setIsMessageStopping(false)
|
||||
setMessages((prevMessages) => {
|
||||
@@ -622,25 +684,6 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
})
|
||||
}
|
||||
|
||||
const updateLastMessageUsedTools = (usedTools) => {
|
||||
setMessages((prevMessages) => {
|
||||
let allMessages = [...cloneDeep(prevMessages)]
|
||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||
allMessages[allMessages.length - 1].usedTools = usedTools
|
||||
return allMessages
|
||||
})
|
||||
}
|
||||
|
||||
const updateLastMessageFileAnnotations = (fileAnnotations) => {
|
||||
setMessages((prevMessages) => {
|
||||
let allMessages = [...cloneDeep(prevMessages)]
|
||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||
allMessages[allMessages.length - 1].fileAnnotations = fileAnnotations
|
||||
return allMessages
|
||||
})
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
const handleError = (message = 'Oops! There seems to be an error. Please try again.') => {
|
||||
message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, '')
|
||||
setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }])
|
||||
@@ -663,6 +706,29 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
handleSubmit(undefined, promptStarterInput)
|
||||
}
|
||||
|
||||
const onSubmitResponse = (actionData, feedback = '', type = '') => {
|
||||
let fbType = feedbackType
|
||||
if (type) {
|
||||
fbType = type
|
||||
}
|
||||
const question = feedback ? feedback : fbType.charAt(0).toUpperCase() + fbType.slice(1)
|
||||
handleSubmit(undefined, question, undefined, {
|
||||
type: fbType,
|
||||
startNodeId: actionData?.nodeId,
|
||||
feedback
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmitFeedback = () => {
|
||||
if (pendingActionData) {
|
||||
onSubmitResponse(pendingActionData, feedback)
|
||||
setOpenFeedbackDialog(false)
|
||||
setFeedback('')
|
||||
setPendingActionData(null)
|
||||
setFeedbackType('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleActionClick = async (elem, action) => {
|
||||
setUserInput(elem.label)
|
||||
setMessages((prevMessages) => {
|
||||
@@ -671,7 +737,19 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
allMessages[allMessages.length - 1].action = null
|
||||
return allMessages
|
||||
})
|
||||
handleSubmit(undefined, elem.label, action)
|
||||
if (elem.type.includes('agentflowv2')) {
|
||||
const type = elem.type.includes('approve') ? 'proceed' : 'reject'
|
||||
setFeedbackType(type)
|
||||
|
||||
if (action.data && action.data.input && action.data.input.humanInputEnableFeedback) {
|
||||
setPendingActionData(action.data)
|
||||
setOpenFeedbackDialog(true)
|
||||
} else {
|
||||
onSubmitResponse(action.data, '', type)
|
||||
}
|
||||
} else {
|
||||
handleSubmit(undefined, elem.label, action)
|
||||
}
|
||||
}
|
||||
|
||||
const updateMetadata = (data, input) => {
|
||||
@@ -773,7 +851,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = async (e, selectedInput, action) => {
|
||||
const handleSubmit = async (e, selectedInput, action, humanInput) => {
|
||||
if (e) e.preventDefault()
|
||||
|
||||
if (!selectedInput && userInput.trim() === '') {
|
||||
@@ -785,13 +863,21 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
|
||||
let input = userInput
|
||||
|
||||
if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput
|
||||
if (typeof selectedInput === 'string') {
|
||||
if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput
|
||||
|
||||
if (input.trim()) {
|
||||
inputHistory.addToHistory(input)
|
||||
if (input.trim()) {
|
||||
inputHistory.addToHistory(input)
|
||||
}
|
||||
} else if (typeof selectedInput === 'object') {
|
||||
input = Object.entries(selectedInput)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
clearAgentflowNodeStatus()
|
||||
|
||||
let uploads = previews.map((item) => {
|
||||
return {
|
||||
data: item.data,
|
||||
@@ -817,9 +903,14 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
question: input,
|
||||
chatId
|
||||
}
|
||||
if (typeof selectedInput === 'object') {
|
||||
params.form = selectedInput
|
||||
delete params.question
|
||||
}
|
||||
if (uploads && uploads.length > 0) params.uploads = uploads
|
||||
if (leadEmail) params.leadEmail = leadEmail
|
||||
if (action) params.action = action
|
||||
if (humanInput) params.humanInput = humanInput
|
||||
|
||||
if (isChatFlowAvailableToStream) {
|
||||
fetchResponseFromEventStream(chatflowid, params)
|
||||
@@ -842,8 +933,10 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
id: data?.chatMessageId,
|
||||
sourceDocuments: data?.sourceDocuments,
|
||||
usedTools: data?.usedTools,
|
||||
calledTools: data?.calledTools,
|
||||
fileAnnotations: data?.fileAnnotations,
|
||||
agentReasoning: data?.agentReasoning,
|
||||
agentFlowExecutedData: data?.agentFlowExecutedData,
|
||||
action: data?.action,
|
||||
artifacts: data?.artifacts,
|
||||
type: 'apiMessage',
|
||||
@@ -908,6 +1001,12 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
case 'agentReasoning':
|
||||
updateLastMessageAgentReasoning(payload.data)
|
||||
break
|
||||
case 'agentFlowEvent':
|
||||
updateAgentFlowEvent(payload.data)
|
||||
break
|
||||
case 'agentFlowExecutedData':
|
||||
updateAgentFlowExecutedData(payload.data)
|
||||
break
|
||||
case 'artifacts':
|
||||
updateLastMessageArtifacts(payload.data)
|
||||
break
|
||||
@@ -917,6 +1016,9 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
case 'nextAgent':
|
||||
updateLastMessageNextAgent(payload.data)
|
||||
break
|
||||
case 'nextAgentFlow':
|
||||
updateLastMessageNextAgentFlow(payload.data)
|
||||
break
|
||||
case 'metadata':
|
||||
updateMetadata(payload.data, input)
|
||||
break
|
||||
@@ -1068,6 +1170,8 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
})
|
||||
}
|
||||
if (message.followUpPrompts) obj.followUpPrompts = JSON.parse(message.followUpPrompts)
|
||||
if (message.role === 'apiMessage' && message.execution && message.execution.executionData)
|
||||
obj.agentFlowExecutedData = JSON.parse(message.execution.executionData)
|
||||
return obj
|
||||
})
|
||||
setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
|
||||
@@ -1077,6 +1181,25 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getChatmessageApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllExecutionsApi.data?.length) {
|
||||
const chatId = getAllExecutionsApi.data[0]?.sessionId
|
||||
setChatId(chatId)
|
||||
const loadedMessages = getAllExecutionsApi.data.map((execution) => {
|
||||
const executionData =
|
||||
typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData
|
||||
const obj = {
|
||||
id: execution.id,
|
||||
agentFlow: executionData
|
||||
}
|
||||
return obj
|
||||
})
|
||||
setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
|
||||
setLocalStorageChatflow(chatflowid, chatId)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getAllExecutionsApi.data])
|
||||
|
||||
// Get chatflow streaming capability
|
||||
useEffect(() => {
|
||||
if (getIsChatflowStreamingApi.data) {
|
||||
@@ -1099,6 +1222,38 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
|
||||
useEffect(() => {
|
||||
if (getChatflowConfig.data) {
|
||||
setIsConfigLoading(false)
|
||||
if (getChatflowConfig.data?.flowData) {
|
||||
let nodes = JSON.parse(getChatflowConfig.data?.flowData).nodes ?? []
|
||||
const startNode = nodes.find((node) => node.data.name === 'startAgentflow')
|
||||
if (startNode) {
|
||||
const startInputType = startNode.data.inputs?.startInputType
|
||||
setStartInputType(startInputType)
|
||||
|
||||
const formInputTypes = startNode.data.inputs?.formInputTypes
|
||||
if (startInputType === 'formInput' && formInputTypes && formInputTypes.length > 0) {
|
||||
for (const formInputType of formInputTypes) {
|
||||
if (formInputType.type === 'options') {
|
||||
formInputType.options = formInputType.addOptions.map((option) => ({
|
||||
label: option.option,
|
||||
name: option.option
|
||||
}))
|
||||
}
|
||||
}
|
||||
setFormInputParams(formInputTypes)
|
||||
setFormInputsData({
|
||||
id: 'formInput',
|
||||
inputs: {},
|
||||
inputParams: formInputTypes
|
||||
})
|
||||
setFormTitle(startNode.data.inputs?.formTitle)
|
||||
setFormDescription(startNode.data.inputs?.formDescription)
|
||||
}
|
||||
|
||||
getAllExecutionsApi.request({ agentflowId: chatflowid })
|
||||
}
|
||||
}
|
||||
|
||||
if (getChatflowConfig.data?.chatbotConfig && JSON.parse(getChatflowConfig.data?.chatbotConfig)) {
|
||||
let config = JSON.parse(getChatflowConfig.data?.chatbotConfig)
|
||||
if (config.starterPrompts) {
|
||||
@@ -1143,6 +1298,13 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getChatflowConfig.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getChatflowConfig.error) {
|
||||
setIsConfigLoading(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getChatflowConfig.error])
|
||||
|
||||
useEffect(() => {
|
||||
if (fullFileUpload) {
|
||||
setIsChatFlowAvailableForFileUploads(true)
|
||||
@@ -1174,10 +1336,13 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
getAllowChatFlowUploads.request(chatflowid)
|
||||
getChatflowConfig.request(chatflowid)
|
||||
|
||||
// Scroll to bottom
|
||||
scrollToBottom()
|
||||
// Add a small delay to ensure content is rendered before scrolling
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 100)
|
||||
|
||||
setIsRecording(false)
|
||||
setIsConfigLoading(true)
|
||||
|
||||
// leads
|
||||
const savedLead = getLocalStorageChatflow(chatflowid)?.lead
|
||||
@@ -1502,35 +1667,124 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MemoizedReactMarkdown chatflowid={chatflowid} isFullWidth={isDialog}>
|
||||
{item.data}
|
||||
</MemoizedReactMarkdown>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isConfigLoading) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
backgroundColor: theme.palette.background.paper
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (startInputType === 'formInput' && messages.length === 1) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 2,
|
||||
backgroundColor: theme.palette.background.paper
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '100%',
|
||||
maxWidth: '600px',
|
||||
maxHeight: '90%', // Limit height to 90% of parent
|
||||
p: 3,
|
||||
backgroundColor: customization.isDarkMode
|
||||
? darken(theme.palette.background.paper, 0.2)
|
||||
: theme.palette.background.paper,
|
||||
boxShadow: customization.isDarkMode ? '0px 0px 15px 0px rgba(255, 255, 255, 0.1)' : theme.shadows[3],
|
||||
borderRadius: 2,
|
||||
overflowY: 'auto' // Enable vertical scrolling if content overflows
|
||||
}}
|
||||
>
|
||||
<Typography variant='h4' sx={{ mb: 1, textAlign: 'center' }}>
|
||||
{formTitle || 'Please Fill Out The Form'}
|
||||
</Typography>
|
||||
<Typography variant='body1' sx={{ mb: 3, textAlign: 'center', color: theme.palette.text.secondary }}>
|
||||
{formDescription || 'Complete all fields below to continue'}
|
||||
</Typography>
|
||||
|
||||
{/* Form inputs */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{formInputParams &&
|
||||
formInputParams.map((inputParam, index) => (
|
||||
<Box key={index} sx={{ mb: 2 }}>
|
||||
<NodeInputHandler
|
||||
inputParam={inputParam}
|
||||
data={formInputsData}
|
||||
isAdditionalParams={true}
|
||||
onCustomDataChange={({ inputParam, newValue }) => {
|
||||
setFormInputsData((prev) => ({
|
||||
...prev,
|
||||
inputs: {
|
||||
...prev.inputs,
|
||||
[inputParam.name]: newValue
|
||||
}
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant='contained'
|
||||
fullWidth
|
||||
disabled={loading}
|
||||
onClick={() => handleSubmit(null, formInputsData.inputs)}
|
||||
sx={{
|
||||
mb: 2,
|
||||
borderRadius: 20,
|
||||
background: 'linear-gradient(45deg, #673ab7 30%, #1e88e5 90%)'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Submitting...' : 'Submit'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div onDragEnter={handleDrag}>
|
||||
{isDragActive && (
|
||||
@@ -1614,224 +1868,37 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{message.agentReasoning && (
|
||||
{message.agentReasoning && message.agentReasoning.length > 0 && (
|
||||
<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={getAgentIcon(agent.nodeName, agent.instructions)}
|
||||
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,
|
||||
borderColor: tool.error ? 'error.main' : undefined,
|
||||
color: tool.error ? 'error.main' : undefined
|
||||
}}
|
||||
variant='outlined'
|
||||
clickable
|
||||
icon={
|
||||
<IconTool
|
||||
size={15}
|
||||
color={
|
||||
tool.error
|
||||
? theme.palette.error.main
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
onClick={() => onSourceDialogClick(tool, 'Used Tools')}
|
||||
/>
|
||||
) : null
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{agent.state && Object.keys(agent.state).length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'block',
|
||||
flexDirection: 'row',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
size='small'
|
||||
label={'State'}
|
||||
component='a'
|
||||
sx={{ mr: 1, mt: 1 }}
|
||||
variant='outlined'
|
||||
clickable
|
||||
icon={<IconDeviceSdCard size={15} />}
|
||||
onClick={() => onSourceDialogClick(agent.state, 'State')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{agent.artifacts && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
{agentReasoningArtifacts(agent.artifacts).map((item, index) => {
|
||||
return item !== null ? (
|
||||
<>{renderArtifacts(item, index, true)}</>
|
||||
) : 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={getLabel(URL, source) || ''}
|
||||
component='a'
|
||||
sx={{ mr: 1, mb: 1 }}
|
||||
variant='outlined'
|
||||
clickable
|
||||
onClick={() =>
|
||||
URL
|
||||
? onURLClick(source.metadata.source)
|
||||
: onSourceDialogClick(source)
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
{message.agentReasoning.map((agent, index) => (
|
||||
<AgentReasoningCard
|
||||
key={index}
|
||||
agent={agent}
|
||||
index={index}
|
||||
customization={customization}
|
||||
chatflowid={chatflowid}
|
||||
isDialog={isDialog}
|
||||
onSourceDialogClick={onSourceDialogClick}
|
||||
renderArtifacts={renderArtifacts}
|
||||
agentReasoningArtifacts={agentReasoningArtifacts}
|
||||
getAgentIcon={getAgentIcon}
|
||||
removeDuplicateURL={removeDuplicateURL}
|
||||
isValidURL={isValidURL}
|
||||
onURLClick={onURLClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{message.agentFlowExecutedData &&
|
||||
Array.isArray(message.agentFlowExecutedData) &&
|
||||
message.agentFlowExecutedData.length > 0 && (
|
||||
<AgentExecutedDataCard
|
||||
status={message.agentFlowEventStatus}
|
||||
execution={message.agentFlowExecutedData}
|
||||
agentflowId={chatflowid}
|
||||
sessionId={chatId}
|
||||
/>
|
||||
)}
|
||||
{message.usedTools && (
|
||||
<div
|
||||
style={{
|
||||
@@ -1959,30 +2026,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{/* Messages are being rendered in Markdown format */}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MemoizedReactMarkdown chatflowid={chatflowid} isFullWidth={isDialog}>
|
||||
{message.message}
|
||||
</MemoizedReactMarkdown>
|
||||
</>
|
||||
@@ -2061,7 +2105,8 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
{(message.action.elements || []).map((elem, index) => {
|
||||
return (
|
||||
<>
|
||||
{elem.type === 'approve-button' && elem.label === 'Yes' ? (
|
||||
{(elem.type === 'approve-button' && elem.label === 'Yes') ||
|
||||
elem.type === 'agentflowv2-approve-button' ? (
|
||||
<Button
|
||||
sx={{
|
||||
width: 'max-content',
|
||||
@@ -2076,7 +2121,8 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
>
|
||||
{elem.label}
|
||||
</Button>
|
||||
) : elem.type === 'reject-button' && elem.label === 'No' ? (
|
||||
) : (elem.type === 'reject-button' && elem.label === 'No') ||
|
||||
elem.type === 'agentflowv2-reject-button' ? (
|
||||
<Button
|
||||
sx={{
|
||||
width: 'max-content',
|
||||
@@ -2421,6 +2467,37 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
|
||||
onCancel={() => setShowFeedbackContentDialog(false)}
|
||||
onConfirm={submitFeedbackContent}
|
||||
/>
|
||||
<Dialog
|
||||
maxWidth='md'
|
||||
fullWidth
|
||||
open={openFeedbackDialog}
|
||||
onClose={() => {
|
||||
setOpenFeedbackDialog(false)
|
||||
setPendingActionData(null)
|
||||
setFeedback('')
|
||||
}}
|
||||
>
|
||||
<DialogTitle variant='h5'>Provide Feedback</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
// eslint-disable-next-line
|
||||
autoFocus
|
||||
margin='dense'
|
||||
label='Feedback'
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
value={feedback}
|
||||
onChange={(e) => setFeedback(e.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleSubmitFeedback}>Cancel</Button>
|
||||
<Button onClick={handleSubmitFeedback} variant='contained'>
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2433,3 +2510,5 @@ ChatMessage.propTypes = {
|
||||
previews: PropTypes.array,
|
||||
setPreviews: PropTypes.func
|
||||
}
|
||||
|
||||
export default memo(ChatMessage)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { memo, useState, useRef, useEffect, useContext } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
@@ -10,7 +10,7 @@ import { IconMessage, IconX, IconEraser, IconArrowsMaximize } from '@tabler/icon
|
||||
import { StyledFab } from '@/ui-component/button/StyledFab'
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import Transitions from '@/ui-component/extended/Transitions'
|
||||
import { ChatMessage } from './ChatMessage'
|
||||
import ChatMessage from './ChatMessage'
|
||||
import ChatExpandDialog from './ChatExpandDialog'
|
||||
|
||||
// api
|
||||
@@ -19,6 +19,7 @@ import chatmessageApi from '@/api/chatmessage'
|
||||
// Hooks
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
|
||||
// Const
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
|
||||
@@ -26,10 +27,11 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba
|
||||
// Utils
|
||||
import { getLocalStorageChatflow, removeLocalStorageChatHistory } from '@/utils/genericHelper'
|
||||
|
||||
export const ChatPopUp = ({ chatflowid, isAgentCanvas }) => {
|
||||
const ChatPopUp = ({ chatflowid, isAgentCanvas, onOpenChange }) => {
|
||||
const theme = useTheme()
|
||||
const { confirm } = useConfirm()
|
||||
const dispatch = useDispatch()
|
||||
const { clearAgentflowNodeStatus } = useContext(flowContext)
|
||||
|
||||
useNotifier()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
@@ -48,10 +50,13 @@ export const ChatPopUp = ({ chatflowid, isAgentCanvas }) => {
|
||||
return
|
||||
}
|
||||
setOpen(false)
|
||||
if (onOpenChange) onOpenChange(false)
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen((prevOpen) => !prevOpen)
|
||||
const newOpenState = !open
|
||||
setOpen(newOpenState)
|
||||
if (onOpenChange) onOpenChange(newOpenState)
|
||||
}
|
||||
|
||||
const expandChat = () => {
|
||||
@@ -69,6 +74,7 @@ export const ChatPopUp = ({ chatflowid, isAgentCanvas }) => {
|
||||
open: false
|
||||
}
|
||||
setExpandDialogProps(props)
|
||||
clearAgentflowNodeStatus()
|
||||
setTimeout(() => {
|
||||
const resetProps = {
|
||||
...expandDialogProps,
|
||||
@@ -127,6 +133,7 @@ export const ChatPopUp = ({ chatflowid, isAgentCanvas }) => {
|
||||
useEffect(() => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
anchorRef.current.focus()
|
||||
if (onOpenChange) onOpenChange(false)
|
||||
}
|
||||
prevOpen.current = open
|
||||
|
||||
@@ -146,6 +153,7 @@ export const ChatPopUp = ({ chatflowid, isAgentCanvas }) => {
|
||||
>
|
||||
{open ? <IconX /> : <IconMessage />}
|
||||
</StyledFab>
|
||||
|
||||
{open && (
|
||||
<StyledFab
|
||||
sx={{ position: 'absolute', right: 80, top: 20 }}
|
||||
@@ -227,4 +235,10 @@ export const ChatPopUp = ({ chatflowid, isAgentCanvas }) => {
|
||||
)
|
||||
}
|
||||
|
||||
ChatPopUp.propTypes = { chatflowid: PropTypes.string, isAgentCanvas: PropTypes.bool }
|
||||
ChatPopUp.propTypes = {
|
||||
chatflowid: PropTypes.string,
|
||||
isAgentCanvas: PropTypes.bool,
|
||||
onOpenChange: PropTypes.func
|
||||
}
|
||||
|
||||
export default memo(ChatPopUp)
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
import { useState, useRef, useEffect, memo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Typography, Box, ClickAwayListener, Paper, Popper, Button } from '@mui/material'
|
||||
import { useTheme, alpha, lighten, darken } from '@mui/material/styles'
|
||||
import { IconCheckbox, IconMessage, IconX, IconExclamationCircle, IconChecklist } from '@tabler/icons-react'
|
||||
|
||||
// project import
|
||||
import { StyledFab } from '@/ui-component/button/StyledFab'
|
||||
import MainCard from '@/ui-component/cards/MainCard'
|
||||
import Transitions from '@/ui-component/extended/Transitions'
|
||||
import validate_empty from '@/assets/images/validate_empty.svg'
|
||||
|
||||
// api
|
||||
import validationApi from '@/api/validation'
|
||||
|
||||
// Hooks
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
|
||||
// Const
|
||||
import { enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
|
||||
import { AGENTFLOW_ICONS } from '@/store/constant'
|
||||
|
||||
// Utils
|
||||
|
||||
const ValidationPopUp = ({ chatflowid, hidden }) => {
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
useNotifier()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [previews, setPreviews] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const anchorRef = useRef(null)
|
||||
const prevOpen = useRef(open)
|
||||
|
||||
const handleClose = (event) => {
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||
return
|
||||
}
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen((prevOpen) => !prevOpen)
|
||||
}
|
||||
|
||||
const validateFlow = async () => {
|
||||
if (!chatflowid) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await validationApi.checkValidation(chatflowid)
|
||||
setPreviews(response.data)
|
||||
|
||||
if (response.data.length === 0) {
|
||||
enqueueSnackbar({
|
||||
message: 'No issues found in your flow!',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
autoHideDuration: 3000
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
enqueueSnackbar({
|
||||
message: error.message || 'Failed to validate flow',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
autoHideDuration: 3000
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
anchorRef.current.focus()
|
||||
}
|
||||
prevOpen.current = open
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, chatflowid])
|
||||
|
||||
const getNodeIcon = (item) => {
|
||||
// Extract node name from the item
|
||||
const nodeName = item.name
|
||||
|
||||
// Find matching icon from AGENTFLOW_ICONS
|
||||
const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === nodeName)
|
||||
|
||||
if (foundIcon) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: foundIcon.color,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white'
|
||||
}}
|
||||
>
|
||||
<foundIcon.icon size={16} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Default icon if no match found
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: item.type === 'LLM' ? '#4747d1' : '#f97316',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white'
|
||||
}}
|
||||
>
|
||||
{item.type === 'LLM' ? <span>ℓ</span> : <IconMessage size={16} />}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hidden && (
|
||||
<StyledFab
|
||||
sx={{ position: 'absolute', right: 80, top: 20 }}
|
||||
ref={anchorRef}
|
||||
size='small'
|
||||
color='teal'
|
||||
aria-label='validation'
|
||||
title='Validate Nodes'
|
||||
onClick={handleToggle}
|
||||
>
|
||||
{open ? <IconX /> : <IconChecklist />}
|
||||
</StyledFab>
|
||||
)}
|
||||
|
||||
<Popper
|
||||
placement='bottom-end'
|
||||
open={open && !hidden}
|
||||
anchorEl={anchorRef.current}
|
||||
role={undefined}
|
||||
transition
|
||||
disablePortal
|
||||
popperOptions={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [80, 14]
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
sx={{ zIndex: 1000 }}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Transitions in={open} {...TransitionProps}>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MainCard
|
||||
elevation={16}
|
||||
border={false}
|
||||
content={false}
|
||||
sx={{
|
||||
p: 2,
|
||||
width: '400px',
|
||||
maxWidth: '100%'
|
||||
}}
|
||||
boxShadow
|
||||
shadow={theme.shadows[16]}
|
||||
>
|
||||
<Typography variant='h4' sx={{ mt: 1, mb: 2 }}>
|
||||
Checklist ({previews.length})
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: '60vh',
|
||||
overflowY: 'auto',
|
||||
pr: 1, // Add some padding to the right to account for scrollbar
|
||||
mr: -1 // Negative margin to compensate for the padding
|
||||
}}
|
||||
>
|
||||
{previews.length > 0 ? (
|
||||
previews.map((item, index) => (
|
||||
<Paper
|
||||
key={index}
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 2,
|
||||
backgroundColor: customization.isDarkMode
|
||||
? theme.palette.background.paper
|
||||
: theme.palette.background.neutral,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${alpha('#FFB938', customization.isDarkMode ? 0.3 : 0.5)}`
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||||
{getNodeIcon(item)}
|
||||
<div style={{ fontWeight: 500 }}>{item.label || item.name}</div>
|
||||
</div>
|
||||
|
||||
<Box sx={{ mt: 2 }}></Box>
|
||||
|
||||
{item.issues.map((issue, issueIndex) => (
|
||||
<Box
|
||||
key={issueIndex}
|
||||
sx={{
|
||||
pt: 2,
|
||||
px: 2,
|
||||
pb: issueIndex === item.issues.length - 1 ? 2 : 1,
|
||||
backgroundColor: customization.isDarkMode
|
||||
? darken('#FFB938', 0.85)
|
||||
: lighten('#FFB938', 0.9),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
<IconExclamationCircle
|
||||
color='#FFB938'
|
||||
size={20}
|
||||
style={{
|
||||
minWidth: '20px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
<span>{issue}</span>
|
||||
</Box>
|
||||
))}
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
height: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ objectFit: 'cover', height: '15vh', width: 'auto' }}
|
||||
src={validate_empty}
|
||||
alt='validate_empty'
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, mb: 1 }}>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='teal'
|
||||
onClick={validateFlow}
|
||||
disabled={loading}
|
||||
startIcon={loading ? null : <IconCheckbox size={18} />}
|
||||
sx={{ color: 'white', minWidth: '120px' }}
|
||||
>
|
||||
{loading ? 'Validating...' : 'Validate Flow'}
|
||||
</Button>
|
||||
</Box>
|
||||
</MainCard>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Transitions>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ValidationPopUp.propTypes = {
|
||||
chatflowid: PropTypes.string,
|
||||
hidden: PropTypes.bool
|
||||
}
|
||||
|
||||
export default memo(ValidationPopUp)
|
||||
@@ -29,6 +29,7 @@ import { initializeDefaultNodeData } from '@/utils/genericHelper'
|
||||
// const
|
||||
import { baseURL, REDACTED_CREDENTIAL_VALUE } from '@/store/constant'
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||
import keySVG from '@/assets/images/key.svg'
|
||||
|
||||
const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
@@ -237,6 +238,11 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm, setEr
|
||||
}}
|
||||
alt={componentCredential.name}
|
||||
src={`${baseURL}/api/v1/components-credentials-icon/${componentCredential.name}`}
|
||||
onError={(e) => {
|
||||
e.target.onerror = null
|
||||
e.target.style.padding = '5px'
|
||||
e.target.src = keySVG
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{componentCredential.label}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { IconSearch, IconX } from '@tabler/icons-react'
|
||||
// const
|
||||
import { baseURL } from '@/store/constant'
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||
import keySVG from '@/assets/images/key.svg'
|
||||
|
||||
const CredentialListDialog = ({ show, dialogProps, onCancel, onCredentialSelected }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
@@ -152,6 +153,11 @@ const CredentialListDialog = ({ show, dialogProps, onCancel, onCredentialSelecte
|
||||
}}
|
||||
alt={componentCredential.name}
|
||||
src={`${baseURL}/api/v1/components-credentials-icon/${componentCredential.name}`}
|
||||
onError={(e) => {
|
||||
e.target.onerror = null
|
||||
e.target.style.padding = '5px'
|
||||
e.target.src = keySVG
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Typography>{componentCredential.label}</Typography>
|
||||
|
||||
@@ -42,6 +42,7 @@ import useNotifier from '@/utils/useNotifier'
|
||||
// Icons
|
||||
import { IconTrash, IconEdit, IconX, IconPlus } from '@tabler/icons-react'
|
||||
import CredentialEmptySVG from '@/assets/images/credential_empty.svg'
|
||||
import keySVG from '@/assets/images/key.svg'
|
||||
|
||||
// const
|
||||
import { baseURL } from '@/store/constant'
|
||||
@@ -233,6 +234,7 @@ const Credentials = () => {
|
||||
search={true}
|
||||
searchPlaceholder='Search Credentials'
|
||||
title='Credentials'
|
||||
description='API keys, tokens, and secrets for 3rd party integrations'
|
||||
>
|
||||
<StyledButton
|
||||
variant='contained'
|
||||
@@ -346,6 +348,11 @@ const Credentials = () => {
|
||||
}}
|
||||
alt={credential.credentialName}
|
||||
src={`${baseURL}/api/v1/components-credentials-icon/${credential.credentialName}`}
|
||||
onError={(e) => {
|
||||
e.target.onerror = null
|
||||
e.target.style.padding = '5px'
|
||||
e.target.src = keySVG
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{credential.name}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
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'
|
||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||
import { CodeBlock } from '@/ui-component/markdown/CodeBlock'
|
||||
import { Typography, Stack, Card, Accordion, AccordionSummary, AccordionDetails, Dialog, DialogContent, DialogTitle } from '@mui/material'
|
||||
import { TableViewOnly } from '@/ui-component/table/Table'
|
||||
import documentstoreApi from '@/api/documentstore'
|
||||
@@ -308,29 +303,7 @@ curl -X POST http://localhost:3000/api/v1/document-store/upsert/${dialogProps.st
|
||||
{dialogProps.title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<MemoizedReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeMathjax, rehypeRaw]}
|
||||
components={{
|
||||
code({ inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
return !inline ? (
|
||||
<CodeBlock
|
||||
isDialog={true}
|
||||
language={(match && match[1]) || ''}
|
||||
value={String(children).replace(/\n$/, '')}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{values}
|
||||
</MemoizedReactMarkdown>
|
||||
<MemoizedReactMarkdown>{values}</MemoizedReactMarkdown>
|
||||
|
||||
<Typography sx={{ mt: 3, mb: 1 }}>You can override existing configurations:</Typography>
|
||||
|
||||
|
||||