Spaces:
Paused
Paused
| 'use client' | |
| import type { FC } from 'react' | |
| import { | |
| memo, | |
| useCallback, | |
| useEffect, | |
| useMemo, | |
| useRef, | |
| useState, | |
| } from 'react' | |
| import useSWR from 'swr' | |
| 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, | |
| SupportUploadFileTypes, | |
| } 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' | |
| import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' | |
| import { fetchFileUploadConfig } from '@/service/common' | |
| 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) | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, []) | |
| 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 { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig) | |
| 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'], | |
| }, | |
| enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled), | |
| allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image], | |
| allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`), | |
| allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'], | |
| number_limits: features.file_upload?.number_limits || features.file_upload?.image?.number_limits || 3, | |
| fileUploadConfig: fileUploadConfigResponse, | |
| }, | |
| 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) | |