node-handle.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import type { MouseEvent } from 'react'
  2. import {
  3. memo,
  4. useCallback,
  5. useEffect,
  6. useState,
  7. } from 'react'
  8. import {
  9. Handle,
  10. Position,
  11. } from 'reactflow'
  12. import { useTranslation } from 'react-i18next'
  13. import {
  14. BlockEnum,
  15. NodeRunningStatus,
  16. } from '../../../types'
  17. import type { Node } from '../../../types'
  18. import BlockSelector from '../../../block-selector'
  19. import type { ToolDefaultValue } from '../../../block-selector/types'
  20. import {
  21. useAvailableBlocks,
  22. useIsChatMode,
  23. useNodesInteractions,
  24. useNodesReadOnly,
  25. useWorkflow,
  26. } from '../../../hooks'
  27. import {
  28. useStore,
  29. } from '../../../store'
  30. import cn from '@/utils/classnames'
  31. type NodeHandleProps = {
  32. handleId: string
  33. handleClassName?: string
  34. nodeSelectorClassName?: string
  35. showExceptionStatus?: boolean
  36. } & Pick<Node, 'id' | 'data'>
  37. export const NodeTargetHandle = memo(({
  38. id,
  39. data,
  40. handleId,
  41. handleClassName,
  42. nodeSelectorClassName,
  43. }: NodeHandleProps) => {
  44. const [open, setOpen] = useState(false)
  45. const { handleNodeAdd } = useNodesInteractions()
  46. const { getNodesReadOnly } = useNodesReadOnly()
  47. const connected = data._connectedTargetHandleIds?.includes(handleId)
  48. const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
  49. const isConnectable = !!availablePrevBlocks.length
  50. const handleOpenChange = useCallback((v: boolean) => {
  51. setOpen(v)
  52. }, [])
  53. const handleHandleClick = useCallback((e: MouseEvent) => {
  54. e.stopPropagation()
  55. if (!connected)
  56. setOpen(v => !v)
  57. }, [connected])
  58. const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue) => {
  59. handleNodeAdd(
  60. {
  61. nodeType: type,
  62. toolDefaultValue,
  63. },
  64. {
  65. nextNodeId: id,
  66. nextNodeTargetHandle: handleId,
  67. },
  68. )
  69. }, [handleNodeAdd, id, handleId])
  70. return (
  71. <>
  72. <Handle
  73. id={handleId}
  74. type='target'
  75. position={Position.Left}
  76. className={cn(
  77. 'z-[1] !h-4 !w-4 !rounded-none !border-none !bg-transparent !outline-none',
  78. 'after:absolute after:left-1.5 after:top-1 after:h-2 after:w-0.5 after:bg-workflow-link-line-handle',
  79. 'transition-all hover:scale-125',
  80. data._runningStatus === NodeRunningStatus.Succeeded && 'after:bg-workflow-link-line-success-handle',
  81. data._runningStatus === NodeRunningStatus.Failed && 'after:bg-workflow-link-line-error-handle',
  82. data._runningStatus === NodeRunningStatus.Exception && 'after:bg-workflow-link-line-failure-handle',
  83. !connected && 'after:opacity-0',
  84. data.type === BlockEnum.Start && 'opacity-0',
  85. handleClassName,
  86. )}
  87. isConnectable={isConnectable}
  88. onClick={handleHandleClick}
  89. >
  90. {
  91. !connected && isConnectable && !getNodesReadOnly() && (
  92. <BlockSelector
  93. open={open}
  94. onOpenChange={handleOpenChange}
  95. onSelect={handleSelect}
  96. asChild
  97. placement='left'
  98. triggerClassName={open => `
  99. hidden absolute left-0 top-0 pointer-events-none
  100. ${nodeSelectorClassName}
  101. group-hover:!flex
  102. ${data.selected && '!flex'}
  103. ${open && '!flex'}
  104. `}
  105. availableBlocksTypes={availablePrevBlocks}
  106. />
  107. )
  108. }
  109. </Handle>
  110. </>
  111. )
  112. })
  113. NodeTargetHandle.displayName = 'NodeTargetHandle'
  114. export const NodeSourceHandle = memo(({
  115. id,
  116. data,
  117. handleId,
  118. handleClassName,
  119. nodeSelectorClassName,
  120. showExceptionStatus,
  121. }: NodeHandleProps) => {
  122. const { t } = useTranslation()
  123. const notInitialWorkflow = useStore(s => s.notInitialWorkflow)
  124. const [open, setOpen] = useState(false)
  125. const { handleNodeAdd } = useNodesInteractions()
  126. const { getNodesReadOnly } = useNodesReadOnly()
  127. const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
  128. const isConnectable = !!availableNextBlocks.length
  129. const isChatMode = useIsChatMode()
  130. const { checkParallelLimit } = useWorkflow()
  131. const connected = data._connectedSourceHandleIds?.includes(handleId)
  132. const handleOpenChange = useCallback((v: boolean) => {
  133. setOpen(v)
  134. }, [])
  135. const handleHandleClick = useCallback((e: MouseEvent) => {
  136. e.stopPropagation()
  137. if (checkParallelLimit(id, handleId))
  138. setOpen(v => !v)
  139. }, [checkParallelLimit, id, handleId])
  140. const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue) => {
  141. handleNodeAdd(
  142. {
  143. nodeType: type,
  144. toolDefaultValue,
  145. },
  146. {
  147. prevNodeId: id,
  148. prevNodeSourceHandle: handleId,
  149. },
  150. )
  151. }, [handleNodeAdd, id, handleId])
  152. useEffect(() => {
  153. if (notInitialWorkflow && data.type === BlockEnum.Start && !isChatMode)
  154. setOpen(true)
  155. }, [notInitialWorkflow, data.type, isChatMode])
  156. return (
  157. <Handle
  158. id={handleId}
  159. type='source'
  160. position={Position.Right}
  161. className={cn(
  162. 'group/handle z-[1] !h-4 !w-4 !rounded-none !border-none !bg-transparent !outline-none',
  163. 'after:absolute after:right-1.5 after:top-1 after:h-2 after:w-0.5 after:bg-workflow-link-line-handle',
  164. 'transition-all hover:scale-125',
  165. data._runningStatus === NodeRunningStatus.Succeeded && 'after:bg-workflow-link-line-success-handle',
  166. data._runningStatus === NodeRunningStatus.Failed && 'after:bg-workflow-link-line-error-handle',
  167. showExceptionStatus && data._runningStatus === NodeRunningStatus.Exception && 'after:bg-workflow-link-line-failure-handle',
  168. !connected && 'after:opacity-0',
  169. handleClassName,
  170. )}
  171. isConnectable={isConnectable}
  172. onClick={handleHandleClick}
  173. >
  174. <div className='absolute -top-1 left-1/2 hidden -translate-x-1/2 -translate-y-full rounded-lg border-[0.5px] border-components-panel-border bg-components-tooltip-bg p-1.5 shadow-lg group-hover/handle:block'>
  175. <div className='system-xs-regular text-text-tertiary'>
  176. <div className=' whitespace-nowrap'>
  177. <span className='system-xs-medium text-text-secondary'>{t('workflow.common.parallelTip.click.title')}</span>
  178. {t('workflow.common.parallelTip.click.desc')}
  179. </div>
  180. <div>
  181. <span className='system-xs-medium text-text-secondary'>{t('workflow.common.parallelTip.drag.title')}</span>
  182. {t('workflow.common.parallelTip.drag.desc')}
  183. </div>
  184. </div>
  185. </div>
  186. {
  187. isConnectable && !getNodesReadOnly() && (
  188. <BlockSelector
  189. open={open}
  190. onOpenChange={handleOpenChange}
  191. onSelect={handleSelect}
  192. asChild
  193. triggerClassName={open => `
  194. hidden absolute top-0 left-0 pointer-events-none
  195. ${nodeSelectorClassName}
  196. group-hover:!flex
  197. ${data.selected && '!flex'}
  198. ${open && '!flex'}
  199. `}
  200. availableBlocksTypes={availableNextBlocks}
  201. />
  202. )
  203. }
  204. </Handle>
  205. )
  206. })
  207. NodeSourceHandle.displayName = 'NodeSourceHandle'