node-handle.tsx 6.2 KB

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