|
'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) |
|
} |
|
|
|
}, []) |
|
|
|
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) |
|
|