mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-12 12:26:54 +08:00
feat: Parallel Execution of Nodes in Workflows (#8192)
Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: Yi <yxiaoisme@gmail.com> Co-authored-by: -LAN- <laipz8200@outlook.com>
This commit is contained in:
@@ -14,9 +14,11 @@ import {
|
||||
} from './store'
|
||||
import { WorkflowHistoryEvent, useNodesInteractions, useWorkflowHistory } from './hooks'
|
||||
import { CUSTOM_NODE } from './constants'
|
||||
import { getIterationStartNode } from './utils'
|
||||
import CustomNode from './nodes'
|
||||
import CustomNoteNode from './note-node'
|
||||
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
||||
import { BlockEnum } from './types'
|
||||
|
||||
const CandidateNode = () => {
|
||||
const store = useStoreApi()
|
||||
@@ -52,6 +54,8 @@ const CandidateNode = () => {
|
||||
y,
|
||||
},
|
||||
})
|
||||
if (candidateNode.data.type === BlockEnum.Iteration)
|
||||
draft.push(getIterationStartNode(candidateNode.id))
|
||||
})
|
||||
setNodes(newNodes)
|
||||
if (candidateNode.type === CUSTOM_NOTE_NODE)
|
||||
|
||||
@@ -15,6 +15,7 @@ import VariableAssignerDefault from './nodes/variable-assigner/default'
|
||||
import AssignerDefault from './nodes/assigner/default'
|
||||
import EndNodeDefault from './nodes/end/default'
|
||||
import IterationDefault from './nodes/iteration/default'
|
||||
import IterationStartDefault from './nodes/iteration-start/default'
|
||||
|
||||
type NodesExtraData = {
|
||||
author: string
|
||||
@@ -89,6 +90,15 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
|
||||
getAvailableNextNodes: IterationDefault.getAvailableNextNodes,
|
||||
checkValid: IterationDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.IterationStart]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: IterationStartDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: IterationStartDefault.getAvailableNextNodes,
|
||||
checkValid: IterationStartDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Code]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
@@ -222,6 +232,12 @@ export const NODES_INITIAL_DATA = {
|
||||
desc: '',
|
||||
...IterationDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.IterationStart]: {
|
||||
type: BlockEnum.IterationStart,
|
||||
title: '',
|
||||
desc: '',
|
||||
...IterationStartDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Code]: {
|
||||
type: BlockEnum.Code,
|
||||
title: '',
|
||||
@@ -305,11 +321,13 @@ export const AUTO_LAYOUT_OFFSET = {
|
||||
export const ITERATION_NODE_Z_INDEX = 1
|
||||
export const ITERATION_CHILDREN_Z_INDEX = 1002
|
||||
export const ITERATION_PADDING = {
|
||||
top: 85,
|
||||
top: 65,
|
||||
right: 16,
|
||||
bottom: 20,
|
||||
left: 16,
|
||||
}
|
||||
export const PARALLEL_LIMIT = 10
|
||||
export const PARALLEL_DEPTH_LIMIT = 3
|
||||
|
||||
export const RETRIEVAL_OUTPUT_STRUCT = `{
|
||||
"content": "",
|
||||
@@ -412,4 +430,5 @@ export const PARAMETER_EXTRACTOR_COMMON_STRUCT: Var[] = [
|
||||
|
||||
export const WORKFLOW_DATA_UPDATE = 'WORKFLOW_DATA_UPDATE'
|
||||
export const CUSTOM_NODE = 'custom'
|
||||
export const CUSTOM_EDGE = 'custom'
|
||||
export const DSL_EXPORT_CHECK = 'DSL_EXPORT_CHECK'
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { unionBy } from 'lodash-es'
|
||||
import type { ToolDefaultValue } from '../block-selector/types'
|
||||
import type {
|
||||
Edge,
|
||||
@@ -25,6 +26,7 @@ import type {
|
||||
import { BlockEnum } from '../types'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import {
|
||||
CUSTOM_EDGE,
|
||||
ITERATION_CHILDREN_Z_INDEX,
|
||||
ITERATION_PADDING,
|
||||
NODES_INITIAL_DATA,
|
||||
@@ -40,6 +42,7 @@ import {
|
||||
} from '../utils'
|
||||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
import type { IterationNodeType } from '../nodes/iteration/types'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
|
||||
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
|
||||
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
|
||||
import { useWorkflowHistoryStore } from '../workflow-history-store'
|
||||
@@ -60,6 +63,7 @@ export const useNodesInteractions = () => {
|
||||
const { store: workflowHistoryStore } = useWorkflowHistoryStore()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const {
|
||||
checkNestedParallelLimit,
|
||||
getAfterNodesInSameBranch,
|
||||
} = useWorkflow()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
@@ -79,7 +83,7 @@ export const useNodesInteractions = () => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (node.data.isIterationStart || node.type === CUSTOM_NOTE_NODE)
|
||||
if (node.type === CUSTOM_ITERATION_START_NODE || node.type === CUSTOM_NOTE_NODE)
|
||||
return
|
||||
|
||||
dragNodeStartPosition.current = { x: node.position.x, y: node.position.y }
|
||||
@@ -89,7 +93,7 @@ export const useNodesInteractions = () => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (node.data.isIterationStart)
|
||||
if (node.type === CUSTOM_ITERATION_START_NODE)
|
||||
return
|
||||
|
||||
const {
|
||||
@@ -156,7 +160,7 @@ export const useNodesInteractions = () => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (node.type === CUSTOM_NOTE_NODE)
|
||||
if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_ITERATION_START_NODE)
|
||||
return
|
||||
|
||||
const {
|
||||
@@ -207,13 +211,30 @@ export const useNodesInteractions = () => {
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
const connectedEdges = getConnectedEdges([node], edges).filter(edge => edge.target === node.id)
|
||||
|
||||
const targetNodes: Node[] = []
|
||||
for (let i = 0; i < connectedEdges.length; i++) {
|
||||
const sourceConnectedEdges = getConnectedEdges([{ id: connectedEdges[i].source } as Node], edges).filter(edge => edge.source === connectedEdges[i].source && edge.sourceHandle === connectedEdges[i].sourceHandle)
|
||||
targetNodes.push(...sourceConnectedEdges.map(edge => nodes.find(n => n.id === edge.target)!))
|
||||
}
|
||||
const uniqTargetNodes = unionBy(targetNodes, 'id')
|
||||
if (uniqTargetNodes.length > 1) {
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((n) => {
|
||||
if (uniqTargetNodes.some(targetNode => n.id === targetNode.id))
|
||||
n.data._inParallelHovering = true
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
}, [store, workflowStore, getNodesReadOnly])
|
||||
|
||||
const handleNodeLeave = useCallback<NodeMouseHandler>((_, node) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (node.type === CUSTOM_NOTE_NODE)
|
||||
if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_ITERATION_START_NODE)
|
||||
return
|
||||
|
||||
const {
|
||||
@@ -229,6 +250,7 @@ export const useNodesInteractions = () => {
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data._isEntering = false
|
||||
node.data._inParallelHovering = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
@@ -287,6 +309,8 @@ export const useNodesInteractions = () => {
|
||||
}, [store, handleSyncWorkflowDraft])
|
||||
|
||||
const handleNodeClick = useCallback<NodeMouseHandler>((_, node) => {
|
||||
if (node.type === CUSTOM_ITERATION_START_NODE)
|
||||
return
|
||||
handleNodeSelect(node.id)
|
||||
}, [handleNodeSelect])
|
||||
|
||||
@@ -314,25 +338,15 @@ export const useNodesInteractions = () => {
|
||||
if (targetNode?.parentId !== sourceNode?.parentId)
|
||||
return
|
||||
|
||||
if (targetNode?.data.isIterationStart)
|
||||
return
|
||||
|
||||
if (sourceNode?.type === CUSTOM_NOTE_NODE || targetNode?.type === CUSTOM_NOTE_NODE)
|
||||
return
|
||||
|
||||
const needDeleteEdges = edges.filter((edge) => {
|
||||
if (
|
||||
(edge.source === source && edge.sourceHandle === sourceHandle)
|
||||
|| (edge.target === target && edge.targetHandle === targetHandle && targetNode?.data.type !== BlockEnum.VariableAssigner && targetNode?.data.type !== BlockEnum.VariableAggregator)
|
||||
)
|
||||
return true
|
||||
if (edges.find(edge => edge.source === source && edge.sourceHandle === sourceHandle && edge.target === target && edge.targetHandle === targetHandle))
|
||||
return
|
||||
|
||||
return false
|
||||
})
|
||||
const needDeleteEdgesIds = needDeleteEdges.map(edge => edge.id)
|
||||
const newEdge = {
|
||||
id: `${source}-${sourceHandle}-${target}-${targetHandle}`,
|
||||
type: 'custom',
|
||||
type: CUSTOM_EDGE,
|
||||
source: source!,
|
||||
target: target!,
|
||||
sourceHandle,
|
||||
@@ -347,7 +361,6 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||
[
|
||||
...needDeleteEdges.map(edge => ({ type: 'remove', edge })),
|
||||
{ type: 'add', edge: newEdge },
|
||||
],
|
||||
nodes,
|
||||
@@ -362,19 +375,26 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const filtered = draft.filter(edge => !needDeleteEdgesIds.includes(edge.id))
|
||||
|
||||
filtered.push(newEdge)
|
||||
|
||||
return filtered
|
||||
draft.push(newEdge)
|
||||
})
|
||||
setEdges(newEdges)
|
||||
|
||||
handleSyncWorkflowDraft()
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeConnect)
|
||||
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
if (checkNestedParallelLimit(newNodes, newEdges, targetNode?.parentId)) {
|
||||
setNodes(newNodes)
|
||||
setEdges(newEdges)
|
||||
|
||||
handleSyncWorkflowDraft()
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeConnect)
|
||||
}
|
||||
else {
|
||||
const {
|
||||
setConnectingNodePayload,
|
||||
setEnteringNodePayload,
|
||||
} = workflowStore.getState()
|
||||
setConnectingNodePayload(undefined)
|
||||
setEnteringNodePayload(undefined)
|
||||
}
|
||||
}, [getNodesReadOnly, store, workflowStore, handleSyncWorkflowDraft, saveStateToHistory, checkNestedParallelLimit])
|
||||
|
||||
const handleNodeConnectStart = useCallback<OnConnectStart>((_, { nodeId, handleType, handleId }) => {
|
||||
if (getNodesReadOnly())
|
||||
@@ -393,14 +413,12 @@ export const useNodesInteractions = () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!node.data.isIterationStart) {
|
||||
setConnectingNodePayload({
|
||||
nodeId,
|
||||
nodeType: node.data.type,
|
||||
handleType,
|
||||
handleId,
|
||||
})
|
||||
}
|
||||
setConnectingNodePayload({
|
||||
nodeId,
|
||||
nodeType: node.data.type,
|
||||
handleType,
|
||||
handleId,
|
||||
})
|
||||
}
|
||||
}, [store, workflowStore, getNodesReadOnly])
|
||||
|
||||
@@ -510,6 +528,12 @@ export const useNodesInteractions = () => {
|
||||
return handleNodeDelete(nodeId)
|
||||
}
|
||||
else {
|
||||
if (iterationChildren.length === 1) {
|
||||
handleNodeDelete(iterationChildren[0].id)
|
||||
handleNodeDelete(nodeId)
|
||||
|
||||
return
|
||||
}
|
||||
const { setShowConfirm, showConfirm } = workflowStore.getState()
|
||||
|
||||
if (!showConfirm) {
|
||||
@@ -541,14 +565,8 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (node.id === currentNode.parentId) {
|
||||
if (node.id === currentNode.parentId)
|
||||
node.data._children = node.data._children?.filter(child => child !== nodeId)
|
||||
|
||||
if (currentNode.id === (node as Node<IterationNodeType>).data.start_node_id) {
|
||||
(node as Node<IterationNodeType>).data.start_node_id = '';
|
||||
(node as Node<IterationNodeType>).data.startNodeType = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
draft.splice(currentNodeIndex, 1)
|
||||
})
|
||||
@@ -559,7 +577,7 @@ export const useNodesInteractions = () => {
|
||||
setEdges(newEdges)
|
||||
handleSyncWorkflowDraft()
|
||||
|
||||
if (currentNode.type === 'custom-note')
|
||||
if (currentNode.type === CUSTOM_NOTE_NODE)
|
||||
saveStateToHistory(WorkflowHistoryEvent.NoteDelete)
|
||||
|
||||
else
|
||||
@@ -591,7 +609,10 @@ export const useNodesInteractions = () => {
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
const newNode = generateNewNode({
|
||||
const {
|
||||
newNode,
|
||||
newIterationStartNode,
|
||||
} = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
@@ -627,7 +648,7 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const newEdge: Edge = {
|
||||
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
|
||||
type: 'custom',
|
||||
type: CUSTOM_EDGE,
|
||||
source: prevNodeId,
|
||||
sourceHandle: prevNodeSourceHandle,
|
||||
target: newNode.id,
|
||||
@@ -662,8 +683,10 @@ export const useNodesInteractions = () => {
|
||||
node.data._children?.push(newNode.id)
|
||||
})
|
||||
draft.push(newNode)
|
||||
if (newIterationStartNode)
|
||||
draft.push(newIterationStartNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
|
||||
if (newNode.data.type === BlockEnum.VariableAssigner || newNode.data.type === BlockEnum.VariableAggregator) {
|
||||
const { setShowAssignVariablePopup } = workflowStore.getState()
|
||||
|
||||
@@ -687,7 +710,14 @@ export const useNodesInteractions = () => {
|
||||
})
|
||||
draft.push(newEdge)
|
||||
})
|
||||
setEdges(newEdges)
|
||||
|
||||
if (checkNestedParallelLimit(newNodes, newEdges, prevNode.parentId)) {
|
||||
setNodes(newNodes)
|
||||
setEdges(newEdges)
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (!prevNodeId && nextNodeId) {
|
||||
const nextNodeIndex = nodes.findIndex(node => node.id === nextNodeId)
|
||||
@@ -706,15 +736,13 @@ export const useNodesInteractions = () => {
|
||||
newNode.data.iteration_id = nextNode.parentId
|
||||
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
|
||||
}
|
||||
if (nextNode.data.isIterationStart)
|
||||
newNode.data.isIterationStart = true
|
||||
|
||||
let newEdge
|
||||
|
||||
if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier)) {
|
||||
newEdge = {
|
||||
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
||||
type: 'custom',
|
||||
type: CUSTOM_EDGE,
|
||||
source: newNode.id,
|
||||
sourceHandle,
|
||||
target: nextNodeId,
|
||||
@@ -763,13 +791,11 @@ export const useNodesInteractions = () => {
|
||||
node.data.start_node_id = newNode.id
|
||||
node.data.startNodeType = newNode.data.type
|
||||
}
|
||||
|
||||
if (node.id === nextNodeId && node.data.isIterationStart)
|
||||
node.data.isIterationStart = false
|
||||
})
|
||||
draft.push(newNode)
|
||||
if (newIterationStartNode)
|
||||
draft.push(newIterationStartNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
if (newEdge) {
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((item) => {
|
||||
@@ -780,7 +806,21 @@ export const useNodesInteractions = () => {
|
||||
})
|
||||
draft.push(newEdge)
|
||||
})
|
||||
setEdges(newEdges)
|
||||
|
||||
if (checkNestedParallelLimit(newNodes, newEdges, nextNode.parentId)) {
|
||||
setNodes(newNodes)
|
||||
setEdges(newEdges)
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (checkNestedParallelLimit(newNodes, edges))
|
||||
setNodes(newNodes)
|
||||
|
||||
else
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (prevNodeId && nextNodeId) {
|
||||
@@ -804,7 +844,7 @@ export const useNodesInteractions = () => {
|
||||
const currentEdgeIndex = edges.findIndex(edge => edge.source === prevNodeId && edge.target === nextNodeId)
|
||||
const newPrevEdge = {
|
||||
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
|
||||
type: 'custom',
|
||||
type: CUSTOM_EDGE,
|
||||
source: prevNodeId,
|
||||
sourceHandle: prevNodeSourceHandle,
|
||||
target: newNode.id,
|
||||
@@ -822,7 +862,7 @@ export const useNodesInteractions = () => {
|
||||
if (nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier) {
|
||||
newNextEdge = {
|
||||
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
||||
type: 'custom',
|
||||
type: CUSTOM_EDGE,
|
||||
source: newNode.id,
|
||||
sourceHandle,
|
||||
target: nextNodeId,
|
||||
@@ -865,6 +905,8 @@ export const useNodesInteractions = () => {
|
||||
node.data._children?.push(newNode.id)
|
||||
})
|
||||
draft.push(newNode)
|
||||
if (newIterationStartNode)
|
||||
draft.push(newIterationStartNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
if (newNode.data.type === BlockEnum.VariableAssigner || newNode.data.type === BlockEnum.VariableAggregator) {
|
||||
@@ -898,7 +940,7 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
handleSyncWorkflowDraft()
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeAdd)
|
||||
}, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch])
|
||||
}, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch, checkNestedParallelLimit])
|
||||
|
||||
const handleNodeChange = useCallback((
|
||||
currentNodeId: string,
|
||||
@@ -919,7 +961,10 @@ export const useNodesInteractions = () => {
|
||||
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({
|
||||
const {
|
||||
newNode: newCurrentNode,
|
||||
newIterationStartNode,
|
||||
} = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
@@ -929,7 +974,6 @@ export const useNodesInteractions = () => {
|
||||
selected: currentNode.data.selected,
|
||||
isInIteration: currentNode.data.isInIteration,
|
||||
iteration_id: currentNode.data.iteration_id,
|
||||
isIterationStart: currentNode.data.isIterationStart,
|
||||
},
|
||||
position: {
|
||||
x: currentNode.position.x,
|
||||
@@ -955,18 +999,12 @@ export const useNodesInteractions = () => {
|
||||
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
|
||||
}
|
||||
}
|
||||
if (node.id === currentNode.parentId && currentNode.data.isIterationStart) {
|
||||
node.data._children = [
|
||||
newCurrentNode.id,
|
||||
...(node.data._children || []),
|
||||
].filter(child => child !== currentNodeId)
|
||||
node.data.start_node_id = newCurrentNode.id
|
||||
node.data.startNodeType = newCurrentNode.data.type
|
||||
}
|
||||
})
|
||||
const index = draft.findIndex(node => node.id === currentNodeId)
|
||||
|
||||
draft.splice(index, 1, newCurrentNode)
|
||||
if (newIterationStartNode)
|
||||
draft.push(newIterationStartNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
@@ -1011,7 +1049,7 @@ export const useNodesInteractions = () => {
|
||||
}, [store])
|
||||
|
||||
const handleNodeContextMenu = useCallback((e: MouseEvent, node: Node) => {
|
||||
if (node.type === CUSTOM_NOTE_NODE)
|
||||
if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_ITERATION_START_NODE)
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
@@ -1041,7 +1079,7 @@ export const useNodesInteractions = () => {
|
||||
|
||||
if (nodeId) {
|
||||
// If nodeId is provided, copy that specific node
|
||||
const nodeToCopy = nodes.find(node => node.id === nodeId && node.data.type !== BlockEnum.Start)
|
||||
const nodeToCopy = nodes.find(node => node.id === nodeId && node.data.type !== BlockEnum.Start && node.type !== CUSTOM_ITERATION_START_NODE)
|
||||
if (nodeToCopy)
|
||||
setClipboardElements([nodeToCopy])
|
||||
}
|
||||
@@ -1087,7 +1125,10 @@ export const useNodesInteractions = () => {
|
||||
clipboardElements.forEach((nodeToPaste, index) => {
|
||||
const nodeType = nodeToPaste.data.type
|
||||
|
||||
const newNode = generateNewNode({
|
||||
const {
|
||||
newNode,
|
||||
newIterationStartNode,
|
||||
} = generateNewNode({
|
||||
type: nodeToPaste.type,
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
@@ -1106,24 +1147,17 @@ export const useNodesInteractions = () => {
|
||||
zIndex: nodeToPaste.zIndex,
|
||||
})
|
||||
newNode.id = newNode.id + index
|
||||
|
||||
// If only the iteration start node is copied, remove the isIterationStart flag
|
||||
// This new node is movable and can be placed anywhere
|
||||
if (clipboardElements.length === 1 && newNode.data.isIterationStart)
|
||||
newNode.data.isIterationStart = false
|
||||
|
||||
let newChildren: Node[] = []
|
||||
if (nodeToPaste.data.type === BlockEnum.Iteration) {
|
||||
newNode.data._children = [];
|
||||
(newNode.data as IterationNodeType).start_node_id = ''
|
||||
newIterationStartNode!.parentId = newNode.id;
|
||||
(newNode.data as IterationNodeType).start_node_id = newIterationStartNode!.id
|
||||
|
||||
newChildren = handleNodeIterationChildrenCopy(nodeToPaste.id, newNode.id)
|
||||
|
||||
newChildren.forEach((child) => {
|
||||
newNode.data._children?.push(child.id)
|
||||
if (child.data.isIterationStart)
|
||||
(newNode.data as IterationNodeType).start_node_id = child.id
|
||||
})
|
||||
newChildren.push(newIterationStartNode!)
|
||||
}
|
||||
|
||||
nodesToPaste.push(newNode)
|
||||
@@ -1230,6 +1264,42 @@ export const useNodesInteractions = () => {
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeResize)
|
||||
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
|
||||
const handleNodeDisconnect = useCallback((nodeId: string) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||
const connectedEdges = getConnectedEdges([currentNode], 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],
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
return draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id))
|
||||
})
|
||||
setEdges(newEdges)
|
||||
handleSyncWorkflowDraft()
|
||||
saveStateToHistory(WorkflowHistoryEvent.EdgeDelete)
|
||||
}, [store, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
|
||||
const handleHistoryBack = useCallback(() => {
|
||||
if (getNodesReadOnly() || getWorkflowReadOnly())
|
||||
return
|
||||
@@ -1282,6 +1352,7 @@ export const useNodesInteractions = () => {
|
||||
handleNodesDuplicate,
|
||||
handleNodesDelete,
|
||||
handleNodeResize,
|
||||
handleNodeDisconnect,
|
||||
handleHistoryBack,
|
||||
handleHistoryForward,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
getIncomers,
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
@@ -8,6 +9,7 @@ import { v4 as uuidV4 } from 'uuid'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { useNodesSyncDraft } from '../hooks'
|
||||
import type { Node } from '../types'
|
||||
import {
|
||||
NodeRunningStatus,
|
||||
WorkflowRunningStatus,
|
||||
@@ -140,9 +142,6 @@ export const useWorkflowRun = () => {
|
||||
resultText: '',
|
||||
})
|
||||
|
||||
let isInIteration = false
|
||||
let iterationLength = 0
|
||||
|
||||
let ttsUrl = ''
|
||||
let ttsIsPublic = false
|
||||
if (params.token) {
|
||||
@@ -186,7 +185,7 @@ export const useWorkflowRun = () => {
|
||||
draft.forEach((edge) => {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_run: false,
|
||||
_runned: false,
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -249,19 +248,20 @@ export const useWorkflowRun = () => {
|
||||
setEdges,
|
||||
transform,
|
||||
} = store.getState()
|
||||
if (isInIteration) {
|
||||
const nodes = getNodes()
|
||||
const node = nodes.find(node => node.id === data.node_id)
|
||||
if (node?.parentId) {
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
const tracing = draft.tracing!
|
||||
const iterations = tracing[tracing.length - 1]
|
||||
const currIteration = iterations.details![iterations.details!.length - 1]
|
||||
currIteration.push({
|
||||
const iterations = tracing.find(trace => trace.node_id === node?.parentId)
|
||||
const currIteration = iterations?.details![node.data.iteration_index] || iterations?.details![iterations.details!.length - 1]
|
||||
currIteration?.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as any)
|
||||
}))
|
||||
}
|
||||
else {
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.tracing!.push({
|
||||
...data,
|
||||
@@ -288,11 +288,12 @@ export const useWorkflowRun = () => {
|
||||
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const incomeNodesId = getIncomers({ id: data.node_id } as Node, newNodes, edges).filter(node => node.data._runningStatus === NodeRunningStatus.Succeeded).map(node => node.id)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const edge = draft.find(edge => edge.target === data.node_id && edge.source === prevNodeId)
|
||||
|
||||
if (edge)
|
||||
edge.data = { ...edge.data, _run: true } as any
|
||||
draft.forEach((edge) => {
|
||||
if (edge.target === data.node_id && incomeNodesId.includes(edge.source))
|
||||
edge.data = { ...edge.data, _runned: true } as any
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}
|
||||
@@ -309,25 +310,46 @@ export const useWorkflowRun = () => {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
if (isInIteration) {
|
||||
const nodes = getNodes()
|
||||
const nodeParentId = nodes.find(node => node.id === data.node_id)!.parentId
|
||||
if (nodeParentId) {
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
const tracing = draft.tracing!
|
||||
const iterations = tracing[tracing.length - 1]
|
||||
const currIteration = iterations.details![iterations.details!.length - 1]
|
||||
const nodeInfo = currIteration[currIteration.length - 1]
|
||||
const iterations = tracing.find(trace => trace.node_id === nodeParentId) // the iteration node
|
||||
|
||||
currIteration[currIteration.length - 1] = {
|
||||
...nodeInfo,
|
||||
...data,
|
||||
status: NodeRunningStatus.Succeeded,
|
||||
} as any
|
||||
if (iterations && iterations.details) {
|
||||
const iterationIndex = data.execution_metadata?.iteration_index || 0
|
||||
if (!iterations.details[iterationIndex])
|
||||
iterations.details[iterationIndex] = []
|
||||
|
||||
const currIteration = iterations.details[iterationIndex]
|
||||
const nodeIndex = currIteration.findIndex(node =>
|
||||
node.node_id === data.node_id && (
|
||||
node.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || node.parallel_id === data.execution_metadata?.parallel_id),
|
||||
)
|
||||
if (data.status === NodeRunningStatus.Succeeded) {
|
||||
if (nodeIndex !== -1) {
|
||||
currIteration[nodeIndex] = {
|
||||
...currIteration[nodeIndex],
|
||||
...data,
|
||||
} as any
|
||||
}
|
||||
else {
|
||||
currIteration.push({
|
||||
...data,
|
||||
} as any)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
else {
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
const currentIndex = draft.tracing!.findIndex(trace => trace.node_id === data.node_id)
|
||||
|
||||
const currentIndex = draft.tracing!.findIndex((trace) => {
|
||||
if (!trace.execution_metadata?.parallel_id)
|
||||
return trace.node_id === data.node_id
|
||||
return trace.node_id === data.node_id && trace.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id
|
||||
})
|
||||
if (currentIndex > -1 && draft.tracing) {
|
||||
draft.tracing[currentIndex] = {
|
||||
...(draft.tracing[currentIndex].extras
|
||||
@@ -337,16 +359,14 @@ export const useWorkflowRun = () => {
|
||||
} 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)
|
||||
},
|
||||
@@ -371,8 +391,6 @@ export const useWorkflowRun = () => {
|
||||
details: [],
|
||||
} as any)
|
||||
}))
|
||||
isInIteration = true
|
||||
iterationLength = data.metadata.iterator_length
|
||||
|
||||
const {
|
||||
setViewport,
|
||||
@@ -398,7 +416,7 @@ export const useWorkflowRun = () => {
|
||||
const edge = draft.find(edge => edge.target === data.node_id && edge.source === prevNodeId)
|
||||
|
||||
if (edge)
|
||||
edge.data = { ...edge.data, _run: true } as any
|
||||
edge.data = { ...edge.data, _runned: true } as any
|
||||
})
|
||||
setEdges(newEdges)
|
||||
|
||||
@@ -418,13 +436,13 @@ export const useWorkflowRun = () => {
|
||||
} = store.getState()
|
||||
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
const iteration = draft.tracing![draft.tracing!.length - 1]
|
||||
if (iteration.details!.length >= iterationLength)
|
||||
return
|
||||
|
||||
iteration.details!.push([])
|
||||
const iteration = draft.tracing!.find(trace => trace.node_id === data.node_id)
|
||||
if (iteration) {
|
||||
if (iteration.details!.length >= iteration.metadata.iterator_length!)
|
||||
return
|
||||
}
|
||||
iteration?.details!.push([])
|
||||
}))
|
||||
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(node => node.id === data.node_id)!
|
||||
@@ -450,13 +468,14 @@ export const useWorkflowRun = () => {
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
const tracing = draft.tracing!
|
||||
tracing[tracing.length - 1] = {
|
||||
...tracing[tracing.length - 1],
|
||||
...data,
|
||||
status: NodeRunningStatus.Succeeded,
|
||||
} as any
|
||||
const currIterationNode = tracing.find(trace => trace.node_id === data.node_id)
|
||||
if (currIterationNode) {
|
||||
Object.assign(currIterationNode, {
|
||||
...data,
|
||||
status: NodeRunningStatus.Succeeded,
|
||||
})
|
||||
}
|
||||
}))
|
||||
isInIteration = false
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(node => node.id === data.node_id)!
|
||||
@@ -470,6 +489,12 @@ export const useWorkflowRun = () => {
|
||||
if (onIterationFinish)
|
||||
onIterationFinish(params)
|
||||
},
|
||||
onParallelBranchStarted: (params) => {
|
||||
// console.log(params, 'parallel start')
|
||||
},
|
||||
onParallelBranchFinished: (params) => {
|
||||
// console.log(params, 'finished')
|
||||
},
|
||||
onTextChunk: (params) => {
|
||||
const { data: { text } } = params
|
||||
const {
|
||||
|
||||
@@ -10,13 +10,13 @@ export const useWorkflowTemplate = () => {
|
||||
const isChatMode = useIsChatMode()
|
||||
const nodesInitialData = useNodesInitialData()
|
||||
|
||||
const startNode = generateNewNode({
|
||||
const { newNode: startNode } = generateNewNode({
|
||||
data: nodesInitialData.start,
|
||||
position: START_INITIAL_POSITION,
|
||||
})
|
||||
|
||||
if (isChatMode) {
|
||||
const llmNode = generateNewNode({
|
||||
const { newNode: llmNode } = generateNewNode({
|
||||
id: 'llm',
|
||||
data: {
|
||||
...nodesInitialData.llm,
|
||||
@@ -31,7 +31,7 @@ export const useWorkflowTemplate = () => {
|
||||
},
|
||||
} as any)
|
||||
|
||||
const answerNode = generateNewNode({
|
||||
const { newNode: answerNode } = generateNewNode({
|
||||
id: 'answer',
|
||||
data: {
|
||||
...nodesInitialData.answer,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { uniqBy } from 'lodash-es'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import {
|
||||
getIncomers,
|
||||
@@ -29,6 +30,11 @@ import {
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
getParallelInfo,
|
||||
} from '../utils'
|
||||
import {
|
||||
PARALLEL_DEPTH_LIMIT,
|
||||
PARALLEL_LIMIT,
|
||||
SUPPORT_OUTPUT_VARS_NODE,
|
||||
} from '../constants'
|
||||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
@@ -50,6 +56,7 @@ import {
|
||||
} from '@/service/tools'
|
||||
import I18n from '@/context/i18n'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
||||
|
||||
export const useIsChatMode = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
@@ -58,6 +65,7 @@ export const useIsChatMode = () => {
|
||||
}
|
||||
|
||||
export const useWorkflow = () => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
@@ -77,7 +85,7 @@ export const useWorkflow = () => {
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
if (currentNode?.parentId)
|
||||
startNode = nodes.find(node => node.parentId === currentNode.parentId && node.data.isIterationStart)
|
||||
startNode = nodes.find(node => node.parentId === currentNode.parentId && node.type === CUSTOM_ITERATION_START_NODE)
|
||||
|
||||
if (!startNode)
|
||||
return []
|
||||
@@ -275,6 +283,45 @@ export const useWorkflow = () => {
|
||||
return isUsed
|
||||
}, [isVarUsedInNodes])
|
||||
|
||||
const checkParallelLimit = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||
const sourceNodeOutgoers = getOutgoers(currentNode, nodes, edges)
|
||||
if (sourceNodeOutgoers.length > PARALLEL_LIMIT - 1) {
|
||||
const { setShowTips } = workflowStore.getState()
|
||||
setShowTips(t('workflow.common.parallelTip.limit', { num: PARALLEL_LIMIT }))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, [store, workflowStore, t])
|
||||
|
||||
const checkNestedParallelLimit = useCallback((nodes: Node[], edges: Edge[], parentNodeId?: string) => {
|
||||
const {
|
||||
parallelList,
|
||||
hasAbnormalEdges,
|
||||
} = getParallelInfo(nodes, edges, parentNodeId)
|
||||
|
||||
if (hasAbnormalEdges)
|
||||
return false
|
||||
|
||||
for (let i = 0; i < parallelList.length; i++) {
|
||||
const parallel = parallelList[i]
|
||||
|
||||
if (parallel.depth > PARALLEL_DEPTH_LIMIT) {
|
||||
const { setShowTips } = workflowStore.getState()
|
||||
setShowTips(t('workflow.common.parallelTip.depthLimit', { num: PARALLEL_DEPTH_LIMIT }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}, [t, workflowStore])
|
||||
|
||||
const isValidConnection = useCallback(({ source, target }: Connection) => {
|
||||
const {
|
||||
edges,
|
||||
@@ -284,12 +331,15 @@ export const useWorkflow = () => {
|
||||
const sourceNode: Node = nodes.find(node => node.id === source)!
|
||||
const targetNode: Node = nodes.find(node => node.id === target)!
|
||||
|
||||
if (targetNode.data.isIterationStart)
|
||||
if (!checkParallelLimit(source!))
|
||||
return false
|
||||
|
||||
if (sourceNode.type === CUSTOM_NOTE_NODE || targetNode.type === CUSTOM_NOTE_NODE)
|
||||
return false
|
||||
|
||||
if (sourceNode.parentId !== targetNode.parentId)
|
||||
return false
|
||||
|
||||
if (sourceNode && targetNode) {
|
||||
const sourceNodeAvailableNextNodes = nodesExtraData[sourceNode.data.type].availableNextNodes
|
||||
const targetNodeAvailablePrevNodes = [...nodesExtraData[targetNode.data.type].availablePrevNodes, BlockEnum.Start]
|
||||
@@ -316,7 +366,7 @@ export const useWorkflow = () => {
|
||||
}
|
||||
|
||||
return !hasCycle(targetNode)
|
||||
}, [store, nodesExtraData])
|
||||
}, [store, nodesExtraData, checkParallelLimit])
|
||||
|
||||
const formatTimeFromNow = useCallback((time: number) => {
|
||||
return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow()
|
||||
@@ -339,6 +389,8 @@ export const useWorkflow = () => {
|
||||
isVarUsedInNodes,
|
||||
removeUsedVarInNodes,
|
||||
isNodeVarsUsedInNodes,
|
||||
checkParallelLimit,
|
||||
checkNestedParallelLimit,
|
||||
isValidConnection,
|
||||
formatTimeFromNow,
|
||||
getNode,
|
||||
|
||||
@@ -55,6 +55,8 @@ import Header from './header'
|
||||
import CustomNode from './nodes'
|
||||
import CustomNoteNode from './note-node'
|
||||
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
||||
import CustomIterationStartNode from './nodes/iteration-start'
|
||||
import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants'
|
||||
import Operator from './operator'
|
||||
import CustomEdge from './custom-edge'
|
||||
import CustomConnectionLine from './custom-connection-line'
|
||||
@@ -67,6 +69,7 @@ import NodeContextmenu from './node-contextmenu'
|
||||
import SyncingDataModal from './syncing-data-modal'
|
||||
import UpdateDSLModal from './update-dsl-modal'
|
||||
import DSLExportConfirmModal from './dsl-export-confirm-modal'
|
||||
import LimitTips from './limit-tips'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
@@ -92,6 +95,7 @@ import Confirm from '@/app/components/base/confirm'
|
||||
const nodeTypes = {
|
||||
[CUSTOM_NODE]: CustomNode,
|
||||
[CUSTOM_NOTE_NODE]: CustomNoteNode,
|
||||
[CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode,
|
||||
}
|
||||
const edgeTypes = {
|
||||
[CUSTOM_NODE]: CustomEdge,
|
||||
@@ -317,6 +321,7 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
/>
|
||||
)
|
||||
}
|
||||
<LimitTips />
|
||||
<ReactFlow
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
|
||||
39
web/app/components/workflow/limit-tips.tsx
Normal file
39
web/app/components/workflow/limit-tips.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
RiAlertFill,
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
import { useStore } from './store'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
const LimitTips = () => {
|
||||
const showTips = useStore(s => s.showTips)
|
||||
const setShowTips = useStore(s => s.setShowTips)
|
||||
|
||||
if (!showTips)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='absolute bottom-16 left-1/2 -translate-x-1/2 flex items-center rounded-xl p-2 h-10 border border-components-panel-border bg-components-panel-bg-blur shadow-md z-[9]'>
|
||||
<div
|
||||
className='absolute inset-0 opacity-[0.4] rounded-xl'
|
||||
style={{
|
||||
background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)',
|
||||
}}
|
||||
></div>
|
||||
<div className='flex items-center justify-center w-5 h-5'>
|
||||
<RiAlertFill className='w-4 h-4 text-text-warning-secondary' />
|
||||
</div>
|
||||
<div className='mx-1 px-1 system-xs-medium text-text-primary'>
|
||||
{showTips}
|
||||
</div>
|
||||
<ActionButton
|
||||
className='z-[1]'
|
||||
onClick={() => setShowTips('')}
|
||||
>
|
||||
<RiCloseLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LimitTips
|
||||
@@ -21,13 +21,13 @@ type AddProps = {
|
||||
nodeId: string
|
||||
nodeData: CommonNodeType
|
||||
sourceHandle: string
|
||||
branchName?: string
|
||||
isParallel?: boolean
|
||||
}
|
||||
const Add = ({
|
||||
nodeId,
|
||||
nodeData,
|
||||
sourceHandle,
|
||||
branchName,
|
||||
isParallel,
|
||||
}: AddProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
@@ -57,23 +57,19 @@ const Add = ({
|
||||
${nodesReadOnly && '!cursor-not-allowed'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
branchName && (
|
||||
<div
|
||||
className='absolute left-1 right-1 -top-[7.5px] flex items-center h-3 text-[10px] text-text-placeholder font-semibold'
|
||||
title={branchName.toLocaleUpperCase()}
|
||||
>
|
||||
<div className='inline-block px-0.5 rounded-[5px] bg-background-default truncate'>{branchName.toLocaleUpperCase()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='flex items-center justify-center mr-1.5 w-5 h-5 rounded-[5px] bg-background-default-dimm'>
|
||||
<RiAddLine className='w-3 h-3' />
|
||||
</div>
|
||||
{t('workflow.panel.selectNextStep')}
|
||||
<div className='flex items-center uppercase'>
|
||||
{
|
||||
isParallel
|
||||
? t('workflow.common.addParallelNode')
|
||||
: t('workflow.panel.selectNextStep')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [branchName, t, nodesReadOnly])
|
||||
}, [t, nodesReadOnly, isParallel])
|
||||
|
||||
return (
|
||||
<BlockSelector
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import Add from './add'
|
||||
import Item from './item'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type ContainerProps = {
|
||||
nodeId: string
|
||||
nodeData: CommonNodeType
|
||||
sourceHandle: string
|
||||
nextNodes: Node[]
|
||||
branchName?: string
|
||||
}
|
||||
|
||||
const Container = ({
|
||||
nodeId,
|
||||
nodeData,
|
||||
sourceHandle,
|
||||
nextNodes,
|
||||
branchName,
|
||||
}: ContainerProps) => {
|
||||
return (
|
||||
<div className='p-0.5 space-y-0.5 rounded-[10px] bg-background-section-burn'>
|
||||
{
|
||||
branchName && (
|
||||
<div
|
||||
className='flex items-center px-2 system-2xs-semibold-uppercase text-text-tertiary truncate'
|
||||
title={branchName}
|
||||
>
|
||||
{branchName}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
nextNodes.map(nextNode => (
|
||||
<Item
|
||||
key={nextNode.id}
|
||||
nodeId={nextNode.id}
|
||||
data={nextNode.data}
|
||||
sourceHandle='source'
|
||||
/>
|
||||
))
|
||||
}
|
||||
<Add
|
||||
isParallel={!!nextNodes.length}
|
||||
nodeId={nodeId}
|
||||
nodeData={nodeData}
|
||||
sourceHandle={sourceHandle}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Container
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo } from 'react'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
getConnectedEdges,
|
||||
getOutgoers,
|
||||
@@ -8,13 +9,11 @@ import {
|
||||
import { useToolIcon } from '../../../../hooks'
|
||||
import BlockIcon from '../../../../block-icon'
|
||||
import type {
|
||||
Branch,
|
||||
Node,
|
||||
} from '../../../../types'
|
||||
import { BlockEnum } from '../../../../types'
|
||||
import Add from './add'
|
||||
import Item from './item'
|
||||
import Line from './line'
|
||||
import Container from './container'
|
||||
|
||||
type NextStepProps = {
|
||||
selectedNode: Node
|
||||
@@ -22,15 +21,33 @@ type NextStepProps = {
|
||||
const NextStep = ({
|
||||
selectedNode,
|
||||
}: NextStepProps) => {
|
||||
const { t } = useTranslation()
|
||||
const data = selectedNode.data
|
||||
const toolIcon = useToolIcon(data)
|
||||
const store = useStoreApi()
|
||||
const branches = data._targetBranches || []
|
||||
const branches = useMemo(() => {
|
||||
return data._targetBranches || []
|
||||
}, [data])
|
||||
const nodeWithBranches = data.type === BlockEnum.IfElse || data.type === BlockEnum.QuestionClassifier
|
||||
const edges = useEdges()
|
||||
const outgoers = getOutgoers(selectedNode as Node, store.getState().getNodes(), edges)
|
||||
const connectedEdges = getConnectedEdges([selectedNode] as Node[], edges).filter(edge => edge.source === selectedNode!.id)
|
||||
|
||||
const branchesOutgoers = useMemo(() => {
|
||||
if (!branches?.length)
|
||||
return []
|
||||
|
||||
return branches.map((branch) => {
|
||||
const connected = connectedEdges.filter(edge => edge.sourceHandle === branch.id)
|
||||
const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!)
|
||||
|
||||
return {
|
||||
branch,
|
||||
nextNodes,
|
||||
}
|
||||
})
|
||||
}, [branches, connectedEdges, outgoers])
|
||||
|
||||
return (
|
||||
<div className='flex py-1'>
|
||||
<div className='shrink-0 relative flex items-center justify-center w-9 h-9 bg-background-default rounded-lg border-[0.5px] border-divider-regular shadow-xs'>
|
||||
@@ -39,59 +56,32 @@ const NextStep = ({
|
||||
toolIcon={toolIcon}
|
||||
/>
|
||||
</div>
|
||||
<Line linesNumber={nodeWithBranches ? branches.length : 1} />
|
||||
<div className='grow'>
|
||||
<Line
|
||||
list={nodeWithBranches ? branchesOutgoers.map(item => item.nextNodes.length + 1) : [1]}
|
||||
/>
|
||||
<div className='grow space-y-2'>
|
||||
{
|
||||
!nodeWithBranches && !!outgoers.length && (
|
||||
<Item
|
||||
nodeId={outgoers[0].id}
|
||||
data={outgoers[0].data}
|
||||
sourceHandle='source'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!nodeWithBranches && !outgoers.length && (
|
||||
<Add
|
||||
!nodeWithBranches && (
|
||||
<Container
|
||||
nodeId={selectedNode!.id}
|
||||
nodeData={selectedNode!.data}
|
||||
sourceHandle='source'
|
||||
nextNodes={outgoers}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!branches?.length && nodeWithBranches && (
|
||||
branches.map((branch: Branch) => {
|
||||
const connected = connectedEdges.find(edge => edge.sourceHandle === branch.id)
|
||||
const target = outgoers.find(outgoer => outgoer.id === connected?.target)
|
||||
|
||||
nodeWithBranches && (
|
||||
branchesOutgoers.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
key={branch.id}
|
||||
className='mb-3 last-of-type:mb-0'
|
||||
>
|
||||
{
|
||||
connected && (
|
||||
<Item
|
||||
data={target!.data!}
|
||||
nodeId={target!.id}
|
||||
sourceHandle={branch.id}
|
||||
branchName={branch.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!connected && (
|
||||
<Add
|
||||
key={branch.id}
|
||||
nodeId={selectedNode!.id}
|
||||
nodeData={selectedNode!.data}
|
||||
sourceHandle={branch.id}
|
||||
branchName={branch.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<Container
|
||||
key={item.branch.id}
|
||||
nodeId={selectedNode!.id}
|
||||
nodeData={selectedNode!.data}
|
||||
sourceHandle={item.branch.id}
|
||||
nextNodes={item.nextNodes}
|
||||
branchName={item.branch.name || `${t('workflow.nodes.questionClassifiers.class')} ${index + 1}`}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,94 +1,82 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { intersection } from 'lodash-es'
|
||||
import Operator from './operator'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useToolIcon,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ItemProps = {
|
||||
nodeId: string
|
||||
sourceHandle: string
|
||||
branchName?: string
|
||||
data: CommonNodeType
|
||||
}
|
||||
const Item = ({
|
||||
nodeId,
|
||||
sourceHandle,
|
||||
branchName,
|
||||
data,
|
||||
}: ItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleNodeChange } = useNodesInteractions()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const toolIcon = useToolIcon(data)
|
||||
const {
|
||||
availablePrevBlocks,
|
||||
availableNextBlocks,
|
||||
} = useAvailableBlocks(data.type, data.isInIteration)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
|
||||
}, [nodeId, sourceHandle, handleNodeChange])
|
||||
const renderTrigger = useCallback((open: boolean) => {
|
||||
return (
|
||||
<Button
|
||||
size='small'
|
||||
className={`
|
||||
hidden group-hover:flex
|
||||
${open && '!bg-gray-100 !flex'}
|
||||
`}
|
||||
>
|
||||
{t('workflow.panel.change')}
|
||||
</Button>
|
||||
)
|
||||
}, [t])
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='relative group flex items-center mb-3 last-of-type:mb-0 px-2 h-9 rounded-lg border-[0.5px] border-divider-regular bg-background-default hover:bg-background-default-hover shadow-xs text-xs text-text-secondary cursor-pointer'
|
||||
className='relative group flex items-center last-of-type:mb-0 px-2 h-9 rounded-lg border-[0.5px] border-divider-regular bg-background-default hover:bg-background-default-hover shadow-xs text-xs text-text-secondary cursor-pointer'
|
||||
>
|
||||
{
|
||||
branchName && (
|
||||
<div
|
||||
className='absolute left-1 right-1 -top-[7.5px] flex items-center h-3 text-[10px] text-gray-500 font-semibold'
|
||||
title={branchName.toLocaleUpperCase()}
|
||||
>
|
||||
<div className='inline-block px-0.5 rounded-[5px] bg-white truncate'>{branchName.toLocaleUpperCase()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<BlockIcon
|
||||
type={data.type}
|
||||
toolIcon={toolIcon}
|
||||
className='shrink-0 mr-1.5'
|
||||
/>
|
||||
<div className='grow system-xs-medium text-text-secondary'>{data.title}</div>
|
||||
<div
|
||||
className='grow system-xs-medium text-text-secondary truncate'
|
||||
title={data.title}
|
||||
>
|
||||
{data.title}
|
||||
</div>
|
||||
{
|
||||
!nodesReadOnly && (
|
||||
<BlockSelector
|
||||
onSelect={handleSelect}
|
||||
placement='top-end'
|
||||
offset={{
|
||||
mainAxis: 6,
|
||||
crossAxis: 8,
|
||||
}}
|
||||
trigger={renderTrigger}
|
||||
popupClassName='!w-[328px]'
|
||||
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks).filter(item => item !== data.type)}
|
||||
/>
|
||||
<>
|
||||
<Button
|
||||
className='hidden group-hover:flex shrink-0 mr-1'
|
||||
size='small'
|
||||
onClick={() => handleNodeSelect(nodeId)}
|
||||
>
|
||||
{t('workflow.common.jumpToNode')}
|
||||
</Button>
|
||||
<div
|
||||
className={cn(
|
||||
'hidden shrink-0 group-hover:flex items-center',
|
||||
open && 'flex',
|
||||
)}
|
||||
>
|
||||
<Operator
|
||||
data={data}
|
||||
nodeId={nodeId}
|
||||
sourceHandle={sourceHandle}
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,56 +1,70 @@
|
||||
import { memo } from 'react'
|
||||
|
||||
type LineProps = {
|
||||
linesNumber: number
|
||||
list: number[]
|
||||
}
|
||||
const Line = ({
|
||||
linesNumber,
|
||||
list,
|
||||
}: LineProps) => {
|
||||
const svgHeight = linesNumber * 36 + (linesNumber - 1) * 12
|
||||
const listHeight = list.map((item) => {
|
||||
return item * 36 + (item - 1) * 2 + 12 + 6
|
||||
})
|
||||
const processedList = listHeight.map((item, index) => {
|
||||
if (index === 0)
|
||||
return item
|
||||
|
||||
return listHeight.slice(0, index).reduce((acc, cur) => acc + cur, 0) + item
|
||||
})
|
||||
const processedListLength = processedList.length
|
||||
const svgHeight = processedList[processedListLength - 1] + (processedListLength - 1) * 8
|
||||
|
||||
return (
|
||||
<svg className='shrink-0 w-6' style={{ height: svgHeight }}>
|
||||
{
|
||||
Array(linesNumber).fill(0).map((_, index) => (
|
||||
<g key={index}>
|
||||
{
|
||||
index === 0 && (
|
||||
<>
|
||||
processedList.map((item, index) => {
|
||||
const prevItem = index > 0 ? processedList[index - 1] : 0
|
||||
const space = prevItem + index * 8 + 16
|
||||
return (
|
||||
<g key={index}>
|
||||
{
|
||||
index === 0 && (
|
||||
<>
|
||||
<path
|
||||
d='M0,18 L24,18'
|
||||
strokeWidth={1}
|
||||
fill='none'
|
||||
className='stroke-divider-solid'
|
||||
/>
|
||||
<rect
|
||||
x={0}
|
||||
y={16}
|
||||
width={1}
|
||||
height={4}
|
||||
className='fill-divider-solid-alt'
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
index > 0 && (
|
||||
<path
|
||||
d='M0,18 L24,18'
|
||||
d={`M0,18 Q12,18 12,28 L12,${space - 10 + 2} Q12,${space + 2} 24,${space + 2}`}
|
||||
strokeWidth={1}
|
||||
fill='none'
|
||||
className='stroke-divider-solid'
|
||||
/>
|
||||
<rect
|
||||
x={0}
|
||||
y={16}
|
||||
width={1}
|
||||
height={4}
|
||||
className='fill-divider-solid-alt'
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
index > 0 && (
|
||||
<path
|
||||
d={`M0,18 Q12,18 12,28 L12,${index * 48 + 18 - 10} Q12,${index * 48 + 18} 24,${index * 48 + 18}`}
|
||||
strokeWidth={1}
|
||||
fill='none'
|
||||
className='stroke-divider-solid'
|
||||
/>
|
||||
)
|
||||
}
|
||||
<rect
|
||||
x={23}
|
||||
y={index * 48 + 18 - 2}
|
||||
width={1}
|
||||
height={4}
|
||||
className='fill-divider-solid-alt'
|
||||
/>
|
||||
</g>
|
||||
))
|
||||
)
|
||||
}
|
||||
<rect
|
||||
x={23}
|
||||
y={space}
|
||||
width={1}
|
||||
height={4}
|
||||
className='fill-divider-solid-alt'
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
})
|
||||
}
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiMoreFill } from '@remixicon/react'
|
||||
import { intersection } from 'lodash-es'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useNodesInteractions,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type ChangeItemProps = {
|
||||
data: CommonNodeType
|
||||
nodeId: string
|
||||
sourceHandle: string
|
||||
}
|
||||
const ChangeItem = ({
|
||||
data,
|
||||
nodeId,
|
||||
sourceHandle,
|
||||
}: ChangeItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { handleNodeChange } = useNodesInteractions()
|
||||
const {
|
||||
availablePrevBlocks,
|
||||
availableNextBlocks,
|
||||
} = useAvailableBlocks(data.type, data.isInIteration)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
|
||||
}, [nodeId, sourceHandle, handleNodeChange])
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
return (
|
||||
<div className='flex items-center px-2 h-8 rounded-lg cursor-pointer hover:bg-state-base-hover'>
|
||||
{t('workflow.panel.change')}
|
||||
</div>
|
||||
)
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<BlockSelector
|
||||
onSelect={handleSelect}
|
||||
placement='top-end'
|
||||
offset={{
|
||||
mainAxis: 6,
|
||||
crossAxis: 8,
|
||||
}}
|
||||
trigger={renderTrigger}
|
||||
popupClassName='!w-[328px]'
|
||||
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks).filter(item => item !== data.type)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type OperatorProps = {
|
||||
open: boolean
|
||||
onOpenChange: (v: boolean) => void
|
||||
data: CommonNodeType
|
||||
nodeId: string
|
||||
sourceHandle: string
|
||||
}
|
||||
const Operator = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
data,
|
||||
nodeId,
|
||||
sourceHandle,
|
||||
}: OperatorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
handleNodeDelete,
|
||||
handleNodeDisconnect,
|
||||
} = useNodesInteractions()
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement='bottom-end'
|
||||
offset={{ mainAxis: 4, crossAxis: -4 }}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => onOpenChange(!open)}>
|
||||
<Button className='p-0 w-6 h-6'>
|
||||
<RiMoreFill className='w-4 h-4' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className='min-w-[120px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg system-md-regular text-text-secondary'>
|
||||
<div className='p-1'>
|
||||
<ChangeItem
|
||||
data={data}
|
||||
nodeId={nodeId}
|
||||
sourceHandle={sourceHandle}
|
||||
/>
|
||||
<div
|
||||
className='flex items-center px-2 h-8 rounded-lg cursor-pointer hover:bg-state-base-hover'
|
||||
onClick={() => handleNodeDisconnect(nodeId)}
|
||||
>
|
||||
{t('workflow.common.disconnect')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className='flex items-center px-2 h-8 rounded-lg cursor-pointer hover:bg-state-base-hover'
|
||||
onClick={() => handleNodeDelete(nodeId)}
|
||||
>
|
||||
{t('common.operation.delete')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default Operator
|
||||
@@ -9,16 +9,22 @@ import {
|
||||
Handle,
|
||||
Position,
|
||||
} from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum } from '../../../types'
|
||||
import type { Node } from '../../../types'
|
||||
import BlockSelector from '../../../block-selector'
|
||||
import type { ToolDefaultValue } from '../../../block-selector/types'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useIsChatMode,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../../../hooks'
|
||||
import { useStore } from '../../../store'
|
||||
import {
|
||||
useStore,
|
||||
} from '../../../store'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type NodeHandleProps = {
|
||||
handleId: string
|
||||
@@ -38,9 +44,7 @@ export const NodeTargetHandle = memo(({
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const connected = data._connectedTargetHandleIds?.includes(handleId)
|
||||
const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration)
|
||||
const isConnectable = !!availablePrevBlocks.length && (
|
||||
!data.isIterationStart
|
||||
)
|
||||
const isConnectable = !!availablePrevBlocks.length
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
@@ -112,12 +116,15 @@ export const NodeSourceHandle = memo(({
|
||||
handleClassName,
|
||||
nodeSelectorClassName,
|
||||
}: NodeHandleProps) => {
|
||||
const { t } = useTranslation()
|
||||
const notInitialWorkflow = useStore(s => s.notInitialWorkflow)
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration)
|
||||
const isConnectable = !!availableNextBlocks.length
|
||||
const isChatMode = useIsChatMode()
|
||||
const { checkParallelLimit } = useWorkflow()
|
||||
|
||||
const connected = data._connectedSourceHandleIds?.includes(handleId)
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
@@ -125,9 +132,9 @@ export const NodeSourceHandle = memo(({
|
||||
}, [])
|
||||
const handleHandleClick = useCallback((e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!connected)
|
||||
if (checkParallelLimit(id))
|
||||
setOpen(v => !v)
|
||||
}, [connected])
|
||||
}, [checkParallelLimit, id])
|
||||
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
@@ -142,12 +149,25 @@ export const NodeSourceHandle = memo(({
|
||||
}, [handleNodeAdd, id, handleId])
|
||||
|
||||
useEffect(() => {
|
||||
if (notInitialWorkflow && data.type === BlockEnum.Start)
|
||||
if (notInitialWorkflow && data.type === BlockEnum.Start && !isChatMode)
|
||||
setOpen(true)
|
||||
}, [notInitialWorkflow, data.type])
|
||||
}, [notInitialWorkflow, data.type, isChatMode])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
<div>
|
||||
<span className='system-xs-medium text-text-secondary'>{t('workflow.common.parallelTip.click.title')}</span>
|
||||
{t('workflow.common.parallelTip.click.desc')}
|
||||
</div>
|
||||
<div>
|
||||
<span className='system-xs-medium text-text-secondary'>{t('workflow.common.parallelTip.drag.title')}</span>
|
||||
{t('workflow.common.parallelTip.drag.desc')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Handle
|
||||
id={handleId}
|
||||
type='source'
|
||||
@@ -163,7 +183,7 @@ export const NodeSourceHandle = memo(({
|
||||
onClick={handleHandleClick}
|
||||
>
|
||||
{
|
||||
!connected && isConnectable && !getNodesReadOnly() && (
|
||||
isConnectable && !getNodesReadOnly() && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
@@ -181,7 +201,7 @@ export const NodeSourceHandle = memo(({
|
||||
)
|
||||
}
|
||||
</Handle>
|
||||
</>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
NodeSourceHandle.displayName = 'NodeSourceHandle'
|
||||
|
||||
@@ -28,8 +28,8 @@ const NodeResizer = ({
|
||||
nodeId,
|
||||
nodeData,
|
||||
icon = <Icon />,
|
||||
minWidth = 272,
|
||||
minHeight = 176,
|
||||
minWidth = 258,
|
||||
minHeight = 152,
|
||||
maxWidth,
|
||||
}: NodeResizerProps) => {
|
||||
const { handleNodeResize } = useNodesInteractions()
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
RiErrorWarningLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { NodeProps } from '../../types'
|
||||
import {
|
||||
BlockEnum,
|
||||
@@ -43,6 +44,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
data,
|
||||
children,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const nodeRef = useRef<HTMLDivElement>(null)
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { handleNodeIterationChildSizeChange } = useNodeIterationInteractions()
|
||||
@@ -80,6 +82,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
className={cn(
|
||||
'flex border-[2px] rounded-2xl',
|
||||
showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
|
||||
!showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
|
||||
)}
|
||||
ref={nodeRef}
|
||||
style={{
|
||||
@@ -100,6 +103,13 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
data._isBundled && '!shadow-lg',
|
||||
)}
|
||||
>
|
||||
{
|
||||
data._inParallelHovering && (
|
||||
<div className='absolute left-2 -top-2.5 top system-2xs-medium-uppercase text-text-tertiary z-10'>
|
||||
{t('workflow.common.parallelRun')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
data._showAddVariablePopup && (
|
||||
<AddVariablePopupWithPosition
|
||||
|
||||
@@ -28,7 +28,6 @@ export enum ComparisonOperator {
|
||||
lessThanOrEqual = '≤',
|
||||
isNull = 'is null',
|
||||
isNotNull = 'is not null',
|
||||
regexMatch = 'regex match',
|
||||
}
|
||||
|
||||
export type Condition = {
|
||||
|
||||
@@ -30,7 +30,6 @@ export const getOperators = (type?: VarType) => {
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
ComparisonOperator.regexMatch,
|
||||
]
|
||||
case VarType.number:
|
||||
return [
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const CUSTOM_ITERATION_START_NODE = 'custom-iteration-start'
|
||||
21
web/app/components/workflow/nodes/iteration-start/default.ts
Normal file
21
web/app/components/workflow/nodes/iteration-start/default.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { IterationStartNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
|
||||
const nodeDefault: NodeDefault<IterationStartNodeType> = {
|
||||
defaultValue: {},
|
||||
getAvailablePrevNodes() {
|
||||
return []
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid() {
|
||||
return {
|
||||
isValid: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
42
web/app/components/workflow/nodes/iteration-start/index.tsx
Normal file
42
web/app/components/workflow/nodes/iteration-start/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import { RiHome5Fill } from '@remixicon/react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { NodeSourceHandle } from '@/app/components/workflow/nodes/_base/components/node-handle'
|
||||
|
||||
const IterationStartNode = ({ id, data }: NodeProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='group flex nodrag items-center justify-center w-11 h-11 rounded-2xl border border-workflow-block-border bg-white'>
|
||||
<Tooltip popupContent={t('workflow.blocks.iteration-start')} asChild={false}>
|
||||
<div className='flex items-center justify-center w-6 h-6 rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500'>
|
||||
<RiHome5Fill className='w-3 h-3 text-text-primary-on-surface' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<NodeSourceHandle
|
||||
id={id}
|
||||
data={data}
|
||||
handleClassName='!top-1/2 !-right-[9px] !-translate-y-1/2'
|
||||
handleId='source'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const IterationStartNodeDumb = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='relative left-[17px] top-[21px] flex nodrag items-center justify-center w-11 h-11 rounded-2xl border border-workflow-block-border bg-white z-[11]'>
|
||||
<Tooltip popupContent={t('workflow.blocks.iteration-start')} asChild={false}>
|
||||
<div className='flex items-center justify-center w-6 h-6 rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500'>
|
||||
<RiHome5Fill className='w-3 h-3 text-text-primary-on-surface' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(IterationStartNode)
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
|
||||
export type IterationStartNodeType = CommonNodeType
|
||||
@@ -2,87 +2,49 @@ import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import produce from 'immer'
|
||||
import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
generateNewNode,
|
||||
} from '../../utils'
|
||||
import {
|
||||
WorkflowHistoryEvent,
|
||||
useAvailableBlocks,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useWorkflowHistory,
|
||||
} from '../../hooks'
|
||||
import { NODES_INITIAL_DATA } from '../../constants'
|
||||
import InsertBlock from './insert-block'
|
||||
import type { IterationNodeType } from './types'
|
||||
import cn from '@/utils/classnames'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import { IterationStart } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import type {
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
BlockEnum,
|
||||
} from '@/app/components/workflow/types'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type AddBlockProps = {
|
||||
iterationNodeId: string
|
||||
iterationNodeData: IterationNodeType
|
||||
}
|
||||
const AddBlock = ({
|
||||
iterationNodeId,
|
||||
iterationNodeData,
|
||||
}: AddBlockProps) => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, true)
|
||||
const { availablePrevBlocks } = useAvailableBlocks(iterationNodeData.startNodeType, true)
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[type],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
|
||||
...(toolDefaultValue || {}),
|
||||
isIterationStart: true,
|
||||
isInIteration: true,
|
||||
iteration_id: iterationNodeId,
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
toolDefaultValue,
|
||||
},
|
||||
position: {
|
||||
x: 117,
|
||||
y: 85,
|
||||
{
|
||||
prevNodeId: iterationNodeData.start_node_id,
|
||||
prevNodeSourceHandle: 'source',
|
||||
},
|
||||
zIndex: 1001,
|
||||
parentId: iterationNodeId,
|
||||
extent: 'parent',
|
||||
})
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (node.id === iterationNodeId) {
|
||||
node.data._children = [newNode.id]
|
||||
node.data.start_node_id = newNode.id
|
||||
node.data.startNodeType = newNode.data.type
|
||||
}
|
||||
})
|
||||
draft.push(newNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeAdd)
|
||||
}, [store, t, iterationNodeId, saveStateToHistory])
|
||||
)
|
||||
}, [handleNodeAdd, iterationNodeData.start_node_id])
|
||||
|
||||
const renderTriggerElement = useCallback((open: boolean) => {
|
||||
return (
|
||||
@@ -98,35 +60,18 @@ const AddBlock = ({
|
||||
}, [nodesReadOnly, t])
|
||||
|
||||
return (
|
||||
<div className='absolute top-12 left-6 flex items-center h-8 z-10'>
|
||||
<Tooltip popupContent={t('workflow.blocks.iteration-start')}>
|
||||
<div className='flex items-center justify-center w-6 h-6 rounded-full border-[0.5px] border-black/[0.02] shadow-md bg-primary-500'>
|
||||
<IterationStart className='w-4 h-4 text-white' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className='absolute top-7 left-14 flex items-center h-8 z-10'>
|
||||
<div className='group/insert relative w-16 h-0.5 bg-gray-300'>
|
||||
{
|
||||
iterationNodeData.startNodeType && (
|
||||
<InsertBlock
|
||||
startNodeId={iterationNodeData.start_node_id}
|
||||
availableBlocksTypes={availablePrevBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className='absolute right-0 top-1/2 -translate-y-1/2 w-0.5 h-2 bg-primary-500'></div>
|
||||
</div>
|
||||
{
|
||||
!iterationNodeData.startNodeType && (
|
||||
<BlockSelector
|
||||
disabled={nodesReadOnly}
|
||||
onSelect={handleSelect}
|
||||
trigger={renderTriggerElement}
|
||||
triggerInnerClassName='inline-flex'
|
||||
popupClassName='!min-w-[256px]'
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<BlockSelector
|
||||
disabled={nodesReadOnly}
|
||||
onSelect={handleSelect}
|
||||
trigger={renderTriggerElement}
|
||||
triggerInnerClassName='inline-flex'
|
||||
popupClassName='!min-w-[256px]'
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const nodeDefault: NodeDefault<IterationNodeType> = {
|
||||
start_node_id: '',
|
||||
iterator_selector: [],
|
||||
output_selector: [],
|
||||
_children: [],
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useNodesInteractions } from '../../hooks'
|
||||
import type {
|
||||
BlockEnum,
|
||||
OnSelectBlock,
|
||||
} from '../../types'
|
||||
import BlockSelector from '../../block-selector'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type InsertBlockProps = {
|
||||
startNodeId: string
|
||||
availableBlocksTypes: BlockEnum[]
|
||||
}
|
||||
const InsertBlock = ({
|
||||
startNodeId,
|
||||
availableBlocksTypes,
|
||||
}: InsertBlockProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
const handleInsert = useCallback<OnSelectBlock>((nodeType, toolDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType,
|
||||
toolDefaultValue,
|
||||
},
|
||||
{
|
||||
nextNodeId: startNodeId,
|
||||
nextNodeTargetHandle: 'target',
|
||||
},
|
||||
)
|
||||
}, [startNodeId, handleNodeAdd])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'nopan nodrag',
|
||||
'hidden group-hover/insert:block absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2',
|
||||
open && '!block',
|
||||
)}
|
||||
>
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
asChild
|
||||
onSelect={handleInsert}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
triggerClassName={() => 'hover:scale-125 transition-all'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(InsertBlock)
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useNodesInitialized,
|
||||
useViewport,
|
||||
} from 'reactflow'
|
||||
import { IterationStartNodeDumb } from '../iteration-start'
|
||||
import { useNodeIterationInteractions } from './use-interactions'
|
||||
import type { IterationNodeType } from './types'
|
||||
import AddBlock from './add-block'
|
||||
@@ -29,7 +30,7 @@ const Node: FC<NodeProps<IterationNodeType>> = ({
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative min-w-[258px] min-h-[118px] w-full h-full rounded-2xl bg-[#F0F2F7]/90',
|
||||
'relative min-w-[240px] min-h-[90px] w-full h-full rounded-2xl bg-[#F0F2F7]/90',
|
||||
)}>
|
||||
<Background
|
||||
id={`iteration-background-${id}`}
|
||||
@@ -38,10 +39,19 @@ const Node: FC<NodeProps<IterationNodeType>> = ({
|
||||
size={2 / zoom}
|
||||
color='#E4E5E7'
|
||||
/>
|
||||
<AddBlock
|
||||
iterationNodeId={id}
|
||||
iterationNodeData={data}
|
||||
/>
|
||||
{
|
||||
data._isCandidate && (
|
||||
<IterationStartNodeDumb />
|
||||
)
|
||||
}
|
||||
{
|
||||
data._children!.length === 1 && (
|
||||
<AddBlock
|
||||
iterationNodeId={id}
|
||||
iterationNodeData={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ITERATION_PADDING,
|
||||
NODES_INITIAL_DATA,
|
||||
} from '../../constants'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '../iteration-start/constants'
|
||||
|
||||
export const useNodeIterationInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -107,12 +108,12 @@ export const useNodeIterationInteractions = () => {
|
||||
const handleNodeIterationChildrenCopy = useCallback((nodeId: string, newNodeId: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const childrenNodes = nodes.filter(n => n.parentId === nodeId)
|
||||
const childrenNodes = nodes.filter(n => n.parentId === nodeId && n.type !== CUSTOM_ITERATION_START_NODE)
|
||||
|
||||
return childrenNodes.map((child, index) => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const newNode = generateNewNode({
|
||||
const { newNode } = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[childNodeType],
|
||||
...child.data,
|
||||
@@ -121,6 +122,7 @@ export const useNodeIterationInteractions = () => {
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${childNodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${childNodeType}`),
|
||||
iteration_id: newNodeId,
|
||||
},
|
||||
position: child.position,
|
||||
positionAbsolute: child.positionAbsolute,
|
||||
|
||||
@@ -55,7 +55,7 @@ const AddBlock = ({
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||
const newNode = generateNewNode({
|
||||
const { newNode } = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[type],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
|
||||
|
||||
@@ -11,7 +11,7 @@ export const useOperator = () => {
|
||||
const { userProfile } = useAppContext()
|
||||
|
||||
const handleAddNote = useCallback(() => {
|
||||
const newNode = generateNewNode({
|
||||
const { newNode } = generateNewNode({
|
||||
type: CUSTOM_NOTE_NODE,
|
||||
data: {
|
||||
title: '',
|
||||
|
||||
@@ -180,8 +180,6 @@ export const useChat = (
|
||||
isAnswer: true,
|
||||
}
|
||||
|
||||
let isInIteration = false
|
||||
|
||||
handleResponding(true)
|
||||
|
||||
const bodyParams = {
|
||||
@@ -317,11 +315,11 @@ export const useChat = (
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
isInIteration = true
|
||||
},
|
||||
onIterationNext: () => {
|
||||
onIterationNext: ({ data }) => {
|
||||
const tracing = responseItem.workflowProcess!.tracing!
|
||||
const iterations = tracing[tracing.length - 1]
|
||||
const iterations = tracing.find(item => item.node_id === data.node_id
|
||||
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
|
||||
iterations.details!.push([])
|
||||
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
@@ -331,9 +329,10 @@ export const useChat = (
|
||||
},
|
||||
onIterationFinish: ({ data }) => {
|
||||
const tracing = responseItem.workflowProcess!.tracing!
|
||||
const iterations = tracing[tracing.length - 1]
|
||||
tracing[tracing.length - 1] = {
|
||||
...iterations,
|
||||
const iterationsIndex = tracing.findIndex(item => item.node_id === data.node_id
|
||||
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
|
||||
tracing[iterationsIndex] = {
|
||||
...tracing[iterationsIndex],
|
||||
...data,
|
||||
status: NodeRunningStatus.Succeeded,
|
||||
} as any
|
||||
@@ -341,67 +340,45 @@ export const useChat = (
|
||||
const currentIndex = draft.length - 1
|
||||
draft[currentIndex] = responseItem
|
||||
}))
|
||||
|
||||
isInIteration = false
|
||||
},
|
||||
onNodeStarted: ({ data }) => {
|
||||
if (isInIteration) {
|
||||
const tracing = responseItem.workflowProcess!.tracing!
|
||||
const iterations = tracing[tracing.length - 1]
|
||||
const currIteration = iterations.details![iterations.details!.length - 1]
|
||||
currIteration.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as any)
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.length - 1
|
||||
draft[currentIndex] = responseItem
|
||||
}))
|
||||
}
|
||||
else {
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as any)
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
}
|
||||
if (data.iteration_id)
|
||||
return
|
||||
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as any)
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
},
|
||||
onNodeFinished: ({ data }) => {
|
||||
if (isInIteration) {
|
||||
const tracing = responseItem.workflowProcess!.tracing!
|
||||
const iterations = tracing[tracing.length - 1]
|
||||
const currIteration = iterations.details![iterations.details!.length - 1]
|
||||
currIteration[currIteration.length - 1] = {
|
||||
...data,
|
||||
status: NodeRunningStatus.Succeeded,
|
||||
} as any
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.length - 1
|
||||
draft[currentIndex] = responseItem
|
||||
}))
|
||||
}
|
||||
else {
|
||||
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
|
||||
responseItem.workflowProcess!.tracing[currentIndex] = {
|
||||
...(responseItem.workflowProcess!.tracing[currentIndex].extras
|
||||
? { extras: responseItem.workflowProcess!.tracing[currentIndex].extras }
|
||||
: {}),
|
||||
...data,
|
||||
} as any
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
}
|
||||
if (data.iteration_id)
|
||||
return
|
||||
|
||||
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex((item) => {
|
||||
if (!item.execution_metadata?.parallel_id)
|
||||
return item.node_id === data.node_id
|
||||
return item.node_id === data.node_id && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id)
|
||||
})
|
||||
responseItem.workflowProcess!.tracing[currentIndex] = {
|
||||
...(responseItem.workflowProcess!.tracing[currentIndex]?.extras
|
||||
? { extras: responseItem.workflowProcess!.tracing[currentIndex].extras }
|
||||
: {}),
|
||||
...data,
|
||||
} as any
|
||||
handleUpdateChatList(produce(chatListRef.current, (draft) => {
|
||||
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
|
||||
draft[currentIndex] = {
|
||||
...draft[currentIndex],
|
||||
...responseItem,
|
||||
}
|
||||
}))
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -63,26 +63,22 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
|
||||
const formatNodeList = useCallback((list: NodeTracing[]) => {
|
||||
const allItems = list.reverse()
|
||||
const result: NodeTracing[] = []
|
||||
let iterationIndex = 0
|
||||
allItems.forEach((item) => {
|
||||
const { node_type, execution_metadata } = item
|
||||
if (node_type !== BlockEnum.Iteration) {
|
||||
const isInIteration = !!execution_metadata?.iteration_id
|
||||
|
||||
if (isInIteration) {
|
||||
const iterationDetails = result[result.length - 1].details!
|
||||
const currentIterationIndex = execution_metadata?.iteration_index
|
||||
const isIterationFirstNode = iterationIndex !== currentIterationIndex || iterationDetails.length === 0
|
||||
const iterationNode = result.find(node => node.node_id === execution_metadata?.iteration_id)
|
||||
const iterationDetails = iterationNode?.details
|
||||
const currentIterationIndex = execution_metadata?.iteration_index ?? 0
|
||||
|
||||
if (isIterationFirstNode) {
|
||||
iterationDetails!.push([item])
|
||||
iterationIndex = currentIterationIndex!
|
||||
if (Array.isArray(iterationDetails)) {
|
||||
if (iterationDetails.length === 0 || !iterationDetails[currentIterationIndex])
|
||||
iterationDetails[currentIterationIndex] = [item]
|
||||
else
|
||||
iterationDetails[currentIterationIndex].push(item)
|
||||
}
|
||||
|
||||
else {
|
||||
iterationDetails[iterationDetails.length - 1].push(item)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
// not in iteration
|
||||
@@ -90,7 +86,6 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
result.push({
|
||||
...item,
|
||||
details: [],
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
import { ArrowNarrowLeft } from '../../base/icons/src/vender/line/arrows'
|
||||
import NodePanel from './node'
|
||||
import TracingPanel from './tracing-panel'
|
||||
import { Iteration } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
@@ -23,43 +27,67 @@ const IterationResultPanel: FC<Props> = ({
|
||||
noWrap,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [expandedIterations, setExpandedIterations] = useState<Record<number, boolean>>([])
|
||||
|
||||
const toggleIteration = useCallback((index: number) => {
|
||||
setExpandedIterations(prev => ({
|
||||
...prev,
|
||||
[index]: !prev[index],
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const main = (
|
||||
<>
|
||||
<div className={cn(!noWrap && 'shrink-0 ', 'pl-4 pr-3 pt-3')}>
|
||||
<div className={cn(!noWrap && 'shrink-0 ', 'px-4 pt-3')}>
|
||||
<div className='shrink-0 flex justify-between items-center h-8'>
|
||||
<div className='text-base font-semibold text-gray-900 truncate'>
|
||||
<div className='system-xl-semibold text-text-primary truncate'>
|
||||
{t(`${i18nPrefix}.testRunIteration`)}
|
||||
</div>
|
||||
<div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={onHide}>
|
||||
<RiCloseLine className='w-4 h-4 text-gray-500 ' />
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center py-2 space-x-1 text-primary-600 cursor-pointer' onClick={onBack}>
|
||||
<div className='flex items-center py-2 space-x-1 text-text-accent-secondary cursor-pointer' onClick={onBack}>
|
||||
<ArrowNarrowLeft className='w-4 h-4' />
|
||||
<div className='leading-[18px] text-[13px] font-medium'>{t(`${i18nPrefix}.back`)}</div>
|
||||
<div className='system-sm-medium'>{t(`${i18nPrefix}.back`)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* List */}
|
||||
<div className={cn(!noWrap ? 'h-0 grow' : 'max-h-full', 'overflow-y-auto px-4 pb-4 bg-gray-50')}>
|
||||
<div className={cn(!noWrap ? 'flex-grow overflow-auto' : 'max-h-full', 'p-2 bg-components-panel-bg')}>
|
||||
{list.map((iteration, index) => (
|
||||
<div key={index} className={cn('my-4', index === 0 && 'mt-2')}>
|
||||
<div className='flex items-center'>
|
||||
<div className='shrink-0 leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t(`${i18nPrefix}.iteration`)} {index + 1}</div>
|
||||
<div
|
||||
className='ml-3 grow w-0 h-px'
|
||||
style={{ background: 'linear-gradient(to right, #F3F4F6, rgba(243, 244, 246, 0))' }}
|
||||
></div>
|
||||
<div key={index} className={cn('mb-1 overflow-hidden rounded-xl bg-background-section-burn border-none')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between w-full px-3 cursor-pointer',
|
||||
expandedIterations[index] ? 'pt-3 pb-2' : 'py-3',
|
||||
'rounded-xl text-left',
|
||||
)}
|
||||
onClick={() => toggleIteration(index)}
|
||||
>
|
||||
<div className={cn('flex items-center gap-2 flex-grow')}>
|
||||
<div className='flex items-center justify-center w-4 h-4 rounded-[5px] border-divider-subtle bg-util-colors-cyan-cyan-500 flex-shrink-0'>
|
||||
<Iteration className='w-3 h-3 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<span className='system-sm-semibold-uppercase text-text-primary flex-grow'>
|
||||
{t(`${i18nPrefix}.iteration`)} {index + 1}
|
||||
</span>
|
||||
<RiArrowRightSLine className={cn(
|
||||
'w-4 h-4 text-text-tertiary transition-transform duration-200 flex-shrink-0',
|
||||
expandedIterations[index] && 'transform rotate-90',
|
||||
)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-0.5 space-y-1'>
|
||||
{iteration.map(node => (
|
||||
<NodePanel
|
||||
key={node.id}
|
||||
className='!px-0 !py-0'
|
||||
nodeInfo={node}
|
||||
notShowIterationNav
|
||||
/>
|
||||
))}
|
||||
{expandedIterations[index] && <div
|
||||
className="flex-grow h-px bg-divider-subtle"
|
||||
></div>}
|
||||
<div className={cn(
|
||||
'overflow-hidden transition-all duration-200',
|
||||
expandedIterations[index] ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0',
|
||||
)}>
|
||||
<TracingPanel
|
||||
list={iteration}
|
||||
className='bg-background-section-burn'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -4,15 +4,17 @@ import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiCheckboxCircleLine,
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
import Split from '../nodes/_base/components/split'
|
||||
import { Iteration } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import cn from '@/utils/classnames'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
@@ -61,31 +63,38 @@ const NodePanel: FC<Props> = ({
|
||||
return `${parseFloat((tokens / 1000000).toFixed(3))}M`
|
||||
}
|
||||
|
||||
const getCount = (iteration_curr_length: number | undefined, iteration_length: number) => {
|
||||
if ((iteration_curr_length && iteration_curr_length < iteration_length) || !iteration_length)
|
||||
return iteration_curr_length
|
||||
|
||||
return iteration_length
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCollapseState(!nodeInfo.expand)
|
||||
}, [nodeInfo.expand, setCollapseState])
|
||||
|
||||
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration
|
||||
const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onShowIterationDetail?.(nodeInfo.details || [])
|
||||
}
|
||||
return (
|
||||
<div className={cn('px-4 py-1', className, hideInfo && '!p-0')}>
|
||||
<div className={cn('group transition-all bg-white border border-gray-100 rounded-2xl shadow-xs hover:shadow-md', hideInfo && '!rounded-lg')}>
|
||||
<div className={cn('px-2 py-1', className)}>
|
||||
<div className='group transition-all bg-background-default border border-components-panel-border rounded-[10px] shadows-shadow-xs hover:shadow-md'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center pl-[6px] pr-3 cursor-pointer',
|
||||
hideInfo ? 'py-2' : 'py-3',
|
||||
!collapseState && (hideInfo ? '!pb-1' : '!pb-2'),
|
||||
'flex items-center pl-1 pr-3 cursor-pointer',
|
||||
hideInfo ? 'py-2' : 'py-1.5',
|
||||
!collapseState && (hideInfo ? '!pb-1' : '!pb-1.5'),
|
||||
)}
|
||||
onClick={() => setCollapseState(!collapseState)}
|
||||
>
|
||||
{!hideProcessDetail && (
|
||||
<RiArrowRightSLine
|
||||
className={cn(
|
||||
'shrink-0 w-3 h-3 mr-1 text-gray-400 transition-all group-hover:text-gray-500',
|
||||
'shrink-0 w-4 h-4 mr-1 text-text-quaternary transition-all group-hover:text-text-tertiary',
|
||||
!collapseState && 'rotate-90',
|
||||
)}
|
||||
/>
|
||||
@@ -93,23 +102,23 @@ const NodePanel: FC<Props> = ({
|
||||
|
||||
<BlockIcon size={hideInfo ? 'xs' : 'sm'} className={cn('shrink-0 mr-2', hideInfo && '!mr-1')} type={nodeInfo.node_type} toolIcon={nodeInfo.extras?.icon || nodeInfo.extras} />
|
||||
<div className={cn(
|
||||
'grow text-gray-700 text-[13px] leading-[16px] font-semibold truncate',
|
||||
'grow text-text-secondary system-xs-semibold-uppercase truncate',
|
||||
hideInfo && '!text-xs',
|
||||
)} title={nodeInfo.title}>{nodeInfo.title}</div>
|
||||
{nodeInfo.status !== 'running' && !hideInfo && (
|
||||
<div className='shrink-0 text-gray-500 text-xs leading-[18px]'>{`${getTime(nodeInfo.elapsed_time || 0)} · ${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens`}</div>
|
||||
<div className='shrink-0 text-text-tertiary system-xs-regular'>{nodeInfo.execution_metadata?.total_tokens ? `${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens · ` : ''}{`${getTime(nodeInfo.elapsed_time || 0)}`}</div>
|
||||
)}
|
||||
{nodeInfo.status === 'succeeded' && (
|
||||
<RiCheckboxCircleLine className='shrink-0 ml-2 w-3.5 h-3.5 text-[#12B76A]' />
|
||||
<RiCheckboxCircleFill className='shrink-0 ml-2 w-3.5 h-3.5 text-text-success' />
|
||||
)}
|
||||
{nodeInfo.status === 'failed' && (
|
||||
<RiErrorWarningLine className='shrink-0 ml-2 w-3.5 h-3.5 text-[#F04438]' />
|
||||
<RiErrorWarningLine className='shrink-0 ml-2 w-3.5 h-3.5 text-text-warning' />
|
||||
)}
|
||||
{nodeInfo.status === 'stopped' && (
|
||||
<AlertTriangle className='shrink-0 ml-2 w-3.5 h-3.5 text-[#F79009]' />
|
||||
)}
|
||||
{nodeInfo.status === 'running' && (
|
||||
<div className='shrink-0 flex items-center text-primary-600 text-[13px] leading-[16px] font-medium'>
|
||||
<div className='shrink-0 flex items-center text-text-accent text-[13px] leading-[16px] font-medium'>
|
||||
<span className='mr-2 text-xs font-normal'>Running</span>
|
||||
<RiLoader2Line className='w-3.5 h-3.5 animate-spin' />
|
||||
</div>
|
||||
@@ -120,25 +129,27 @@ const NodePanel: FC<Props> = ({
|
||||
{/* The nav to the iteration detail */}
|
||||
{isIterationNode && !notShowIterationNav && (
|
||||
<div className='mt-2 mb-1 !px-2'>
|
||||
<div
|
||||
className='flex items-center h-[34px] justify-between px-3 bg-gray-100 border-[0.5px] border-gray-200 rounded-lg cursor-pointer'
|
||||
onClick={handleOnShowIterationDetail}>
|
||||
<div className='leading-[18px] text-[13px] font-medium text-gray-700'>{t('workflow.nodes.iteration.iteration', { count: nodeInfo.metadata?.iterator_length || nodeInfo.details?.length })}</div>
|
||||
<Button
|
||||
className='flex items-center w-full self-stretch gap-2 px-3 py-2 bg-components-button-tertiary-bg-hover hover:bg-components-button-tertiary-bg-hover rounded-lg cursor-pointer border-none'
|
||||
onClick={handleOnShowIterationDetail}
|
||||
>
|
||||
<Iteration className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
|
||||
<div className='flex-1 text-left system-sm-medium text-components-button-tertiary-text'>{t('workflow.nodes.iteration.iteration', { count: getCount(nodeInfo.details?.length, nodeInfo.metadata?.iterator_length) })}</div>
|
||||
{justShowIterationNavArrow
|
||||
? (
|
||||
<RiArrowRightSLine className='w-3.5 h-3.5 text-gray-500' />
|
||||
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
|
||||
)
|
||||
: (
|
||||
<div className='flex items-center space-x-1 text-[#155EEF]'>
|
||||
<div className='text-[13px] font-normal '>{t('workflow.common.viewDetailInTracingPanel')}</div>
|
||||
<RiArrowRightSLine className='w-3.5 h-3.5' />
|
||||
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
<Split className='mt-2' />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('px-[10px] py-1', hideInfo && '!px-2 !py-0.5')}>
|
||||
<div className={cn('px-[10px]', hideInfo && '!px-2 !py-0.5')}>
|
||||
{nodeInfo.status === 'stopped' && (
|
||||
<div className='px-3 py-[10px] bg-[#fffaeb] rounded-lg border-[0.5px] border-[rbga(0,0,0,0.05)] text-xs leading-[18px] text-[#dc6803] shadow-xs'>{t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })}</div>
|
||||
)}
|
||||
|
||||
@@ -1,24 +1,266 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import
|
||||
React,
|
||||
{
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiMenu4Line,
|
||||
} from '@remixicon/react'
|
||||
import NodePanel from './node'
|
||||
import {
|
||||
BlockEnum,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
type TracingPanelProps = {
|
||||
list: NodeTracing[]
|
||||
onShowIterationDetail: (detail: NodeTracing[][]) => void
|
||||
onShowIterationDetail?: (detail: NodeTracing[][]) => void
|
||||
className?: string
|
||||
hideNodeInfo?: boolean
|
||||
hideNodeProcessDetail?: boolean
|
||||
}
|
||||
|
||||
const TracingPanel: FC<TracingPanelProps> = ({ list, onShowIterationDetail }) => {
|
||||
type TracingNodeProps = {
|
||||
id: string
|
||||
uniqueId: string
|
||||
isParallel: boolean
|
||||
data: NodeTracing | null
|
||||
children: TracingNodeProps[]
|
||||
parallelTitle?: string
|
||||
branchTitle?: string
|
||||
hideNodeInfo?: boolean
|
||||
hideNodeProcessDetail?: boolean
|
||||
}
|
||||
|
||||
function buildLogTree(nodes: NodeTracing[]): TracingNodeProps[] {
|
||||
const rootNodes: TracingNodeProps[] = []
|
||||
const parallelStacks: { [key: string]: TracingNodeProps } = {}
|
||||
const levelCounts: { [key: string]: number } = {}
|
||||
const parallelChildCounts: { [key: string]: Set<string> } = {}
|
||||
let uniqueIdCounter = 0
|
||||
const getUniqueId = () => {
|
||||
uniqueIdCounter++
|
||||
return `unique-${uniqueIdCounter}`
|
||||
}
|
||||
|
||||
const getParallelTitle = (parentId: string | null): string => {
|
||||
const levelKey = parentId || 'root'
|
||||
if (!levelCounts[levelKey])
|
||||
levelCounts[levelKey] = 0
|
||||
|
||||
levelCounts[levelKey]++
|
||||
|
||||
const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
|
||||
const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1
|
||||
const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
|
||||
return `PARALLEL-${levelNumber}${letter}`
|
||||
}
|
||||
|
||||
const getBranchTitle = (parentId: string | null, branchNum: number): string => {
|
||||
const levelKey = parentId || 'root'
|
||||
const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
|
||||
const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1
|
||||
const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
|
||||
const branchLetter = String.fromCharCode(64 + branchNum)
|
||||
return `BRANCH-${levelNumber}${letter}-${branchLetter}`
|
||||
}
|
||||
|
||||
// Count parallel children (for figuring out if we need to use letters)
|
||||
for (const node of nodes) {
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
|
||||
if (parallel_id) {
|
||||
const parentKey = parent_parallel_id || 'root'
|
||||
if (!parallelChildCounts[parentKey])
|
||||
parallelChildCounts[parentKey] = new Set()
|
||||
|
||||
parallelChildCounts[parentKey].add(parallel_id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null
|
||||
const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null
|
||||
const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null
|
||||
const parent_parallel_start_node_id = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null
|
||||
|
||||
if (!parallel_id || node.node_type === BlockEnum.End) {
|
||||
rootNodes.push({
|
||||
id: node.id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: false,
|
||||
data: node,
|
||||
children: [],
|
||||
})
|
||||
}
|
||||
else {
|
||||
if (!parallelStacks[parallel_id]) {
|
||||
const newParallelGroup: TracingNodeProps = {
|
||||
id: parallel_id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: true,
|
||||
data: null,
|
||||
children: [],
|
||||
parallelTitle: '',
|
||||
}
|
||||
parallelStacks[parallel_id] = newParallelGroup
|
||||
|
||||
if (parent_parallel_id && parallelStacks[parent_parallel_id]) {
|
||||
const sameBranchIndex = parallelStacks[parent_parallel_id].children.findLastIndex(c =>
|
||||
c.data?.execution_metadata?.parallel_start_node_id === parent_parallel_start_node_id || c.data?.parallel_start_node_id === parent_parallel_start_node_id,
|
||||
)
|
||||
parallelStacks[parent_parallel_id].children.splice(sameBranchIndex + 1, 0, newParallelGroup)
|
||||
newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
|
||||
}
|
||||
else {
|
||||
newParallelGroup.parallelTitle = getParallelTitle(parent_parallel_id)
|
||||
rootNodes.push(newParallelGroup)
|
||||
}
|
||||
}
|
||||
const branchTitle = parallel_start_node_id === node.node_id ? getBranchTitle(parent_parallel_id, parallelStacks[parallel_id].children.length + 1) : ''
|
||||
if (branchTitle) {
|
||||
parallelStacks[parallel_id].children.push({
|
||||
id: node.id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: false,
|
||||
data: node,
|
||||
children: [],
|
||||
branchTitle,
|
||||
})
|
||||
}
|
||||
else {
|
||||
let sameBranchIndex = parallelStacks[parallel_id].children.findLastIndex(c =>
|
||||
c.data?.execution_metadata?.parallel_start_node_id === parallel_start_node_id || c.data?.parallel_start_node_id === parallel_start_node_id,
|
||||
)
|
||||
if (parallelStacks[parallel_id].children[sameBranchIndex + 1]?.isParallel)
|
||||
sameBranchIndex++
|
||||
|
||||
parallelStacks[parallel_id].children.splice(sameBranchIndex + 1, 0, {
|
||||
id: node.id,
|
||||
uniqueId: getUniqueId(),
|
||||
isParallel: false,
|
||||
data: node,
|
||||
children: [],
|
||||
branchTitle,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootNodes
|
||||
}
|
||||
|
||||
const TracingPanel: FC<TracingPanelProps> = ({
|
||||
list,
|
||||
onShowIterationDetail,
|
||||
className,
|
||||
hideNodeInfo = false,
|
||||
hideNodeProcessDetail = false,
|
||||
}) => {
|
||||
const treeNodes = buildLogTree(list)
|
||||
const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(new Set())
|
||||
const [hoveredParallel, setHoveredParallel] = useState<string | null>(null)
|
||||
|
||||
const toggleCollapse = (id: string) => {
|
||||
setCollapsedNodes((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(id))
|
||||
newSet.delete(id)
|
||||
|
||||
else
|
||||
newSet.add(id)
|
||||
|
||||
return newSet
|
||||
})
|
||||
}
|
||||
|
||||
const handleParallelMouseEnter = useCallback((id: string) => {
|
||||
setHoveredParallel(id)
|
||||
}, [])
|
||||
|
||||
const handleParallelMouseLeave = useCallback((e: React.MouseEvent) => {
|
||||
const relatedTarget = e.relatedTarget as Element | null
|
||||
if (relatedTarget && 'closest' in relatedTarget) {
|
||||
const closestParallel = relatedTarget.closest('[data-parallel-id]')
|
||||
if (closestParallel)
|
||||
setHoveredParallel(closestParallel.getAttribute('data-parallel-id'))
|
||||
|
||||
else
|
||||
setHoveredParallel(null)
|
||||
}
|
||||
else {
|
||||
setHoveredParallel(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const renderNode = (node: TracingNodeProps) => {
|
||||
if (node.isParallel) {
|
||||
const isCollapsed = collapsedNodes.has(node.id)
|
||||
const isHovered = hoveredParallel === node.id
|
||||
return (
|
||||
<div
|
||||
key={node.uniqueId}
|
||||
className="ml-4 mb-2 relative"
|
||||
data-parallel-id={node.id}
|
||||
onMouseEnter={() => handleParallelMouseEnter(node.id)}
|
||||
onMouseLeave={handleParallelMouseLeave}
|
||||
>
|
||||
<div className="flex items-center mb-1">
|
||||
<button
|
||||
onClick={() => toggleCollapse(node.id)}
|
||||
className={cn(
|
||||
'mr-2 transition-colors',
|
||||
isHovered ? 'rounded border-components-button-primary-border bg-components-button-primary-bg text-text-primary-on-surface' : 'text-text-secondary hover:text-text-primary',
|
||||
)}
|
||||
>
|
||||
{isHovered ? <RiArrowDownSLine className="w-3 h-3" /> : <RiMenu4Line className="w-3 h-3 text-text-tertiary" />}
|
||||
</button>
|
||||
<div className="system-xs-semibold-uppercase text-text-secondary flex items-center">
|
||||
<span>{node.parallelTitle}</span>
|
||||
</div>
|
||||
<div
|
||||
className="mx-2 flex-grow h-px bg-divider-subtle"
|
||||
style={{ background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08), rgba(255, 255, 255, 0)' }}
|
||||
></div>
|
||||
</div>
|
||||
<div className={`pl-2 relative ${isCollapsed ? 'hidden' : ''}`}>
|
||||
<div className={cn(
|
||||
'absolute top-0 bottom-0 left-[5px] w-[2px]',
|
||||
isHovered ? 'bg-text-accent-secondary' : 'bg-divider-subtle',
|
||||
)}></div>
|
||||
{node.children.map(renderNode)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else {
|
||||
const isHovered = hoveredParallel === node.id
|
||||
return (
|
||||
<div key={node.uniqueId}>
|
||||
<div className={cn('pl-4 -mb-1.5 system-2xs-medium-uppercase', isHovered ? 'text-text-tertiary' : 'text-text-quaternary')}>
|
||||
{node.branchTitle}
|
||||
</div>
|
||||
<NodePanel
|
||||
nodeInfo={node.data!}
|
||||
onShowIterationDetail={onShowIterationDetail}
|
||||
justShowIterationNavArrow={true}
|
||||
hideInfo={hideNodeInfo}
|
||||
hideProcessDetail={hideNodeProcessDetail}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='bg-gray-50 py-2'>
|
||||
{list.map(node => (
|
||||
<NodePanel
|
||||
key={node.id}
|
||||
nodeInfo={node}
|
||||
onShowIterationDetail={onShowIterationDetail}
|
||||
justShowIterationNavArrow
|
||||
/>
|
||||
))}
|
||||
<div className={cn(className || 'bg-components-panel-bg', 'py-2')}>
|
||||
{treeNodes.map(renderNode)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -162,6 +162,8 @@ type Shape = {
|
||||
setControlPromptEditorRerenderKey: (controlPromptEditorRerenderKey: number) => void
|
||||
showImportDSLModal: boolean
|
||||
setShowImportDSLModal: (showImportDSLModal: boolean) => void
|
||||
showTips: string
|
||||
setShowTips: (showTips: string) => void
|
||||
}
|
||||
|
||||
export const createWorkflowStore = () => {
|
||||
@@ -262,6 +264,8 @@ export const createWorkflowStore = () => {
|
||||
setControlPromptEditorRerenderKey: controlPromptEditorRerenderKey => set(() => ({ controlPromptEditorRerenderKey })),
|
||||
showImportDSLModal: false,
|
||||
setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })),
|
||||
showTips: '',
|
||||
setShowTips: showTips => set(() => ({ showTips })),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export enum BlockEnum {
|
||||
Tool = 'tool',
|
||||
ParameterExtractor = 'parameter-extractor',
|
||||
Iteration = 'iteration',
|
||||
IterationStart = 'iteration-start',
|
||||
Assigner = 'assigner', // is now named as VariableAssigner
|
||||
}
|
||||
|
||||
@@ -54,7 +55,7 @@ export type CommonNodeType<T = {}> = {
|
||||
_holdAddVariablePopup?: boolean
|
||||
_iterationLength?: number
|
||||
_iterationIndex?: number
|
||||
isIterationStart?: boolean
|
||||
_inParallelHovering?: boolean
|
||||
isInIteration?: boolean
|
||||
iteration_id?: string
|
||||
selected?: boolean
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import {
|
||||
Position,
|
||||
getConnectedEdges,
|
||||
getIncomers,
|
||||
getOutgoers,
|
||||
} from 'reactflow'
|
||||
import dagre from '@dagrejs/dagre'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import {
|
||||
cloneDeep,
|
||||
groupBy,
|
||||
isEqual,
|
||||
uniqBy,
|
||||
} from 'lodash-es'
|
||||
import type {
|
||||
@@ -19,14 +22,17 @@ import type {
|
||||
import { BlockEnum } from './types'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
ITERATION_CHILDREN_Z_INDEX,
|
||||
ITERATION_NODE_Z_INDEX,
|
||||
NODE_WIDTH_X_OFFSET,
|
||||
START_INITIAL_POSITION,
|
||||
} from './constants'
|
||||
import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants'
|
||||
import type { QuestionClassifierNodeType } from './nodes/question-classifier/types'
|
||||
import type { IfElseNodeType } from './nodes/if-else/types'
|
||||
import { branchNameCorrect } from './nodes/if-else/utils'
|
||||
import type { ToolNodeType } from './nodes/tool/types'
|
||||
import type { IterationNodeType } from './nodes/iteration/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
|
||||
@@ -84,9 +90,130 @@ const getCycleEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
return cycleEdges
|
||||
}
|
||||
|
||||
export function getIterationStartNode(iterationId: string): Node {
|
||||
return generateNewNode({
|
||||
id: `${iterationId}start`,
|
||||
type: CUSTOM_ITERATION_START_NODE,
|
||||
data: {
|
||||
title: '',
|
||||
desc: '',
|
||||
type: BlockEnum.IterationStart,
|
||||
isInIteration: true,
|
||||
},
|
||||
position: {
|
||||
x: 24,
|
||||
y: 68,
|
||||
},
|
||||
zIndex: ITERATION_CHILDREN_Z_INDEX,
|
||||
parentId: iterationId,
|
||||
selectable: false,
|
||||
draggable: false,
|
||||
}).newNode
|
||||
}
|
||||
|
||||
export function generateNewNode({ data, position, id, zIndex, type, ...rest }: Omit<Node, 'id'> & { id?: string }): {
|
||||
newNode: Node
|
||||
newIterationStartNode?: Node
|
||||
} {
|
||||
const newNode = {
|
||||
id: id || `${Date.now()}`,
|
||||
type: type || CUSTOM_NODE,
|
||||
data,
|
||||
position,
|
||||
targetPosition: Position.Left,
|
||||
sourcePosition: Position.Right,
|
||||
zIndex: data.type === BlockEnum.Iteration ? ITERATION_NODE_Z_INDEX : zIndex,
|
||||
...rest,
|
||||
} as Node
|
||||
|
||||
if (data.type === BlockEnum.Iteration) {
|
||||
const newIterationStartNode = getIterationStartNode(newNode.id);
|
||||
(newNode.data as IterationNodeType).start_node_id = newIterationStartNode.id;
|
||||
(newNode.data as IterationNodeType)._children = [newIterationStartNode.id]
|
||||
return {
|
||||
newNode,
|
||||
newIterationStartNode,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
newNode,
|
||||
}
|
||||
}
|
||||
|
||||
export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
const hasIterationNode = nodes.some(node => node.data.type === BlockEnum.Iteration)
|
||||
|
||||
if (!hasIterationNode) {
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
}
|
||||
}
|
||||
const nodesMap = nodes.reduce((prev, next) => {
|
||||
prev[next.id] = next
|
||||
return prev
|
||||
}, {} as Record<string, Node>)
|
||||
const iterationNodesWithStartNode = []
|
||||
const iterationNodesWithoutStartNode = []
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const currentNode = nodes[i] as Node<IterationNodeType>
|
||||
|
||||
if (currentNode.data.type === BlockEnum.Iteration) {
|
||||
if (currentNode.data.start_node_id) {
|
||||
if (nodesMap[currentNode.data.start_node_id]?.type !== CUSTOM_ITERATION_START_NODE)
|
||||
iterationNodesWithStartNode.push(currentNode)
|
||||
}
|
||||
else {
|
||||
iterationNodesWithoutStartNode.push(currentNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
const newIterationStartNodesMap = {} as Record<string, Node>
|
||||
const newIterationStartNodes = [...iterationNodesWithStartNode, ...iterationNodesWithoutStartNode].map((iterationNode, index) => {
|
||||
const newNode = getIterationStartNode(iterationNode.id)
|
||||
newNode.id = newNode.id + index
|
||||
newIterationStartNodesMap[iterationNode.id] = newNode
|
||||
return newNode
|
||||
})
|
||||
const newEdges = iterationNodesWithStartNode.map((iterationNode) => {
|
||||
const newNode = newIterationStartNodesMap[iterationNode.id]
|
||||
const startNode = nodesMap[iterationNode.data.start_node_id]
|
||||
const source = newNode.id
|
||||
const sourceHandle = 'source'
|
||||
const target = startNode.id
|
||||
const targetHandle = 'target'
|
||||
return {
|
||||
id: `${source}-${sourceHandle}-${target}-${targetHandle}`,
|
||||
type: 'custom',
|
||||
source,
|
||||
sourceHandle,
|
||||
target,
|
||||
targetHandle,
|
||||
data: {
|
||||
sourceType: newNode.data.type,
|
||||
targetType: startNode.data.type,
|
||||
isInIteration: true,
|
||||
iteration_id: startNode.parentId,
|
||||
_connectedNodeIsSelected: true,
|
||||
},
|
||||
zIndex: ITERATION_CHILDREN_Z_INDEX,
|
||||
}
|
||||
})
|
||||
nodes.forEach((node) => {
|
||||
if (node.data.type === BlockEnum.Iteration && newIterationStartNodesMap[node.id])
|
||||
(node.data as IterationNodeType).start_node_id = newIterationStartNodesMap[node.id].id
|
||||
})
|
||||
|
||||
return {
|
||||
nodes: [...nodes, ...newIterationStartNodes],
|
||||
edges: [...edges, ...newEdges],
|
||||
}
|
||||
}
|
||||
|
||||
export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
const nodes = cloneDeep(originNodes)
|
||||
const edges = cloneDeep(originEdges)
|
||||
const { nodes, edges } = preprocessNodesAndEdges(cloneDeep(originNodes), cloneDeep(originEdges))
|
||||
const firstNode = nodes[0]
|
||||
|
||||
if (!firstNode?.position) {
|
||||
@@ -148,8 +275,7 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
}
|
||||
|
||||
export const initialEdges = (originEdges: Edge[], originNodes: Node[]) => {
|
||||
const nodes = cloneDeep(originNodes)
|
||||
const edges = cloneDeep(originEdges)
|
||||
const { nodes, edges } = preprocessNodesAndEdges(cloneDeep(originNodes), cloneDeep(originEdges))
|
||||
let selectedNode: Node | null = null
|
||||
const nodesMap = nodes.reduce((acc, node) => {
|
||||
acc[node.id] = node
|
||||
@@ -291,19 +417,6 @@ export const getNodesConnectedSourceOrTargetHandleIdsMap = (changes: ConnectedSo
|
||||
return nodesConnectedSourceOrTargetHandleIdsMap
|
||||
}
|
||||
|
||||
export const generateNewNode = ({ data, position, id, zIndex, type, ...rest }: Omit<Node, 'id'> & { id?: string }) => {
|
||||
return {
|
||||
id: id || `${Date.now()}`,
|
||||
type: type || CUSTOM_NODE,
|
||||
data,
|
||||
position,
|
||||
targetPosition: Position.Left,
|
||||
sourcePosition: Position.Right,
|
||||
zIndex: data.type === BlockEnum.Iteration ? ITERATION_NODE_Z_INDEX : zIndex,
|
||||
...rest,
|
||||
} as Node
|
||||
}
|
||||
|
||||
export const genNewNodeTitleFromOld = (oldTitle: string) => {
|
||||
const regex = /^(.+?)\s*\((\d+)\)\s*$/
|
||||
const match = oldTitle.match(regex)
|
||||
@@ -479,3 +592,167 @@ export const variableTransformer = (v: ValueSelector | string) => {
|
||||
|
||||
return `{{#${v.join('.')}#}}`
|
||||
}
|
||||
|
||||
type ParallelInfoItem = {
|
||||
parallelNodeId: string
|
||||
depth: number
|
||||
isBranch?: boolean
|
||||
}
|
||||
type NodeParallelInfo = {
|
||||
parallelNodeId: string
|
||||
edgeHandleId: string
|
||||
depth: number
|
||||
}
|
||||
type NodeHandle = {
|
||||
node: Node
|
||||
handle: string
|
||||
}
|
||||
type NodeStreamInfo = {
|
||||
upstreamNodes: Set<string>
|
||||
downstreamEdges: Set<string>
|
||||
}
|
||||
export const getParallelInfo = (nodes: Node[], edges: Edge[], parentNodeId?: string) => {
|
||||
let startNode
|
||||
|
||||
if (parentNodeId) {
|
||||
const parentNode = nodes.find(node => node.id === parentNodeId)
|
||||
if (!parentNode)
|
||||
throw new Error('Parent node not found')
|
||||
|
||||
startNode = nodes.find(node => node.id === (parentNode.data as IterationNodeType).start_node_id)
|
||||
}
|
||||
else {
|
||||
startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
}
|
||||
if (!startNode)
|
||||
throw new Error('Start node not found')
|
||||
|
||||
const parallelList = [] as ParallelInfoItem[]
|
||||
const nextNodeHandles = [{ node: startNode, handle: 'source' }]
|
||||
let hasAbnormalEdges = false
|
||||
|
||||
const traverse = (firstNodeHandle: NodeHandle) => {
|
||||
const nodeEdgesSet = {} as Record<string, Set<string>>
|
||||
const totalEdgesSet = new Set<string>()
|
||||
const nextHandles = [firstNodeHandle]
|
||||
const streamInfo = {} as Record<string, NodeStreamInfo>
|
||||
const parallelListItem = {
|
||||
parallelNodeId: '',
|
||||
depth: 0,
|
||||
} as ParallelInfoItem
|
||||
const nodeParallelInfoMap = {} as Record<string, NodeParallelInfo>
|
||||
nodeParallelInfoMap[firstNodeHandle.node.id] = {
|
||||
parallelNodeId: '',
|
||||
edgeHandleId: '',
|
||||
depth: 0,
|
||||
}
|
||||
|
||||
while (nextHandles.length) {
|
||||
const currentNodeHandle = nextHandles.shift()!
|
||||
const { node: currentNode, handle: currentHandle = 'source' } = currentNodeHandle
|
||||
const currentNodeHandleKey = currentNode.id
|
||||
const connectedEdges = edges.filter(edge => edge.source === currentNode.id && edge.sourceHandle === currentHandle)
|
||||
const connectedEdgesLength = connectedEdges.length
|
||||
const outgoers = nodes.filter(node => connectedEdges.some(edge => edge.target === node.id))
|
||||
const incomers = getIncomers(currentNode, nodes, edges)
|
||||
|
||||
if (!streamInfo[currentNodeHandleKey]) {
|
||||
streamInfo[currentNodeHandleKey] = {
|
||||
upstreamNodes: new Set<string>(),
|
||||
downstreamEdges: new Set<string>(),
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeEdgesSet[currentNodeHandleKey]?.size > 0 && incomers.length > 1) {
|
||||
const newSet = new Set<string>()
|
||||
for (const item of totalEdgesSet) {
|
||||
if (!streamInfo[currentNodeHandleKey].downstreamEdges.has(item))
|
||||
newSet.add(item)
|
||||
}
|
||||
if (isEqual(nodeEdgesSet[currentNodeHandleKey], newSet)) {
|
||||
parallelListItem.depth = nodeParallelInfoMap[currentNode.id].depth
|
||||
nextNodeHandles.push({ node: currentNode, handle: currentHandle })
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeParallelInfoMap[currentNode.id].depth > parallelListItem.depth)
|
||||
parallelListItem.depth = nodeParallelInfoMap[currentNode.id].depth
|
||||
|
||||
outgoers.forEach((outgoer) => {
|
||||
const outgoerConnectedEdges = getConnectedEdges([outgoer], edges).filter(edge => edge.source === outgoer.id)
|
||||
const sourceEdgesGroup = groupBy(outgoerConnectedEdges, 'sourceHandle')
|
||||
const incomers = getIncomers(outgoer, nodes, edges)
|
||||
|
||||
if (outgoers.length > 1 && incomers.length > 1)
|
||||
hasAbnormalEdges = true
|
||||
|
||||
Object.keys(sourceEdgesGroup).forEach((sourceHandle) => {
|
||||
nextHandles.push({ node: outgoer, handle: sourceHandle })
|
||||
})
|
||||
if (!outgoerConnectedEdges.length)
|
||||
nextHandles.push({ node: outgoer, handle: 'source' })
|
||||
|
||||
const outgoerKey = outgoer.id
|
||||
if (!nodeEdgesSet[outgoerKey])
|
||||
nodeEdgesSet[outgoerKey] = new Set<string>()
|
||||
|
||||
if (nodeEdgesSet[currentNodeHandleKey]) {
|
||||
for (const item of nodeEdgesSet[currentNodeHandleKey])
|
||||
nodeEdgesSet[outgoerKey].add(item)
|
||||
}
|
||||
|
||||
if (!streamInfo[outgoerKey]) {
|
||||
streamInfo[outgoerKey] = {
|
||||
upstreamNodes: new Set<string>(),
|
||||
downstreamEdges: new Set<string>(),
|
||||
}
|
||||
}
|
||||
|
||||
if (!nodeParallelInfoMap[outgoer.id]) {
|
||||
nodeParallelInfoMap[outgoer.id] = {
|
||||
...nodeParallelInfoMap[currentNode.id],
|
||||
}
|
||||
}
|
||||
|
||||
if (connectedEdgesLength > 1) {
|
||||
const edge = connectedEdges.find(edge => edge.target === outgoer.id)!
|
||||
nodeEdgesSet[outgoerKey].add(edge.id)
|
||||
totalEdgesSet.add(edge.id)
|
||||
|
||||
streamInfo[currentNodeHandleKey].downstreamEdges.add(edge.id)
|
||||
streamInfo[outgoerKey].upstreamNodes.add(currentNodeHandleKey)
|
||||
|
||||
for (const item of streamInfo[currentNodeHandleKey].upstreamNodes)
|
||||
streamInfo[item].downstreamEdges.add(edge.id)
|
||||
|
||||
if (!parallelListItem.parallelNodeId)
|
||||
parallelListItem.parallelNodeId = currentNode.id
|
||||
|
||||
const prevDepth = nodeParallelInfoMap[currentNode.id].depth + 1
|
||||
const currentDepth = nodeParallelInfoMap[outgoer.id].depth
|
||||
|
||||
nodeParallelInfoMap[outgoer.id].depth = Math.max(prevDepth, currentDepth)
|
||||
}
|
||||
else {
|
||||
for (const item of streamInfo[currentNodeHandleKey].upstreamNodes)
|
||||
streamInfo[outgoerKey].upstreamNodes.add(item)
|
||||
|
||||
nodeParallelInfoMap[outgoer.id].depth = nodeParallelInfoMap[currentNode.id].depth
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
parallelList.push(parallelListItem)
|
||||
}
|
||||
|
||||
while (nextNodeHandles.length) {
|
||||
const nodeHandle = nextNodeHandles.shift()!
|
||||
traverse(nodeHandle)
|
||||
}
|
||||
|
||||
return {
|
||||
parallelList,
|
||||
hasAbnormalEdges,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user