'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 = memo(({ nodes: originalNodes, edges: originalEdges, viewport, }) => { const workflowContainerRef = useRef(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([]) 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 (
{ showFeaturesPanel && } { !!showConfirm && ( setShowConfirm(undefined)} onConfirm={showConfirm.onConfirm} title={showConfirm.title} content={showConfirm.desc} /> ) } { showImportDSLModal && ( setShowImportDSLModal(false)} onBackup={exportCheck} onImport={handlePaneContextmenuCancel} /> ) } { secretEnvList.length > 0 && ( setSecretEnvList([])} /> ) }
) }) 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 (
) } 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 ( ) }) WorkflowWrap.displayName = 'WorkflowWrap' const WorkflowContainer = () => { return ( ) } export default memo(WorkflowContainer)