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:
takatost
2024-04-08 18:51:46 +08:00
committed by GitHub
parent 2fb9850af5
commit 7753ba2d37
1161 changed files with 103836 additions and 10327 deletions

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

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

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

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

View 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])
}

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

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

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

View 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: [],
}
}
}

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