You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

node.tsx 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import type {
  2. FC,
  3. ReactElement,
  4. } from 'react'
  5. import {
  6. cloneElement,
  7. memo,
  8. useEffect,
  9. useMemo,
  10. useRef,
  11. } from 'react'
  12. import {
  13. RiAlertFill,
  14. RiCheckboxCircleFill,
  15. RiErrorWarningFill,
  16. RiLoader2Line,
  17. } from '@remixicon/react'
  18. import { useTranslation } from 'react-i18next'
  19. import type { NodeProps } from '../../types'
  20. import {
  21. BlockEnum,
  22. NodeRunningStatus,
  23. } from '../../types'
  24. import {
  25. useNodesReadOnly,
  26. useToolIcon,
  27. } from '../../hooks'
  28. import {
  29. hasErrorHandleNode,
  30. hasRetryNode,
  31. } from '../../utils'
  32. import { useNodeIterationInteractions } from '../iteration/use-interactions'
  33. import { useNodeLoopInteractions } from '../loop/use-interactions'
  34. import type { IterationNodeType } from '../iteration/types'
  35. import CopyID from '../tool/components/copy-id'
  36. import {
  37. NodeSourceHandle,
  38. NodeTargetHandle,
  39. } from './components/node-handle'
  40. import NodeResizer from './components/node-resizer'
  41. import NodeControl from './components/node-control'
  42. import ErrorHandleOnNode from './components/error-handle/error-handle-on-node'
  43. import RetryOnNode from './components/retry/retry-on-node'
  44. import AddVariablePopupWithPosition from './components/add-variable-popup-with-position'
  45. import cn from '@/utils/classnames'
  46. import BlockIcon from '@/app/components/workflow/block-icon'
  47. import Tooltip from '@/app/components/base/tooltip'
  48. import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
  49. import { ToolTypeEnum } from '../../block-selector/types'
  50. type BaseNodeProps = {
  51. children: ReactElement
  52. id: NodeProps['id']
  53. data: NodeProps['data']
  54. }
  55. const BaseNode: FC<BaseNodeProps> = ({
  56. id,
  57. data,
  58. children,
  59. }) => {
  60. const { t } = useTranslation()
  61. const nodeRef = useRef<HTMLDivElement>(null)
  62. const { nodesReadOnly } = useNodesReadOnly()
  63. const { handleNodeIterationChildSizeChange } = useNodeIterationInteractions()
  64. const { handleNodeLoopChildSizeChange } = useNodeLoopInteractions()
  65. const toolIcon = useToolIcon(data)
  66. useEffect(() => {
  67. if (nodeRef.current && data.selected && data.isInIteration) {
  68. const resizeObserver = new ResizeObserver(() => {
  69. handleNodeIterationChildSizeChange(id)
  70. })
  71. resizeObserver.observe(nodeRef.current)
  72. return () => {
  73. resizeObserver.disconnect()
  74. }
  75. }
  76. }, [data.isInIteration, data.selected, id, handleNodeIterationChildSizeChange])
  77. useEffect(() => {
  78. if (nodeRef.current && data.selected && data.isInLoop) {
  79. const resizeObserver = new ResizeObserver(() => {
  80. handleNodeLoopChildSizeChange(id)
  81. })
  82. resizeObserver.observe(nodeRef.current)
  83. return () => {
  84. resizeObserver.disconnect()
  85. }
  86. }
  87. }, [data.isInLoop, data.selected, id, handleNodeLoopChildSizeChange])
  88. const { hasNodeInspectVars } = useInspectVarsCrud()
  89. const isLoading = data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running
  90. const hasVarValue = hasNodeInspectVars(id)
  91. const showSelectedBorder = data.selected || data._isBundled || data._isEntering
  92. const {
  93. showRunningBorder,
  94. showSuccessBorder,
  95. showFailedBorder,
  96. showExceptionBorder,
  97. } = useMemo(() => {
  98. return {
  99. showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
  100. showSuccessBorder: (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue) && !showSelectedBorder,
  101. showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
  102. showExceptionBorder: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder,
  103. }
  104. }, [data._runningStatus, hasVarValue, showSelectedBorder])
  105. const LoopIndex = useMemo(() => {
  106. let text = ''
  107. if (data._runningStatus === NodeRunningStatus.Running)
  108. text = t('workflow.nodes.loop.currentLoopCount', { count: data._loopIndex })
  109. if (data._runningStatus === NodeRunningStatus.Succeeded || data._runningStatus === NodeRunningStatus.Failed)
  110. text = t('workflow.nodes.loop.totalLoopCount', { count: data._loopIndex })
  111. if (text) {
  112. return (
  113. <div
  114. className={cn(
  115. 'system-xs-medium mr-2 text-text-tertiary',
  116. data._runningStatus === NodeRunningStatus.Running && 'text-text-accent',
  117. )}
  118. >
  119. {text}
  120. </div>
  121. )
  122. }
  123. return null
  124. }, [data._loopIndex, data._runningStatus, t])
  125. return (
  126. <div
  127. className={cn(
  128. 'relative flex rounded-2xl border',
  129. showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
  130. !showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
  131. data._waitingRun && 'opacity-70',
  132. data._dimmed && 'opacity-30',
  133. )}
  134. ref={nodeRef}
  135. style={{
  136. width: (data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) ? data.width : 'auto',
  137. height: (data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) ? data.height : 'auto',
  138. }}
  139. >
  140. {
  141. data.type === BlockEnum.DataSource && (
  142. <div className='absolute inset-[-2px] top-[-22px] z-[-1] rounded-[18px] bg-node-data-source-bg p-0.5 backdrop-blur-[6px]'>
  143. <div className='system-2xs-semibold-uppercase flex h-5 items-center px-2.5 text-text-tertiary'>
  144. {t('workflow.blocks.datasource')}
  145. </div>
  146. </div>
  147. )
  148. }
  149. <div
  150. className={cn(
  151. 'group relative pb-1 shadow-xs',
  152. 'rounded-[15px] border border-transparent',
  153. (data.type !== BlockEnum.Iteration && data.type !== BlockEnum.Loop) && 'w-[240px] bg-workflow-block-bg',
  154. (data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) && 'flex h-full w-full flex-col border-workflow-block-border bg-workflow-block-bg-transparent',
  155. !data._runningStatus && 'hover:shadow-lg',
  156. showRunningBorder && '!border-state-accent-solid',
  157. showSuccessBorder && '!border-state-success-solid',
  158. showFailedBorder && '!border-state-destructive-solid',
  159. showExceptionBorder && '!border-state-warning-solid',
  160. data._isBundled && '!shadow-lg',
  161. )}
  162. >
  163. {
  164. data._inParallelHovering && (
  165. <div className='top system-2xs-medium-uppercase absolute -top-2.5 left-2 z-10 text-text-tertiary'>
  166. {t('workflow.common.parallelRun')}
  167. </div>
  168. )
  169. }
  170. {
  171. data._showAddVariablePopup && (
  172. <AddVariablePopupWithPosition
  173. nodeId={id}
  174. nodeData={data}
  175. />
  176. )
  177. }
  178. {
  179. data.type === BlockEnum.Iteration && (
  180. <NodeResizer
  181. nodeId={id}
  182. nodeData={data}
  183. />
  184. )
  185. }
  186. {
  187. data.type === BlockEnum.Loop && (
  188. <NodeResizer
  189. nodeId={id}
  190. nodeData={data}
  191. />
  192. )
  193. }
  194. {
  195. !data._isCandidate && (
  196. <NodeTargetHandle
  197. id={id}
  198. data={data}
  199. handleClassName='!top-4 !-left-[9px] !translate-y-0'
  200. handleId='target'
  201. />
  202. )
  203. }
  204. {
  205. data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && !data._isCandidate && (
  206. <NodeSourceHandle
  207. id={id}
  208. data={data}
  209. handleClassName='!top-4 !-right-[9px] !translate-y-0'
  210. handleId='source'
  211. />
  212. )
  213. }
  214. {
  215. !data._runningStatus && !nodesReadOnly && !data._isCandidate && (
  216. <NodeControl
  217. id={id}
  218. data={data}
  219. />
  220. )
  221. }
  222. <div className={cn(
  223. 'flex items-center rounded-t-2xl px-3 pb-2 pt-3',
  224. (data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) && 'bg-transparent',
  225. )}>
  226. <BlockIcon
  227. className='mr-2 shrink-0'
  228. type={data.type}
  229. size='md'
  230. toolIcon={toolIcon}
  231. />
  232. <div
  233. title={data.title}
  234. className='system-sm-semibold-uppercase mr-1 flex grow items-center truncate text-text-primary'
  235. >
  236. <div>
  237. {data.title}
  238. </div>
  239. {
  240. data.type === BlockEnum.Iteration && (data as IterationNodeType).is_parallel && (
  241. <Tooltip popupContent={
  242. <div className='w-[180px]'>
  243. <div className='font-extrabold'>
  244. {t('workflow.nodes.iteration.parallelModeEnableTitle')}
  245. </div>
  246. {t('workflow.nodes.iteration.parallelModeEnableDesc')}
  247. </div>}
  248. >
  249. <div className='system-2xs-medium-uppercase ml-1 flex items-center justify-center rounded-[5px] border-[1px] border-text-warning px-[5px] py-[3px] text-text-warning '>
  250. {t('workflow.nodes.iteration.parallelModeUpper')}
  251. </div>
  252. </Tooltip>
  253. )
  254. }
  255. </div>
  256. {
  257. data._iterationLength && data._iterationIndex && data._runningStatus === NodeRunningStatus.Running && (
  258. <div className='mr-1.5 text-xs font-medium text-text-accent'>
  259. {data._iterationIndex > data._iterationLength ? data._iterationLength : data._iterationIndex}/{data._iterationLength}
  260. </div>
  261. )
  262. }
  263. {
  264. data.type === BlockEnum.Loop && data._loopIndex && LoopIndex
  265. }
  266. {
  267. isLoading && (
  268. <RiLoader2Line className='h-3.5 w-3.5 animate-spin text-text-accent' />
  269. )
  270. }
  271. {
  272. (!isLoading && (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue)) && (
  273. <RiCheckboxCircleFill className='h-3.5 w-3.5 text-text-success' />
  274. )
  275. }
  276. {
  277. data._runningStatus === NodeRunningStatus.Failed && (
  278. <RiErrorWarningFill className='h-3.5 w-3.5 text-text-destructive' />
  279. )
  280. }
  281. {
  282. data._runningStatus === NodeRunningStatus.Exception && (
  283. <RiAlertFill className='h-3.5 w-3.5 text-text-warning-secondary' />
  284. )
  285. }
  286. </div>
  287. {
  288. data.type !== BlockEnum.Iteration && data.type !== BlockEnum.Loop && (
  289. cloneElement(children, { id, data })
  290. )
  291. }
  292. {
  293. (data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) && (
  294. <div className='grow pb-1 pl-1 pr-1'>
  295. {cloneElement(children, { id, data })}
  296. </div>
  297. )
  298. }
  299. {
  300. hasRetryNode(data.type) && (
  301. <RetryOnNode
  302. id={id}
  303. data={data}
  304. />
  305. )
  306. }
  307. {
  308. hasErrorHandleNode(data.type) && (
  309. <ErrorHandleOnNode
  310. id={id}
  311. data={data}
  312. />
  313. )
  314. }
  315. {
  316. data.desc && data.type !== BlockEnum.Iteration && data.type !== BlockEnum.Loop && (
  317. <div className='system-xs-regular whitespace-pre-line break-words px-3 pb-2 pt-1 text-text-tertiary'>
  318. {data.desc}
  319. </div>
  320. )
  321. }
  322. {data.type === BlockEnum.Tool && data.provider_type === ToolTypeEnum.MCP && (
  323. <div className='px-3 pb-2'>
  324. <CopyID content={data.provider_id || ''} />
  325. </div>
  326. )}
  327. </div>
  328. </div>
  329. )
  330. }
  331. export default memo(BaseNode)