index.tsx 11 KB

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