mirror of
				https://github.com/langgenius/dify.git
				synced 2025-10-31 10:53:02 +00:00 
			
		
		
		
	 dabfd74622
			
		
	
	
		dabfd74622
		
			
		
	
	
	
	
		
			
			Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: Yi <yxiaoisme@gmail.com> Co-authored-by: -LAN- <laipz8200@outlook.com>
		
			
				
	
	
		
			452 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			452 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 'use client'
 | |
| 
 | |
| import type { FC } from 'react'
 | |
| import {
 | |
|   memo,
 | |
|   useCallback,
 | |
|   useEffect,
 | |
|   useMemo,
 | |
|   useRef,
 | |
|   useState,
 | |
| } from 'react'
 | |
| import { setAutoFreeze } from 'immer'
 | |
| import {
 | |
|   useEventListener,
 | |
| } from 'ahooks'
 | |
| import ReactFlow, {
 | |
|   Background,
 | |
|   ReactFlowProvider,
 | |
|   SelectionMode,
 | |
|   useEdgesState,
 | |
|   useNodesState,
 | |
|   useOnViewportChange,
 | |
|   useReactFlow,
 | |
|   useStoreApi,
 | |
| } from 'reactflow'
 | |
| import type {
 | |
|   Viewport,
 | |
| } from 'reactflow'
 | |
| import 'reactflow/dist/style.css'
 | |
| import './style.css'
 | |
| import type {
 | |
|   Edge,
 | |
|   EnvironmentVariable,
 | |
|   Node,
 | |
| } from './types'
 | |
| import {
 | |
|   ControlMode,
 | |
| } from './types'
 | |
| import { WorkflowContextProvider } from './context'
 | |
| import {
 | |
|   useDSL,
 | |
|   useEdgesInteractions,
 | |
|   useNodesInteractions,
 | |
|   useNodesReadOnly,
 | |
|   useNodesSyncDraft,
 | |
|   usePanelInteractions,
 | |
|   useSelectionInteractions,
 | |
|   useShortcuts,
 | |
|   useWorkflow,
 | |
|   useWorkflowInit,
 | |
|   useWorkflowReadOnly,
 | |
|   useWorkflowUpdate,
 | |
| } from './hooks'
 | |
| import Header from './header'
 | |
| import CustomNode from './nodes'
 | |
| import CustomNoteNode from './note-node'
 | |
| import { CUSTOM_NOTE_NODE } from './note-node/constants'
 | |
| import CustomIterationStartNode from './nodes/iteration-start'
 | |
| import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants'
 | |
| import Operator from './operator'
 | |
| import CustomEdge from './custom-edge'
 | |
| import CustomConnectionLine from './custom-connection-line'
 | |
| import Panel from './panel'
 | |
| import Features from './features'
 | |
| import HelpLine from './help-line'
 | |
| import CandidateNode from './candidate-node'
 | |
| import PanelContextmenu from './panel-contextmenu'
 | |
| import NodeContextmenu from './node-contextmenu'
 | |
| import SyncingDataModal from './syncing-data-modal'
 | |
| import UpdateDSLModal from './update-dsl-modal'
 | |
| import DSLExportConfirmModal from './dsl-export-confirm-modal'
 | |
| import LimitTips from './limit-tips'
 | |
| import {
 | |
|   useStore,
 | |
|   useWorkflowStore,
 | |
| } from './store'
 | |
| import {
 | |
|   initialEdges,
 | |
|   initialNodes,
 | |
| } from './utils'
 | |
| import {
 | |
|   CUSTOM_NODE,
 | |
|   DSL_EXPORT_CHECK,
 | |
|   ITERATION_CHILDREN_Z_INDEX,
 | |
|   WORKFLOW_DATA_UPDATE,
 | |
| } from './constants'
 | |
| import { WorkflowHistoryProvider } from './workflow-history-store'
 | |
| import Loading from '@/app/components/base/loading'
 | |
| import { FeaturesProvider } from '@/app/components/base/features'
 | |
| import type { Features as FeaturesData } from '@/app/components/base/features/types'
 | |
| import { useFeaturesStore } from '@/app/components/base/features/hooks'
 | |
| import { useEventEmitterContextContext } from '@/context/event-emitter'
 | |
| import Confirm from '@/app/components/base/confirm'
 | |
| 
 | |
| const nodeTypes = {
 | |
|   [CUSTOM_NODE]: CustomNode,
 | |
|   [CUSTOM_NOTE_NODE]: CustomNoteNode,
 | |
|   [CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode,
 | |
| }
 | |
| const edgeTypes = {
 | |
|   [CUSTOM_NODE]: CustomEdge,
 | |
| }
 | |
| 
 | |
| type WorkflowProps = {
 | |
|   nodes: Node[]
 | |
|   edges: Edge[]
 | |
|   viewport?: Viewport
 | |
| }
 | |
| const Workflow: FC<WorkflowProps> = memo(({
 | |
|   nodes: originalNodes,
 | |
|   edges: originalEdges,
 | |
|   viewport,
 | |
| }) => {
 | |
|   const workflowContainerRef = useRef<HTMLDivElement>(null)
 | |
|   const workflowStore = useWorkflowStore()
 | |
|   const reactflow = useReactFlow()
 | |
|   const featuresStore = useFeaturesStore()
 | |
|   const [nodes, setNodes] = useNodesState(originalNodes)
 | |
|   const [edges, setEdges] = useEdgesState(originalEdges)
 | |
|   const showFeaturesPanel = useStore(state => state.showFeaturesPanel)
 | |
|   const controlMode = useStore(s => s.controlMode)
 | |
|   const nodeAnimation = useStore(s => s.nodeAnimation)
 | |
|   const showConfirm = useStore(s => s.showConfirm)
 | |
|   const showImportDSLModal = useStore(s => s.showImportDSLModal)
 | |
| 
 | |
|   const {
 | |
|     setShowConfirm,
 | |
|     setControlPromptEditorRerenderKey,
 | |
|     setShowImportDSLModal,
 | |
|     setSyncWorkflowDraftHash,
 | |
|   } = workflowStore.getState()
 | |
|   const {
 | |
|     handleSyncWorkflowDraft,
 | |
|     syncWorkflowDraftWhenPageClose,
 | |
|   } = useNodesSyncDraft()
 | |
|   const { workflowReadOnly } = useWorkflowReadOnly()
 | |
|   const { nodesReadOnly } = useNodesReadOnly()
 | |
| 
 | |
|   const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
 | |
| 
 | |
|   const { eventEmitter } = useEventEmitterContextContext()
 | |
| 
 | |
|   eventEmitter?.useSubscription((v: any) => {
 | |
|     if (v.type === WORKFLOW_DATA_UPDATE) {
 | |
|       setNodes(v.payload.nodes)
 | |
|       setEdges(v.payload.edges)
 | |
| 
 | |
|       if (v.payload.viewport)
 | |
|         reactflow.setViewport(v.payload.viewport)
 | |
| 
 | |
|       if (v.payload.features && featuresStore) {
 | |
|         const { setFeatures } = featuresStore.getState()
 | |
| 
 | |
|         setFeatures(v.payload.features)
 | |
|       }
 | |
| 
 | |
|       if (v.payload.hash)
 | |
|         setSyncWorkflowDraftHash(v.payload.hash)
 | |
| 
 | |
|       setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
 | |
|     }
 | |
|     if (v.type === DSL_EXPORT_CHECK)
 | |
|       setSecretEnvList(v.payload.data as EnvironmentVariable[])
 | |
|   })
 | |
| 
 | |
|   useEffect(() => {
 | |
|     setAutoFreeze(false)
 | |
| 
 | |
|     return () => {
 | |
|       setAutoFreeze(true)
 | |
|     }
 | |
|   }, [])
 | |
| 
 | |
|   useEffect(() => {
 | |
|     return () => {
 | |
|       handleSyncWorkflowDraft(true, true)
 | |
|     }
 | |
|   }, [])
 | |
| 
 | |
|   const { handleRefreshWorkflowDraft } = useWorkflowUpdate()
 | |
|   const handleSyncWorkflowDraftWhenPageClose = useCallback(() => {
 | |
|     if (document.visibilityState === 'hidden')
 | |
|       syncWorkflowDraftWhenPageClose()
 | |
|     else if (document.visibilityState === 'visible')
 | |
|       setTimeout(() => handleRefreshWorkflowDraft(), 500)
 | |
|   }, [syncWorkflowDraftWhenPageClose, handleRefreshWorkflowDraft])
 | |
| 
 | |
|   useEffect(() => {
 | |
|     document.addEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose)
 | |
| 
 | |
|     return () => {
 | |
|       document.removeEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose)
 | |
|     }
 | |
|   }, [handleSyncWorkflowDraftWhenPageClose])
 | |
| 
 | |
|   useEventListener('keydown', (e) => {
 | |
|     if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey))
 | |
|       e.preventDefault()
 | |
|     if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey))
 | |
|       e.preventDefault()
 | |
|     if ((e.key === 'y' || e.key === 'Y') && (e.ctrlKey || e.metaKey))
 | |
|       e.preventDefault()
 | |
|     if ((e.key === 's' || e.key === 'S') && (e.ctrlKey || e.metaKey))
 | |
|       e.preventDefault()
 | |
|   })
 | |
|   useEventListener('mousemove', (e) => {
 | |
|     const containerClientRect = workflowContainerRef.current?.getBoundingClientRect()
 | |
| 
 | |
|     if (containerClientRect) {
 | |
|       workflowStore.setState({
 | |
|         mousePosition: {
 | |
|           pageX: e.clientX,
 | |
|           pageY: e.clientY,
 | |
|           elementX: e.clientX - containerClientRect.left,
 | |
|           elementY: e.clientY - containerClientRect.top,
 | |
|         },
 | |
|       })
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   const {
 | |
|     handleNodeDragStart,
 | |
|     handleNodeDrag,
 | |
|     handleNodeDragStop,
 | |
|     handleNodeEnter,
 | |
|     handleNodeLeave,
 | |
|     handleNodeClick,
 | |
|     handleNodeConnect,
 | |
|     handleNodeConnectStart,
 | |
|     handleNodeConnectEnd,
 | |
|     handleNodeContextMenu,
 | |
|     handleHistoryBack,
 | |
|     handleHistoryForward,
 | |
|   } = useNodesInteractions()
 | |
|   const {
 | |
|     handleEdgeEnter,
 | |
|     handleEdgeLeave,
 | |
|     handleEdgesChange,
 | |
|   } = useEdgesInteractions()
 | |
|   const {
 | |
|     handleSelectionStart,
 | |
|     handleSelectionChange,
 | |
|     handleSelectionDrag,
 | |
|   } = useSelectionInteractions()
 | |
|   const {
 | |
|     handlePaneContextMenu,
 | |
|     handlePaneContextmenuCancel,
 | |
|   } = usePanelInteractions()
 | |
|   const {
 | |
|     isValidConnection,
 | |
|   } = useWorkflow()
 | |
|   const {
 | |
|     exportCheck,
 | |
|     handleExportDSL,
 | |
|   } = useDSL()
 | |
| 
 | |
|   useOnViewportChange({
 | |
|     onEnd: () => {
 | |
|       handleSyncWorkflowDraft()
 | |
|     },
 | |
|   })
 | |
| 
 | |
|   useShortcuts()
 | |
| 
 | |
|   const store = useStoreApi()
 | |
|   if (process.env.NODE_ENV === 'development') {
 | |
|     store.getState().onError = (code, message) => {
 | |
|       if (code === '002')
 | |
|         return
 | |
|       console.warn(message)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return (
 | |
|     <div
 | |
|       id='workflow-container'
 | |
|       className={`
 | |
|         relative w-full min-w-[960px] h-full bg-[#F0F2F7]
 | |
|         ${workflowReadOnly && 'workflow-panel-animation'}
 | |
|         ${nodeAnimation && 'workflow-node-animation'}
 | |
|       `}
 | |
|       ref={workflowContainerRef}
 | |
|     >
 | |
|       <SyncingDataModal />
 | |
|       <CandidateNode />
 | |
|       <Header />
 | |
|       <Panel />
 | |
|       <Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} />
 | |
|       {
 | |
|         showFeaturesPanel && <Features />
 | |
|       }
 | |
|       <PanelContextmenu />
 | |
|       <NodeContextmenu />
 | |
|       <HelpLine />
 | |
|       {
 | |
|         !!showConfirm && (
 | |
|           <Confirm
 | |
|             isShow
 | |
|             onCancel={() => setShowConfirm(undefined)}
 | |
|             onConfirm={showConfirm.onConfirm}
 | |
|             title={showConfirm.title}
 | |
|             content={showConfirm.desc}
 | |
|           />
 | |
|         )
 | |
|       }
 | |
|       {
 | |
|         showImportDSLModal && (
 | |
|           <UpdateDSLModal
 | |
|             onCancel={() => setShowImportDSLModal(false)}
 | |
|             onBackup={exportCheck}
 | |
|             onImport={handlePaneContextmenuCancel}
 | |
|           />
 | |
|         )
 | |
|       }
 | |
|       {
 | |
|         secretEnvList.length > 0 && (
 | |
|           <DSLExportConfirmModal
 | |
|             envList={secretEnvList}
 | |
|             onConfirm={handleExportDSL}
 | |
|             onClose={() => setSecretEnvList([])}
 | |
|           />
 | |
|         )
 | |
|       }
 | |
|       <LimitTips />
 | |
|       <ReactFlow
 | |
|         nodeTypes={nodeTypes}
 | |
|         edgeTypes={edgeTypes}
 | |
|         nodes={nodes}
 | |
|         edges={edges}
 | |
|         onNodeDragStart={handleNodeDragStart}
 | |
|         onNodeDrag={handleNodeDrag}
 | |
|         onNodeDragStop={handleNodeDragStop}
 | |
|         onNodeMouseEnter={handleNodeEnter}
 | |
|         onNodeMouseLeave={handleNodeLeave}
 | |
|         onNodeClick={handleNodeClick}
 | |
|         onNodeContextMenu={handleNodeContextMenu}
 | |
|         onConnect={handleNodeConnect}
 | |
|         onConnectStart={handleNodeConnectStart}
 | |
|         onConnectEnd={handleNodeConnectEnd}
 | |
|         onEdgeMouseEnter={handleEdgeEnter}
 | |
|         onEdgeMouseLeave={handleEdgeLeave}
 | |
|         onEdgesChange={handleEdgesChange}
 | |
|         onSelectionStart={handleSelectionStart}
 | |
|         onSelectionChange={handleSelectionChange}
 | |
|         onSelectionDrag={handleSelectionDrag}
 | |
|         onPaneContextMenu={handlePaneContextMenu}
 | |
|         connectionLineComponent={CustomConnectionLine}
 | |
|         connectionLineContainerStyle={{ zIndex: ITERATION_CHILDREN_Z_INDEX }}
 | |
|         defaultViewport={viewport}
 | |
|         multiSelectionKeyCode={null}
 | |
|         deleteKeyCode={null}
 | |
|         nodesDraggable={!nodesReadOnly}
 | |
|         nodesConnectable={!nodesReadOnly}
 | |
|         nodesFocusable={!nodesReadOnly}
 | |
|         edgesFocusable={!nodesReadOnly}
 | |
|         panOnDrag={controlMode === ControlMode.Hand && !workflowReadOnly}
 | |
|         zoomOnPinch={!workflowReadOnly}
 | |
|         zoomOnScroll={!workflowReadOnly}
 | |
|         zoomOnDoubleClick={!workflowReadOnly}
 | |
|         isValidConnection={isValidConnection}
 | |
|         selectionKeyCode={null}
 | |
|         selectionMode={SelectionMode.Partial}
 | |
|         selectionOnDrag={controlMode === ControlMode.Pointer && !workflowReadOnly}
 | |
|         minZoom={0.25}
 | |
|       >
 | |
|         <Background
 | |
|           gap={[14, 14]}
 | |
|           size={2}
 | |
|           color='#E4E5E7'
 | |
|         />
 | |
|       </ReactFlow>
 | |
|     </div>
 | |
|   )
 | |
| })
 | |
| Workflow.displayName = 'Workflow'
 | |
| 
 | |
| const WorkflowWrap = memo(() => {
 | |
|   const {
 | |
|     data,
 | |
|     isLoading,
 | |
|   } = useWorkflowInit()
 | |
| 
 | |
|   const nodesData = useMemo(() => {
 | |
|     if (data)
 | |
|       return initialNodes(data.graph.nodes, data.graph.edges)
 | |
| 
 | |
|     return []
 | |
|   }, [data])
 | |
|   const edgesData = useMemo(() => {
 | |
|     if (data)
 | |
|       return initialEdges(data.graph.edges, data.graph.nodes)
 | |
| 
 | |
|     return []
 | |
|   }, [data])
 | |
| 
 | |
|   if (!data || isLoading) {
 | |
|     return (
 | |
|       <div className='flex justify-center items-center relative w-full h-full bg-[#F0F2F7]'>
 | |
|         <Loading />
 | |
|       </div>
 | |
|     )
 | |
|   }
 | |
| 
 | |
|   const features = data.features || {}
 | |
|   const initialFeatures: FeaturesData = {
 | |
|     file: {
 | |
|       image: {
 | |
|         enabled: !!features.file_upload?.image.enabled,
 | |
|         number_limits: features.file_upload?.image.number_limits || 3,
 | |
|         transfer_methods: features.file_upload?.image.transfer_methods || ['local_file', 'remote_url'],
 | |
|       },
 | |
|     },
 | |
|     opening: {
 | |
|       enabled: !!features.opening_statement,
 | |
|       opening_statement: features.opening_statement,
 | |
|       suggested_questions: features.suggested_questions,
 | |
|     },
 | |
|     suggested: features.suggested_questions_after_answer || { enabled: false },
 | |
|     speech2text: features.speech_to_text || { enabled: false },
 | |
|     text2speech: features.text_to_speech || { enabled: false },
 | |
|     citation: features.retriever_resource || { enabled: false },
 | |
|     moderation: features.sensitive_word_avoidance || { enabled: false },
 | |
|   }
 | |
| 
 | |
|   return (
 | |
|     <ReactFlowProvider>
 | |
|       <WorkflowHistoryProvider
 | |
|         nodes={nodesData}
 | |
|         edges={edgesData} >
 | |
|         <FeaturesProvider features={initialFeatures}>
 | |
|           <Workflow
 | |
|             nodes={nodesData}
 | |
|             edges={edgesData}
 | |
|             viewport={data?.graph.viewport}
 | |
|           />
 | |
|         </FeaturesProvider>
 | |
|       </WorkflowHistoryProvider>
 | |
|     </ReactFlowProvider>
 | |
|   )
 | |
| })
 | |
| WorkflowWrap.displayName = 'WorkflowWrap'
 | |
| 
 | |
| const WorkflowContainer = () => {
 | |
|   return (
 | |
|     <WorkflowContextProvider>
 | |
|       <WorkflowWrap />
 | |
|     </WorkflowContextProvider>
 | |
|   )
 | |
| }
 | |
| 
 | |
| export default memo(WorkflowContainer)
 |