index.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. 'use client'
  2. import type { FC } from 'react'
  3. import {
  4. memo,
  5. useCallback,
  6. useEffect,
  7. useMemo,
  8. useRef,
  9. } from 'react'
  10. import { setAutoFreeze } from 'immer'
  11. import {
  12. useEventListener,
  13. useKeyPress,
  14. } from 'ahooks'
  15. import ReactFlow, {
  16. Background,
  17. ReactFlowProvider,
  18. SelectionMode,
  19. useEdgesState,
  20. useNodesState,
  21. useOnViewportChange,
  22. } from 'reactflow'
  23. import type {
  24. Viewport,
  25. } from 'reactflow'
  26. import 'reactflow/dist/style.css'
  27. import './style.css'
  28. import type {
  29. Edge,
  30. Node,
  31. } from './types'
  32. import { WorkflowContextProvider } from './context'
  33. import {
  34. useEdgesInteractions,
  35. useNodesInteractions,
  36. useNodesReadOnly,
  37. useNodesSyncDraft,
  38. usePanelInteractions,
  39. useSelectionInteractions,
  40. useWorkflow,
  41. useWorkflowInit,
  42. useWorkflowReadOnly,
  43. useWorkflowStartRun,
  44. } from './hooks'
  45. import Header from './header'
  46. import CustomNode from './nodes'
  47. import Operator from './operator'
  48. import CustomEdge from './custom-edge'
  49. import CustomConnectionLine from './custom-connection-line'
  50. import Panel from './panel'
  51. import Features from './features'
  52. import HelpLine from './help-line'
  53. import CandidateNode from './candidate-node'
  54. import PanelContextmenu from './panel-contextmenu'
  55. import NodeContextmenu from './node-contextmenu'
  56. import {
  57. useStore,
  58. useWorkflowStore,
  59. } from './store'
  60. import {
  61. getKeyboardKeyCodeBySystem,
  62. initialEdges,
  63. initialNodes,
  64. } from './utils'
  65. import { WORKFLOW_DATA_UPDATE } from './constants'
  66. import Loading from '@/app/components/base/loading'
  67. import { FeaturesProvider } from '@/app/components/base/features'
  68. import type { Features as FeaturesData } from '@/app/components/base/features/types'
  69. import { useEventEmitterContextContext } from '@/context/event-emitter'
  70. const nodeTypes = {
  71. custom: CustomNode,
  72. }
  73. const edgeTypes = {
  74. custom: CustomEdge,
  75. }
  76. type WorkflowProps = {
  77. nodes: Node[]
  78. edges: Edge[]
  79. viewport?: Viewport
  80. }
  81. const Workflow: FC<WorkflowProps> = memo(({
  82. nodes: originalNodes,
  83. edges: originalEdges,
  84. viewport,
  85. }) => {
  86. const workflowContainerRef = useRef<HTMLDivElement>(null)
  87. const workflowStore = useWorkflowStore()
  88. const [nodes, setNodes] = useNodesState(originalNodes)
  89. const [edges, setEdges] = useEdgesState(originalEdges)
  90. const showFeaturesPanel = useStore(state => state.showFeaturesPanel)
  91. const controlMode = useStore(s => s.controlMode)
  92. const nodeAnimation = useStore(s => s.nodeAnimation)
  93. const {
  94. handleSyncWorkflowDraft,
  95. syncWorkflowDraftWhenPageClose,
  96. } = useNodesSyncDraft()
  97. const { workflowReadOnly } = useWorkflowReadOnly()
  98. const { nodesReadOnly } = useNodesReadOnly()
  99. const { eventEmitter } = useEventEmitterContextContext()
  100. eventEmitter?.useSubscription((v: any) => {
  101. if (v.type === WORKFLOW_DATA_UPDATE) {
  102. setNodes(v.payload.nodes)
  103. setEdges(v.payload.edges)
  104. }
  105. })
  106. useEffect(() => {
  107. setAutoFreeze(false)
  108. return () => {
  109. setAutoFreeze(true)
  110. }
  111. }, [])
  112. useEffect(() => {
  113. return () => {
  114. handleSyncWorkflowDraft(true)
  115. }
  116. }, [])
  117. const handleSyncWorkflowDraftWhenPageClose = useCallback(() => {
  118. if (document.visibilityState === 'hidden')
  119. syncWorkflowDraftWhenPageClose()
  120. }, [syncWorkflowDraftWhenPageClose])
  121. useEffect(() => {
  122. document.addEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose)
  123. return () => {
  124. document.removeEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose)
  125. }
  126. }, [handleSyncWorkflowDraftWhenPageClose])
  127. useEventListener('keydown', (e) => {
  128. if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey))
  129. e.preventDefault()
  130. })
  131. useEventListener('mousemove', (e) => {
  132. const containerClientRect = workflowContainerRef.current?.getBoundingClientRect()
  133. if (containerClientRect) {
  134. workflowStore.setState({
  135. mousePosition: {
  136. pageX: e.clientX,
  137. pageY: e.clientY,
  138. elementX: e.clientX - containerClientRect.left,
  139. elementY: e.clientY - containerClientRect.top,
  140. },
  141. })
  142. }
  143. })
  144. const {
  145. handleNodeDragStart,
  146. handleNodeDrag,
  147. handleNodeDragStop,
  148. handleNodeEnter,
  149. handleNodeLeave,
  150. handleNodeClick,
  151. handleNodeConnect,
  152. handleNodeConnectStart,
  153. handleNodeConnectEnd,
  154. handleNodeContextMenu,
  155. handleNodesCopy,
  156. handleNodesPaste,
  157. handleNodesDuplicate,
  158. handleNodesDelete,
  159. } = useNodesInteractions()
  160. const {
  161. handleEdgeEnter,
  162. handleEdgeLeave,
  163. handleEdgeDelete,
  164. handleEdgesChange,
  165. } = useEdgesInteractions()
  166. const {
  167. handleSelectionStart,
  168. handleSelectionChange,
  169. handleSelectionDrag,
  170. } = useSelectionInteractions()
  171. const {
  172. handlePaneContextMenu,
  173. } = usePanelInteractions()
  174. const {
  175. isValidConnection,
  176. } = useWorkflow()
  177. const { handleStartWorkflowRun } = useWorkflowStartRun()
  178. useOnViewportChange({
  179. onEnd: () => {
  180. handleSyncWorkflowDraft()
  181. },
  182. })
  183. useKeyPress('delete', handleNodesDelete)
  184. useKeyPress('delete', handleEdgeDelete)
  185. useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, handleNodesCopy, { exactMatch: true, useCapture: true })
  186. useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.v`, handleNodesPaste, { exactMatch: true, useCapture: true })
  187. useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.d`, handleNodesDuplicate, { exactMatch: true, useCapture: true })
  188. useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, handleStartWorkflowRun, { exactMatch: true, useCapture: true })
  189. return (
  190. <div
  191. id='workflow-container'
  192. className={`
  193. relative w-full min-w-[960px] h-full bg-[#F0F2F7]
  194. ${workflowReadOnly && 'workflow-panel-animation'}
  195. ${nodeAnimation && 'workflow-node-animation'}
  196. `}
  197. ref={workflowContainerRef}
  198. >
  199. <CandidateNode />
  200. <Header />
  201. <Panel />
  202. <Operator />
  203. {
  204. showFeaturesPanel && <Features />
  205. }
  206. <PanelContextmenu />
  207. <NodeContextmenu />
  208. <HelpLine />
  209. <ReactFlow
  210. nodeTypes={nodeTypes}
  211. edgeTypes={edgeTypes}
  212. nodes={nodes}
  213. edges={edges}
  214. onNodeDragStart={handleNodeDragStart}
  215. onNodeDrag={handleNodeDrag}
  216. onNodeDragStop={handleNodeDragStop}
  217. onNodeMouseEnter={handleNodeEnter}
  218. onNodeMouseLeave={handleNodeLeave}
  219. onNodeClick={handleNodeClick}
  220. onNodeContextMenu={handleNodeContextMenu}
  221. onConnect={handleNodeConnect}
  222. onConnectStart={handleNodeConnectStart}
  223. onConnectEnd={handleNodeConnectEnd}
  224. onEdgeMouseEnter={handleEdgeEnter}
  225. onEdgeMouseLeave={handleEdgeLeave}
  226. onEdgesChange={handleEdgesChange}
  227. onSelectionStart={handleSelectionStart}
  228. onSelectionChange={handleSelectionChange}
  229. onSelectionDrag={handleSelectionDrag}
  230. onPaneContextMenu={handlePaneContextMenu}
  231. connectionLineComponent={CustomConnectionLine}
  232. defaultViewport={viewport}
  233. multiSelectionKeyCode={null}
  234. deleteKeyCode={null}
  235. nodesDraggable={!nodesReadOnly}
  236. nodesConnectable={!nodesReadOnly}
  237. nodesFocusable={!nodesReadOnly}
  238. edgesFocusable={!nodesReadOnly}
  239. panOnDrag={controlMode === 'hand' && !workflowReadOnly}
  240. zoomOnPinch={!workflowReadOnly}
  241. zoomOnScroll={!workflowReadOnly}
  242. zoomOnDoubleClick={!workflowReadOnly}
  243. isValidConnection={isValidConnection}
  244. selectionKeyCode={null}
  245. selectionMode={SelectionMode.Partial}
  246. selectionOnDrag={controlMode === 'pointer' && !workflowReadOnly}
  247. minZoom={0.25}
  248. >
  249. <Background
  250. gap={[14, 14]}
  251. size={2}
  252. color='#E4E5E7'
  253. />
  254. </ReactFlow>
  255. </div>
  256. )
  257. })
  258. Workflow.displayName = 'Workflow'
  259. const WorkflowWrap = memo(() => {
  260. const {
  261. data,
  262. isLoading,
  263. } = useWorkflowInit()
  264. const nodesData = useMemo(() => {
  265. if (data)
  266. return initialNodes(data.graph.nodes, data.graph.edges)
  267. return []
  268. }, [data])
  269. const edgesData = useMemo(() => {
  270. if (data)
  271. return initialEdges(data.graph.edges, data.graph.nodes)
  272. return []
  273. }, [data])
  274. if (!data || isLoading) {
  275. return (
  276. <div className='flex justify-center items-center relative w-full h-full bg-[#F0F2F7]'>
  277. <Loading />
  278. </div>
  279. )
  280. }
  281. const features = data.features || {}
  282. const initialFeatures: FeaturesData = {
  283. file: {
  284. image: {
  285. enabled: !!features.file_upload?.image.enabled,
  286. number_limits: features.file_upload?.image.number_limits || 3,
  287. transfer_methods: features.file_upload?.image.transfer_methods || ['local_file', 'remote_url'],
  288. },
  289. },
  290. opening: {
  291. enabled: !!features.opening_statement,
  292. opening_statement: features.opening_statement,
  293. suggested_questions: features.suggested_questions,
  294. },
  295. suggested: features.suggested_questions_after_answer || { enabled: false },
  296. speech2text: features.speech_to_text || { enabled: false },
  297. text2speech: features.text_to_speech || { enabled: false },
  298. citation: features.retriever_resource || { enabled: false },
  299. moderation: features.sensitive_word_avoidance || { enabled: false },
  300. }
  301. return (
  302. <ReactFlowProvider>
  303. <FeaturesProvider features={initialFeatures}>
  304. <Workflow
  305. nodes={nodesData}
  306. edges={edgesData}
  307. viewport={data?.graph.viewport}
  308. />
  309. </FeaturesProvider>
  310. </ReactFlowProvider>
  311. )
  312. })
  313. WorkflowWrap.displayName = 'WorkflowWrap'
  314. const WorkflowContainer = () => {
  315. return (
  316. <WorkflowContextProvider>
  317. <WorkflowWrap />
  318. </WorkflowContextProvider>
  319. )
  320. }
  321. export default memo(WorkflowContainer)