feat: Human Input Node (#32060)

The frontend and backend implementation for the human input node.

Co-authored-by: twwu <twwu@dify.ai>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zhsama <torvalds@linux.do>
This commit is contained in:
QuantumGhost
2026-02-09 14:57:23 +08:00
committed by GitHub
parent 56e3a55023
commit a1fc280102
474 changed files with 32667 additions and 2050 deletions

View File

@@ -1,4 +1,5 @@
import type { ElkNode, LayoutOptions } from 'elkjs/lib/elk-api'
import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types'
import type { CaseItem, IfElseNodeType } from '@/app/components/workflow/nodes/if-else/types'
import type {
Edge,
@@ -345,6 +346,76 @@ const buildIfElseWithPorts = (
}
}
/**
* Build Human Input node with ELK native Ports for multiple branches
* Handles user actions as branches with __timeout as the last fixed branch
*/
const buildHumanInputWithPorts = (
humanInputNode: Node,
edges: Edge[],
): { node: ElkNodeShape, portMap: Map<string, string> } | null => {
const childEdges = edges.filter(edge => edge.source === humanInputNode.id)
if (childEdges.length <= 1)
return null
// Sort child edges: user actions first (by order), then __timeout last
const sortedChildEdges = [...childEdges].sort((edgeA, edgeB) => {
const handleA = edgeA.sourceHandle
const handleB = edgeB.sourceHandle
if (handleA && handleB) {
const userActions = (humanInputNode.data as HumanInputNodeType).user_actions || []
const isATimeout = handleA === '__timeout'
const isBTimeout = handleB === '__timeout'
// __timeout should always be last
if (isATimeout)
return 1
if (isBTimeout)
return -1
// Sort by user_actions order
const indexA = userActions.findIndex(action => action.id === handleA)
const indexB = userActions.findIndex(action => action.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: `${humanInputNode.id}-port-${edge.sourceHandle || index}`,
layoutOptions: {
'port.side': 'EAST',
'port.index': String(index),
},
}))
// Build port mapping: edge.id -> portId
const portMap = new Map<string, string>()
sortedChildEdges.forEach((edge, index) => {
const portId = `${humanInputNode.id}-port-${edge.sourceHandle || index}`
portMap.set(edge.id, portId)
})
return {
node: {
id: humanInputNode.id,
width: humanInputNode.width ?? DEFAULT_NODE_WIDTH,
height: humanInputNode.height ?? DEFAULT_NODE_HEIGHT,
ports,
layoutOptions: {
'elk.portConstraints': 'FIXED_ORDER',
},
},
portMap,
}
}
const normaliseBounds = (layout: LayoutResult): LayoutResult => {
const {
nodes,
@@ -388,7 +459,7 @@ export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[])
// 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
// Build nodes with ports for If/Else and Human Input nodes
nodes.forEach((node) => {
if (node.data.type === BlockEnum.IfElse) {
const portsResult = buildIfElseWithPorts(node, edges)
@@ -405,6 +476,21 @@ export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[])
elkNodes.push(toElkNode(node))
}
}
else if (node.data.type === BlockEnum.HumanInput) {
const portsResult = buildHumanInputWithPorts(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))
}

View File

@@ -33,6 +33,7 @@ export const canRunBySingle = (nodeType: BlockEnum, isChildNode: boolean) => {
|| nodeType === BlockEnum.IfElse
|| nodeType === BlockEnum.VariableAggregator
|| nodeType === BlockEnum.Assigner
|| nodeType === BlockEnum.HumanInput
|| nodeType === BlockEnum.DataSource
|| nodeType === BlockEnum.TriggerSchedule
|| nodeType === BlockEnum.TriggerWebhook