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

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