mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-15 13:56:53 +08:00
FEAT: NEW WORKFLOW ENGINE (#3160)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Yeuoly <admin@srmxy.cn> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: jyong <jyong@dify.ai> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
9
web/app/components/workflow/hooks/index.ts
Normal file
9
web/app/components/workflow/hooks/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './use-edges-interactions'
|
||||
export * from './use-node-data-update'
|
||||
export * from './use-nodes-interactions'
|
||||
export * from './use-nodes-data'
|
||||
export * from './use-nodes-sync-draft'
|
||||
export * from './use-workflow'
|
||||
export * from './use-workflow-run'
|
||||
export * from './use-workflow-template'
|
||||
export * from './use-checklist'
|
||||
152
web/app/components/workflow/hooks/use-checklist.ts
Normal file
152
web/app/components/workflow/hooks/use-checklist.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
} from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import { useStore } from '../store'
|
||||
import {
|
||||
getToolCheckParams,
|
||||
getValidTreeNodes,
|
||||
} from '../utils'
|
||||
import { MAX_TREE_DEEPTH } from '../constants'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
import { useNodesExtraData } from './use-nodes-data'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
|
||||
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const isChatMode = useIsChatMode()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
|
||||
const needWarningNodes = useMemo(() => {
|
||||
const list = []
|
||||
const { validNodes } = getValidTreeNodes(nodes, edges)
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
let toolIcon
|
||||
let moreDataForCheckValid
|
||||
|
||||
if (node.data.type === BlockEnum.Tool) {
|
||||
const { provider_type } = node.data
|
||||
const isBuiltIn = provider_type === CollectionType.builtIn
|
||||
|
||||
moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools, customTools, language)
|
||||
if (isBuiltIn)
|
||||
toolIcon = buildInTools.find(tool => tool.id === node.data.provider_id)?.icon
|
||||
|
||||
if (!isBuiltIn)
|
||||
toolIcon = customTools.find(tool => tool.id === node.data.provider_id)?.icon
|
||||
}
|
||||
const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t, moreDataForCheckValid)
|
||||
|
||||
if (errorMessage || !validNodes.find(n => n.id === node.id)) {
|
||||
list.push({
|
||||
id: node.id,
|
||||
type: node.data.type,
|
||||
title: node.data.title,
|
||||
toolIcon,
|
||||
unConnected: !validNodes.find(n => n.id === node.id),
|
||||
errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isChatMode && !nodes.find(node => node.data.type === BlockEnum.Answer)) {
|
||||
list.push({
|
||||
id: 'answer-need-added',
|
||||
type: BlockEnum.Answer,
|
||||
title: t('workflow.blocks.answer'),
|
||||
errorMessage: t('workflow.common.needAnswerNode'),
|
||||
})
|
||||
}
|
||||
|
||||
if (!isChatMode && !nodes.find(node => node.data.type === BlockEnum.End)) {
|
||||
list.push({
|
||||
id: 'end-need-added',
|
||||
type: BlockEnum.End,
|
||||
title: t('workflow.blocks.end'),
|
||||
errorMessage: t('workflow.common.needEndNode'),
|
||||
})
|
||||
}
|
||||
|
||||
return list
|
||||
}, [t, nodes, edges, nodesExtraData, buildInTools, customTools, language, isChatMode])
|
||||
|
||||
return needWarningNodes
|
||||
}
|
||||
|
||||
export const useChecklistBeforePublish = () => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const { notify } = useToastContext()
|
||||
const isChatMode = useIsChatMode()
|
||||
const store = useStoreApi()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
|
||||
const handleCheckBeforePublish = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const {
|
||||
validNodes,
|
||||
maxDepth,
|
||||
} = getValidTreeNodes(nodes, edges)
|
||||
|
||||
if (maxDepth > MAX_TREE_DEEPTH) {
|
||||
notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEEPTH }) })
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
let moreDataForCheckValid
|
||||
if (node.data.type === BlockEnum.Tool)
|
||||
moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools, customTools, language)
|
||||
|
||||
const { errorMessage } = nodesExtraData[node.data.type as BlockEnum].checkValid(node.data, t, moreDataForCheckValid)
|
||||
|
||||
if (errorMessage) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${errorMessage}` })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!validNodes.find(n => n.id === node.id)) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.common.needConnecttip')}` })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (isChatMode && !nodes.find(node => node.data.type === BlockEnum.Answer)) {
|
||||
notify({ type: 'error', message: t('workflow.common.needAnswerNode') })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isChatMode && !nodes.find(node => node.data.type === BlockEnum.End)) {
|
||||
notify({ type: 'error', message: t('workflow.common.needEndNode') })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, [nodesExtraData, notify, t, store, isChatMode, buildInTools, customTools, language])
|
||||
|
||||
return {
|
||||
handleCheckBeforePublish,
|
||||
}
|
||||
}
|
||||
212
web/app/components/workflow/hooks/use-edges-interactions.ts
Normal file
212
web/app/components/workflow/hooks/use-edges-interactions.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import type {
|
||||
EdgeMouseHandler,
|
||||
OnEdgesChange,
|
||||
} from 'reactflow'
|
||||
import {
|
||||
getConnectedEdges,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
} from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import { getNodesConnectedSourceOrTargetHandleIdsMap } from '../utils'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
|
||||
export const useEdgesInteractions = () => {
|
||||
const store = useStoreApi()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
|
||||
const handleEdgeEnter = useCallback<EdgeMouseHandler>((_, edge) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const currentEdge = draft.find(e => e.id === edge.id)!
|
||||
|
||||
currentEdge.data._hovering = true
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store, getNodesReadOnly])
|
||||
|
||||
const handleEdgeLeave = useCallback<EdgeMouseHandler>((_, edge) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const currentEdge = draft.find(e => e.id === edge.id)!
|
||||
|
||||
currentEdge.data._hovering = false
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store, getNodesReadOnly])
|
||||
|
||||
const handleEdgeDeleteByDeleteBranch = useCallback((nodeId: string, branchId: string) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const currentEdgeIndex = edges.findIndex(edge => edge.source === nodeId && edge.sourceHandle === branchId)
|
||||
|
||||
if (currentEdgeIndex < 0)
|
||||
return
|
||||
|
||||
const currentEdge = edges[currentEdgeIndex]
|
||||
const newNodes = produce(getNodes(), (draft: Node[]) => {
|
||||
const sourceNode = draft.find(node => node.id === currentEdge.source)
|
||||
const targetNode = draft.find(node => node.id === currentEdge.target)
|
||||
|
||||
if (sourceNode)
|
||||
sourceNode.data._connectedSourceHandleIds = sourceNode.data._connectedSourceHandleIds?.filter(handleId => handleId !== currentEdge.sourceHandle)
|
||||
|
||||
if (targetNode)
|
||||
targetNode.data._connectedTargetHandleIds = targetNode.data._connectedTargetHandleIds?.filter(handleId => handleId !== currentEdge.targetHandle)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.splice(currentEdgeIndex, 1)
|
||||
})
|
||||
setEdges(newEdges)
|
||||
handleSyncWorkflowDraft()
|
||||
}, [store, handleSyncWorkflowDraft, getNodesReadOnly])
|
||||
|
||||
const handleEdgeDelete = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const currentEdgeIndex = edges.findIndex(edge => edge.selected)
|
||||
|
||||
if (currentEdgeIndex < 0)
|
||||
return
|
||||
const currentEdge = edges[currentEdgeIndex]
|
||||
const nodes = getNodes()
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||
[
|
||||
{ type: 'remove', edge: currentEdge },
|
||||
],
|
||||
nodes,
|
||||
)
|
||||
const newNodes = produce(nodes, (draft: Node[]) => {
|
||||
draft.forEach((node) => {
|
||||
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.splice(currentEdgeIndex, 1)
|
||||
})
|
||||
setEdges(newEdges)
|
||||
handleSyncWorkflowDraft()
|
||||
}, [store, getNodesReadOnly, handleSyncWorkflowDraft])
|
||||
|
||||
const handleEdgesChange = useCallback<OnEdgesChange>((changes) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
changes.forEach((change) => {
|
||||
if (change.type === 'select')
|
||||
draft.find(edge => edge.id === change.id)!.selected = change.selected
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store, getNodesReadOnly])
|
||||
|
||||
const handleVariableAssignerEdgesChange = useCallback((nodeId: string, variables: any) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const newEdgesTargetHandleIds = variables.map((item: any) => item[0])
|
||||
const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges).filter(edge => edge.target === nodeId)
|
||||
const needDeleteEdges = connectedEdges.filter(edge => !newEdgesTargetHandleIds.includes(edge.targetHandle))
|
||||
const needAddEdgesTargetHandleIds = newEdgesTargetHandleIds.filter((targetHandle: string) => !connectedEdges.some(edge => edge.targetHandle === targetHandle))
|
||||
const needAddEdges = needAddEdgesTargetHandleIds.map((targetHandle: string) => {
|
||||
return {
|
||||
id: `${targetHandle}-${nodeId}`,
|
||||
type: 'custom',
|
||||
source: targetHandle,
|
||||
sourceHandle: 'source',
|
||||
target: nodeId,
|
||||
targetHandle,
|
||||
data: {
|
||||
sourceType: nodes.find(node => node.id === targetHandle)?.data.type,
|
||||
targetType: BlockEnum.VariableAssigner,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||
[
|
||||
...needDeleteEdges.map(edge => ({ type: 'remove', edge })),
|
||||
...needAddEdges.map((edge: Edge) => ({ type: 'add', edge })),
|
||||
],
|
||||
nodes,
|
||||
)
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const filtered = draft.filter(edge => !needDeleteEdges.map(needDeleteEdge => needDeleteEdge.id).includes(edge.id))
|
||||
|
||||
filtered.push(...needAddEdges)
|
||||
|
||||
return filtered
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
handleEdgeEnter,
|
||||
handleEdgeLeave,
|
||||
handleEdgeDeleteByDeleteBranch,
|
||||
handleEdgeDelete,
|
||||
handleEdgesChange,
|
||||
handleVariableAssignerEdgesChange,
|
||||
}
|
||||
}
|
||||
42
web/app/components/workflow/hooks/use-node-data-update.ts
Normal file
42
web/app/components/workflow/hooks/use-node-data-update.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
|
||||
type NodeDataUpdatePayload = {
|
||||
id: string
|
||||
data: Record<string, any>
|
||||
}
|
||||
|
||||
export const useNodeDataUpdate = () => {
|
||||
const store = useStoreApi()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
|
||||
const handleNodeDataUpdate = useCallback(({ id, data }: NodeDataUpdatePayload) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
const currentNode = draft.find(node => node.id === id)!
|
||||
|
||||
currentNode.data = { ...currentNode.data, ...data }
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
const handleNodeDataUpdateWithSyncDraft = useCallback((payload: NodeDataUpdatePayload) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
handleNodeDataUpdate(payload)
|
||||
handleSyncWorkflowDraft()
|
||||
}, [handleSyncWorkflowDraft, handleNodeDataUpdate, getNodesReadOnly])
|
||||
|
||||
return {
|
||||
handleNodeDataUpdate,
|
||||
handleNodeDataUpdateWithSyncDraft,
|
||||
}
|
||||
}
|
||||
32
web/app/components/workflow/hooks/use-nodes-data.ts
Normal file
32
web/app/components/workflow/hooks/use-nodes-data.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import type { BlockEnum } from '../types'
|
||||
import {
|
||||
NODES_EXTRA_DATA,
|
||||
NODES_INITIAL_DATA,
|
||||
} from '../constants'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
|
||||
export const useNodesInitialData = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(() => produce(NODES_INITIAL_DATA, (draft) => {
|
||||
Object.keys(draft).forEach((key) => {
|
||||
draft[key as BlockEnum].title = t(`workflow.blocks.${key}`)
|
||||
})
|
||||
}), [t])
|
||||
}
|
||||
|
||||
export const useNodesExtraData = () => {
|
||||
const { t } = useTranslation()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
return useMemo(() => produce(NODES_EXTRA_DATA, (draft) => {
|
||||
Object.keys(draft).forEach((key) => {
|
||||
draft[key as BlockEnum].about = t(`workflow.blocksAbout.${key}`)
|
||||
draft[key as BlockEnum].availablePrevNodes = draft[key as BlockEnum].getAvailablePrevNodes(isChatMode)
|
||||
draft[key as BlockEnum].availableNextNodes = draft[key as BlockEnum].getAvailableNextNodes(isChatMode)
|
||||
})
|
||||
}), [t, isChatMode])
|
||||
}
|
||||
721
web/app/components/workflow/hooks/use-nodes-interactions.ts
Normal file
721
web/app/components/workflow/hooks/use-nodes-interactions.ts
Normal file
@@ -0,0 +1,721 @@
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import type {
|
||||
HandleType,
|
||||
NodeDragHandler,
|
||||
NodeMouseHandler,
|
||||
OnConnect,
|
||||
OnConnectStart,
|
||||
} from 'reactflow'
|
||||
import {
|
||||
getConnectedEdges,
|
||||
getOutgoers,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import type { ToolDefaultValue } from '../block-selector/types'
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
OnNodeAdd,
|
||||
} from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import {
|
||||
NODES_INITIAL_DATA,
|
||||
NODE_WIDTH_X_OFFSET,
|
||||
Y_OFFSET,
|
||||
} from '../constants'
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodesConnectedSourceOrTargetHandleIdsMap,
|
||||
} from '../utils'
|
||||
import { useNodesExtraData } from './use-nodes-data'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from './use-workflow'
|
||||
|
||||
export const useNodesInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { getAfterNodesInSameBranch } = useWorkflow()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const dragNodeStartPosition = useRef({ x: 0, y: 0 } as { x: number; y: number })
|
||||
const connectingNodeRef = useRef<{ nodeId: string; handleType: HandleType } | null>(null)
|
||||
|
||||
const handleNodeDragStart = useCallback<NodeDragHandler>((_, node) => {
|
||||
workflowStore.setState({ nodeAnimation: false })
|
||||
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
dragNodeStartPosition.current = { x: node.position.x, y: node.position.y }
|
||||
}, [workflowStore, getNodesReadOnly])
|
||||
|
||||
const handleNodeDrag = useCallback<NodeDragHandler>((e, node: Node) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const {
|
||||
setHelpLineHorizontal,
|
||||
setHelpLineVertical,
|
||||
} = workflowStore.getState()
|
||||
e.stopPropagation()
|
||||
|
||||
const nodes = getNodes()
|
||||
|
||||
const showHorizontalHelpLineNodes = nodes.filter((n) => {
|
||||
if (n.id === node.id)
|
||||
return false
|
||||
|
||||
const nY = Math.ceil(n.position.y)
|
||||
const nodeY = Math.ceil(node.position.y)
|
||||
|
||||
if (nY - nodeY < 5 && nY - nodeY > -5)
|
||||
return true
|
||||
|
||||
return false
|
||||
}).sort((a, b) => a.position.x - b.position.x)
|
||||
const showHorizontalHelpLineNodesLength = showHorizontalHelpLineNodes.length
|
||||
if (showHorizontalHelpLineNodesLength > 0) {
|
||||
const first = showHorizontalHelpLineNodes[0]
|
||||
const last = showHorizontalHelpLineNodes[showHorizontalHelpLineNodesLength - 1]
|
||||
|
||||
const helpLine = {
|
||||
top: first.position.y,
|
||||
left: first.position.x,
|
||||
width: last.position.x + last.width! - first.position.x,
|
||||
}
|
||||
|
||||
if (node.position.x < first.position.x) {
|
||||
helpLine.left = node.position.x
|
||||
helpLine.width = first.position.x + first.width! - node.position.x
|
||||
}
|
||||
|
||||
if (node.position.x > last.position.x)
|
||||
helpLine.width = node.position.x + node.width! - first.position.x
|
||||
|
||||
setHelpLineHorizontal(helpLine)
|
||||
}
|
||||
else {
|
||||
setHelpLineHorizontal()
|
||||
}
|
||||
|
||||
const showVerticalHelpLineNodes = nodes.filter((n) => {
|
||||
if (n.id === node.id)
|
||||
return false
|
||||
|
||||
const nX = Math.ceil(n.position.x)
|
||||
const nodeX = Math.ceil(node.position.x)
|
||||
|
||||
if (nX - nodeX < 5 && nX - nodeX > -5)
|
||||
return true
|
||||
|
||||
return false
|
||||
}).sort((a, b) => a.position.x - b.position.x)
|
||||
const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length
|
||||
|
||||
if (showVerticalHelpLineNodesLength > 0) {
|
||||
const first = showVerticalHelpLineNodes[0]
|
||||
const last = showVerticalHelpLineNodes[showVerticalHelpLineNodesLength - 1]
|
||||
|
||||
const helpLine = {
|
||||
top: first.position.y,
|
||||
left: first.position.x,
|
||||
height: last.position.y + last.height! - first.position.y,
|
||||
}
|
||||
|
||||
if (node.position.y < first.position.y) {
|
||||
helpLine.top = node.position.y
|
||||
helpLine.height = first.position.y + first.height! - node.position.y
|
||||
}
|
||||
|
||||
if (node.position.y > last.position.y)
|
||||
helpLine.height = node.position.y + node.height! - first.position.y
|
||||
|
||||
setHelpLineVertical(helpLine)
|
||||
}
|
||||
else {
|
||||
setHelpLineVertical()
|
||||
}
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(n => n.id === node.id)!
|
||||
|
||||
currentNode.position = {
|
||||
x: showVerticalHelpLineNodesLength > 0 ? showVerticalHelpLineNodes[0].position.x : node.position.x,
|
||||
y: showHorizontalHelpLineNodesLength > 0 ? showHorizontalHelpLineNodes[0].position.y : node.position.y,
|
||||
}
|
||||
})
|
||||
|
||||
setNodes(newNodes)
|
||||
}, [store, workflowStore, getNodesReadOnly])
|
||||
|
||||
const handleNodeDragStop = useCallback<NodeDragHandler>((_, node) => {
|
||||
const {
|
||||
setHelpLineHorizontal,
|
||||
setHelpLineVertical,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const { x, y } = dragNodeStartPosition.current
|
||||
if (!(x === node.position.x && y === node.position.y)) {
|
||||
setHelpLineHorizontal()
|
||||
setHelpLineVertical()
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, [handleSyncWorkflowDraft, workflowStore, getNodesReadOnly])
|
||||
|
||||
const handleNodeEnter = useCallback<NodeMouseHandler>((_, node) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
if (connectingNodeRef.current && connectingNodeRef.current.nodeId !== node.id) {
|
||||
const connectingNode: Node = nodes.find(n => n.id === connectingNodeRef.current!.nodeId)!
|
||||
const handleType = connectingNodeRef.current.handleType
|
||||
const currentNodeIndex = nodes.findIndex(n => n.id === node.id)
|
||||
const availablePrevNodes = nodesExtraData[connectingNode.data.type].availablePrevNodes
|
||||
const availableNextNodes = nodesExtraData[connectingNode.data.type].availableNextNodes
|
||||
const availableNodes = handleType === 'source' ? availableNextNodes : [...availablePrevNodes, BlockEnum.Start]
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
if (!availableNodes.includes(draft[currentNodeIndex].data.type))
|
||||
draft[currentNodeIndex].data._isInvalidConnection = true
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const connectedEdges = getConnectedEdges([node], edges)
|
||||
|
||||
connectedEdges.forEach((edge) => {
|
||||
const currentEdge = draft.find(e => e.id === edge.id)
|
||||
if (currentEdge)
|
||||
currentEdge.data._connectedNodeIsHovering = true
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store, nodesExtraData, getNodesReadOnly])
|
||||
|
||||
const handleNodeLeave = useCallback<NodeMouseHandler>(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data._isInvalidConnection = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
edge.data._connectedNodeIsHovering = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store, getNodesReadOnly])
|
||||
|
||||
const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean) => {
|
||||
if (getNodesReadOnly() && !workflowStore.getState().isRestoring)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const selectedNode = nodes.find(node => node.data.selected)
|
||||
|
||||
if (!cancelSelection && selectedNode?.id === nodeId)
|
||||
return
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (node.id === nodeId)
|
||||
node.data.selected = !cancelSelection
|
||||
else
|
||||
node.data.selected = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
|
||||
const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges).map(edge => edge.id)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
if (connectedEdges.includes(edge.id)) {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_connectedNodeIsSelected: !cancelSelection,
|
||||
}
|
||||
}
|
||||
else {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_connectedNodeIsSelected: false,
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
|
||||
handleSyncWorkflowDraft()
|
||||
}, [store, handleSyncWorkflowDraft, getNodesReadOnly, workflowStore])
|
||||
|
||||
const handleNodeClick = useCallback<NodeMouseHandler>((_, node) => {
|
||||
if (getNodesReadOnly() && !workflowStore.getState().isRestoring)
|
||||
return
|
||||
|
||||
handleNodeSelect(node.id)
|
||||
}, [handleNodeSelect, getNodesReadOnly, workflowStore])
|
||||
|
||||
const handleNodeConnect = useCallback<OnConnect>(({
|
||||
source,
|
||||
sourceHandle,
|
||||
target,
|
||||
targetHandle,
|
||||
}) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const needDeleteEdges = edges.filter((edge) => {
|
||||
if (edge.source === source) {
|
||||
if (edge.sourceHandle)
|
||||
return edge.sourceHandle === sourceHandle
|
||||
else
|
||||
return true
|
||||
}
|
||||
if (edge.target === target) {
|
||||
if (edge.targetHandle)
|
||||
return edge.targetHandle === targetHandle
|
||||
else
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
const needDeleteEdgesIds = needDeleteEdges.map(edge => edge.id)
|
||||
const newEdge = {
|
||||
id: `${source}-${target}`,
|
||||
type: 'custom',
|
||||
source: source!,
|
||||
target: target!,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
data: {
|
||||
sourceType: nodes.find(node => node.id === source)!.data.type,
|
||||
targetType: nodes.find(node => node.id === target)!.data.type,
|
||||
},
|
||||
}
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||
[
|
||||
...needDeleteEdges.map(edge => ({ type: 'remove', edge })),
|
||||
{ type: 'add', edge: newEdge },
|
||||
],
|
||||
nodes,
|
||||
)
|
||||
const newNodes = produce(nodes, (draft: Node[]) => {
|
||||
draft.forEach((node) => {
|
||||
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const filtered = draft.filter(edge => !needDeleteEdgesIds.includes(edge.id))
|
||||
|
||||
filtered.push(newEdge)
|
||||
|
||||
return filtered
|
||||
})
|
||||
setEdges(newEdges)
|
||||
handleSyncWorkflowDraft()
|
||||
}, [store, handleSyncWorkflowDraft, getNodesReadOnly])
|
||||
|
||||
const handleNodeConnectStart = useCallback<OnConnectStart>((_, { nodeId, handleType }) => {
|
||||
if (nodeId && handleType) {
|
||||
connectingNodeRef.current = {
|
||||
nodeId,
|
||||
handleType,
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleNodeConnectEnd = useCallback(() => {
|
||||
connectingNodeRef.current = null
|
||||
}, [])
|
||||
|
||||
const handleNodeDelete = useCallback((nodeId: string) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const currentNodeIndex = nodes.findIndex(node => node.id === nodeId)
|
||||
const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges)
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(connectedEdges.map(edge => ({ type: 'remove', edge })), nodes)
|
||||
const newNodes = produce(nodes, (draft: Node[]) => {
|
||||
draft.forEach((node) => {
|
||||
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
|
||||
}
|
||||
}
|
||||
})
|
||||
draft.splice(currentNodeIndex, 1)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
return draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id))
|
||||
})
|
||||
setEdges(newEdges)
|
||||
handleSyncWorkflowDraft()
|
||||
}, [store, handleSyncWorkflowDraft, getNodesReadOnly])
|
||||
|
||||
const handleNodeAdd = useCallback<OnNodeAdd>((
|
||||
{
|
||||
nodeType,
|
||||
sourceHandle = 'source',
|
||||
targetHandle = 'target',
|
||||
toolDefaultValue,
|
||||
},
|
||||
{
|
||||
prevNodeId,
|
||||
prevNodeSourceHandle,
|
||||
nextNodeId,
|
||||
nextNodeTargetHandle,
|
||||
},
|
||||
) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (nodeType === BlockEnum.VariableAssigner)
|
||||
targetHandle = 'varNotSet'
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
...(toolDefaultValue || {}),
|
||||
selected: true,
|
||||
},
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
})
|
||||
if (prevNodeId && !nextNodeId) {
|
||||
const prevNodeIndex = nodes.findIndex(node => node.id === prevNodeId)
|
||||
const prevNode = nodes[prevNodeIndex]
|
||||
const outgoers = getOutgoers(prevNode, nodes, edges).sort((a, b) => a.position.y - b.position.y)
|
||||
const lastOutgoer = outgoers[outgoers.length - 1]
|
||||
if (prevNode.data.type === BlockEnum.KnowledgeRetrieval)
|
||||
targetHandle = prevNodeId
|
||||
newNode.data._connectedTargetHandleIds = [targetHandle]
|
||||
newNode.data._connectedSourceHandleIds = []
|
||||
newNode.position = {
|
||||
x: lastOutgoer ? lastOutgoer.position.x : prevNode.position.x + NODE_WIDTH_X_OFFSET,
|
||||
y: lastOutgoer ? lastOutgoer.position.y + lastOutgoer.height! + Y_OFFSET : prevNode.position.y,
|
||||
}
|
||||
|
||||
const newEdge = {
|
||||
id: `${prevNodeId}-${newNode.id}`,
|
||||
type: 'custom',
|
||||
source: prevNodeId,
|
||||
sourceHandle: prevNodeSourceHandle,
|
||||
target: newNode.id,
|
||||
targetHandle,
|
||||
data: {
|
||||
sourceType: prevNode.data.type,
|
||||
targetType: newNode.data.type,
|
||||
_connectedNodeIsSelected: true,
|
||||
},
|
||||
}
|
||||
const newNodes = produce(nodes, (draft: Node[]) => {
|
||||
draft.forEach((node) => {
|
||||
node.data.selected = false
|
||||
|
||||
if (node.id === prevNode.id)
|
||||
node.data._connectedSourceHandleIds?.push(prevNodeSourceHandle!)
|
||||
})
|
||||
draft.push(newNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((item) => {
|
||||
item.data = {
|
||||
...item.data,
|
||||
_connectedNodeIsSelected: false,
|
||||
}
|
||||
})
|
||||
draft.push(newEdge)
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}
|
||||
if (!prevNodeId && nextNodeId) {
|
||||
const nextNodeIndex = nodes.findIndex(node => node.id === nextNodeId)
|
||||
const nextNode = nodes[nextNodeIndex]!
|
||||
newNode.data._connectedSourceHandleIds = [sourceHandle]
|
||||
newNode.data._connectedTargetHandleIds = []
|
||||
newNode.position = {
|
||||
x: nextNode.position.x,
|
||||
y: nextNode.position.y,
|
||||
}
|
||||
|
||||
const newEdge = {
|
||||
id: `${newNode.id}-${nextNodeId}`,
|
||||
type: 'custom',
|
||||
source: newNode.id,
|
||||
sourceHandle,
|
||||
target: nextNodeId,
|
||||
targetHandle: nextNodeTargetHandle,
|
||||
data: {
|
||||
sourceType: newNode.data.type,
|
||||
targetType: nextNode.data.type,
|
||||
_connectedNodeIsSelected: true,
|
||||
},
|
||||
}
|
||||
const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!)
|
||||
const afterNodesInSameBranchIds = afterNodesInSameBranch.map(node => node.id)
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data.selected = false
|
||||
|
||||
if (afterNodesInSameBranchIds.includes(node.id))
|
||||
node.position.x += NODE_WIDTH_X_OFFSET
|
||||
|
||||
if (node.id === nextNodeId)
|
||||
node.data._connectedTargetHandleIds?.push(nextNodeTargetHandle!)
|
||||
})
|
||||
draft.push(newNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((item) => {
|
||||
item.data = {
|
||||
...item.data,
|
||||
_connectedNodeIsSelected: false,
|
||||
}
|
||||
})
|
||||
draft.push(newEdge)
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}
|
||||
if (prevNodeId && nextNodeId) {
|
||||
const prevNode = nodes.find(node => node.id === prevNodeId)!
|
||||
const nextNode = nodes.find(node => node.id === nextNodeId)!
|
||||
if (prevNode.data.type === BlockEnum.KnowledgeRetrieval)
|
||||
targetHandle = prevNodeId
|
||||
newNode.data._connectedTargetHandleIds = [targetHandle]
|
||||
newNode.data._connectedSourceHandleIds = [sourceHandle]
|
||||
newNode.position = {
|
||||
x: nextNode.position.x,
|
||||
y: nextNode.position.y,
|
||||
}
|
||||
|
||||
const currentEdgeIndex = edges.findIndex(edge => edge.source === prevNodeId && edge.target === nextNodeId)
|
||||
const newPrevEdge = {
|
||||
id: `${prevNodeId}-${newNode.id}`,
|
||||
type: 'custom',
|
||||
source: prevNodeId,
|
||||
sourceHandle: prevNodeSourceHandle,
|
||||
target: newNode.id,
|
||||
targetHandle,
|
||||
data: {
|
||||
sourceType: prevNode.data.type,
|
||||
targetType: newNode.data.type,
|
||||
_connectedNodeIsSelected: true,
|
||||
},
|
||||
}
|
||||
let newNextEdge: Edge | null = null
|
||||
if (nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier) {
|
||||
newNextEdge = {
|
||||
id: `${newNode.id}-${nextNodeId}`,
|
||||
type: 'custom',
|
||||
source: newNode.id,
|
||||
sourceHandle,
|
||||
target: nextNodeId,
|
||||
targetHandle: nextNodeTargetHandle,
|
||||
data: {
|
||||
sourceType: newNode.data.type,
|
||||
targetType: nextNode.data.type,
|
||||
_connectedNodeIsSelected: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||
[
|
||||
{ type: 'remove', edge: edges[currentEdgeIndex] },
|
||||
{ type: 'add', edge: newPrevEdge },
|
||||
...(newNextEdge ? [{ type: 'add', edge: newNextEdge }] : []),
|
||||
],
|
||||
[...nodes, newNode],
|
||||
)
|
||||
|
||||
const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!)
|
||||
const afterNodesInSameBranchIds = afterNodesInSameBranch.map(node => node.id)
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data.selected = false
|
||||
|
||||
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
|
||||
}
|
||||
}
|
||||
if (afterNodesInSameBranchIds.includes(node.id))
|
||||
node.position.x += NODE_WIDTH_X_OFFSET
|
||||
})
|
||||
draft.push(newNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.splice(currentEdgeIndex, 1)
|
||||
draft.forEach((item) => {
|
||||
item.data = {
|
||||
...item.data,
|
||||
_connectedNodeIsSelected: false,
|
||||
}
|
||||
})
|
||||
draft.push(newPrevEdge)
|
||||
|
||||
if (newNextEdge)
|
||||
draft.push(newNextEdge)
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}
|
||||
handleSyncWorkflowDraft()
|
||||
}, [store, handleSyncWorkflowDraft, getAfterNodesInSameBranch, getNodesReadOnly, t])
|
||||
|
||||
const handleNodeChange = useCallback((
|
||||
currentNodeId: string,
|
||||
nodeType: BlockEnum,
|
||||
sourceHandle: string,
|
||||
toolDefaultValue?: ToolDefaultValue,
|
||||
) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(node => node.id === currentNodeId)!
|
||||
const connectedEdges = getConnectedEdges([currentNode], edges)
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
const newCurrentNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
...(toolDefaultValue || {}),
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
selected: currentNode.data.selected,
|
||||
},
|
||||
position: {
|
||||
x: currentNode.position.x,
|
||||
y: currentNode.position.y,
|
||||
},
|
||||
})
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||
[
|
||||
...connectedEdges.map(edge => ({ type: 'remove', edge })),
|
||||
],
|
||||
nodes,
|
||||
)
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data.selected = false
|
||||
|
||||
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
|
||||
}
|
||||
}
|
||||
})
|
||||
const index = draft.findIndex(node => node.id === currentNodeId)
|
||||
|
||||
draft.splice(index, 1, newCurrentNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const filtered = draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id))
|
||||
|
||||
return filtered
|
||||
})
|
||||
setEdges(newEdges)
|
||||
handleSyncWorkflowDraft()
|
||||
}, [store, handleSyncWorkflowDraft, getNodesReadOnly, t])
|
||||
|
||||
return {
|
||||
handleNodeDragStart,
|
||||
handleNodeDrag,
|
||||
handleNodeDragStop,
|
||||
handleNodeEnter,
|
||||
handleNodeLeave,
|
||||
handleNodeSelect,
|
||||
handleNodeClick,
|
||||
handleNodeConnect,
|
||||
handleNodeConnectStart,
|
||||
handleNodeConnectEnd,
|
||||
handleNodeDelete,
|
||||
handleNodeChange,
|
||||
handleNodeAdd,
|
||||
}
|
||||
}
|
||||
118
web/app/components/workflow/hooks/use-nodes-sync-draft.ts
Normal file
118
web/app/components/workflow/hooks/use-nodes-sync-draft.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import { BlockEnum } from '../types'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
import { syncWorkflowDraft } from '@/service/workflow'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { API_PREFIX } from '@/config'
|
||||
|
||||
export const useNodesSyncDraft = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const debouncedSyncWorkflowDraft = useStore(s => s.debouncedSyncWorkflowDraft)
|
||||
const params = useParams()
|
||||
|
||||
const getPostParams = useCallback((appIdParams?: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
transform,
|
||||
} = store.getState()
|
||||
const [x, y, zoom] = transform
|
||||
const appId = workflowStore.getState().appId
|
||||
|
||||
if (appId || appIdParams) {
|
||||
const nodes = getNodes()
|
||||
const hasStartNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
|
||||
if (!hasStartNode)
|
||||
return
|
||||
|
||||
const features = featuresStore!.getState().features
|
||||
const producedNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
Object.keys(node.data).forEach((key) => {
|
||||
if (key.startsWith('_'))
|
||||
delete node.data[key]
|
||||
})
|
||||
})
|
||||
})
|
||||
const producedEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
Object.keys(edge.data).forEach((key) => {
|
||||
if (key.startsWith('_'))
|
||||
delete edge.data[key]
|
||||
})
|
||||
})
|
||||
})
|
||||
return {
|
||||
url: `/apps/${appId || appIdParams}/workflows/draft`,
|
||||
params: {
|
||||
graph: {
|
||||
nodes: producedNodes,
|
||||
edges: producedEdges,
|
||||
viewport: {
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
},
|
||||
},
|
||||
features: {
|
||||
opening_statement: features.opening?.opening_statement || '',
|
||||
suggested_questions: features.opening?.suggested_questions || [],
|
||||
suggested_questions_after_answer: features.suggested,
|
||||
text_to_speech: features.text2speech,
|
||||
speech_to_text: features.speech2text,
|
||||
retriever_resource: features.citation,
|
||||
sensitive_word_avoidance: features.moderation,
|
||||
file_upload: features.file,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}, [store, featuresStore, workflowStore])
|
||||
|
||||
const syncWorkflowDraftWhenPageClose = useCallback(() => {
|
||||
const postParams = getPostParams()
|
||||
|
||||
if (postParams) {
|
||||
navigator.sendBeacon(
|
||||
`${API_PREFIX}/apps/${params.appId}/workflows/draft?_token=${localStorage.getItem('console_token')}`,
|
||||
JSON.stringify(postParams.params),
|
||||
)
|
||||
}
|
||||
}, [getPostParams, params.appId])
|
||||
|
||||
const doSyncWorkflowDraft = useCallback(async (appId?: string) => {
|
||||
const postParams = getPostParams(appId)
|
||||
|
||||
if (postParams) {
|
||||
const res = await syncWorkflowDraft(postParams)
|
||||
workflowStore.getState().setDraftUpdatedAt(res.updated_at)
|
||||
}
|
||||
}, [workflowStore, getPostParams])
|
||||
|
||||
const handleSyncWorkflowDraft = useCallback((sync?: boolean, appId?: string) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (sync)
|
||||
doSyncWorkflowDraft(appId)
|
||||
else
|
||||
debouncedSyncWorkflowDraft(doSyncWorkflowDraft)
|
||||
}, [debouncedSyncWorkflowDraft, doSyncWorkflowDraft, getNodesReadOnly])
|
||||
|
||||
return {
|
||||
doSyncWorkflowDraft,
|
||||
handleSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
}
|
||||
}
|
||||
352
web/app/components/workflow/hooks/use-workflow-run.ts
Normal file
352
web/app/components/workflow/hooks/use-workflow-run.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import {
|
||||
NodeRunningStatus,
|
||||
WorkflowRunningStatus,
|
||||
} from '../types'
|
||||
import { useWorkflow } from './use-workflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import type { IOtherOptions } from '@/service/base'
|
||||
import { ssePost } from '@/service/base'
|
||||
import {
|
||||
fetchPublishedWorkflow,
|
||||
stopWorkflowRun,
|
||||
} from '@/service/workflow'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
|
||||
export const useWorkflowRun = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const { renderTreeFromRecord } = useWorkflow()
|
||||
|
||||
const handleBackupDraft = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const { getViewport } = reactflow
|
||||
const {
|
||||
backupDraft,
|
||||
setBackupDraft,
|
||||
} = workflowStore.getState()
|
||||
const { features } = featuresStore!.getState()
|
||||
|
||||
if (!backupDraft) {
|
||||
setBackupDraft({
|
||||
nodes: getNodes(),
|
||||
edges,
|
||||
viewport: getViewport(),
|
||||
features,
|
||||
})
|
||||
}
|
||||
}, [reactflow, workflowStore, store, featuresStore])
|
||||
|
||||
const handleLoadBackupDraft = useCallback(() => {
|
||||
const {
|
||||
setNodes,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const { setViewport } = reactflow
|
||||
const {
|
||||
backupDraft,
|
||||
setBackupDraft,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (backupDraft) {
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
features,
|
||||
} = backupDraft
|
||||
setNodes(nodes)
|
||||
setEdges(edges)
|
||||
setViewport(viewport)
|
||||
featuresStore!.setState({ features })
|
||||
setBackupDraft(undefined)
|
||||
}
|
||||
}, [store, reactflow, workflowStore, featuresStore])
|
||||
|
||||
const handleRunSetting = useCallback((shouldClear?: boolean) => {
|
||||
if (shouldClear) {
|
||||
workflowStore.setState({
|
||||
workflowRunningData: undefined,
|
||||
historyWorkflowData: undefined,
|
||||
showInputsPanel: false,
|
||||
})
|
||||
}
|
||||
else {
|
||||
workflowStore.setState({
|
||||
workflowRunningData: {
|
||||
result: {
|
||||
status: shouldClear ? '' : WorkflowRunningStatus.Waiting,
|
||||
},
|
||||
tracing: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
setNodes,
|
||||
getNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
|
||||
if (shouldClear) {
|
||||
handleLoadBackupDraft()
|
||||
}
|
||||
else {
|
||||
handleBackupDraft()
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data._runningStatus = NodeRunningStatus.Waiting
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
edge.data._runned = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}
|
||||
}, [store, handleLoadBackupDraft, handleBackupDraft, workflowStore])
|
||||
|
||||
const handleRun = useCallback((
|
||||
params: any,
|
||||
callback?: IOtherOptions,
|
||||
) => {
|
||||
const {
|
||||
onWorkflowStarted,
|
||||
onWorkflowFinished,
|
||||
onNodeStarted,
|
||||
onNodeFinished,
|
||||
onError,
|
||||
...restCallback
|
||||
} = callback || {}
|
||||
workflowStore.setState({ historyWorkflowData: undefined })
|
||||
const appDetail = useAppStore.getState().appDetail
|
||||
const workflowContainer = document.getElementById('workflow-container')
|
||||
|
||||
const {
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
} = workflowContainer!
|
||||
|
||||
let url = ''
|
||||
if (appDetail?.mode === 'advanced-chat')
|
||||
url = `/apps/${appDetail.id}/advanced-chat/workflows/draft/run`
|
||||
|
||||
if (appDetail?.mode === 'workflow')
|
||||
url = `/apps/${appDetail.id}/workflows/draft/run`
|
||||
|
||||
let prevNodeId = ''
|
||||
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.result = {
|
||||
...draft?.result,
|
||||
status: WorkflowRunningStatus.Running,
|
||||
}
|
||||
}))
|
||||
|
||||
ssePost(
|
||||
url,
|
||||
{
|
||||
body: params,
|
||||
},
|
||||
{
|
||||
onWorkflowStarted: (params) => {
|
||||
const { task_id, data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.task_id = task_id
|
||||
draft.result = {
|
||||
...draft?.result,
|
||||
...data,
|
||||
status: WorkflowRunningStatus.Running,
|
||||
}
|
||||
}))
|
||||
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data._runningStatus = NodeRunningStatus.Waiting
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
|
||||
if (onWorkflowStarted)
|
||||
onWorkflowStarted(params)
|
||||
},
|
||||
onWorkflowFinished: (params) => {
|
||||
const { data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.result = {
|
||||
...draft.result,
|
||||
...data,
|
||||
}
|
||||
}))
|
||||
|
||||
prevNodeId = ''
|
||||
|
||||
if (onWorkflowFinished)
|
||||
onWorkflowFinished(params)
|
||||
},
|
||||
onError: (params) => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.result = {
|
||||
...draft.result,
|
||||
status: WorkflowRunningStatus.Failed,
|
||||
}
|
||||
}))
|
||||
|
||||
if (onError)
|
||||
onError(params)
|
||||
},
|
||||
onNodeStarted: (params) => {
|
||||
const { data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as any)
|
||||
}))
|
||||
|
||||
const {
|
||||
setViewport,
|
||||
} = reactflow
|
||||
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
|
||||
const currentNode = nodes[currentNodeIndex]
|
||||
const position = currentNode.position
|
||||
const zoom = 1
|
||||
|
||||
setViewport({
|
||||
x: (clientWidth - 400 - currentNode.width!) / 2 - position.x,
|
||||
y: (clientHeight - currentNode.height!) / 2 - position.y,
|
||||
zoom,
|
||||
})
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const edge = draft.find(edge => edge.target === data.node_id && edge.source === prevNodeId)
|
||||
|
||||
if (edge)
|
||||
edge.data = { ...edge.data, _runned: true } as any
|
||||
})
|
||||
setEdges(newEdges)
|
||||
|
||||
if (onNodeStarted)
|
||||
onNodeStarted(params)
|
||||
},
|
||||
onNodeFinished: (params) => {
|
||||
const { data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
const currentIndex = draft.tracing!.findIndex(trace => trace.node_id === data.node_id)
|
||||
|
||||
if (currentIndex > -1 && draft.tracing) {
|
||||
draft.tracing[currentIndex] = {
|
||||
...(draft.tracing[currentIndex].extras
|
||||
? { extras: draft.tracing[currentIndex].extras }
|
||||
: {}),
|
||||
...data,
|
||||
} as any
|
||||
}
|
||||
}))
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(node => node.id === data.node_id)!
|
||||
|
||||
currentNode.data._runningStatus = data.status as any
|
||||
})
|
||||
setNodes(newNodes)
|
||||
|
||||
prevNodeId = data.node_id
|
||||
|
||||
if (onNodeFinished)
|
||||
onNodeFinished(params)
|
||||
},
|
||||
...restCallback,
|
||||
},
|
||||
)
|
||||
}, [store, reactflow, workflowStore])
|
||||
|
||||
const handleStopRun = useCallback((taskId: string) => {
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
|
||||
stopWorkflowRun(`/apps/${appId}/workflow-runs/tasks/${taskId}/stop`)
|
||||
}, [])
|
||||
|
||||
const handleRestoreFromPublishedWorkflow = useCallback(async () => {
|
||||
const appDetail = useAppStore.getState().appDetail
|
||||
const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`)
|
||||
|
||||
if (publishedWorkflow) {
|
||||
const nodes = publishedWorkflow.graph.nodes
|
||||
const edges = publishedWorkflow.graph.edges
|
||||
const viewport = publishedWorkflow.graph.viewport
|
||||
|
||||
renderTreeFromRecord(nodes, edges, viewport)
|
||||
featuresStore?.setState({ features: publishedWorkflow.features })
|
||||
workflowStore.getState().setPublishedAt(publishedWorkflow.created_at)
|
||||
}
|
||||
}, [featuresStore, workflowStore, renderTreeFromRecord])
|
||||
|
||||
return {
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRunSetting,
|
||||
handleRun,
|
||||
handleStopRun,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
}
|
||||
}
|
||||
73
web/app/components/workflow/hooks/use-workflow-template.ts
Normal file
73
web/app/components/workflow/hooks/use-workflow-template.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { generateNewNode } from '../utils'
|
||||
import {
|
||||
NODE_WIDTH_X_OFFSET,
|
||||
START_INITIAL_POSITION,
|
||||
} from '../constants'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
import { useNodesInitialData } from './use-nodes-data'
|
||||
|
||||
export const useWorkflowTemplate = () => {
|
||||
const isChatMode = useIsChatMode()
|
||||
const nodesInitialData = useNodesInitialData()
|
||||
|
||||
const startNode = generateNewNode({
|
||||
data: nodesInitialData.start,
|
||||
position: START_INITIAL_POSITION,
|
||||
})
|
||||
|
||||
if (isChatMode) {
|
||||
const llmNode = generateNewNode({
|
||||
id: 'llm',
|
||||
data: {
|
||||
...nodesInitialData.llm,
|
||||
memory: {
|
||||
window: { enabled: false, size: 10 },
|
||||
},
|
||||
selected: true,
|
||||
},
|
||||
position: {
|
||||
x: START_INITIAL_POSITION.x + NODE_WIDTH_X_OFFSET,
|
||||
y: START_INITIAL_POSITION.y,
|
||||
},
|
||||
} as any)
|
||||
|
||||
const answerNode = generateNewNode({
|
||||
id: 'answer',
|
||||
data: {
|
||||
...nodesInitialData.answer,
|
||||
answer: `{{#${llmNode.id}.text#}}`,
|
||||
},
|
||||
position: {
|
||||
x: START_INITIAL_POSITION.x + NODE_WIDTH_X_OFFSET * 2,
|
||||
y: START_INITIAL_POSITION.y,
|
||||
},
|
||||
} as any)
|
||||
|
||||
const startToLlmEdge = {
|
||||
id: `${startNode.id}-${llmNode.id}`,
|
||||
source: startNode.id,
|
||||
sourceHandle: 'source',
|
||||
target: llmNode.id,
|
||||
targetHandle: 'target',
|
||||
}
|
||||
|
||||
const llmToAnswerEdge = {
|
||||
id: `${llmNode.id}-${answerNode.id}`,
|
||||
source: llmNode.id,
|
||||
sourceHandle: 'source',
|
||||
target: answerNode.id,
|
||||
targetHandle: 'target',
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: [startNode, llmNode, answerNode],
|
||||
edges: [startToLlmEdge, llmToAnswerEdge],
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
nodes: [startNode],
|
||||
edges: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
472
web/app/components/workflow/hooks/use-workflow.ts
Normal file
472
web/app/components/workflow/hooks/use-workflow.ts
Normal file
@@ -0,0 +1,472 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { uniqBy } from 'lodash-es'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import useSWR from 'swr'
|
||||
import produce from 'immer'
|
||||
import {
|
||||
getIncomers,
|
||||
getOutgoers,
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import type {
|
||||
Connection,
|
||||
Viewport,
|
||||
} from 'reactflow'
|
||||
import {
|
||||
getLayoutByDagre,
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from '../utils'
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
ValueSelector,
|
||||
} from '../types'
|
||||
import {
|
||||
BlockEnum,
|
||||
WorkflowRunningStatus,
|
||||
} from '../types'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
AUTO_LAYOUT_OFFSET,
|
||||
SUPPORT_OUTPUT_VARS_NODE,
|
||||
} from '../constants'
|
||||
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
|
||||
import { useNodesExtraData } from './use-nodes-data'
|
||||
import { useWorkflowTemplate } from './use-workflow-template'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
fetchNodesDefaultConfigs,
|
||||
fetchPublishedWorkflow,
|
||||
fetchWorkflowDraft,
|
||||
syncWorkflowDraft,
|
||||
} from '@/service/workflow'
|
||||
import {
|
||||
fetchAllBuiltInTools,
|
||||
fetchAllCustomTools,
|
||||
} from '@/service/tools'
|
||||
import I18n from '@/context/i18n'
|
||||
export const useIsChatMode = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
|
||||
return appDetail?.mode === 'advanced-chat'
|
||||
}
|
||||
|
||||
export const useWorkflow = () => {
|
||||
const { locale } = useContext(I18n)
|
||||
const store = useStoreApi()
|
||||
const reactflow = useReactFlow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const handleLayout = useCallback(async () => {
|
||||
workflowStore.setState({ nodeAnimation: true })
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const { setViewport } = reactflow
|
||||
const nodes = getNodes()
|
||||
const layout = getLayoutByDagre(nodes, edges)
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
const nodeWithPosition = layout.node(node.id)
|
||||
node.position = {
|
||||
x: nodeWithPosition.x + AUTO_LAYOUT_OFFSET.x,
|
||||
y: nodeWithPosition.y + AUTO_LAYOUT_OFFSET.y,
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const zoom = 0.7
|
||||
setViewport({
|
||||
x: 0,
|
||||
y: 0,
|
||||
zoom,
|
||||
})
|
||||
setTimeout(() => {
|
||||
handleSyncWorkflowDraft()
|
||||
})
|
||||
}, [store, reactflow, handleSyncWorkflowDraft, workflowStore])
|
||||
|
||||
const getTreeLeafNodes = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
|
||||
if (!startNode)
|
||||
return []
|
||||
|
||||
const list: Node[] = []
|
||||
const preOrder = (root: Node, callback: (node: Node) => void) => {
|
||||
if (root.id === nodeId)
|
||||
return
|
||||
const outgoers = getOutgoers(root, nodes, edges)
|
||||
|
||||
if (outgoers.length) {
|
||||
outgoers.forEach((outgoer) => {
|
||||
preOrder(outgoer, callback)
|
||||
})
|
||||
}
|
||||
else {
|
||||
if (root.id !== nodeId)
|
||||
callback(root)
|
||||
}
|
||||
}
|
||||
preOrder(startNode, (node) => {
|
||||
list.push(node)
|
||||
})
|
||||
|
||||
const incomers = getIncomers({ id: nodeId } as Node, nodes, edges)
|
||||
|
||||
list.push(...incomers)
|
||||
|
||||
return uniqBy(list, 'id').filter((item) => {
|
||||
return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
|
||||
})
|
||||
}, [store])
|
||||
|
||||
const getBeforeNodesInSameBranch = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
const list: Node[] = []
|
||||
|
||||
if (!currentNode)
|
||||
return list
|
||||
|
||||
const traverse = (root: Node, callback: (node: Node) => void) => {
|
||||
if (root) {
|
||||
const incomers = getIncomers(root, nodes, edges)
|
||||
|
||||
if (incomers.length) {
|
||||
incomers.forEach((node) => {
|
||||
callback(node)
|
||||
traverse(node, callback)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
traverse(currentNode, (node) => {
|
||||
list.push(node)
|
||||
})
|
||||
|
||||
const length = list.length
|
||||
if (length) {
|
||||
return uniqBy(list, 'id').reverse().filter((item) => {
|
||||
return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
|
||||
})
|
||||
}
|
||||
|
||||
return []
|
||||
}, [store])
|
||||
|
||||
const getAfterNodesInSameBranch = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
if (!currentNode)
|
||||
return []
|
||||
const list: Node[] = [currentNode]
|
||||
|
||||
const traverse = (root: Node, callback: (node: Node) => void) => {
|
||||
if (root) {
|
||||
const outgoers = getOutgoers(root, nodes, edges)
|
||||
|
||||
if (outgoers.length) {
|
||||
outgoers.forEach((node) => {
|
||||
callback(node)
|
||||
traverse(node, callback)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
traverse(currentNode, (node) => {
|
||||
list.push(node)
|
||||
})
|
||||
|
||||
return uniqBy(list, 'id')
|
||||
}, [store])
|
||||
|
||||
const getBeforeNodeById = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const node = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
return getIncomers(node, nodes, edges)
|
||||
}, [store])
|
||||
|
||||
const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const afterNodes = getAfterNodesInSameBranch(nodeId)
|
||||
const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes)
|
||||
// console.log(effectNodes)
|
||||
if (effectNodes.length > 0) {
|
||||
const newNodes = getNodes().map((node) => {
|
||||
if (effectNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, oldValeSelector, newVarSelector)
|
||||
|
||||
return node
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [store])
|
||||
|
||||
const isVarUsedInNodes = useCallback((varSelector: ValueSelector) => {
|
||||
const nodeId = varSelector[0]
|
||||
const afterNodes = getAfterNodesInSameBranch(nodeId)
|
||||
const effectNodes = findUsedVarNodes(varSelector, afterNodes)
|
||||
return effectNodes.length > 0
|
||||
}, [getAfterNodesInSameBranch])
|
||||
|
||||
const removeUsedVarInNodes = useCallback((varSelector: ValueSelector) => {
|
||||
const nodeId = varSelector[0]
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const afterNodes = getAfterNodesInSameBranch(nodeId)
|
||||
const effectNodes = findUsedVarNodes(varSelector, afterNodes)
|
||||
if (effectNodes.length > 0) {
|
||||
const newNodes = getNodes().map((node) => {
|
||||
if (effectNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, varSelector, [])
|
||||
|
||||
return node
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
}, [getAfterNodesInSameBranch, store])
|
||||
|
||||
const isNodeVarsUsedInNodes = useCallback((node: Node, isChatMode: boolean) => {
|
||||
const outputVars = getNodeOutputVars(node, isChatMode)
|
||||
const isUsed = outputVars.some((varSelector) => {
|
||||
return isVarUsedInNodes(varSelector)
|
||||
})
|
||||
return isUsed
|
||||
}, [isVarUsedInNodes])
|
||||
|
||||
const isValidConnection = useCallback(({ source, target }: Connection) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const sourceNode: Node = nodes.find(node => node.id === source)!
|
||||
const targetNode: Node = nodes.find(node => node.id === target)!
|
||||
|
||||
if (sourceNode && targetNode) {
|
||||
const sourceNodeAvailableNextNodes = nodesExtraData[sourceNode.data.type].availableNextNodes
|
||||
const targetNodeAvailablePrevNodes = [...nodesExtraData[targetNode.data.type].availablePrevNodes, BlockEnum.Start]
|
||||
if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type))
|
||||
return false
|
||||
|
||||
if (!targetNodeAvailablePrevNodes.includes(sourceNode.data.type))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, [store, nodesExtraData])
|
||||
|
||||
const formatTimeFromNow = useCallback((time: number) => {
|
||||
return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow()
|
||||
}, [locale])
|
||||
|
||||
const renderTreeFromRecord = useCallback((nodes: Node[], edges: Edge[], viewport?: Viewport) => {
|
||||
const { setNodes } = store.getState()
|
||||
const { setViewport, setEdges } = reactflow
|
||||
|
||||
setNodes(initialNodes(nodes, edges))
|
||||
setEdges(initialEdges(edges, nodes))
|
||||
|
||||
if (viewport)
|
||||
setViewport(viewport)
|
||||
}, [store, reactflow])
|
||||
|
||||
const getNode = useCallback((nodeId?: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
return nodes.find(node => node.id === nodeId) || nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
handleLayout,
|
||||
getTreeLeafNodes,
|
||||
getBeforeNodesInSameBranch,
|
||||
getAfterNodesInSameBranch,
|
||||
handleOutVarRenameChange,
|
||||
isVarUsedInNodes,
|
||||
removeUsedVarInNodes,
|
||||
isNodeVarsUsedInNodes,
|
||||
isValidConnection,
|
||||
formatTimeFromNow,
|
||||
renderTreeFromRecord,
|
||||
getNode,
|
||||
getBeforeNodeById,
|
||||
}
|
||||
}
|
||||
|
||||
export const useFetchToolsData = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleFetchAllTools = useCallback(async (type: string) => {
|
||||
if (type === 'builtin') {
|
||||
const buildInTools = await fetchAllBuiltInTools()
|
||||
|
||||
workflowStore.setState({
|
||||
buildInTools: buildInTools || [],
|
||||
})
|
||||
}
|
||||
if (type === 'custom') {
|
||||
const customTools = await fetchAllCustomTools()
|
||||
|
||||
workflowStore.setState({
|
||||
customTools: customTools || [],
|
||||
})
|
||||
}
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handleFetchAllTools,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowInit = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const {
|
||||
nodes: nodesTemplate,
|
||||
edges: edgesTemplate,
|
||||
} = useWorkflowTemplate()
|
||||
const { handleFetchAllTools } = useFetchToolsData()
|
||||
const appDetail = useAppStore(state => state.appDetail)!
|
||||
const { data, isLoading, error, mutate } = useSWR(`/apps/${appDetail.id}/workflows/draft`, fetchWorkflowDraft)
|
||||
workflowStore.setState({ appId: appDetail.id })
|
||||
|
||||
const handleFetchPreloadData = useCallback(async () => {
|
||||
try {
|
||||
const nodesDefaultConfigsData = await fetchNodesDefaultConfigs(`/apps/${appDetail?.id}/workflows/default-workflow-block-configs`)
|
||||
const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`)
|
||||
workflowStore.setState({
|
||||
nodesDefaultConfigs: nodesDefaultConfigsData.reduce((acc, block) => {
|
||||
if (!acc[block.type])
|
||||
acc[block.type] = { ...block.config }
|
||||
return acc
|
||||
}, {} as Record<string, any>),
|
||||
})
|
||||
workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at)
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
}
|
||||
}, [workflowStore, appDetail])
|
||||
|
||||
useEffect(() => {
|
||||
handleFetchPreloadData()
|
||||
handleFetchAllTools('builtin')
|
||||
handleFetchAllTools('custom')
|
||||
}, [handleFetchPreloadData, handleFetchAllTools])
|
||||
|
||||
useEffect(() => {
|
||||
if (data)
|
||||
workflowStore.getState().setDraftUpdatedAt(data.updated_at)
|
||||
}, [data, workflowStore])
|
||||
|
||||
if (error && error.json && !error.bodyUsed && appDetail) {
|
||||
error.json().then((err: any) => {
|
||||
if (err.code === 'draft_workflow_not_exist') {
|
||||
workflowStore.setState({ notInitialWorkflow: true })
|
||||
syncWorkflowDraft({
|
||||
url: `/apps/${appDetail.id}/workflows/draft`,
|
||||
params: {
|
||||
graph: {
|
||||
nodes: nodesTemplate,
|
||||
edges: edgesTemplate,
|
||||
},
|
||||
features: {},
|
||||
},
|
||||
}).then((res) => {
|
||||
workflowStore.getState().setDraftUpdatedAt(res.updated_at)
|
||||
mutate()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowReadOnly = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
|
||||
const getWorkflowReadOnly = useCallback(() => {
|
||||
return workflowStore.getState().workflowRunningData?.result.status === WorkflowRunningStatus.Running
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
workflowReadOnly: workflowRunningData?.result.status === WorkflowRunningStatus.Running,
|
||||
getWorkflowReadOnly,
|
||||
}
|
||||
}
|
||||
export const useNodesReadOnly = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const isRestoring = useStore(s => s.isRestoring)
|
||||
|
||||
const getNodesReadOnly = useCallback(() => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
historyWorkflowData,
|
||||
isRestoring,
|
||||
} = workflowStore.getState()
|
||||
|
||||
return workflowRunningData || historyWorkflowData || isRestoring
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
nodesReadOnly: !!(workflowRunningData || historyWorkflowData || isRestoring),
|
||||
getNodesReadOnly,
|
||||
}
|
||||
}
|
||||
|
||||
export const useToolIcon = (data: Node['data']) => {
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const toolIcon = useMemo(() => {
|
||||
if (data.type === BlockEnum.Tool) {
|
||||
if (data.provider_type === 'builtin')
|
||||
return buildInTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.icon
|
||||
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.icon
|
||||
}
|
||||
}, [data, buildInTools, customTools])
|
||||
|
||||
return toolIcon
|
||||
}
|
||||
Reference in New Issue
Block a user