mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
838 lines
30 KiB
TypeScript
838 lines
30 KiB
TypeScript
'use client'
|
|
|
|
import type { FC } from 'react'
|
|
import type { Viewport } from 'reactflow'
|
|
import type { CursorPosition, OnlineUser } from './collaboration/types/collaboration'
|
|
import type { Shape as HooksStoreShape } from './hooks-store'
|
|
import type { WorkflowSliceShape } from './store/workflow/workflow-slice'
|
|
import type { ConversationVariable, Edge, EnvironmentVariable, Node } from './types'
|
|
import type { EventEmitterValue } from '@/context/event-emitter'
|
|
import type { VarInInspect } from '@/types/workflow'
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogActions,
|
|
AlertDialogCancelButton,
|
|
AlertDialogConfirmButton,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogTitle,
|
|
} from '@langgenius/dify-ui/alert-dialog'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import { toast } from '@langgenius/dify-ui/toast'
|
|
import { useEventListener } from 'ahooks'
|
|
import { isEqual } from 'es-toolkit/predicate'
|
|
import { setAutoFreeze } from 'immer'
|
|
import { Fragment, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import ReactFlow, {
|
|
Background,
|
|
ReactFlowProvider,
|
|
SelectionMode,
|
|
useEdgesState,
|
|
useNodes,
|
|
useNodesState,
|
|
useOnViewportChange,
|
|
useReactFlow,
|
|
useStoreApi,
|
|
} from 'reactflow'
|
|
import { IS_DEV } from '@/config'
|
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
|
import {
|
|
useAllBuiltInTools,
|
|
useAllCustomTools,
|
|
useAllMCPTools,
|
|
useAllWorkflowTools,
|
|
} from '@/service/use-tools'
|
|
import { fetchAllInspectVars } from '@/service/workflow'
|
|
import CandidateNode from './candidate-node'
|
|
import UserCursors from './collaboration/components/user-cursors'
|
|
import { collaborationManager } from './collaboration/core/collaboration-manager'
|
|
import CommentManager from './comment-manager'
|
|
import { CommentIcon } from './comment/comment-icon'
|
|
import { CommentInput } from './comment/comment-input'
|
|
import { CommentCursor } from './comment/cursor'
|
|
import { CommentThread } from './comment/thread'
|
|
import { CUSTOM_EDGE, CUSTOM_NODE, WORKFLOW_DATA_UPDATE } from './constants'
|
|
import CustomConnectionLine from './custom-connection-line'
|
|
import CustomEdge from './custom-edge'
|
|
import DatasetsDetailProvider from './datasets-detail-store/provider'
|
|
import HelpLine from './help-line'
|
|
import {
|
|
useEdgesInteractions,
|
|
useNodesInteractions,
|
|
useNodesReadOnly,
|
|
useNodesSyncDraft,
|
|
usePanelInteractions,
|
|
useSelectionInteractions,
|
|
useSetWorkflowVarsWithValue,
|
|
useWorkflow,
|
|
useWorkflowReadOnly,
|
|
useWorkflowRefreshDraft,
|
|
} from './hooks'
|
|
import { HooksStoreContextProvider, useHooksStore } from './hooks-store'
|
|
import { useWorkflowComment } from './hooks/use-workflow-comment'
|
|
import { useWorkflowSearch } from './hooks/use-workflow-search'
|
|
import CustomNode from './nodes'
|
|
import useMatchSchemaType from './nodes/_base/components/variable/use-match-schema-type'
|
|
import CustomDataSourceEmptyNode from './nodes/data-source-empty'
|
|
import { CUSTOM_DATA_SOURCE_EMPTY_NODE } from './nodes/data-source-empty/constants'
|
|
import CustomIterationStartNode from './nodes/iteration-start'
|
|
import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants'
|
|
import CustomLoopStartNode from './nodes/loop-start'
|
|
import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants'
|
|
import CustomNoteNode from './note-node'
|
|
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
|
import Operator from './operator'
|
|
import Control from './operator/control'
|
|
import { WorkflowLocalStorageBridge } from './persistence/local-storage-bridge'
|
|
import { useWorkflowHotkeys } from './shortcuts/use-workflow-hotkeys'
|
|
import CustomSimpleNode from './simple-node'
|
|
import { CUSTOM_SIMPLE_NODE } from './simple-node/constants'
|
|
import { useStore, useWorkflowStore } from './store/workflow'
|
|
import SyncingDataModal from './syncing-data-modal'
|
|
import { ControlMode, WorkflowRunningStatus } from './types'
|
|
import { setupScrollToNodeListener } from './utils/node-navigation'
|
|
import { WorkflowContextmenu } from './workflow-contextmenu'
|
|
import 'reactflow/dist/style.css'
|
|
import './style.css'
|
|
|
|
const nodeTypes = {
|
|
[CUSTOM_NODE]: CustomNode,
|
|
[CUSTOM_NOTE_NODE]: CustomNoteNode,
|
|
[CUSTOM_SIMPLE_NODE]: CustomSimpleNode,
|
|
[CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode,
|
|
[CUSTOM_LOOP_START_NODE]: CustomLoopStartNode,
|
|
[CUSTOM_DATA_SOURCE_EMPTY_NODE]: CustomDataSourceEmptyNode,
|
|
}
|
|
const edgeTypes = {
|
|
[CUSTOM_EDGE]: CustomEdge,
|
|
}
|
|
|
|
type WorkflowDataUpdatePayload = {
|
|
nodes: Node[]
|
|
edges: Edge[]
|
|
viewport?: Viewport
|
|
hash?: string
|
|
features?: unknown
|
|
conversation_variables?: ConversationVariable[]
|
|
environment_variables?: EnvironmentVariable[]
|
|
}
|
|
|
|
export type WorkflowProps = {
|
|
nodes: Node[]
|
|
edges: Edge[]
|
|
viewport?: Viewport
|
|
children?: React.ReactNode
|
|
onWorkflowDataUpdate?: (v: WorkflowDataUpdatePayload) => void
|
|
cursors?: Record<string, CursorPosition>
|
|
myUserId?: string | null
|
|
onlineUsers?: OnlineUser[]
|
|
}
|
|
|
|
const CommentPlacementPreview = memo(
|
|
({
|
|
onSubmit,
|
|
onCancel,
|
|
}: {
|
|
onSubmit: (content: string, mentionedUserIds: string[]) => void
|
|
onCancel: () => void
|
|
}) => {
|
|
const isCommentPlacing = useStore((s) => s.isCommentPlacing)
|
|
const pendingComment = useStore((s) => s.pendingComment)
|
|
const mousePosition = useStore((s) => s.mousePosition)
|
|
|
|
if (!isCommentPlacing || pendingComment) return null
|
|
|
|
return (
|
|
<CommentInput
|
|
position={{
|
|
x: mousePosition.elementX,
|
|
y: mousePosition.elementY,
|
|
}}
|
|
onSubmit={onSubmit}
|
|
onCancel={onCancel}
|
|
autoFocus={false}
|
|
disabled
|
|
/>
|
|
)
|
|
},
|
|
)
|
|
|
|
CommentPlacementPreview.displayName = 'CommentPlacementPreview'
|
|
|
|
export const Workflow: FC<WorkflowProps> = memo(
|
|
({
|
|
nodes: originalNodes,
|
|
edges: originalEdges,
|
|
viewport,
|
|
children,
|
|
onWorkflowDataUpdate,
|
|
cursors,
|
|
myUserId,
|
|
onlineUsers,
|
|
}) => {
|
|
const { t } = useTranslation()
|
|
const workflowContainerRef = useRef<HTMLDivElement>(null)
|
|
const workflowStore = useWorkflowStore()
|
|
const reactflow = useReactFlow()
|
|
const store = useStoreApi()
|
|
const [isMouseOverCanvas, setIsMouseOverCanvas] = useState(false)
|
|
const [nodes, setNodes] = useNodesState(originalNodes)
|
|
const [edges, setEdges] = useEdgesState(originalEdges)
|
|
const controlMode = useStore((s) => s.controlMode)
|
|
const nodeAnimation = useStore((s) => s.nodeAnimation)
|
|
const showConfirm = useStore((s) => s.showConfirm)
|
|
const workflowCanvasHeight = useStore((s) => s.workflowCanvasHeight)
|
|
const bottomPanelHeight = useStore((s) => s.bottomPanelHeight)
|
|
const setWorkflowCanvasWidth = useStore((s) => s.setWorkflowCanvasWidth)
|
|
const setWorkflowCanvasHeight = useStore((s) => s.setWorkflowCanvasHeight)
|
|
const workflowCanvasSizeRef = useRef<{ width?: number; height?: number }>({})
|
|
const workflowCanvasResizeFrameRef = useRef<number | undefined>(undefined)
|
|
const controlHeight = useMemo(() => {
|
|
if (!workflowCanvasHeight) return '100%'
|
|
return workflowCanvasHeight - bottomPanelHeight
|
|
}, [workflowCanvasHeight, bottomPanelHeight])
|
|
|
|
// update workflow Canvas width and height
|
|
useEffect(() => {
|
|
if (workflowContainerRef.current) {
|
|
const updateWorkflowCanvasSize = (width: number, height: number) => {
|
|
if (
|
|
workflowCanvasSizeRef.current.width === width &&
|
|
workflowCanvasSizeRef.current.height === height
|
|
)
|
|
return
|
|
|
|
workflowCanvasSizeRef.current = { width, height }
|
|
if (workflowCanvasResizeFrameRef.current)
|
|
cancelAnimationFrame(workflowCanvasResizeFrameRef.current)
|
|
|
|
workflowCanvasResizeFrameRef.current = requestAnimationFrame(() => {
|
|
workflowCanvasResizeFrameRef.current = undefined
|
|
setWorkflowCanvasWidth(width)
|
|
setWorkflowCanvasHeight(height)
|
|
})
|
|
}
|
|
|
|
const resizeContainerObserver = new ResizeObserver((entries) => {
|
|
for (const entry of entries) {
|
|
const { inlineSize, blockSize } = entry.borderBoxSize[0]!
|
|
updateWorkflowCanvasSize(inlineSize, blockSize)
|
|
}
|
|
})
|
|
resizeContainerObserver.observe(workflowContainerRef.current)
|
|
return () => {
|
|
if (workflowCanvasResizeFrameRef.current) {
|
|
cancelAnimationFrame(workflowCanvasResizeFrameRef.current)
|
|
workflowCanvasResizeFrameRef.current = undefined
|
|
}
|
|
resizeContainerObserver.disconnect()
|
|
}
|
|
}
|
|
}, [setWorkflowCanvasHeight, setWorkflowCanvasWidth])
|
|
|
|
const {
|
|
setShowConfirm,
|
|
setControlPromptEditorRerenderKey,
|
|
setSyncWorkflowDraftHash,
|
|
setNodes: setNodesInStore,
|
|
} = workflowStore.getState()
|
|
const currentNodes = useNodes()
|
|
const setNodesOnlyChangeWithData = useCallback(
|
|
(nodes: Node[]) => {
|
|
const nodesData = nodes.map((node) => ({
|
|
id: node.id,
|
|
data: node.data,
|
|
}))
|
|
const oldData = workflowStore.getState().nodes.map((node) => ({
|
|
id: node.id,
|
|
data: node.data,
|
|
}))
|
|
if (!isEqual(oldData, nodesData)) setNodesInStore(nodes)
|
|
},
|
|
[setNodesInStore, workflowStore],
|
|
)
|
|
useEffect(() => {
|
|
setNodesOnlyChangeWithData(currentNodes as Node[])
|
|
}, [currentNodes, setNodesOnlyChangeWithData])
|
|
useEffect(() => {
|
|
return collaborationManager.onGraphImport(
|
|
({ nodes: importedNodes, edges: importedEdges }) => {
|
|
if (!isEqual(nodes, importedNodes)) {
|
|
setNodes(importedNodes)
|
|
store.getState().setNodes(importedNodes)
|
|
}
|
|
if (!isEqual(edges, importedEdges)) {
|
|
setEdges(importedEdges)
|
|
store.getState().setEdges(importedEdges)
|
|
}
|
|
},
|
|
)
|
|
}, [edges, nodes, setEdges, setNodes, store])
|
|
|
|
useEffect(() => {
|
|
return collaborationManager.onHistoryAction((_) => {
|
|
toast.info(t(($) => $['collaboration.historyAction.generic'], { ns: 'workflow' }))
|
|
})
|
|
}, [t])
|
|
|
|
useEffect(() => {
|
|
return collaborationManager.onRestoreIntent((data) => {
|
|
toast.info(
|
|
t(($) => $['versionHistory.action.restoreInProgress'], {
|
|
ns: 'workflow',
|
|
userName: data.initiatorName,
|
|
versionName: data.versionName || data.versionId,
|
|
}),
|
|
)
|
|
})
|
|
}, [t])
|
|
|
|
const { handleSyncWorkflowDraft, syncWorkflowDraftWhenPageClose } = useNodesSyncDraft()
|
|
const { workflowReadOnly } = useWorkflowReadOnly()
|
|
const { nodesReadOnly } = useNodesReadOnly()
|
|
const { eventEmitter } = useEventEmitterContextContext()
|
|
const {
|
|
comments,
|
|
pendingComment,
|
|
activeComment,
|
|
activeCommentLoading,
|
|
replySubmitting,
|
|
replyUpdating,
|
|
handleCommentSubmit,
|
|
handleCommentCancel,
|
|
handleCommentIconClick,
|
|
handleActiveCommentClose,
|
|
handleCommentResolve,
|
|
handleCommentDelete,
|
|
handleCommentUpdate,
|
|
handleCommentReply,
|
|
handleCommentReplyUpdate,
|
|
handleCommentReplyDelete,
|
|
handleCommentPositionUpdate,
|
|
} = useWorkflowComment()
|
|
const showUserComments = useStore((s) => s.showUserComments)
|
|
const showUserCursors = useStore((s) => s.showUserCursors)
|
|
const showResolvedComments = useStore((s) => s.showResolvedComments)
|
|
const isCommentPreviewHovering = useStore((s) => s.isCommentPreviewHovering)
|
|
const isCommentPlacing = useStore((s) => s.isCommentPlacing)
|
|
const setCommentPlacing = useStore((s) => s.setCommentPlacing)
|
|
const setCommentQuickAdd = useStore((s) => s.setCommentQuickAdd)
|
|
const setPendingCommentState = useStore((s) => s.setPendingComment)
|
|
const isCommentInputActive = Boolean(pendingComment) || isCommentPlacing
|
|
const visibleComments = useMemo(() => {
|
|
if (showResolvedComments) return comments
|
|
return comments.filter((comment) => !comment.resolved)
|
|
}, [comments, showResolvedComments])
|
|
const handleVisibleCommentNavigate = useCallback(
|
|
(direction: 'prev' | 'next') => {
|
|
if (!activeComment) return
|
|
const idx = visibleComments.findIndex((comment) => comment.id === activeComment.id)
|
|
if (idx === -1) return
|
|
const target = direction === 'prev' ? visibleComments[idx - 1] : visibleComments[idx + 1]
|
|
if (target) handleCommentIconClick(target)
|
|
},
|
|
[activeComment, handleCommentIconClick, visibleComments],
|
|
)
|
|
|
|
eventEmitter?.useSubscription((v: EventEmitterValue) => {
|
|
if (typeof v === 'object' && v.type === WORKFLOW_DATA_UPDATE) {
|
|
const payload = v.payload as WorkflowDataUpdatePayload
|
|
setNodes(payload.nodes)
|
|
store.getState().setNodes(payload.nodes)
|
|
setEdges(payload.edges)
|
|
workflowStore.setState({ contextMenuTarget: undefined })
|
|
|
|
if (payload.viewport) reactflow.setViewport(payload.viewport)
|
|
|
|
if (payload.hash) setSyncWorkflowDraftHash(payload.hash)
|
|
|
|
onWorkflowDataUpdate?.(payload)
|
|
|
|
setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
|
|
}
|
|
})
|
|
|
|
useEffect(() => {
|
|
setAutoFreeze(false)
|
|
|
|
return () => {
|
|
setAutoFreeze(true)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
handleSyncWorkflowDraft(true, true)
|
|
}
|
|
}, [handleSyncWorkflowDraft])
|
|
|
|
const handlePendingCommentPositionChange = useCallback(
|
|
(position: NonNullable<WorkflowSliceShape['pendingComment']>) => {
|
|
setPendingCommentState(position)
|
|
},
|
|
[setPendingCommentState],
|
|
)
|
|
|
|
const handleCommentPlacementCancel = useCallback(() => {
|
|
setPendingCommentState(null)
|
|
setCommentPlacing(false)
|
|
setCommentQuickAdd(false)
|
|
}, [setCommentPlacing, setCommentQuickAdd, setPendingCommentState])
|
|
|
|
const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft()
|
|
const handleSyncWorkflowDraftWhenPageClose = useCallback(() => {
|
|
if (document.visibilityState === 'hidden') {
|
|
syncWorkflowDraftWhenPageClose()
|
|
return
|
|
}
|
|
|
|
if (document.visibilityState === 'visible') {
|
|
const { isListening, workflowRunningData } = workflowStore.getState()
|
|
const status = workflowRunningData?.result?.status
|
|
// Avoid resetting UI state when user comes back while a run is active or listening for triggers
|
|
if (isListening || status === WorkflowRunningStatus.Running) return
|
|
|
|
setTimeout(() => handleRefreshWorkflowDraft(), 500)
|
|
}
|
|
}, [syncWorkflowDraftWhenPageClose, handleRefreshWorkflowDraft, workflowStore])
|
|
|
|
// Also add beforeunload handler as additional safety net for tab close
|
|
const handleBeforeUnload = useCallback(() => {
|
|
syncWorkflowDraftWhenPageClose()
|
|
}, [syncWorkflowDraftWhenPageClose])
|
|
|
|
// Optimized comment deletion using showConfirm
|
|
const handleCommentDeleteClick = useCallback(
|
|
(commentId: string) => {
|
|
if (!showConfirm) {
|
|
setShowConfirm({
|
|
title: t(($) => $['comments.confirm.deleteThreadTitle'], { ns: 'workflow' }),
|
|
desc: t(($) => $['comments.confirm.deleteThreadDesc'], { ns: 'workflow' }),
|
|
onConfirm: async () => {
|
|
await handleCommentDelete(commentId)
|
|
setShowConfirm(undefined)
|
|
},
|
|
})
|
|
}
|
|
},
|
|
[showConfirm, setShowConfirm, handleCommentDelete, t],
|
|
)
|
|
|
|
const handleCommentReplyDeleteClick = useCallback(
|
|
(commentId: string, replyId: string) => {
|
|
if (!showConfirm) {
|
|
setShowConfirm({
|
|
title: t(($) => $['comments.confirm.deleteReplyTitle'], { ns: 'workflow' }),
|
|
desc: t(($) => $['comments.confirm.deleteReplyDesc'], { ns: 'workflow' }),
|
|
onConfirm: async () => {
|
|
await handleCommentReplyDelete(commentId, replyId)
|
|
setShowConfirm(undefined)
|
|
},
|
|
})
|
|
}
|
|
},
|
|
[showConfirm, setShowConfirm, handleCommentReplyDelete, t],
|
|
)
|
|
|
|
useEffect(() => {
|
|
document.addEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose)
|
|
window.addEventListener('beforeunload', handleBeforeUnload)
|
|
|
|
return () => {
|
|
document.removeEventListener('visibilitychange', handleSyncWorkflowDraftWhenPageClose)
|
|
window.removeEventListener('beforeunload', handleBeforeUnload)
|
|
}
|
|
}, [handleSyncWorkflowDraftWhenPageClose, handleBeforeUnload])
|
|
|
|
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 target = e.target as HTMLElement
|
|
const onPane = !!target?.closest('.react-flow__pane')
|
|
setIsMouseOverCanvas(onPane)
|
|
}
|
|
})
|
|
|
|
// Prevent browser zoom interactions from hijacking gestures meant for the workflow canvas
|
|
useEffect(() => {
|
|
const preventBrowserZoom = (event: WheelEvent) => {
|
|
if (!isCommentPreviewHovering && !isCommentInputActive) return
|
|
|
|
if (event.ctrlKey || event.metaKey) event.preventDefault()
|
|
}
|
|
|
|
const preventGestureZoom = (event: Event) => {
|
|
if (!isCommentPreviewHovering && !isCommentInputActive) return
|
|
|
|
event.preventDefault()
|
|
}
|
|
|
|
window.addEventListener('wheel', preventBrowserZoom, { passive: false })
|
|
const gestureEvents: Array<'gesturestart' | 'gesturechange' | 'gestureend'> = [
|
|
'gesturestart',
|
|
'gesturechange',
|
|
'gestureend',
|
|
]
|
|
gestureEvents.forEach((eventName) => {
|
|
window.addEventListener(eventName, preventGestureZoom, { passive: false })
|
|
})
|
|
|
|
return () => {
|
|
window.removeEventListener('wheel', preventBrowserZoom)
|
|
gestureEvents.forEach((eventName) => {
|
|
window.removeEventListener(eventName, preventGestureZoom)
|
|
})
|
|
}
|
|
}, [isCommentPreviewHovering, isCommentInputActive])
|
|
|
|
const {
|
|
handleNodeDragStart,
|
|
handleNodeDrag,
|
|
handleNodeDragStop,
|
|
handleNodeEnter,
|
|
handleNodeLeave,
|
|
handleNodeClick,
|
|
handleNodeConnect,
|
|
handleNodeConnectStart,
|
|
handleNodeConnectEnd,
|
|
handleNodeContextMenu,
|
|
handleHistoryBack,
|
|
handleHistoryForward,
|
|
} = useNodesInteractions()
|
|
const { handleEdgeEnter, handleEdgeLeave, handleEdgesChange, handleEdgeContextMenu } =
|
|
useEdgesInteractions()
|
|
const {
|
|
handleSelectionStart,
|
|
handleSelectionChange,
|
|
handleSelectionDrag,
|
|
handleSelectionContextMenu,
|
|
} = useSelectionInteractions()
|
|
const { handlePaneContextMenu } = usePanelInteractions()
|
|
const { isValidConnection } = useWorkflow()
|
|
|
|
useOnViewportChange({
|
|
onEnd: () => {
|
|
handleSyncWorkflowDraft()
|
|
},
|
|
})
|
|
|
|
useWorkflowHotkeys()
|
|
// Initialize workflow node search functionality
|
|
useWorkflowSearch()
|
|
|
|
// Set up scroll to node event listener using the utility function
|
|
useEffect(() => {
|
|
return setupScrollToNodeListener(nodes, reactflow)
|
|
}, [nodes, reactflow])
|
|
|
|
const { schemaTypeDefinitions } = useMatchSchemaType()
|
|
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
|
|
const { data: buildInTools } = useAllBuiltInTools()
|
|
const { data: customTools } = useAllCustomTools()
|
|
const { data: workflowTools } = useAllWorkflowTools()
|
|
const { data: mcpTools } = useAllMCPTools()
|
|
const dataSourceList = useStore((s) => s.dataSourceList)
|
|
// buildInTools, customTools, workflowTools, mcpTools, dataSourceList
|
|
const configsMap = useHooksStore((s) => s.configsMap)
|
|
const [isLoadedVars, setIsLoadedVars] = useState(false)
|
|
const [vars, setVars] = useState<VarInInspect[]>([])
|
|
useEffect(() => {
|
|
;(async () => {
|
|
if (!configsMap?.flowType || !configsMap?.flowId) return
|
|
const data = await fetchAllInspectVars(configsMap.flowType, configsMap.flowId)
|
|
setVars(data)
|
|
setIsLoadedVars(true)
|
|
})()
|
|
}, [configsMap?.flowType, configsMap?.flowId])
|
|
useEffect(() => {
|
|
if (schemaTypeDefinitions && isLoadedVars) {
|
|
fetchInspectVars({
|
|
passInVars: true,
|
|
vars,
|
|
passedInAllPluginInfoList: {
|
|
buildInTools: buildInTools || [],
|
|
customTools: customTools || [],
|
|
workflowTools: workflowTools || [],
|
|
mcpTools: mcpTools || [],
|
|
dataSourceList: dataSourceList ?? [],
|
|
},
|
|
passedInSchemaTypeDefinitions: schemaTypeDefinitions,
|
|
})
|
|
}
|
|
}, [
|
|
schemaTypeDefinitions,
|
|
fetchInspectVars,
|
|
isLoadedVars,
|
|
vars,
|
|
customTools,
|
|
buildInTools,
|
|
workflowTools,
|
|
mcpTools,
|
|
dataSourceList,
|
|
])
|
|
|
|
if (IS_DEV) {
|
|
store.getState().onError = (code, message) => {
|
|
if (code === '002') return
|
|
console.warn(message)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
id="workflow-container"
|
|
className={cn(
|
|
'relative isolate h-full w-full min-w-[960px] overflow-hidden',
|
|
workflowReadOnly && 'workflow-panel-animation',
|
|
nodeAnimation && 'workflow-node-animation',
|
|
)}
|
|
ref={workflowContainerRef}
|
|
>
|
|
<CandidateNode />
|
|
<CommentManager />
|
|
<div
|
|
className="pointer-events-none absolute top-0 left-0 z-10 flex w-12 items-center justify-center p-1 pl-2"
|
|
style={{ height: controlHeight }}
|
|
>
|
|
<Control />
|
|
</div>
|
|
<Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} />
|
|
<HelpLine />
|
|
<AlertDialog
|
|
open={!!showConfirm}
|
|
onOpenChange={(open) => !open && setShowConfirm(undefined)}
|
|
>
|
|
<AlertDialogContent>
|
|
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
|
|
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
|
|
{showConfirm?.title}
|
|
</AlertDialogTitle>
|
|
{showConfirm?.desc && (
|
|
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
|
|
{showConfirm.desc}
|
|
</AlertDialogDescription>
|
|
)}
|
|
</div>
|
|
<AlertDialogActions>
|
|
<AlertDialogCancelButton>
|
|
{t(($) => $['operation.cancel'], { ns: 'common' })}
|
|
</AlertDialogCancelButton>
|
|
<AlertDialogConfirmButton onClick={showConfirm?.onConfirm}>
|
|
{t(($) => $['operation.confirm'], { ns: 'common' })}
|
|
</AlertDialogConfirmButton>
|
|
</AlertDialogActions>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
{controlMode === ControlMode.Comment && isMouseOverCanvas && <CommentCursor />}
|
|
<CommentPlacementPreview
|
|
onSubmit={handleCommentSubmit}
|
|
onCancel={handleCommentPlacementCancel}
|
|
/>
|
|
{pendingComment && (
|
|
<CommentInput
|
|
position={{
|
|
x: pendingComment.elementX,
|
|
y: pendingComment.elementY,
|
|
}}
|
|
onSubmit={handleCommentSubmit}
|
|
onCancel={handleCommentCancel}
|
|
onPositionChange={handlePendingCommentPositionChange}
|
|
/>
|
|
)}
|
|
{visibleComments.map((comment, index) => {
|
|
const isActive = activeComment?.id === comment.id
|
|
|
|
if (isActive && activeComment) {
|
|
const canGoPrev = index > 0
|
|
const canGoNext = index < visibleComments.length - 1
|
|
return (
|
|
<Fragment key={comment.id}>
|
|
<CommentIcon
|
|
key={`${comment.id}-icon`}
|
|
comment={comment}
|
|
onClick={() => handleCommentIconClick(comment)}
|
|
isActive={true}
|
|
onPositionUpdate={(position) => handleCommentPositionUpdate(comment.id, position)}
|
|
/>
|
|
<CommentThread
|
|
key={`${comment.id}-thread`}
|
|
comment={activeComment}
|
|
loading={activeCommentLoading}
|
|
replySubmitting={replySubmitting}
|
|
replyUpdating={replyUpdating}
|
|
onClose={handleActiveCommentClose}
|
|
onResolve={() => handleCommentResolve(comment.id)}
|
|
onDelete={() => handleCommentDeleteClick(comment.id)}
|
|
onCommentEdit={(content, ids) =>
|
|
handleCommentUpdate(comment.id, content, ids ?? [])
|
|
}
|
|
onPrev={canGoPrev ? () => handleVisibleCommentNavigate('prev') : undefined}
|
|
onNext={canGoNext ? () => handleVisibleCommentNavigate('next') : undefined}
|
|
onReply={(content, ids) => handleCommentReply(comment.id, content, ids ?? [])}
|
|
onReplyEdit={(replyId, content, ids) =>
|
|
handleCommentReplyUpdate(comment.id, replyId, content, ids ?? [])
|
|
}
|
|
onReplyDelete={(replyId) => handleCommentReplyDeleteClick(comment.id, replyId)}
|
|
onReplyDeleteDirect={(replyId) => handleCommentReplyDelete(comment.id, replyId)}
|
|
canGoPrev={canGoPrev}
|
|
canGoNext={canGoNext}
|
|
/>
|
|
</Fragment>
|
|
)
|
|
}
|
|
|
|
return showUserComments || controlMode === ControlMode.Comment ? (
|
|
<CommentIcon
|
|
key={comment.id}
|
|
comment={comment}
|
|
onClick={() => handleCommentIconClick(comment)}
|
|
onPositionUpdate={(position) => handleCommentPositionUpdate(comment.id, position)}
|
|
/>
|
|
) : null
|
|
})}
|
|
{children}
|
|
<WorkflowContextmenu>
|
|
<ReactFlow
|
|
nodeTypes={nodeTypes}
|
|
edgeTypes={edgeTypes}
|
|
nodes={nodes}
|
|
edges={edges}
|
|
className={controlMode === ControlMode.Comment ? 'comment-mode-flow' : ''}
|
|
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}
|
|
onEdgeContextMenu={handleEdgeContextMenu}
|
|
onSelectionStart={handleSelectionStart}
|
|
onSelectionChange={handleSelectionChange}
|
|
onSelectionDrag={handleSelectionDrag}
|
|
onPaneContextMenu={handlePaneContextMenu}
|
|
onSelectionContextMenu={handleSelectionContextMenu}
|
|
connectionLineComponent={CustomConnectionLine}
|
|
defaultViewport={viewport}
|
|
multiSelectionKeyCode={null}
|
|
deleteKeyCode={null}
|
|
nodesDraggable={!nodesReadOnly && controlMode !== ControlMode.Comment}
|
|
nodesConnectable={!nodesReadOnly}
|
|
nodesFocusable={!nodesReadOnly}
|
|
edgesFocusable={!nodesReadOnly}
|
|
panOnScroll={controlMode === ControlMode.Pointer && !workflowReadOnly}
|
|
panOnDrag={controlMode === ControlMode.Hand || [1]}
|
|
zoomOnPinch={true}
|
|
zoomOnScroll={true}
|
|
zoomOnDoubleClick={true}
|
|
isValidConnection={isValidConnection}
|
|
selectionKeyCode={null}
|
|
selectionMode={SelectionMode.Partial}
|
|
selectionOnDrag={controlMode === ControlMode.Pointer && !workflowReadOnly}
|
|
minZoom={0.25}
|
|
>
|
|
<Background
|
|
gap={[14, 14]}
|
|
size={2}
|
|
className="bg-workflow-canvas-workflow-bg"
|
|
color="var(--color-workflow-canvas-workflow-dot-color)"
|
|
/>
|
|
{showUserCursors && cursors && (
|
|
<UserCursors
|
|
cursors={cursors}
|
|
myUserId={myUserId || null}
|
|
onlineUsers={onlineUsers || []}
|
|
/>
|
|
)}
|
|
</ReactFlow>
|
|
</WorkflowContextmenu>
|
|
<SyncingDataModal />
|
|
</div>
|
|
)
|
|
},
|
|
)
|
|
|
|
type WorkflowWithInnerContextProps = WorkflowProps & {
|
|
hooksStore?: Partial<HooksStoreShape>
|
|
cursors?: Record<string, CursorPosition>
|
|
myUserId?: string | null
|
|
onlineUsers?: OnlineUser[]
|
|
}
|
|
export const WorkflowWithInnerContext = memo(
|
|
({ hooksStore, cursors, myUserId, onlineUsers, ...restProps }: WorkflowWithInnerContextProps) => {
|
|
return (
|
|
<HooksStoreContextProvider {...hooksStore}>
|
|
<Workflow {...restProps} cursors={cursors} myUserId={myUserId} onlineUsers={onlineUsers} />
|
|
</HooksStoreContextProvider>
|
|
)
|
|
},
|
|
)
|
|
|
|
type WorkflowWithDefaultContextProps = Pick<WorkflowProps, 'edges' | 'nodes'> & {
|
|
children: React.ReactNode
|
|
}
|
|
|
|
const WorkflowHistoryStoreInitializer = ({
|
|
nodes,
|
|
edges,
|
|
children,
|
|
}: WorkflowWithDefaultContextProps) => {
|
|
const workflowStore = useWorkflowStore()
|
|
const initializedRef = useRef(false)
|
|
|
|
if (!initializedRef.current) {
|
|
workflowStore.temporal.getState().pause()
|
|
workflowStore.getState().setWorkflowHistory({
|
|
nodes,
|
|
edges,
|
|
workflowHistoryEvent: undefined,
|
|
workflowHistoryEventMeta: undefined,
|
|
})
|
|
workflowStore.temporal.getState().clear()
|
|
workflowStore.temporal.getState().resume()
|
|
initializedRef.current = true
|
|
}
|
|
|
|
return children
|
|
}
|
|
|
|
const WorkflowWithDefaultContext = ({
|
|
nodes,
|
|
edges,
|
|
children,
|
|
}: WorkflowWithDefaultContextProps) => {
|
|
return (
|
|
<ReactFlowProvider>
|
|
<WorkflowHistoryStoreInitializer nodes={nodes} edges={edges}>
|
|
<Suspense fallback={null}>
|
|
<WorkflowLocalStorageBridge />
|
|
</Suspense>
|
|
<DatasetsDetailProvider nodes={nodes}>{children}</DatasetsDetailProvider>
|
|
</WorkflowHistoryStoreInitializer>
|
|
</ReactFlowProvider>
|
|
)
|
|
}
|
|
|
|
export default memo(WorkflowWithDefaultContext)
|