index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. 'use client'
  2. import type { FC } from 'react'
  3. import {
  4. memo,
  5. useCallback,
  6. useEffect,
  7. useMemo,
  8. useRef,
  9. useState,
  10. } from 'react'
  11. import useSWR from 'swr'
  12. import { setAutoFreeze } from 'immer'
  13. import {
  14. useEventListener,
  15. } from 'ahooks'
  16. import ReactFlow, {
  17. Background,
  18. ReactFlowProvider,
  19. SelectionMode,
  20. useEdgesState,
  21. useNodesState,
  22. useOnViewportChange,
  23. useReactFlow,
  24. useStoreApi,
  25. } from 'reactflow'
  26. import type {
  27. Viewport,
  28. } from 'reactflow'
  29. import 'reactflow/dist/style.css'
  30. import './style.css'
  31. import type {
  32. Edge,
  33. EnvironmentVariable,
  34. Node,
  35. } from './types'
  36. import {
  37. ControlMode,
  38. SupportUploadFileTypes,
  39. } from './types'
  40. import { WorkflowContextProvider } from './context'
  41. import {
  42. useDSL,
  43. useEdgesInteractions,
  44. useNodesInteractions,
  45. useNodesReadOnly,
  46. useNodesSyncDraft,
  47. usePanelInteractions,
  48. useSelectionInteractions,
  49. useShortcuts,
  50. useWorkflow,
  51. useWorkflowInit,
  52. useWorkflowReadOnly,
  53. useWorkflowUpdate,
  54. } from './hooks'
  55. import Header from './header'
  56. import CustomNode from './nodes'
  57. import CustomNoteNode from './note-node'
  58. import { CUSTOM_NOTE_NODE } from './note-node/constants'
  59. import CustomIterationStartNode from './nodes/iteration-start'
  60. import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants'
  61. import CustomLoopStartNode from './nodes/loop-start'
  62. import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants'
  63. import Operator from './operator'
  64. import CustomEdge from './custom-edge'
  65. import CustomConnectionLine from './custom-connection-line'
  66. import Panel from './panel'
  67. import Features from './features'
  68. import HelpLine from './help-line'
  69. import CandidateNode from './candidate-node'
  70. import PanelContextmenu from './panel-contextmenu'
  71. import NodeContextmenu from './node-contextmenu'
  72. import SyncingDataModal from './syncing-data-modal'
  73. import UpdateDSLModal from './update-dsl-modal'
  74. import DSLExportConfirmModal from './dsl-export-confirm-modal'
  75. import LimitTips from './limit-tips'
  76. import PluginDependency from './plugin-dependency'
  77. import {
  78. useStore,
  79. useWorkflowStore,
  80. } from './store'
  81. import {
  82. initialEdges,
  83. initialNodes,
  84. } from './utils'
  85. import {
  86. CUSTOM_EDGE,
  87. CUSTOM_NODE,
  88. DSL_EXPORT_CHECK,
  89. ITERATION_CHILDREN_Z_INDEX,
  90. WORKFLOW_DATA_UPDATE,
  91. } from './constants'
  92. import { WorkflowHistoryProvider } from './workflow-history-store'
  93. import Loading from '@/app/components/base/loading'
  94. import { FeaturesProvider } from '@/app/components/base/features'
  95. import type { Features as FeaturesData } from '@/app/components/base/features/types'
  96. import { useFeaturesStore } from '@/app/components/base/features/hooks'
  97. import { useEventEmitterContextContext } from '@/context/event-emitter'
  98. import Confirm from '@/app/components/base/confirm'
  99. import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
  100. import { fetchFileUploadConfig } from '@/service/common'
  101. import DatasetsDetailProvider from './datasets-detail-store/provider'
  102. const nodeTypes = {
  103. [CUSTOM_NODE]: CustomNode,
  104. [CUSTOM_NOTE_NODE]: CustomNoteNode,
  105. [CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode,
  106. [CUSTOM_LOOP_START_NODE]: CustomLoopStartNode,
  107. }
  108. const edgeTypes = {
  109. [CUSTOM_EDGE]: CustomEdge,
  110. }
  111. type WorkflowProps = {
  112. nodes: Node[]
  113. edges: Edge[]
  114. viewport?: Viewport
  115. }
  116. const Workflow: FC<WorkflowProps> = memo(({
  117. nodes: originalNodes,
  118. edges: originalEdges,
  119. viewport,
  120. }) => {
  121. const workflowContainerRef = useRef<HTMLDivElement>(null)
  122. const workflowStore = useWorkflowStore()
  123. const reactflow = useReactFlow()
  124. const featuresStore = useFeaturesStore()
  125. const [nodes, setNodes] = useNodesState(originalNodes)
  126. const [edges, setEdges] = useEdgesState(originalEdges)
  127. const showFeaturesPanel = useStore(state => state.showFeaturesPanel)
  128. const controlMode = useStore(s => s.controlMode)
  129. const nodeAnimation = useStore(s => s.nodeAnimation)
  130. const showConfirm = useStore(s => s.showConfirm)
  131. const showImportDSLModal = useStore(s => s.showImportDSLModal)
  132. const {
  133. setShowConfirm,
  134. setControlPromptEditorRerenderKey,
  135. setShowImportDSLModal,
  136. setSyncWorkflowDraftHash,
  137. } = workflowStore.getState()
  138. const {
  139. handleSyncWorkflowDraft,
  140. syncWorkflowDraftWhenPageClose,
  141. } = useNodesSyncDraft()
  142. const { workflowReadOnly } = useWorkflowReadOnly()
  143. const { nodesReadOnly } = useNodesReadOnly()
  144. const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
  145. const { eventEmitter } = useEventEmitterContextContext()
  146. eventEmitter?.useSubscription((v: any) => {
  147. if (v.type === WORKFLOW_DATA_UPDATE) {
  148. setNodes(v.payload.nodes)
  149. setEdges(v.payload.edges)
  150. if (v.payload.viewport)
  151. reactflow.setViewport(v.payload.viewport)
  152. if (v.payload.features && featuresStore) {
  153. const { setFeatures } = featuresStore.getState()
  154. setFeatures(v.payload.features)
  155. }
  156. if (v.payload.hash)
  157. setSyncWorkflowDraftHash(v.payload.hash)
  158. setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
  159. }
  160. if (v.type === DSL_EXPORT_CHECK)
  161. setSecretEnvList(v.payload.data as EnvironmentVariable[])
  162. })
  163. useEffect(() => {
  164. setAutoFreeze(false)
  165. return () => {
  166. setAutoFreeze(true)
  167. }
  168. }, [])
  169. useEffect(() => {
  170. return () => {
  171. handleSyncWorkflowDraft(true, true)
  172. }
  173. // eslint-disable-next-line react-hooks/exhaustive-deps
  174. }, [])
  175. const { handleRefreshWorkflowDraft } = useWorkflowUpdate()
  176. const handleSyncWorkflowDraftWhenPageClose = useCallback(() => {
  177. if (document.visibilityState === 'hidden')
  178. syncWorkflowDraftWhenPageClose()
  179. else if (document.visibilityState === 'visible')
  180. setTimeout(() => handleRefreshWorkflowDraft(), 500)
  181. }, [syncWorkflowDraftWhenPageClose, handleRefreshWorkflowDraft])
  182. useEffect(() => {
  183. document.addEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose)
  184. return () => {
  185. document.removeEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose)
  186. }
  187. }, [handleSyncWorkflowDraftWhenPageClose])
  188. useEventListener('keydown', (e) => {
  189. if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey))
  190. e.preventDefault()
  191. if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey))
  192. e.preventDefault()
  193. if ((e.key === 'y' || e.key === 'Y') && (e.ctrlKey || e.metaKey))
  194. e.preventDefault()
  195. if ((e.key === 's' || e.key === 'S') && (e.ctrlKey || e.metaKey))
  196. e.preventDefault()
  197. })
  198. useEventListener('mousemove', (e) => {
  199. const containerClientRect = workflowContainerRef.current?.getBoundingClientRect()
  200. if (containerClientRect) {
  201. workflowStore.setState({
  202. mousePosition: {
  203. pageX: e.clientX,
  204. pageY: e.clientY,
  205. elementX: e.clientX - containerClientRect.left,
  206. elementY: e.clientY - containerClientRect.top,
  207. },
  208. })
  209. }
  210. })
  211. const {
  212. handleNodeDragStart,
  213. handleNodeDrag,
  214. handleNodeDragStop,
  215. handleNodeEnter,
  216. handleNodeLeave,
  217. handleNodeClick,
  218. handleNodeConnect,
  219. handleNodeConnectStart,
  220. handleNodeConnectEnd,
  221. handleNodeContextMenu,
  222. handleHistoryBack,
  223. handleHistoryForward,
  224. } = useNodesInteractions()
  225. const {
  226. handleEdgeEnter,
  227. handleEdgeLeave,
  228. handleEdgesChange,
  229. } = useEdgesInteractions()
  230. const {
  231. handleSelectionStart,
  232. handleSelectionChange,
  233. handleSelectionDrag,
  234. } = useSelectionInteractions()
  235. const {
  236. handlePaneContextMenu,
  237. handlePaneContextmenuCancel,
  238. } = usePanelInteractions()
  239. const {
  240. isValidConnection,
  241. } = useWorkflow()
  242. const {
  243. exportCheck,
  244. handleExportDSL,
  245. } = useDSL()
  246. useOnViewportChange({
  247. onEnd: () => {
  248. handleSyncWorkflowDraft()
  249. },
  250. })
  251. useShortcuts()
  252. const store = useStoreApi()
  253. if (process.env.NODE_ENV === 'development') {
  254. store.getState().onError = (code, message) => {
  255. if (code === '002')
  256. return
  257. console.warn(message)
  258. }
  259. }
  260. return (
  261. <div
  262. id='workflow-container'
  263. className={`
  264. relative h-full w-full min-w-[960px]
  265. ${workflowReadOnly && 'workflow-panel-animation'}
  266. ${nodeAnimation && 'workflow-node-animation'}
  267. `}
  268. ref={workflowContainerRef}
  269. >
  270. <SyncingDataModal />
  271. <CandidateNode />
  272. <Header />
  273. <Panel />
  274. <Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} />
  275. {
  276. showFeaturesPanel && <Features />
  277. }
  278. <PanelContextmenu />
  279. <NodeContextmenu />
  280. <HelpLine />
  281. {
  282. !!showConfirm && (
  283. <Confirm
  284. isShow
  285. onCancel={() => setShowConfirm(undefined)}
  286. onConfirm={showConfirm.onConfirm}
  287. title={showConfirm.title}
  288. content={showConfirm.desc}
  289. />
  290. )
  291. }
  292. {
  293. showImportDSLModal && (
  294. <UpdateDSLModal
  295. onCancel={() => setShowImportDSLModal(false)}
  296. onBackup={exportCheck}
  297. onImport={handlePaneContextmenuCancel}
  298. />
  299. )
  300. }
  301. {
  302. secretEnvList.length > 0 && (
  303. <DSLExportConfirmModal
  304. envList={secretEnvList}
  305. onConfirm={handleExportDSL}
  306. onClose={() => setSecretEnvList([])}
  307. />
  308. )
  309. }
  310. <LimitTips />
  311. <PluginDependency />
  312. <ReactFlow
  313. nodeTypes={nodeTypes}
  314. edgeTypes={edgeTypes}
  315. nodes={nodes}
  316. edges={edges}
  317. onNodeDragStart={handleNodeDragStart}
  318. onNodeDrag={handleNodeDrag}
  319. onNodeDragStop={handleNodeDragStop}
  320. onNodeMouseEnter={handleNodeEnter}
  321. onNodeMouseLeave={handleNodeLeave}
  322. onNodeClick={handleNodeClick}
  323. onNodeContextMenu={handleNodeContextMenu}
  324. onConnect={handleNodeConnect}
  325. onConnectStart={handleNodeConnectStart}
  326. onConnectEnd={handleNodeConnectEnd}
  327. onEdgeMouseEnter={handleEdgeEnter}
  328. onEdgeMouseLeave={handleEdgeLeave}
  329. onEdgesChange={handleEdgesChange}
  330. onSelectionStart={handleSelectionStart}
  331. onSelectionChange={handleSelectionChange}
  332. onSelectionDrag={handleSelectionDrag}
  333. onPaneContextMenu={handlePaneContextMenu}
  334. connectionLineComponent={CustomConnectionLine}
  335. // TODO: For LOOP node, how to distinguish between ITERATION and LOOP here? Maybe both are the same?
  336. connectionLineContainerStyle={{ zIndex: ITERATION_CHILDREN_Z_INDEX }}
  337. defaultViewport={viewport}
  338. multiSelectionKeyCode={null}
  339. deleteKeyCode={null}
  340. nodesDraggable={!nodesReadOnly}
  341. nodesConnectable={!nodesReadOnly}
  342. nodesFocusable={!nodesReadOnly}
  343. edgesFocusable={!nodesReadOnly}
  344. panOnDrag={controlMode === ControlMode.Hand && !workflowReadOnly}
  345. zoomOnPinch={!workflowReadOnly}
  346. zoomOnScroll={!workflowReadOnly}
  347. zoomOnDoubleClick={!workflowReadOnly}
  348. isValidConnection={isValidConnection}
  349. selectionKeyCode={null}
  350. selectionMode={SelectionMode.Partial}
  351. selectionOnDrag={controlMode === ControlMode.Pointer && !workflowReadOnly}
  352. minZoom={0.25}
  353. >
  354. <Background
  355. gap={[14, 14]}
  356. size={2}
  357. className="bg-workflow-canvas-workflow-bg"
  358. color='var(--color-workflow-canvas-workflow-dot-color)'
  359. />
  360. </ReactFlow>
  361. </div>
  362. )
  363. })
  364. Workflow.displayName = 'Workflow'
  365. const WorkflowWrap = memo(() => {
  366. const {
  367. data,
  368. isLoading,
  369. } = useWorkflowInit()
  370. const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
  371. const nodesData = useMemo(() => {
  372. if (data)
  373. return initialNodes(data.graph.nodes, data.graph.edges)
  374. return []
  375. }, [data])
  376. const edgesData = useMemo(() => {
  377. if (data)
  378. return initialEdges(data.graph.edges, data.graph.nodes)
  379. return []
  380. }, [data])
  381. if (!data || isLoading) {
  382. return (
  383. <div className='relative flex h-full w-full items-center justify-center'>
  384. <Loading />
  385. </div>
  386. )
  387. }
  388. const features = data.features || {}
  389. const initialFeatures: FeaturesData = {
  390. file: {
  391. image: {
  392. enabled: !!features.file_upload?.image?.enabled,
  393. number_limits: features.file_upload?.image?.number_limits || 3,
  394. transfer_methods: features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
  395. },
  396. enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled),
  397. allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
  398. allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
  399. allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
  400. number_limits: features.file_upload?.number_limits || features.file_upload?.image?.number_limits || 3,
  401. fileUploadConfig: fileUploadConfigResponse,
  402. },
  403. opening: {
  404. enabled: !!features.opening_statement,
  405. opening_statement: features.opening_statement,
  406. suggested_questions: features.suggested_questions,
  407. },
  408. suggested: features.suggested_questions_after_answer || { enabled: false },
  409. speech2text: features.speech_to_text || { enabled: false },
  410. text2speech: features.text_to_speech || { enabled: false },
  411. citation: features.retriever_resource || { enabled: false },
  412. moderation: features.sensitive_word_avoidance || { enabled: false },
  413. }
  414. return (
  415. <ReactFlowProvider>
  416. <WorkflowHistoryProvider
  417. nodes={nodesData}
  418. edges={edgesData} >
  419. <FeaturesProvider features={initialFeatures}>
  420. <DatasetsDetailProvider nodes={nodesData}>
  421. <Workflow
  422. nodes={nodesData}
  423. edges={edgesData}
  424. viewport={data?.graph.viewport}
  425. />
  426. </DatasetsDetailProvider>
  427. </FeaturesProvider>
  428. </WorkflowHistoryProvider>
  429. </ReactFlowProvider>
  430. )
  431. })
  432. WorkflowWrap.displayName = 'WorkflowWrap'
  433. const WorkflowContainer = () => {
  434. return (
  435. <WorkflowContextProvider>
  436. <WorkflowWrap />
  437. </WorkflowContextProvider>
  438. )
  439. }
  440. export default memo(WorkflowContainer)