index.tsx 7.9 KB

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