mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-12 04:16:54 +08:00
feat: workflow interaction (#4214)
This commit is contained in:
@@ -9,3 +9,6 @@ export * from './use-workflow-template'
|
||||
export * from './use-checklist'
|
||||
export * from './use-workflow-mode'
|
||||
export * from './use-workflow-interactions'
|
||||
export * from './use-selection-interactions'
|
||||
export * from './use-panel-interactions'
|
||||
export * from './use-workflow-start-run'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
@@ -11,6 +12,7 @@ import type {
|
||||
import {
|
||||
getConnectedEdges,
|
||||
getOutgoers,
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import type { ToolDefaultValue } from '../block-selector/types'
|
||||
@@ -29,6 +31,7 @@ import {
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodesConnectedSourceOrTargetHandleIdsMap,
|
||||
getTopLeftNodePosition,
|
||||
} from '../utils'
|
||||
import { useNodesExtraData } from './use-nodes-data'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
@@ -41,6 +44,7 @@ export const useNodesInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const {
|
||||
@@ -705,132 +709,6 @@ export const useNodesInteractions = () => {
|
||||
handleSyncWorkflowDraft()
|
||||
}, [store, handleSyncWorkflowDraft, getNodesReadOnly, t])
|
||||
|
||||
const handleNodeCopySelected = useCallback((): undefined | Node[] => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
setClipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const nodesToCopy = nodes.filter(node => node.data.selected && node.data.type !== BlockEnum.Start)
|
||||
|
||||
setClipboardElements(nodesToCopy)
|
||||
|
||||
return nodesToCopy
|
||||
}, [getNodesReadOnly, store, workflowStore])
|
||||
|
||||
const handleNodePaste = useCallback((): undefined | Node[] => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
clipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodesToPaste: Node[] = []
|
||||
const nodes = getNodes()
|
||||
|
||||
for (const nodeToPaste of clipboardElements) {
|
||||
const nodeType = nodeToPaste.data.type
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
...nodeToPaste.data,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
selected: true,
|
||||
},
|
||||
position: {
|
||||
x: nodeToPaste.position.x + 10,
|
||||
y: nodeToPaste.position.y + 10,
|
||||
},
|
||||
})
|
||||
nodesToPaste.push(newNode)
|
||||
}
|
||||
|
||||
setNodes([...nodes.map((n: Node) => ({ ...n, selected: false, data: { ...n.data, selected: false } })), ...nodesToPaste])
|
||||
|
||||
handleSyncWorkflowDraft()
|
||||
|
||||
return nodesToPaste
|
||||
}, [getNodesReadOnly, handleSyncWorkflowDraft, store, t, workflowStore])
|
||||
|
||||
const handleNodeDuplicateSelected = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
handleNodeCopySelected()
|
||||
handleNodePaste()
|
||||
}, [getNodesReadOnly, handleNodeCopySelected, handleNodePaste])
|
||||
|
||||
const handleNodeCut = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const nodesToCut = handleNodeCopySelected()
|
||||
if (!nodesToCut)
|
||||
return
|
||||
|
||||
for (const node of nodesToCut)
|
||||
handleNodeDelete(node.id)
|
||||
}, [getNodesReadOnly, handleNodeCopySelected, handleNodeDelete])
|
||||
|
||||
const handleNodeDeleteSelected = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
|
||||
const currentEdgeIndex = edges.findIndex(edge => edge.selected)
|
||||
|
||||
if (currentEdgeIndex > -1)
|
||||
return
|
||||
|
||||
const nodes = getNodes()
|
||||
const nodesToDelete = nodes.filter(node => node.data.selected)
|
||||
|
||||
if (!nodesToDelete)
|
||||
return
|
||||
|
||||
for (const node of nodesToDelete)
|
||||
handleNodeDelete(node.id)
|
||||
}, [getNodesReadOnly, handleNodeDelete, store, workflowStore])
|
||||
|
||||
const handleNodeCancelRunningStatus = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
@@ -861,6 +739,173 @@ export const useNodesInteractions = () => {
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
const handleNodeContextMenu = useCallback((e: MouseEvent, node: Node) => {
|
||||
e.preventDefault()
|
||||
const container = document.querySelector('#workflow-container')
|
||||
const { x, y } = container!.getBoundingClientRect()
|
||||
workflowStore.setState({
|
||||
nodeMenu: {
|
||||
top: e.clientY - y,
|
||||
left: e.clientX - x,
|
||||
nodeId: node.id,
|
||||
},
|
||||
})
|
||||
handleNodeSelect(node.id)
|
||||
}, [workflowStore, handleNodeSelect])
|
||||
|
||||
const handleNodesCopy = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
setClipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (bundledNodes.length) {
|
||||
setClipboardElements(bundledNodes)
|
||||
return
|
||||
}
|
||||
|
||||
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (selectedNode)
|
||||
setClipboardElements([selectedNode])
|
||||
}, [getNodesReadOnly, store, workflowStore])
|
||||
|
||||
const handleNodesPaste = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
clipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
mousePosition,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodesToPaste: Node[] = []
|
||||
const nodes = getNodes()
|
||||
|
||||
if (clipboardElements.length) {
|
||||
const { x, y } = getTopLeftNodePosition(clipboardElements)
|
||||
const { screenToFlowPosition } = reactflow
|
||||
const currentPosition = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||
const offsetX = currentPosition.x - x
|
||||
const offsetY = currentPosition.y - y
|
||||
clipboardElements.forEach((nodeToPaste, index) => {
|
||||
const nodeType = nodeToPaste.data.type
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
...nodeToPaste.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
},
|
||||
position: {
|
||||
x: nodeToPaste.position.x + offsetX,
|
||||
y: nodeToPaste.position.y + offsetY,
|
||||
},
|
||||
})
|
||||
newNode.id = newNode.id + index
|
||||
nodesToPaste.push(newNode)
|
||||
})
|
||||
|
||||
setNodes([...nodes, ...nodesToPaste])
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, [t, getNodesReadOnly, store, workflowStore, handleSyncWorkflowDraft, reactflow])
|
||||
|
||||
const handleNodesDuplicate = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (selectedNode) {
|
||||
const nodeType = selectedNode.data.type
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType as BlockEnum],
|
||||
...selectedNode.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
},
|
||||
position: {
|
||||
x: selectedNode.position.x + selectedNode.width! + 10,
|
||||
y: selectedNode.position.y,
|
||||
},
|
||||
})
|
||||
|
||||
setNodes([...nodes, newNode])
|
||||
}
|
||||
}, [store, t, getNodesReadOnly])
|
||||
|
||||
const handleNodesDelete = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (bundledNodes.length) {
|
||||
bundledNodes.forEach(node => handleNodeDelete(node.id))
|
||||
return
|
||||
}
|
||||
|
||||
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (selectedNode)
|
||||
handleNodeDelete(selectedNode.id)
|
||||
}, [store, workflowStore, getNodesReadOnly, handleNodeDelete])
|
||||
|
||||
return {
|
||||
handleNodeDragStart,
|
||||
handleNodeDrag,
|
||||
@@ -875,12 +920,12 @@ export const useNodesInteractions = () => {
|
||||
handleNodeDelete,
|
||||
handleNodeChange,
|
||||
handleNodeAdd,
|
||||
handleNodeDuplicateSelected,
|
||||
handleNodeCopySelected,
|
||||
handleNodeCut,
|
||||
handleNodeDeleteSelected,
|
||||
handleNodePaste,
|
||||
handleNodeCancelRunningStatus,
|
||||
handleNodesCancelSelected,
|
||||
handleNodeContextMenu,
|
||||
handleNodesCopy,
|
||||
handleNodesPaste,
|
||||
handleNodesDuplicate,
|
||||
handleNodesDelete,
|
||||
}
|
||||
}
|
||||
|
||||
37
web/app/components/workflow/hooks/use-panel-interactions.ts
Normal file
37
web/app/components/workflow/hooks/use-panel-interactions.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useWorkflowStore } from '../store'
|
||||
|
||||
export const usePanelInteractions = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handlePaneContextMenu = useCallback((e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
const container = document.querySelector('#workflow-container')
|
||||
const { x, y } = container!.getBoundingClientRect()
|
||||
workflowStore.setState({
|
||||
panelMenu: {
|
||||
top: e.clientY - y,
|
||||
left: e.clientX - x,
|
||||
},
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
const handlePaneContextmenuCancel = useCallback(() => {
|
||||
workflowStore.setState({
|
||||
panelMenu: undefined,
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
const handleNodeContextmenuCancel = useCallback(() => {
|
||||
workflowStore.setState({
|
||||
nodeMenu: undefined,
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handlePaneContextMenu,
|
||||
handlePaneContextmenuCancel,
|
||||
handleNodeContextmenuCancel,
|
||||
}
|
||||
}
|
||||
109
web/app/components/workflow/hooks/use-selection-interactions.ts
Normal file
109
web/app/components/workflow/hooks/use-selection-interactions.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import produce from 'immer'
|
||||
import type {
|
||||
OnSelectionChangeFunc,
|
||||
} from 'reactflow'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import type { Node } from '../types'
|
||||
|
||||
export const useSelectionInteractions = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleSelectionStart = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
userSelectionRect,
|
||||
} = store.getState()
|
||||
|
||||
if (!userSelectionRect?.width || !userSelectionRect?.height) {
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (node.data._isBundled)
|
||||
node.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
if (edge.data._isBundled)
|
||||
edge.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const handleSelectionChange = useCallback<OnSelectionChangeFunc>(({ nodes: nodesInSelection, edges: edgesInSelection }) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
userSelectionRect,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
|
||||
if (!userSelectionRect?.width || !userSelectionRect?.height)
|
||||
return
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
const nodeInSelection = nodesInSelection.find(n => n.id === node.id)
|
||||
|
||||
if (nodeInSelection)
|
||||
node.data._isBundled = true
|
||||
else
|
||||
node.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
const edgeInSelection = edgesInSelection.find(e => e.id === edge.id)
|
||||
|
||||
if (edgeInSelection)
|
||||
edge.data._isBundled = true
|
||||
else
|
||||
edge.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store])
|
||||
|
||||
const handleSelectionDrag = useCallback((_: MouseEvent, nodesWithDrag: Node[]) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
workflowStore.setState({
|
||||
nodeAnimation: false,
|
||||
})
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
const dragNode = nodesWithDrag.find(n => n.id === node.id)
|
||||
|
||||
if (dragNode)
|
||||
node.position = dragNode.position
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store, workflowStore])
|
||||
|
||||
return {
|
||||
handleSelectionStart,
|
||||
handleSelectionChange,
|
||||
handleSelectionDrag,
|
||||
}
|
||||
}
|
||||
88
web/app/components/workflow/hooks/use-workflow-start-run.tsx
Normal file
88
web/app/components/workflow/hooks/use-workflow-start-run.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import {
|
||||
BlockEnum,
|
||||
WorkflowRunningStatus,
|
||||
} from '../types'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesSyncDraft,
|
||||
useWorkflowInteractions,
|
||||
useWorkflowRun,
|
||||
} from './index'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
|
||||
export const useWorkflowStartRun = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const isChatMode = useIsChatMode()
|
||||
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||
const { handleRun } = useWorkflowRun()
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const handleWorkflowStartRunInWorkflow = useCallback(async () => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (workflowRunningData?.result.status === WorkflowRunningStatus.Running)
|
||||
return
|
||||
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
const startVariables = startNode?.data.variables || []
|
||||
const fileSettings = featuresStore!.getState().features.file
|
||||
const {
|
||||
showDebugAndPreviewPanel,
|
||||
setShowDebugAndPreviewPanel,
|
||||
setShowInputsPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (showDebugAndPreviewPanel) {
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
return
|
||||
}
|
||||
|
||||
if (!startVariables.length && !fileSettings?.image?.enabled) {
|
||||
await doSyncWorkflowDraft()
|
||||
handleRun({ inputs: {}, files: [] })
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
setShowInputsPanel(false)
|
||||
}
|
||||
else {
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
setShowInputsPanel(true)
|
||||
}
|
||||
}, [store, workflowStore, featuresStore, handleCancelDebugAndPreviewPanel, handleRun, doSyncWorkflowDraft])
|
||||
|
||||
const handleWorkflowStartRunInChatflow = useCallback(async () => {
|
||||
const {
|
||||
showDebugAndPreviewPanel,
|
||||
setShowDebugAndPreviewPanel,
|
||||
setHistoryWorkflowData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (showDebugAndPreviewPanel)
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
else
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
|
||||
setHistoryWorkflowData(undefined)
|
||||
}, [workflowStore, handleCancelDebugAndPreviewPanel])
|
||||
|
||||
const handleStartWorkflowRun = useCallback(() => {
|
||||
if (!isChatMode)
|
||||
handleWorkflowStartRunInWorkflow()
|
||||
else
|
||||
handleWorkflowStartRunInChatflow()
|
||||
}, [isChatMode, handleWorkflowStartRunInWorkflow, handleWorkflowStartRunInChatflow])
|
||||
|
||||
return {
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user