feat: workflow interaction (#4214)

This commit is contained in:
zxhlyh
2024-05-09 17:18:51 +08:00
committed by GitHub
parent 487ce7c82a
commit 9b24f12bf5
54 changed files with 1955 additions and 431 deletions

View File

@@ -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'

View File

@@ -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,
}
}

View 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,
}
}

View 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,
}
}

View 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,
}
}