index.tsx 11 KB

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