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.name}
)}
{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.model} {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)