feat: implement edge source handle change functionality and enhance node interactions for HumanInput node

This commit is contained in:
twwu 2025-12-30 16:05:33 +08:00
parent 0e2b59d661
commit 44a688cb81
9 changed files with 80 additions and 1 deletions

View File

@ -151,11 +151,65 @@ export const useEdgesInteractions = () => {
setEdges(newEdges)
}, [store, getNodesReadOnly])
const handleEdgeSourceHandleChange = useCallback((nodeId: string, oldHandleId: string, newHandleId: string) => {
if (getNodesReadOnly())
return
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
// Find edges connected to the old handle
const affectedEdges = edges.filter(
edge => edge.source === nodeId && edge.sourceHandle === oldHandleId,
)
if (affectedEdges.length === 0)
return
// Update node metadata: remove old handle, add new handle
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
[
...affectedEdges.map(edge => ({ type: 'remove', edge })),
...affectedEdges.map(edge => ({
type: 'add',
edge: { ...edge, sourceHandle: newHandleId },
})),
],
nodes,
)
const newNodes = produce(nodes, (draft: Node[]) => {
draft.forEach((node) => {
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
})
})
setNodes(newNodes)
// Update edges to use new sourceHandle and regenerate edge IDs
const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
if (edge.source === nodeId && edge.sourceHandle === oldHandleId) {
edge.sourceHandle = newHandleId
edge.id = `${edge.source}-${newHandleId}-${edge.target}-${edge.targetHandle}`
}
})
})
setEdges(newEdges)
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.EdgeSourceHandleChange)
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
return {
handleEdgeEnter,
handleEdgeLeave,
handleEdgeDeleteByDeleteBranch,
handleEdgeDelete,
handleEdgesChange,
handleEdgeSourceHandleChange,
}
}

View File

@ -299,6 +299,7 @@ export const useNodesInteractions = () => {
|| connectingNode.data.type === BlockEnum.VariableAggregator)
&& node.data.type !== BlockEnum.IfElse
&& node.data.type !== BlockEnum.QuestionClassifier
&& node.data.type !== BlockEnum.HumanInput
) {
n.data._isEntering = true
}
@ -1017,6 +1018,7 @@ export const useNodesInteractions = () => {
if (
nodeType !== BlockEnum.IfElse
&& nodeType !== BlockEnum.QuestionClassifier
&& nodeType !== BlockEnum.HumanInput
) {
newNode.data._connectedSourceHandleIds = [sourceHandle]
}
@ -1053,6 +1055,7 @@ export const useNodesInteractions = () => {
if (
nodeType !== BlockEnum.IfElse
&& nodeType !== BlockEnum.QuestionClassifier
&& nodeType !== BlockEnum.HumanInput
&& nodeType !== BlockEnum.LoopEnd
) {
newEdge = {
@ -1244,6 +1247,7 @@ export const useNodesInteractions = () => {
if (
nodeType !== BlockEnum.IfElse
&& nodeType !== BlockEnum.QuestionClassifier
&& nodeType !== BlockEnum.HumanInput
&& nodeType !== BlockEnum.LoopEnd
) {
newNextEdge = {

View File

@ -27,6 +27,7 @@ export const WorkflowHistoryEvent = {
NodeDelete: 'NodeDelete',
EdgeDelete: 'EdgeDelete',
EdgeDeleteByDeleteBranch: 'EdgeDeleteByDeleteBranch',
EdgeSourceHandleChange: 'EdgeSourceHandleChange',
NodeAdd: 'NodeAdd',
NodeResize: 'NodeResize',
NoteAdd: 'NoteAdd',

View File

@ -49,6 +49,7 @@ export const useWorkflowNodeFinished = () => {
if (data.node_type === BlockEnum.QuestionClassifier)
currentNode.data._runningBranchId = data?.outputs?.class_id
// todo: add human-input node support
}
})
setNodes(newNodes)

View File

@ -1309,6 +1309,7 @@ const replaceOldVarInText = (
)
}
// todo: add human-input node support
export const getNodeUsedVars = (node: Node): ValueSelector[] => {
const { data } = node
const { type } = data
@ -1505,6 +1506,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
}
// can be used in iteration node
// todo: add human-input node
export const getNodeUsedVarPassToServerKey = (
node: Node,
valueSelector: ValueSelector,
@ -1615,6 +1617,7 @@ export const findUsedVarNodes = (
return res
}
// todo: add human-input node
export const updateNodeVars = (
oldNode: Node,
oldVarSelector: ValueSelector,

View File

@ -1,16 +1,20 @@
import type { DeliveryMethod, HumanInputNodeType, UserAction } from '../types'
import { produce } from 'immer'
import { useState } from 'react'
import { useUpdateNodeInternals } from 'reactflow'
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import { useEdgesInteractions } from '@/app/components/workflow/hooks/use-edges-interactions'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import useFormContent from './use-form-content'
const useConfig = (id: string, payload: HumanInputNodeType) => {
const updateNodeInternals = useUpdateNodeInternals()
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { inputs, setInputs } = useNodeCrud<HumanInputNodeType>(id, payload)
const formContentHook = useFormContent(id, payload)
const { handleEdgeDeleteByDeleteBranch, handleEdgeSourceHandleChange } = useEdgesInteractions()
const [structuredOutputCollapsed, setStructuredOutputCollapsed] = useState(true)
const handleDeliveryMethodChange = (methods: DeliveryMethod[]) => {
@ -36,6 +40,14 @@ const useConfig = (id: string, payload: HumanInputNodeType) => {
...inputs,
user_actions: newActions,
})
// Update edges to use the new handle
const oldAction = inputs.user_actions[index]
if (oldAction && oldAction.id !== updatedAction.id) {
handleEdgeSourceHandleChange(id, oldAction.id, updatedAction.id)
updateNodeInternals(id) // Update handles
}
}
const handleUserActionDelete = (actionId: string) => {
@ -44,6 +56,8 @@ const useConfig = (id: string, payload: HumanInputNodeType) => {
...inputs,
user_actions: newActions,
})
// Delete edges connected to this action
handleEdgeDeleteByDeleteBranch(id, actionId)
}
const handleTimeoutChange = ({ timeout, unit }: { timeout: number, unit: 'hour' | 'day' }) => {

View File

@ -389,6 +389,7 @@ export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[])
const edgeToPortMap = new Map<string, string>()
// Build nodes with ports for If/Else nodes
// todo: add human-input node support
nodes.forEach((node) => {
if (node.data.type === BlockEnum.IfElse) {
const portsResult = buildIfElseWithPorts(node, edges)

View File

@ -68,7 +68,7 @@ const BaseCard = ({
handleId="target"
/>
{
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && (
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.HumanInput && (
<NodeSourceHandle
id={id}
data={data}

View File

@ -4,6 +4,7 @@ import IterationNode from './iteration/node'
import LoopNode from './loop/node'
import QuestionClassifierNode from './question-classifier/node'
// todo: add human-input node support
export const NodeComponentMap: Record<string, any> = {
[BlockEnum.QuestionClassifier]: QuestionClassifierNode,
[BlockEnum.IfElse]: IfElseNode,