This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
export const isMac = () => {
return navigator.userAgent.toUpperCase().includes('MAC')
}
const specialKeysNameMap: Record<string, string | undefined> = {
ctrl: '⌘',
alt: '⌥',
shift: '⇧',
}
export const getKeyboardKeyNameBySystem = (key: string) => {
if (isMac())
return specialKeysNameMap[key] || key
return key
}
const specialKeysCodeMap: Record<string, string | undefined> = {
ctrl: 'meta',
}
export const getKeyboardKeyCodeBySystem = (key: string) => {
if (isMac())
return specialKeysCodeMap[key] || key
return key
}
export const isEventTargetInputArea = (target: HTMLElement) => {
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')
return true
if (target.contentEditable === 'true')
return true
}
/**
* Format workflow run identifier using finished_at timestamp
* @param finishedAt - Unix timestamp in seconds
* @param fallbackText - Text to show when finishedAt is not available (default: 'Running')
* @returns Formatted string like " (14:30:25)" or " (Running)"
*/
export const formatWorkflowRunIdentifier = (finishedAt?: number, fallbackText = 'Running'): string => {
if (!finishedAt)
return ` (${fallbackText})`
const date = new Date(finishedAt * 1000)
const timeStr = date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
return ` (${timeStr})`
}

View File

@@ -0,0 +1,37 @@
import type {
InputVar,
ToolWithProvider,
} from '../types'
import type { DataSourceNodeType } from '../nodes/data-source/types'
import { CollectionType } from '@/app/components/tools/types'
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
export const getDataSourceCheckParams = (
toolData: DataSourceNodeType,
dataSourceList: ToolWithProvider[],
language: string,
) => {
const { plugin_id, provider_type, datasource_name } = toolData
const isBuiltIn = provider_type === CollectionType.builtIn
const currentDataSource = dataSourceList.find(item => item.plugin_id === plugin_id)
const currentDataSourceItem = currentDataSource?.tools.find(tool => tool.name === datasource_name)
const formSchemas = currentDataSourceItem ? toolParametersToFormSchemas(currentDataSourceItem.parameters) : []
return {
dataSourceInputsSchema: (() => {
const formInputs: InputVar[] = []
formSchemas.forEach((item: any) => {
formInputs.push({
label: item.label[language] || item.label.en_US,
variable: item.variable,
type: item.type,
required: item.required,
hide: item.hide,
})
})
return formInputs
})(),
notAuthed: isBuiltIn && !!currentDataSource?.allow_delete && !currentDataSource?.is_authorized,
language,
}
}

View File

@@ -0,0 +1,28 @@
import type { VarInInspect } from '@/types/workflow'
import { VarInInspectType } from '@/types/workflow'
import { VarType } from '../types'
type OutputToVarInInspectParams = {
nodeId: string
name: string
value: any
}
export const outputToVarInInspect = ({
nodeId,
name,
value,
}: OutputToVarInInspectParams): VarInInspect => {
return {
id: `${Date.now()}`, // TODO: wait for api
type: VarInInspectType.node,
name,
description: '',
selector: [nodeId, name],
value_type: VarType.string, // TODO: wait for api or get from node
value,
edited: false,
visible: true,
is_truncated: false,
full_content: { size_bytes: 0, download_url: '' },
}
}

View File

@@ -0,0 +1,23 @@
import {
NodeRunningStatus,
} from '../types'
export const getEdgeColor = (nodeRunningStatus?: NodeRunningStatus, isFailBranch?: boolean) => {
if (nodeRunningStatus === NodeRunningStatus.Succeeded)
return 'var(--color-workflow-link-line-success-handle)'
if (nodeRunningStatus === NodeRunningStatus.Failed)
return 'var(--color-workflow-link-line-error-handle)'
if (nodeRunningStatus === NodeRunningStatus.Exception)
return 'var(--color-workflow-link-line-failure-handle)'
if (nodeRunningStatus === NodeRunningStatus.Running) {
if (isFailBranch)
return 'var(--color-workflow-link-line-failure-handle)'
return 'var(--color-workflow-link-line-handle)'
}
return 'var(--color-workflow-link-line-normal)'
}

View File

@@ -0,0 +1,529 @@
import ELK from 'elkjs/lib/elk.bundled.js'
import type { ElkNode, LayoutOptions } from 'elkjs/lib/elk-api'
import { cloneDeep } from 'lodash-es'
import type {
Edge,
Node,
} from '@/app/components/workflow/types'
import {
BlockEnum,
} from '@/app/components/workflow/types'
import {
CUSTOM_NODE,
NODE_LAYOUT_HORIZONTAL_PADDING,
NODE_LAYOUT_VERTICAL_PADDING,
} from '@/app/components/workflow/constants'
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
import type { CaseItem, IfElseNodeType } from '@/app/components/workflow/nodes/if-else/types'
// Although the file name refers to Dagre, the implementation now relies on ELK's layered algorithm.
// Keep the export signatures unchanged to minimise the blast radius while we migrate the layout stack.
const elk = new ELK()
const DEFAULT_NODE_WIDTH = 244
const DEFAULT_NODE_HEIGHT = 100
const ROOT_LAYOUT_OPTIONS = {
'elk.algorithm': 'layered',
'elk.direction': 'RIGHT',
// === Spacing - Maximum spacing to prevent any overlap ===
'elk.layered.spacing.nodeNodeBetweenLayers': '100',
'elk.spacing.nodeNode': '80',
'elk.spacing.edgeNode': '50',
'elk.spacing.edgeEdge': '30',
'elk.spacing.edgeLabel': '10',
'elk.spacing.portPort': '20',
// === Port Configuration ===
'elk.portConstraints': 'FIXED_ORDER',
'elk.layered.considerModelOrder.strategy': 'PREFER_EDGES',
'elk.port.side': 'SOUTH',
// === Node Placement - Best quality ===
'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX',
'elk.layered.nodePlacement.favorStraightEdges': 'true',
'elk.layered.nodePlacement.linearSegments.deflectionDampening': '0.5',
'elk.layered.nodePlacement.networkSimplex.nodeFlexibility': 'NODE_SIZE',
// === Edge Routing - Maximum quality ===
'elk.edgeRouting': 'SPLINES',
'elk.layered.edgeRouting.selfLoopPlacement': 'NORTH',
'elk.layered.edgeRouting.sloppySplineRouting': 'false',
'elk.layered.edgeRouting.splines.mode': 'CONSERVATIVE',
'elk.layered.edgeRouting.splines.sloppy.layerSpacingFactor': '1.2',
// === Crossing Minimization - Most aggressive ===
'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
'elk.layered.crossingMinimization.greedySwitch.type': 'TWO_SIDED',
'elk.layered.crossingMinimization.greedySwitchHierarchical.type': 'TWO_SIDED',
'elk.layered.crossingMinimization.semiInteractive': 'true',
'elk.layered.crossingMinimization.hierarchicalSweepiness': '0.9',
// === Layering Strategy - Best quality ===
'elk.layered.layering.strategy': 'NETWORK_SIMPLEX',
'elk.layered.layering.networkSimplex.nodeFlexibility': 'NODE_SIZE',
'elk.layered.layering.layerConstraint': 'NONE',
'elk.layered.layering.minWidth.upperBoundOnWidth': '4',
// === Cycle Breaking ===
'elk.layered.cycleBreaking.strategy': 'DEPTH_FIRST',
// === Connected Components ===
'elk.separateConnectedComponents': 'true',
'elk.spacing.componentComponent': '100',
// === Node Size Constraints ===
'elk.nodeSize.constraints': 'NODE_LABELS',
'elk.nodeSize.options': 'DEFAULT_MINIMUM_SIZE MINIMUM_SIZE_ACCOUNTS_FOR_PADDING',
// === Edge Label Placement ===
'elk.edgeLabels.placement': 'CENTER',
'elk.edgeLabels.inline': 'true',
// === Compaction ===
'elk.layered.compaction.postCompaction.strategy': 'EDGE_LENGTH',
'elk.layered.compaction.postCompaction.constraints': 'EDGE_LENGTH',
// === High-Quality Mode ===
'elk.layered.thoroughness': '10',
'elk.layered.wrapping.strategy': 'OFF',
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
// === Additional Optimizations ===
'elk.layered.feedbackEdges': 'true',
'elk.layered.mergeEdges': 'false',
'elk.layered.mergeHierarchyEdges': 'false',
'elk.layered.allowNonFlowPortsToSwitchSides': 'false',
'elk.layered.northOrSouthPort': 'false',
'elk.partitioning.activate': 'false',
'elk.junctionPoints': 'true',
// === Content Alignment ===
'elk.contentAlignment': 'V_TOP H_LEFT',
'elk.alignment': 'AUTOMATIC',
}
const CHILD_LAYOUT_OPTIONS = {
'elk.algorithm': 'layered',
'elk.direction': 'RIGHT',
// === Spacing - High quality for child nodes ===
'elk.layered.spacing.nodeNodeBetweenLayers': '80',
'elk.spacing.nodeNode': '60',
'elk.spacing.edgeNode': '40',
'elk.spacing.edgeEdge': '25',
'elk.spacing.edgeLabel': '8',
'elk.spacing.portPort': '15',
// === Node Placement - Best quality ===
'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX',
'elk.layered.nodePlacement.favorStraightEdges': 'true',
'elk.layered.nodePlacement.linearSegments.deflectionDampening': '0.5',
'elk.layered.nodePlacement.networkSimplex.nodeFlexibility': 'NODE_SIZE',
// === Edge Routing - Maximum quality ===
'elk.edgeRouting': 'SPLINES',
'elk.layered.edgeRouting.sloppySplineRouting': 'false',
'elk.layered.edgeRouting.splines.mode': 'CONSERVATIVE',
// === Crossing Minimization - Aggressive ===
'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
'elk.layered.crossingMinimization.greedySwitch.type': 'TWO_SIDED',
'elk.layered.crossingMinimization.semiInteractive': 'true',
// === Layering Strategy ===
'elk.layered.layering.strategy': 'NETWORK_SIMPLEX',
'elk.layered.layering.networkSimplex.nodeFlexibility': 'NODE_SIZE',
// === Cycle Breaking ===
'elk.layered.cycleBreaking.strategy': 'DEPTH_FIRST',
// === Node Size ===
'elk.nodeSize.constraints': 'NODE_LABELS',
// === Compaction ===
'elk.layered.compaction.postCompaction.strategy': 'EDGE_LENGTH',
// === High-Quality Mode ===
'elk.layered.thoroughness': '10',
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
// === Additional Optimizations ===
'elk.layered.feedbackEdges': 'true',
'elk.layered.mergeEdges': 'false',
'elk.junctionPoints': 'true',
}
type LayoutInfo = {
x: number
y: number
width: number
height: number
layer?: number
}
type LayoutBounds = {
minX: number
minY: number
maxX: number
maxY: number
}
export type LayoutResult = {
nodes: Map<string, LayoutInfo>
bounds: LayoutBounds
}
// ELK Port definition for native port support
type ElkPortShape = {
id: string
layoutOptions?: LayoutOptions
}
type ElkNodeShape = {
id: string
width: number
height: number
ports?: ElkPortShape[]
layoutOptions?: LayoutOptions
children?: ElkNodeShape[]
}
type ElkEdgeShape = {
id: string
sources: string[]
targets: string[]
sourcePort?: string
targetPort?: string
}
const toElkNode = (node: Node): ElkNodeShape => ({
id: node.id,
width: node.width ?? DEFAULT_NODE_WIDTH,
height: node.height ?? DEFAULT_NODE_HEIGHT,
})
let edgeCounter = 0
const nextEdgeId = () => `elk-edge-${edgeCounter++}`
const createEdge = (
source: string,
target: string,
sourcePort?: string,
targetPort?: string,
): ElkEdgeShape => ({
id: nextEdgeId(),
sources: [source],
targets: [target],
sourcePort,
targetPort,
})
const collectLayout = (graph: ElkNode, predicate: (id: string) => boolean): LayoutResult => {
const result = new Map<string, LayoutInfo>()
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
const visit = (node: ElkNode) => {
node.children?.forEach((child: ElkNode) => {
if (predicate(child.id)) {
const x = child.x ?? 0
const y = child.y ?? 0
const width = child.width ?? DEFAULT_NODE_WIDTH
const height = child.height ?? DEFAULT_NODE_HEIGHT
const layer = child?.layoutOptions?.['org.eclipse.elk.layered.layerIndex']
result.set(child.id, {
x,
y,
width,
height,
layer: layer ? Number.parseInt(layer) : undefined,
})
minX = Math.min(minX, x)
minY = Math.min(minY, y)
maxX = Math.max(maxX, x + width)
maxY = Math.max(maxY, y + height)
}
if (child.children?.length)
visit(child)
})
}
visit(graph)
if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
minX = 0
minY = 0
maxX = 0
maxY = 0
}
return {
nodes: result,
bounds: {
minX,
minY,
maxX,
maxY,
},
}
}
/**
* Build If/Else node with ELK native Ports instead of dummy nodes
* This is the recommended approach for handling multiple branches
*/
const buildIfElseWithPorts = (
ifElseNode: Node,
edges: Edge[],
): { node: ElkNodeShape; portMap: Map<string, string> } | null => {
const childEdges = edges.filter(edge => edge.source === ifElseNode.id)
if (childEdges.length <= 1)
return null
// Sort child edges according to case order
const sortedChildEdges = [...childEdges].sort((edgeA, edgeB) => {
const handleA = edgeA.sourceHandle
const handleB = edgeB.sourceHandle
if (handleA && handleB) {
const cases = (ifElseNode.data as IfElseNodeType).cases || []
const isAElse = handleA === 'false'
const isBElse = handleB === 'false'
if (isAElse)
return 1
if (isBElse)
return -1
const indexA = cases.findIndex((c: CaseItem) => c.case_id === handleA)
const indexB = cases.findIndex((c: CaseItem) => c.case_id === handleB)
if (indexA !== -1 && indexB !== -1)
return indexA - indexB
}
return 0
})
// Create ELK ports for each branch
const ports: ElkPortShape[] = sortedChildEdges.map((edge, index) => ({
id: `${ifElseNode.id}-port-${edge.sourceHandle || index}`,
layoutOptions: {
'port.side': 'EAST', // Ports on the right side (matching 'RIGHT' direction)
'port.index': String(index),
},
}))
// Build port mapping: sourceHandle -> portId
const portMap = new Map<string, string>()
sortedChildEdges.forEach((edge, index) => {
const portId = `${ifElseNode.id}-port-${edge.sourceHandle || index}`
portMap.set(edge.id, portId)
})
return {
node: {
id: ifElseNode.id,
width: ifElseNode.width ?? DEFAULT_NODE_WIDTH,
height: ifElseNode.height ?? DEFAULT_NODE_HEIGHT,
ports,
layoutOptions: {
'elk.portConstraints': 'FIXED_ORDER',
},
},
portMap,
}
}
const normaliseBounds = (layout: LayoutResult): LayoutResult => {
const {
nodes,
bounds,
} = layout
if (nodes.size === 0)
return layout
const offsetX = bounds.minX
const offsetY = bounds.minY
const adjustedNodes = new Map<string, LayoutInfo>()
nodes.forEach((info, id) => {
adjustedNodes.set(id, {
...info,
x: info.x - offsetX,
y: info.y - offsetY,
})
})
return {
nodes: adjustedNodes,
bounds: {
minX: 0,
minY: 0,
maxX: bounds.maxX - offsetX,
maxY: bounds.maxY - offsetY,
},
}
}
export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[]): Promise<LayoutResult> => {
edgeCounter = 0
const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE)
const edges = cloneDeep(originEdges).filter(edge => (!edge.data?.isInIteration && !edge.data?.isInLoop))
const elkNodes: ElkNodeShape[] = []
const elkEdges: ElkEdgeShape[] = []
// Track which edges have been processed for If/Else nodes with ports
const edgeToPortMap = new Map<string, string>()
// Build nodes with ports for If/Else nodes
nodes.forEach((node) => {
if (node.data.type === BlockEnum.IfElse) {
const portsResult = buildIfElseWithPorts(node, edges)
if (portsResult) {
// Use node with ports
elkNodes.push(portsResult.node)
// Store port mappings for edges
portsResult.portMap.forEach((portId, edgeId) => {
edgeToPortMap.set(edgeId, portId)
})
}
else {
// No multiple branches, use normal node
elkNodes.push(toElkNode(node))
}
}
else {
elkNodes.push(toElkNode(node))
}
})
// Build edges with port connections
edges.forEach((edge) => {
const sourcePort = edgeToPortMap.get(edge.id)
elkEdges.push(createEdge(edge.source, edge.target, sourcePort))
})
const graph = {
id: 'workflow-root',
layoutOptions: ROOT_LAYOUT_OPTIONS,
children: elkNodes,
edges: elkEdges,
}
const layoutedGraph = await elk.layout(graph)
// No need to filter dummy nodes anymore, as we're using ports
const layout = collectLayout(layoutedGraph, () => true)
return normaliseBounds(layout)
}
const normaliseChildLayout = (
layout: LayoutResult,
nodes: Node[],
): LayoutResult => {
const result = new Map<string, LayoutInfo>()
layout.nodes.forEach((info, id) => {
result.set(id, info)
})
// Ensure iteration / loop start nodes do not collapse into the children.
const startNode = nodes.find(node =>
node.type === CUSTOM_ITERATION_START_NODE
|| node.type === CUSTOM_LOOP_START_NODE
|| node.data?.type === BlockEnum.LoopStart
|| node.data?.type === BlockEnum.IterationStart,
)
if (startNode) {
const startLayout = result.get(startNode.id)
if (startLayout) {
const desiredMinX = NODE_LAYOUT_HORIZONTAL_PADDING / 1.5
if (startLayout.x > desiredMinX) {
const shiftX = startLayout.x - desiredMinX
result.forEach((value, key) => {
result.set(key, {
...value,
x: value.x - shiftX,
})
})
}
const desiredMinY = startLayout.y
const deltaY = NODE_LAYOUT_VERTICAL_PADDING / 2
result.forEach((value, key) => {
result.set(key, {
...value,
y: value.y - desiredMinY + deltaY,
})
})
}
}
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
result.forEach((value) => {
minX = Math.min(minX, value.x)
minY = Math.min(minY, value.y)
maxX = Math.max(maxX, value.x + value.width)
maxY = Math.max(maxY, value.y + value.height)
})
if (!Number.isFinite(minX) || !Number.isFinite(minY))
return layout
return normaliseBounds({
nodes: result,
bounds: {
minX,
minY,
maxX,
maxY,
},
})
}
export const getLayoutForChildNodes = async (
parentNodeId: string,
originNodes: Node[],
originEdges: Edge[],
): Promise<LayoutResult | null> => {
edgeCounter = 0
const nodes = cloneDeep(originNodes).filter(node => node.parentId === parentNodeId)
if (!nodes.length)
return null
const edges = cloneDeep(originEdges).filter(edge =>
(edge.data?.isInIteration && edge.data?.iteration_id === parentNodeId)
|| (edge.data?.isInLoop && edge.data?.loop_id === parentNodeId),
)
const elkNodes: ElkNodeShape[] = nodes.map(toElkNode)
const elkEdges: ElkEdgeShape[] = edges.map(edge => createEdge(edge.source, edge.target))
const graph = {
id: parentNodeId,
layoutOptions: CHILD_LAYOUT_OPTIONS,
children: elkNodes,
edges: elkEdges,
}
const layoutedGraph = await elk.layout(graph)
const layout = collectLayout(layoutedGraph, () => true)
return normaliseChildLayout(layout, nodes)
}

View File

@@ -0,0 +1,43 @@
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
import type { BlockEnum } from '@/app/components/workflow/types'
export type GenNodeMetaDataParams = {
classification?: BlockClassificationEnum
sort: number
type: BlockEnum
title?: string
author?: string
helpLinkUri?: string
isRequired?: boolean
isUndeletable?: boolean
isStart?: boolean
isSingleton?: boolean
isTypeFixed?: boolean
}
export const genNodeMetaData = ({
classification = BlockClassificationEnum.Default,
sort,
type,
title = '',
author = 'Dify',
helpLinkUri,
isRequired = false,
isUndeletable = false,
isStart = false,
isSingleton = false,
isTypeFixed = false,
}: GenNodeMetaDataParams) => {
return {
classification,
sort,
type,
title,
author,
helpLinkUri: helpLinkUri || type,
isRequired,
isUndeletable,
isStart,
isSingleton,
isTypeFixed,
}
}

View File

@@ -0,0 +1,10 @@
export * from './node'
export * from './edge'
export * from './workflow-init'
export * from './elk-layout'
export * from './common'
export * from './tool'
export * from './workflow'
export * from './variable'
export * from './gen-node-meta-data'
export * from './data-source'

View File

@@ -0,0 +1,125 @@
/**
* Node navigation utilities for workflow
* This module provides functions for node selection, focusing and scrolling in workflow
*/
/**
* Interface for node selection event detail
*/
export type NodeSelectionDetail = {
nodeId: string;
focus?: boolean;
}
/**
* Select a node in the workflow
* @param nodeId - The ID of the node to select
* @param focus - Whether to focus/scroll to the node
*/
export function selectWorkflowNode(nodeId: string, focus = false): void {
// Create and dispatch a custom event for node selection
const event = new CustomEvent('workflow:select-node', {
detail: {
nodeId,
focus,
},
})
document.dispatchEvent(event)
}
/**
* Scroll to a specific node in the workflow
* @param nodeId - The ID of the node to scroll to
*/
export function scrollToWorkflowNode(nodeId: string): void {
// Create and dispatch a custom event for scrolling to node
const event = new CustomEvent('workflow:scroll-to-node', {
detail: { nodeId },
})
document.dispatchEvent(event)
}
/**
* Setup node selection event listener
* @param handleNodeSelect - Function to handle node selection
* @returns Cleanup function
*/
export function setupNodeSelectionListener(
handleNodeSelect: (nodeId: string) => void,
): () => void {
// Event handler for node selection
const handleNodeSelection = (event: CustomEvent<NodeSelectionDetail>) => {
const { nodeId, focus } = event.detail
if (nodeId) {
// Select the node
handleNodeSelect(nodeId)
// If focus is requested, scroll to the node
if (focus) {
// Use a small timeout to ensure node selection happens first
setTimeout(() => {
scrollToWorkflowNode(nodeId)
}, 100)
}
}
}
// Add event listener
document.addEventListener(
'workflow:select-node',
handleNodeSelection as EventListener,
)
// Return cleanup function
return () => {
document.removeEventListener(
'workflow:select-node',
handleNodeSelection as EventListener,
)
}
}
/**
* Setup scroll to node event listener with ReactFlow
* @param nodes - The workflow nodes
* @param reactflow - The ReactFlow instance
* @returns Cleanup function
*/
export function setupScrollToNodeListener(
nodes: any[],
reactflow: any,
): () => void {
// Event handler for scrolling to node
const handleScrollToNode = (event: CustomEvent<NodeSelectionDetail>) => {
const { nodeId } = event.detail
if (nodeId) {
// Find the target node
const node = nodes.find(n => n.id === nodeId)
if (node) {
// Use ReactFlow's fitView API to scroll to the node
const nodePosition = { x: node.position.x, y: node.position.y }
// Calculate position to place node in top-left area
// Move the center point right and down to show node in top-left
const targetX = nodePosition.x + window.innerWidth * 0.25
const targetY = nodePosition.y + window.innerHeight * 0.25
reactflow.setCenter(targetX, targetY, { zoom: 1, duration: 800 })
}
}
}
// Add event listener
document.addEventListener(
'workflow:scroll-to-node',
handleScrollToNode as EventListener,
)
// Return cleanup function
return () => {
document.removeEventListener(
'workflow:scroll-to-node',
handleScrollToNode as EventListener,
)
}
}

View File

@@ -0,0 +1,152 @@
import {
Position,
} from 'reactflow'
import type {
Node,
} from '../types'
import {
BlockEnum,
} from '../types'
import {
CUSTOM_NODE,
ITERATION_CHILDREN_Z_INDEX,
ITERATION_NODE_Z_INDEX,
LOOP_CHILDREN_Z_INDEX,
LOOP_NODE_Z_INDEX,
} from '../constants'
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
import type { IterationNodeType } from '../nodes/iteration/types'
import type { LoopNodeType } from '../nodes/loop/types'
import { CUSTOM_SIMPLE_NODE } from '@/app/components/workflow/simple-node/constants'
export function generateNewNode({ data, position, id, zIndex, type, ...rest }: Omit<Node, 'id'> & { id?: string }): {
newNode: Node
newIterationStartNode?: Node
newLoopStartNode?: 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 : (data.type === BlockEnum.Loop ? LOOP_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 = [{ nodeId: newIterationStartNode.id, nodeType: BlockEnum.IterationStart }]
return {
newNode,
newIterationStartNode,
}
}
if (data.type === BlockEnum.Loop) {
const newLoopStartNode = getLoopStartNode(newNode.id);
(newNode.data as LoopNodeType).start_node_id = newLoopStartNode.id;
(newNode.data as LoopNodeType)._children = [{ nodeId: newLoopStartNode.id, nodeType: BlockEnum.LoopStart }]
return {
newNode,
newLoopStartNode,
}
}
return {
newNode,
}
}
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 getLoopStartNode(loopId: string): Node {
return generateNewNode({
id: `${loopId}start`,
type: CUSTOM_LOOP_START_NODE,
data: {
title: '',
desc: '',
type: BlockEnum.LoopStart,
isInLoop: true,
},
position: {
x: 24,
y: 68,
},
zIndex: LOOP_CHILDREN_Z_INDEX,
parentId: loopId,
selectable: false,
draggable: false,
}).newNode
}
export const genNewNodeTitleFromOld = (oldTitle: string) => {
const regex = /^(.+?)\s*\((\d+)\)\s*$/
const match = oldTitle.match(regex)
if (match) {
const title = match[1]
const num = Number.parseInt(match[2], 10)
return `${title} (${num + 1})`
}
else {
return `${oldTitle} (1)`
}
}
export const getTopLeftNodePosition = (nodes: Node[]) => {
let minX = Infinity
let minY = Infinity
nodes.forEach((node) => {
if (node.position.x < minX)
minX = node.position.x
if (node.position.y < minY)
minY = node.position.y
})
return {
x: minX,
y: minY,
}
}
export const getNestedNodePosition = (node: Node, parentNode: Node) => {
return {
x: node.position.x - parentNode.position.x,
y: node.position.y - parentNode.position.y,
}
}
export const hasRetryNode = (nodeType?: BlockEnum) => {
return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code
}
export const getNodeCustomTypeByNodeDataType = (nodeType: BlockEnum) => {
if (nodeType === BlockEnum.LoopEnd)
return CUSTOM_SIMPLE_NODE
}

View File

@@ -0,0 +1,67 @@
import type {
InputVar,
ToolWithProvider,
} from '../types'
import type { ToolNodeType } from '../nodes/tool/types'
import { CollectionType } from '@/app/components/tools/types'
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { canFindTool } from '@/utils'
import type { StructuredOutput } from '@/app/components/workflow/nodes/llm/types'
import { Type } from '@/app/components/workflow/nodes/llm/types'
export const getToolCheckParams = (
toolData: ToolNodeType,
buildInTools: ToolWithProvider[],
customTools: ToolWithProvider[],
workflowTools: ToolWithProvider[],
language: string,
) => {
const { provider_id, provider_type, tool_name } = toolData
const isBuiltIn = provider_type === CollectionType.builtIn
const currentTools = provider_type === CollectionType.builtIn ? buildInTools : provider_type === CollectionType.custom ? customTools : workflowTools
const currCollection = currentTools.find(item => canFindTool(item.id, provider_id))
const currTool = currCollection?.tools.find(tool => tool.name === tool_name)
const formSchemas = currTool ? toolParametersToFormSchemas(currTool.parameters) : []
const toolInputVarSchema = formSchemas.filter(item => item.form === 'llm')
const toolSettingSchema = formSchemas.filter(item => item.form !== 'llm')
return {
toolInputsSchema: (() => {
const formInputs: InputVar[] = []
toolInputVarSchema.forEach((item: any) => {
formInputs.push({
label: item.label[language] || item.label.en_US,
variable: item.variable,
type: item.type,
required: item.required,
})
})
return formInputs
})(),
notAuthed: isBuiltIn && !!currCollection?.allow_delete && !currCollection?.is_team_authorization,
toolSettingSchema,
language,
}
}
export const CHUNK_TYPE_MAP = {
general_chunks: 'GeneralStructureChunk',
parent_child_chunks: 'ParentChildStructureChunk',
qa_chunks: 'QAStructureChunk',
}
export const wrapStructuredVarItem = (outputItem: any, matchedSchemaType: string): StructuredOutput => {
const dataType = Type.object
return {
schema: {
type: dataType,
properties: {
[outputItem.name]: {
...outputItem.value,
schemaType: matchedSchemaType,
},
},
additionalProperties: false,
},
}
}

View File

@@ -0,0 +1,52 @@
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
import type { PluginTriggerNodeType } from '@/app/components/workflow/nodes/trigger-plugin/types'
export type TriggerCheckParams = {
triggerInputsSchema: Array<{
variable: string
label: string
required?: boolean
}>
isReadyForCheckValid: boolean
}
export const getTriggerCheckParams = (
triggerData: PluginTriggerNodeType,
triggerProviders: TriggerWithProvider[] | undefined,
language: string,
): TriggerCheckParams => {
if (!triggerProviders) {
return {
triggerInputsSchema: [],
isReadyForCheckValid: false,
}
}
const {
provider_id,
provider_name,
event_name,
} = triggerData
const provider = triggerProviders.find(item =>
item.name === provider_name
|| item.id === provider_id
|| (provider_id && item.plugin_id === provider_id),
)
const currentEvent = provider?.events.find(event => event.name === event_name)
const triggerInputsSchema = (currentEvent?.parameters || []).map((parameter) => {
const label = parameter.label?.[language] || parameter.label?.en_US || parameter.name
return {
variable: parameter.name,
label,
required: parameter.required,
}
})
return {
triggerInputsSchema,
isReadyForCheckValid: true,
}
}

View File

@@ -0,0 +1,18 @@
import type {
ValueSelector,
} from '../types'
import type {
BlockEnum,
} from '../types'
import { hasErrorHandleNode } from '.'
export const variableTransformer = (v: ValueSelector | string) => {
if (typeof v === 'string')
return v.replace(/^{{#|#}}$/g, '').split('.')
return `{{#${v.join('.')}#}}`
}
export const isExceptionVariable = (variable: string, nodeType?: BlockEnum) => {
return (variable === 'error_message' || variable === 'error_type') && hasErrorHandleNode(nodeType)
}

View File

@@ -0,0 +1,26 @@
import { BlockEnum, type Node, isTriggerNode } from '../types'
/**
* Get the workflow entry node
* Priority: trigger nodes > start node
*/
export function getWorkflowEntryNode(nodes: Node[]): Node | undefined {
const triggerNode = nodes.find(node => isTriggerNode(node.data.type))
if (triggerNode) return triggerNode
return nodes.find(node => node.data.type === BlockEnum.Start)
}
/**
* Check if a node type is a workflow entry node
*/
export function isWorkflowEntryNode(nodeType: BlockEnum): boolean {
return nodeType === BlockEnum.Start || isTriggerNode(nodeType)
}
/**
* Check if workflow is in trigger mode
*/
export function isTriggerWorkflow(nodes: Node[]): boolean {
return nodes.some(node => isTriggerNode(node.data.type))
}

View File

@@ -0,0 +1,69 @@
import { preprocessNodesAndEdges } from './workflow-init'
import { BlockEnum } from '@/app/components/workflow/types'
import type {
Node,
} from '@/app/components/workflow/types'
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
describe('preprocessNodesAndEdges', () => {
it('process nodes without iteration node or loop node should return origin nodes and edges.', () => {
const nodes = [
{
data: {
type: BlockEnum.Code,
},
},
]
const result = preprocessNodesAndEdges(nodes as Node[], [])
expect(result).toEqual({
nodes,
edges: [],
})
})
it('process nodes with iteration node should return nodes with iteration start node', () => {
const nodes = [
{
id: 'iteration',
data: {
type: BlockEnum.Iteration,
},
},
]
const result = preprocessNodesAndEdges(nodes as Node[], [])
expect(result.nodes).toEqual(
expect.arrayContaining([
expect.objectContaining({
data: expect.objectContaining({
type: BlockEnum.IterationStart,
}),
}),
]),
)
})
it('process nodes with iteration node start should return origin', () => {
const nodes = [
{
data: {
type: BlockEnum.Iteration,
start_node_id: 'iterationStart',
},
},
{
id: 'iterationStart',
type: CUSTOM_ITERATION_START_NODE,
data: {
type: BlockEnum.IterationStart,
},
},
]
const result = preprocessNodesAndEdges(nodes as Node[], [])
expect(result).toEqual({
nodes,
edges: [],
})
})
})

View File

@@ -0,0 +1,363 @@
import {
getConnectedEdges,
} from 'reactflow'
import {
cloneDeep,
} from 'lodash-es'
import type {
Edge,
Node,
} from '../types'
import {
BlockEnum,
ErrorHandleMode,
} from '../types'
import {
CUSTOM_NODE,
DEFAULT_RETRY_INTERVAL,
DEFAULT_RETRY_MAX,
ITERATION_CHILDREN_Z_INDEX,
LOOP_CHILDREN_Z_INDEX,
NODE_WIDTH_X_OFFSET,
START_INITIAL_POSITION,
} from '../constants'
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-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 { IterationNodeType } from '../nodes/iteration/types'
import type { LoopNodeType } from '../nodes/loop/types'
import type { ToolNodeType } from '../nodes/tool/types'
import {
getIterationStartNode,
getLoopStartNode,
} from '.'
import { correctModelProvider } from '@/utils'
const WHITE = 'WHITE'
const GRAY = 'GRAY'
const BLACK = 'BLACK'
const isCyclicUtil = (nodeId: string, color: Record<string, string>, adjList: Record<string, string[]>, stack: string[]) => {
color[nodeId] = GRAY
stack.push(nodeId)
for (let i = 0; i < adjList[nodeId].length; ++i) {
const childId = adjList[nodeId][i]
if (color[childId] === GRAY) {
stack.push(childId)
return true
}
if (color[childId] === WHITE && isCyclicUtil(childId, color, adjList, stack))
return true
}
color[nodeId] = BLACK
if (stack.length > 0 && stack[stack.length - 1] === nodeId)
stack.pop()
return false
}
const getCycleEdges = (nodes: Node[], edges: Edge[]) => {
const adjList: Record<string, string[]> = {}
const color: Record<string, string> = {}
const stack: string[] = []
for (const node of nodes) {
color[node.id] = WHITE
adjList[node.id] = []
}
for (const edge of edges)
adjList[edge.source]?.push(edge.target)
for (let i = 0; i < nodes.length; i++) {
if (color[nodes[i].id] === WHITE)
isCyclicUtil(nodes[i].id, color, adjList, stack)
}
const cycleEdges = []
if (stack.length > 0) {
const cycleNodes = new Set(stack)
for (const edge of edges) {
if (cycleNodes.has(edge.source) && cycleNodes.has(edge.target))
cycleEdges.push(edge)
}
}
return cycleEdges
}
export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
const hasIterationNode = nodes.some(node => node.data.type === BlockEnum.Iteration)
const hasLoopNode = nodes.some(node => node.data.type === BlockEnum.Loop)
if (!hasIterationNode && !hasLoopNode) {
return {
nodes,
edges,
}
}
const nodesMap = nodes.reduce((prev, next) => {
prev[next.id] = next
return prev
}, {} as Record<string, Node>)
const iterationNodesWithStartNode = []
const iterationNodesWithoutStartNode = []
const loopNodesWithStartNode = []
const loopNodesWithoutStartNode = []
for (let i = 0; i < nodes.length; i++) {
const currentNode = nodes[i] as Node<IterationNodeType | LoopNodeType>
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)
}
}
if (currentNode.data.type === BlockEnum.Loop) {
if (currentNode.data.start_node_id) {
if (nodesMap[currentNode.data.start_node_id]?.type !== CUSTOM_LOOP_START_NODE)
loopNodesWithStartNode.push(currentNode)
}
else {
loopNodesWithoutStartNode.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 newLoopStartNodesMap = {} as Record<string, Node>
const newLoopStartNodes = [...loopNodesWithStartNode, ...loopNodesWithoutStartNode].map((loopNode, index) => {
const newNode = getLoopStartNode(loopNode.id)
newNode.id = newNode.id + index
newLoopStartNodesMap[loopNode.id] = newNode
return newNode
})
const newEdges = [...iterationNodesWithStartNode, ...loopNodesWithStartNode].map((nodeItem) => {
const isIteration = nodeItem.data.type === BlockEnum.Iteration
const newNode = (isIteration ? newIterationStartNodesMap : newLoopStartNodesMap)[nodeItem.id]
const startNode = nodesMap[nodeItem.data.start_node_id]
const source = newNode.id
const sourceHandle = 'source'
const target = startNode.id
const targetHandle = 'target'
const parentNode = nodes.find(node => node.id === startNode.parentId) || null
const isInIteration = !!parentNode && parentNode.data.type === BlockEnum.Iteration
const isInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop
return {
id: `${source}-${sourceHandle}-${target}-${targetHandle}`,
type: 'custom',
source,
sourceHandle,
target,
targetHandle,
data: {
sourceType: newNode.data.type,
targetType: startNode.data.type,
isInIteration,
iteration_id: isInIteration ? startNode.parentId : undefined,
isInLoop,
loop_id: isInLoop ? startNode.parentId : undefined,
_connectedNodeIsSelected: true,
},
zIndex: isIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_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
if (node.data.type === BlockEnum.Loop && newLoopStartNodesMap[node.id])
(node.data as LoopNodeType).start_node_id = newLoopStartNodesMap[node.id].id
})
return {
nodes: [...nodes, ...newIterationStartNodes, ...newLoopStartNodes],
edges: [...edges, ...newEdges],
}
}
export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
const { nodes, edges } = preprocessNodesAndEdges(cloneDeep(originNodes), cloneDeep(originEdges))
const firstNode = nodes[0]
if (!firstNode?.position) {
nodes.forEach((node, index) => {
node.position = {
x: START_INITIAL_POSITION.x + index * NODE_WIDTH_X_OFFSET,
y: START_INITIAL_POSITION.y,
}
})
}
const iterationOrLoopNodeMap = nodes.reduce((acc, node) => {
if (node.parentId) {
if (acc[node.parentId])
acc[node.parentId].push({ nodeId: node.id, nodeType: node.data.type })
else
acc[node.parentId] = [{ nodeId: node.id, nodeType: node.data.type }]
}
return acc
}, {} as Record<string, { nodeId: string; nodeType: BlockEnum }[]>)
return nodes.map((node) => {
if (!node.type)
node.type = CUSTOM_NODE
const connectedEdges = getConnectedEdges([node], edges)
node.data._connectedSourceHandleIds = connectedEdges.filter(edge => edge.source === node.id).map(edge => edge.sourceHandle || 'source')
node.data._connectedTargetHandleIds = connectedEdges.filter(edge => edge.target === node.id).map(edge => edge.targetHandle || 'target')
if (node.data.type === BlockEnum.IfElse) {
const nodeData = node.data as IfElseNodeType
if (!nodeData.cases && nodeData.logical_operator && nodeData.conditions) {
(node.data as IfElseNodeType).cases = [
{
case_id: 'true',
logical_operator: nodeData.logical_operator,
conditions: nodeData.conditions,
},
]
}
node.data._targetBranches = branchNameCorrect([
...(node.data as IfElseNodeType).cases.map(item => ({ id: item.case_id, name: '' })),
{ id: 'false', name: '' },
])
// delete conditions and logical_operator if cases is not empty
if (nodeData.cases.length > 0 && nodeData.conditions && nodeData.logical_operator) {
delete nodeData.conditions
delete nodeData.logical_operator
}
}
if (node.data.type === BlockEnum.QuestionClassifier) {
node.data._targetBranches = (node.data as QuestionClassifierNodeType).classes.map((topic) => {
return topic
})
}
if (node.data.type === BlockEnum.Iteration) {
const iterationNodeData = node.data as IterationNodeType
iterationNodeData._children = iterationOrLoopNodeMap[node.id] || []
iterationNodeData.is_parallel = iterationNodeData.is_parallel || false
iterationNodeData.parallel_nums = iterationNodeData.parallel_nums || 10
iterationNodeData.error_handle_mode = iterationNodeData.error_handle_mode || ErrorHandleMode.Terminated
}
// TODO: loop error handle mode
if (node.data.type === BlockEnum.Loop) {
const loopNodeData = node.data as LoopNodeType
loopNodeData._children = iterationOrLoopNodeMap[node.id] || []
loopNodeData.error_handle_mode = loopNodeData.error_handle_mode || ErrorHandleMode.Terminated
}
// legacy provider handle
if (node.data.type === BlockEnum.LLM)
(node as any).data.model.provider = correctModelProvider((node as any).data.model.provider)
if (node.data.type === BlockEnum.KnowledgeRetrieval && (node as any).data.multiple_retrieval_config?.reranking_model)
(node as any).data.multiple_retrieval_config.reranking_model.provider = correctModelProvider((node as any).data.multiple_retrieval_config?.reranking_model.provider)
if (node.data.type === BlockEnum.QuestionClassifier)
(node as any).data.model.provider = correctModelProvider((node as any).data.model.provider)
if (node.data.type === BlockEnum.ParameterExtractor)
(node as any).data.model.provider = correctModelProvider((node as any).data.model.provider)
if (node.data.type === BlockEnum.HttpRequest && !node.data.retry_config) {
node.data.retry_config = {
retry_enabled: true,
max_retries: DEFAULT_RETRY_MAX,
retry_interval: DEFAULT_RETRY_INTERVAL,
}
}
if (node.data.type === BlockEnum.Tool && !(node as Node<ToolNodeType>).data.version && !(node as Node<ToolNodeType>).data.tool_node_version) {
(node as Node<ToolNodeType>).data.tool_node_version = '2'
const toolConfigurations = (node as Node<ToolNodeType>).data.tool_configurations
if (toolConfigurations && Object.keys(toolConfigurations).length > 0) {
const newValues = { ...toolConfigurations }
Object.keys(toolConfigurations).forEach((key) => {
if (typeof toolConfigurations[key] !== 'object' || toolConfigurations[key] === null) {
newValues[key] = {
type: 'constant',
value: toolConfigurations[key],
}
}
});
(node as Node<ToolNodeType>).data.tool_configurations = newValues
}
}
return node
})
}
export const initialEdges = (originEdges: Edge[], originNodes: Node[]) => {
const { nodes, edges } = preprocessNodesAndEdges(cloneDeep(originNodes), cloneDeep(originEdges))
let selectedNode: Node | null = null
const nodesMap = nodes.reduce((acc, node) => {
acc[node.id] = node
if (node.data?.selected)
selectedNode = node
return acc
}, {} as Record<string, Node>)
const cycleEdges = getCycleEdges(nodes, edges)
return edges.filter((edge) => {
return !cycleEdges.find(cycEdge => cycEdge.source === edge.source && cycEdge.target === edge.target)
}).map((edge) => {
edge.type = 'custom'
if (!edge.sourceHandle)
edge.sourceHandle = 'source'
if (!edge.targetHandle)
edge.targetHandle = 'target'
if (!edge.data?.sourceType && edge.source && nodesMap[edge.source]) {
edge.data = {
...edge.data,
sourceType: nodesMap[edge.source].data.type!,
} as any
}
if (!edge.data?.targetType && edge.target && nodesMap[edge.target]) {
edge.data = {
...edge.data,
targetType: nodesMap[edge.target].data.type!,
} as any
}
if (selectedNode) {
edge.data = {
...edge.data,
_connectedNodeIsSelected: edge.source === selectedNode.id || edge.target === selectedNode.id,
} as any
}
return edge
})
}

View File

@@ -0,0 +1,187 @@
import {
getOutgoers,
} from 'reactflow'
import { v4 as uuid4 } from 'uuid'
import {
uniqBy,
} from 'lodash-es'
import type {
Edge,
Node,
} from '../types'
import {
BlockEnum,
} from '../types'
export const canRunBySingle = (nodeType: BlockEnum, isChildNode: boolean) => {
// child node means in iteration or loop. Set value to iteration(or loop) may cause variable not exit problem in backend.
if(isChildNode && nodeType === BlockEnum.Assigner)
return false
return nodeType === BlockEnum.LLM
|| nodeType === BlockEnum.KnowledgeRetrieval
|| nodeType === BlockEnum.Code
|| nodeType === BlockEnum.TemplateTransform
|| nodeType === BlockEnum.QuestionClassifier
|| nodeType === BlockEnum.HttpRequest
|| nodeType === BlockEnum.Tool
|| nodeType === BlockEnum.ParameterExtractor
|| nodeType === BlockEnum.Iteration
|| nodeType === BlockEnum.Agent
|| nodeType === BlockEnum.DocExtractor
|| nodeType === BlockEnum.Loop
|| nodeType === BlockEnum.Start
|| nodeType === BlockEnum.IfElse
|| nodeType === BlockEnum.VariableAggregator
|| nodeType === BlockEnum.Assigner
|| nodeType === BlockEnum.DataSource
|| nodeType === BlockEnum.TriggerSchedule
|| nodeType === BlockEnum.TriggerWebhook
|| nodeType === BlockEnum.TriggerPlugin
}
export const isSupportCustomRunForm = (nodeType: BlockEnum) => {
return nodeType === BlockEnum.DataSource
}
type ConnectedSourceOrTargetNodesChange = {
type: string
edge: Edge
}[]
export const getNodesConnectedSourceOrTargetHandleIdsMap = (changes: ConnectedSourceOrTargetNodesChange, nodes: Node[]) => {
const nodesConnectedSourceOrTargetHandleIdsMap = {} as Record<string, any>
changes.forEach((change) => {
const {
edge,
type,
} = change
const sourceNode = nodes.find(node => node.id === edge.source)!
if (sourceNode) {
nodesConnectedSourceOrTargetHandleIdsMap[sourceNode.id] = nodesConnectedSourceOrTargetHandleIdsMap[sourceNode.id] || {
_connectedSourceHandleIds: [...(sourceNode?.data._connectedSourceHandleIds || [])],
_connectedTargetHandleIds: [...(sourceNode?.data._connectedTargetHandleIds || [])],
}
}
const targetNode = nodes.find(node => node.id === edge.target)!
if (targetNode) {
nodesConnectedSourceOrTargetHandleIdsMap[targetNode.id] = nodesConnectedSourceOrTargetHandleIdsMap[targetNode.id] || {
_connectedSourceHandleIds: [...(targetNode?.data._connectedSourceHandleIds || [])],
_connectedTargetHandleIds: [...(targetNode?.data._connectedTargetHandleIds || [])],
}
}
if (sourceNode) {
if (type === 'remove') {
const index = nodesConnectedSourceOrTargetHandleIdsMap[sourceNode.id]._connectedSourceHandleIds.findIndex((handleId: string) => handleId === edge.sourceHandle)
nodesConnectedSourceOrTargetHandleIdsMap[sourceNode.id]._connectedSourceHandleIds.splice(index, 1)
}
if (type === 'add')
nodesConnectedSourceOrTargetHandleIdsMap[sourceNode.id]._connectedSourceHandleIds.push(edge.sourceHandle || 'source')
}
if (targetNode) {
if (type === 'remove') {
const index = nodesConnectedSourceOrTargetHandleIdsMap[targetNode.id]._connectedTargetHandleIds.findIndex((handleId: string) => handleId === edge.targetHandle)
nodesConnectedSourceOrTargetHandleIdsMap[targetNode.id]._connectedTargetHandleIds.splice(index, 1)
}
if (type === 'add')
nodesConnectedSourceOrTargetHandleIdsMap[targetNode.id]._connectedTargetHandleIds.push(edge.targetHandle || 'target')
}
})
return nodesConnectedSourceOrTargetHandleIdsMap
}
export const getValidTreeNodes = (nodes: Node[], edges: Edge[]) => {
// Find all start nodes (Start and Trigger nodes)
const startNodes = nodes.filter(node =>
node.data.type === BlockEnum.Start
|| node.data.type === BlockEnum.TriggerSchedule
|| node.data.type === BlockEnum.TriggerWebhook
|| node.data.type === BlockEnum.TriggerPlugin,
)
if (startNodes.length === 0) {
return {
validNodes: [],
maxDepth: 0,
}
}
const list: Node[] = []
let maxDepth = 0
const traverse = (root: Node, depth: number) => {
// Add the current node to the list
list.push(root)
if (depth > maxDepth)
maxDepth = depth
const outgoers = getOutgoers(root, nodes, edges)
if (outgoers.length) {
outgoers.forEach((outgoer) => {
// Only traverse if we haven't processed this node yet (avoid cycles)
if (!list.find(n => n.id === outgoer.id)) {
if (outgoer.data.type === BlockEnum.Iteration)
list.push(...nodes.filter(node => node.parentId === outgoer.id))
if (outgoer.data.type === BlockEnum.Loop)
list.push(...nodes.filter(node => node.parentId === outgoer.id))
traverse(outgoer, depth + 1)
}
})
}
else {
// Leaf node - add iteration/loop children if any
if (root.data.type === BlockEnum.Iteration)
list.push(...nodes.filter(node => node.parentId === root.id))
if (root.data.type === BlockEnum.Loop)
list.push(...nodes.filter(node => node.parentId === root.id))
}
}
// Start traversal from all start nodes
startNodes.forEach((startNode) => {
if (!list.find(n => n.id === startNode.id))
traverse(startNode, 1)
})
return {
validNodes: uniqBy(list, 'id'),
maxDepth,
}
}
export const changeNodesAndEdgesId = (nodes: Node[], edges: Edge[]) => {
const idMap = nodes.reduce((acc, node) => {
acc[node.id] = uuid4()
return acc
}, {} as Record<string, string>)
const newNodes = nodes.map((node) => {
return {
...node,
id: idMap[node.id],
}
})
const newEdges = edges.map((edge) => {
return {
...edge,
source: idMap[edge.source],
target: idMap[edge.target],
}
})
return [newNodes, newEdges] as [Node[], Edge[]]
}
export const hasErrorHandleNode = (nodeType?: BlockEnum) => {
return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code
}