import type { FC, ReactElement, } from 'react' import type { IterationNodeType } from '@/app/components/workflow/nodes/iteration/types' import type { NodeProps } from '@/app/components/workflow/types' import { RiAlertFill, RiCheckboxCircleFill, RiErrorWarningFill, RiLoader2Line, } from '@remixicon/react' import { cloneElement, memo, useEffect, useMemo, useRef, } from 'react' import { useTranslation } from 'react-i18next' import Tooltip from '@/app/components/base/tooltip' import { UserAvatarList } from '@/app/components/base/user-avatar-list' import BlockIcon from '@/app/components/workflow/block-icon' import { ToolTypeEnum } from '@/app/components/workflow/block-selector/types' import { useCollaboration } from '@/app/components/workflow/collaboration/hooks/use-collaboration' import { useNodesReadOnly, useToolIcon } from '@/app/components/workflow/hooks' import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud' import { useNodeIterationInteractions } from '@/app/components/workflow/nodes/iteration/use-interactions' import { useNodeLoopInteractions } from '@/app/components/workflow/nodes/loop/use-interactions' import CopyID from '@/app/components/workflow/nodes/tool/components/copy-id' import { useStore } from '@/app/components/workflow/store' import { BlockEnum, ControlMode, isTriggerNode, NodeRunningStatus, } from '@/app/components/workflow/types' import { hasErrorHandleNode, hasRetryNode } from '@/app/components/workflow/utils' import { useAppContext } from '@/context/app-context' import { cn } from '@/utils/classnames' import AddVariablePopupWithPosition from './components/add-variable-popup-with-position' import EntryNodeContainer, { StartNodeTypeEnum } from './components/entry-node-container' import ErrorHandleOnNode from './components/error-handle/error-handle-on-node' import NodeControl from './components/node-control' import { NodeSourceHandle, NodeTargetHandle, } from './components/node-handle' import NodeResizer from './components/node-resizer' import RetryOnNode from './components/retry/retry-on-node' type NodeChildProps = { id: string data: NodeProps['data'] } type BaseNodeProps = { children: ReactElement> id: NodeProps['id'] data: NodeProps['data'] } const BaseNode: FC = ({ id, data, children, }) => { const { t } = useTranslation() const nodeRef = useRef(null) const { nodesReadOnly } = useNodesReadOnly() const { handleNodeIterationChildSizeChange } = useNodeIterationInteractions() const { handleNodeLoopChildSizeChange } = useNodeLoopInteractions() const toolIcon = useToolIcon(data) const { userProfile } = useAppContext() const appId = useStore(s => s.appId) const { nodePanelPresence } = useCollaboration(appId as string) const controlMode = useStore(s => s.controlMode) const currentUserPresence = useMemo(() => { const userId = userProfile?.id || '' const username = userProfile?.name || userProfile?.email || 'User' const avatar = userProfile?.avatar_url || userProfile?.avatar || null return { userId, username, avatar, } }, [userProfile?.avatar, userProfile?.avatar_url, userProfile?.email, userProfile?.id, userProfile?.name]) const viewingUsers = useMemo(() => { const presence = nodePanelPresence?.[id] if (!presence) return [] return Object.values(presence) .filter(viewer => viewer.userId && viewer.userId !== currentUserPresence.userId) .map(viewer => ({ id: viewer.userId, name: viewer.username, avatar_url: viewer.avatar || null, })) }, [currentUserPresence.userId, id, nodePanelPresence]) useEffect(() => { if (nodeRef.current && data.selected && data.isInIteration) { const resizeObserver = new ResizeObserver(() => { handleNodeIterationChildSizeChange(id) }) resizeObserver.observe(nodeRef.current) return () => { resizeObserver.disconnect() } } }, [data.isInIteration, data.selected, id, handleNodeIterationChildSizeChange]) useEffect(() => { if (nodeRef.current && data.selected && data.isInLoop) { const resizeObserver = new ResizeObserver(() => { handleNodeLoopChildSizeChange(id) }) resizeObserver.observe(nodeRef.current) return () => { resizeObserver.disconnect() } } }, [data.isInLoop, data.selected, id, handleNodeLoopChildSizeChange]) const { hasNodeInspectVars } = useInspectVarsCrud() const isLoading = data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running const hasVarValue = hasNodeInspectVars(id) const showSelectedBorder = data.selected || data._isBundled || data._isEntering const { showRunningBorder, showSuccessBorder, showFailedBorder, showExceptionBorder, } = useMemo(() => { return { showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder, showSuccessBorder: (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue) && !showSelectedBorder, showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder, showExceptionBorder: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder, } }, [data._runningStatus, hasVarValue, showSelectedBorder]) const LoopIndex = useMemo(() => { let text = '' if (data._runningStatus === NodeRunningStatus.Running) text = t('nodes.loop.currentLoopCount', { ns: 'workflow', count: data._loopIndex }) if (data._runningStatus === NodeRunningStatus.Succeeded || data._runningStatus === NodeRunningStatus.Failed) text = t('nodes.loop.totalLoopCount', { ns: 'workflow', count: data._loopIndex }) if (text) { return (
{text}
) } return null }, [data._loopIndex, data._runningStatus, t]) const nodeContent = (
{(data._dimmed || data._pluginInstallLocked) && (
)} { data.type === BlockEnum.DataSource && (
{t('blocks.datasource', { ns: 'workflow' })}
) }
{ data._showAddVariablePopup && ( ) } { data.type === BlockEnum.Iteration && ( ) } { data.type === BlockEnum.Loop && ( ) } { !data._isCandidate && ( ) } { data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && !data._isCandidate && ( ) } { !data._runningStatus && !nodesReadOnly && !data._isCandidate && ( ) }
{data.title}
{ data.type === BlockEnum.Iteration && (data as IterationNodeType).is_parallel && (
{t('nodes.iteration.parallelModeEnableTitle', { ns: 'workflow' })}
{t('nodes.iteration.parallelModeEnableDesc', { ns: 'workflow' })}
)} >
{t('nodes.iteration.parallelModeUpper', { ns: 'workflow' })}
) } {viewingUsers.length > 0 && (
)}
{ data._iterationLength && data._iterationIndex && data._runningStatus === NodeRunningStatus.Running && (
{data._iterationIndex > data._iterationLength ? data._iterationLength : data._iterationIndex} / {data._iterationLength}
) } { data.type === BlockEnum.Loop && data._loopIndex && LoopIndex } { isLoading ? : data._runningStatus === NodeRunningStatus.Failed ? : data._runningStatus === NodeRunningStatus.Exception ? : (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue) ? : null }
{ data.type !== BlockEnum.Iteration && data.type !== BlockEnum.Loop && ( cloneElement(children, { id, data } as any) ) } { (data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) && (
{cloneElement(children, { id, data } as any)}
) } { hasRetryNode(data.type) && ( ) } { hasErrorHandleNode(data.type) && ( ) } { data.desc && data.type !== BlockEnum.Iteration && data.type !== BlockEnum.Loop && (
{data.desc}
) } {data.type === BlockEnum.Tool && data.provider_type === ToolTypeEnum.MCP && (
)}
) const isStartNode = data.type === BlockEnum.Start const isEntryNode = isTriggerNode(data.type as any) || isStartNode return isEntryNode ? ( {nodeContent} ) : nodeContent } export default memo(BaseNode)