index.tsx 12 KB

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