node.tsx 7.7 KB

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