node.tsx 8.7 KB

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