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,
IconAlertCircleFilled,
IconCode,
IconWorldWww,
IconPhoto,
IconBrandGoogle,
IconBrowserCheck
} 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 canvas = useSelector((state) => state.canvas)
const ref = useRef(null)
const updateNodeInternals = useUpdateNodeInternals()
// eslint-disable-next-line
const [position, setPosition] = useState(0)
const [isHovered, setIsHovered] = useState(false)
const [warningMessage, setWarningMessage] = useState('')
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
}
const getBuiltInOpenAIToolIcon = (toolName) => {
switch (toolName) {
case 'web_search_preview':
return
case 'code_interpreter':
return
case 'image_generation':
return
default:
return null
}
}
const getBuiltInGeminiToolIcon = (toolName) => {
switch (toolName) {
case 'urlContext':
return
case 'googleSearch':
return
case 'codeExecution':
return
default:
return null
}
}
const getBuiltInAnthropicToolIcon = (toolName) => {
switch (toolName) {
case 'web_search_20250305':
return
case 'web_fetch_20250910':
return
default:
return null
}
}
useEffect(() => {
if (ref.current) {
setTimeout(() => {
setPosition(ref.current?.offsetTop + ref.current?.clientHeight / 2)
updateNodeInternals(data.id)
}, 10)
}
}, [data, ref, updateNodeInternals])
useEffect(() => {
const nodeOutdatedMessage = (oldVersion, newVersion) =>
`Node version ${oldVersion} outdated\nUpdate to latest version ${newVersion}`
const nodeVersionEmptyMessage = (newVersion) => `Node outdated\nUpdate to latest version ${newVersion}`
const componentNode = canvas.componentNodes.find((nd) => nd.name === data.name)
if (componentNode) {
if (!data.version) {
setWarningMessage(nodeVersionEmptyMessage(componentNode.version))
} else if (data.version && componentNode.version > data.version) {
setWarningMessage(nodeOutdatedMessage(data.version, componentNode.version))
} else if (componentNode.badge === 'DEPRECATING') {
setWarningMessage(
componentNode?.deprecateMessage ??
'This node will be deprecated in the next release. Change to a new node tagged with NEW'
)
} else if (componentNode.warning) {
setWarningMessage(componentNode.warning)
} else {
setWarningMessage('')
}
}
}, [canvas.componentNodes, data.name, data.version])
return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
{data.name !== 'startAgentflow' && (
{
duplicateNode(data.id)
}}
sx={{
color: customization.isDarkMode ? 'white' : 'inherit',
'&:hover': {
color: theme.palette.primary.main
}
}}
>
)}
{
deleteNode(data.id)
}}
sx={{
color: customization.isDarkMode ? 'white' : 'inherit',
'&:hover': {
color: theme.palette.error.main
}
}}
>
{
setInfoDialogProps({ data })
setShowInfoDialog(true)
}}
sx={{
color: customization.isDarkMode ? 'white' : 'inherit',
'&:hover': {
color: theme.palette.info.main
}
}}
>
{data && data.status && (
{data.status === 'INPROGRESS' ? (
) : data.status === 'ERROR' ? (
) : data.status === 'TERMINATED' ? (
) : data.status === 'STOPPED' ? (
) : (
)}
)}
{warningMessage && (
{warningMessage}}>
)}
{!data.hideInput && (
)}
{data.color && !data.icon ? (
{renderIcon(data)}
) : (
)}
{data.label}
{(() => {
// 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) => (
{item.config.modelName || item.config.model}
))
})()}
{(() => {
// 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 ?? data.inputs?.toolAgentflowSelectedTool
? [{ selectedTool: data.inputs?.selectedTool ?? data.inputs?.toolAgentflowSelectedTool }]
: [],
toolProperty: ['selectedTool', 'toolAgentflowSelectedTool']
},
{ tools: data.inputs?.agentKnowledgeVSEmbeddings, toolProperty: ['vectorStore', 'embeddingModel'] },
{
tools: data.inputs?.agentToolsBuiltInOpenAI
? (typeof data.inputs.agentToolsBuiltInOpenAI === 'string'
? JSON.parse(data.inputs.agentToolsBuiltInOpenAI)
: data.inputs.agentToolsBuiltInOpenAI
).map((tool) => ({ builtInTool: tool }))
: [],
toolProperty: 'builtInTool',
isBuiltInOpenAI: true
},
{
tools: data.inputs?.agentToolsBuiltInGemini
? (typeof data.inputs.agentToolsBuiltInGemini === 'string'
? JSON.parse(data.inputs.agentToolsBuiltInGemini)
: data.inputs.agentToolsBuiltInGemini
).map((tool) => ({ builtInTool: tool }))
: [],
toolProperty: 'builtInTool',
isBuiltInGemini: true
},
{
tools: data.inputs?.agentToolsBuiltInAnthropic
? (typeof data.inputs.agentToolsBuiltInAnthropic === 'string'
? JSON.parse(data.inputs.agentToolsBuiltInAnthropic)
: data.inputs.agentToolsBuiltInAnthropic
).map((tool) => ({ builtInTool: tool }))
: [],
toolProperty: 'builtInTool',
isBuiltInAnthropic: true
}
]
// Filter out undefined tools and render each valid collection
return toolConfigs
.filter((config) => config.tools && config.tools.length > 0)
.map((config, configIndex) => (
{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 (
)
})
} else {
const toolName = tool[config.toolProperty]
if (!toolName) return []
// Handle built-in OpenAI tools with icons
if (config.isBuiltInOpenAI) {
const icon = getBuiltInOpenAIToolIcon(toolName)
if (!icon) return []
return [
{icon}
]
}
// Handle built-in Gemini tools with icons
if (config.isBuiltInGemini) {
const icon = getBuiltInGeminiToolIcon(toolName)
if (!icon) return []
return [
{icon}
]
}
// Handle built-in Anthropic tools with icons
if (config.isBuiltInAnthropic) {
const icon = getBuiltInAnthropicToolIcon(toolName)
if (!icon) return []
return [
{icon}
]
}
return [
]
}
})}
))
})()}
{getOutputAnchors().map((outputAnchor, index) => {
return (
)
})}
setShowInfoDialog(false)}>
)
}
AgentFlowNode.propTypes = {
data: PropTypes.object
}
export default memo(AgentFlowNode)