node-handle.tsx 5.2 KB

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