index.tsx 10 KB

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