|
|
@ -158,6 +158,11 @@ interface ConnectionDropState { |
|
|
sourceNodeType?: NodeType; |
|
|
sourceNodeType?: NodeType; |
|
|
sourceNodeId: string | null; |
|
|
sourceNodeId: string | null; |
|
|
sourceHandleId: string | null; |
|
|
sourceHandleId: string | null; |
|
|
|
|
|
sourceGroupId?: string; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function getNodeGroupId(node: Node): string | undefined { |
|
|
|
|
|
return (node as Node & { groupId?: string }).groupId; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function uniqueConnectionMediaTypes(mediaTypes: ConcreteHandleType[]): ConcreteHandleType[] { |
|
|
function uniqueConnectionMediaTypes(mediaTypes: ConcreteHandleType[]): ConcreteHandleType[] { |
|
|
@ -248,6 +253,7 @@ interface MultiAngleDialogState { |
|
|
sourceNodeId: string; |
|
|
sourceNodeId: string; |
|
|
sourceHandleId: string; |
|
|
sourceHandleId: string; |
|
|
flowPosition: { x: number; y: number }; |
|
|
flowPosition: { x: number; y: number }; |
|
|
|
|
|
sourceGroupId?: string; |
|
|
settings: MultiAngleSettings; |
|
|
settings: MultiAngleSettings; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -424,6 +430,7 @@ export function WorkflowCanvas() { |
|
|
const onEdgesChange = useWorkflowStore((state) => state.onEdgesChange); |
|
|
const onEdgesChange = useWorkflowStore((state) => state.onEdgesChange); |
|
|
const onConnect = useWorkflowStore((state) => state.onConnect); |
|
|
const onConnect = useWorkflowStore((state) => state.onConnect); |
|
|
const addNode = useWorkflowStore((state) => state.addNode); |
|
|
const addNode = useWorkflowStore((state) => state.addNode); |
|
|
|
|
|
const addNodeToGroup = useWorkflowStore((state) => state.addNodeToGroup); |
|
|
const updateNodeData = useWorkflowStore((state) => state.updateNodeData); |
|
|
const updateNodeData = useWorkflowStore((state) => state.updateNodeData); |
|
|
const loadWorkflow = useWorkflowStore((state) => state.loadWorkflow); |
|
|
const loadWorkflow = useWorkflowStore((state) => state.loadWorkflow); |
|
|
const getNodeById = useWorkflowStore((state) => state.getNodeById); |
|
|
const getNodeById = useWorkflowStore((state) => state.getNodeById); |
|
|
@ -461,6 +468,17 @@ export function WorkflowCanvas() { |
|
|
const reactFlowWrapper = useRef<HTMLDivElement>(null); |
|
|
const reactFlowWrapper = useRef<HTMLDivElement>(null); |
|
|
const tutorialViewportSet = useRef(false); |
|
|
const tutorialViewportSet = useRef(false); |
|
|
|
|
|
|
|
|
|
|
|
const addNodeWithOptionalGroup = useCallback(( |
|
|
|
|
|
type: NodeType, |
|
|
|
|
|
position: { x: number; y: number }, |
|
|
|
|
|
sourceGroupId: string | undefined, |
|
|
|
|
|
initialData?: Parameters<typeof addNode>[2] |
|
|
|
|
|
) => ( |
|
|
|
|
|
sourceGroupId |
|
|
|
|
|
? addNodeToGroup(type, position, sourceGroupId, initialData) |
|
|
|
|
|
: addNode(type, position, initialData) |
|
|
|
|
|
), [addNode, addNodeToGroup]); |
|
|
|
|
|
|
|
|
const syncZoomPercent = useCallback(() => { |
|
|
const syncZoomPercent = useCallback(() => { |
|
|
setZoomPercent(Math.round(getViewport().zoom * 100)); |
|
|
setZoomPercent(Math.round(getViewport().zoom * 100)); |
|
|
}, [getViewport]); |
|
|
}, [getViewport]); |
|
|
@ -849,12 +867,13 @@ export function WorkflowCanvas() { |
|
|
if (!source) return; |
|
|
if (!source) return; |
|
|
|
|
|
|
|
|
const { width } = getNodeDimensions(node); |
|
|
const { width } = getNodeDimensions(node); |
|
|
|
|
|
const absolutePosition = absoluteNodePositions.get(node.id) ?? node.position; |
|
|
setCanvasNodeMenu(null); |
|
|
setCanvasNodeMenu(null); |
|
|
setConnectionDrop({ |
|
|
setConnectionDrop({ |
|
|
position: clientPosition, |
|
|
position: clientPosition, |
|
|
flowPosition: { |
|
|
flowPosition: { |
|
|
x: node.position.x + width + 160, |
|
|
x: absolutePosition.x + width + 160, |
|
|
y: node.position.y, |
|
|
y: absolutePosition.y, |
|
|
}, |
|
|
}, |
|
|
intent: { |
|
|
intent: { |
|
|
direction: "fromOutput", |
|
|
direction: "fromOutput", |
|
|
@ -869,6 +888,7 @@ export function WorkflowCanvas() { |
|
|
sourceNodeType: node.type as NodeType, |
|
|
sourceNodeType: node.type as NodeType, |
|
|
sourceNodeId: node.id, |
|
|
sourceNodeId: node.id, |
|
|
sourceHandleId: source.handleId, |
|
|
sourceHandleId: source.handleId, |
|
|
|
|
|
sourceGroupId: getNodeGroupId(node), |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
if (tutorialActive) { |
|
|
if (tutorialActive) { |
|
|
@ -876,7 +896,7 @@ export function WorkflowCanvas() { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return true; |
|
|
return true; |
|
|
}, [edges, tutorialActive]); |
|
|
}, [absoluteNodePositions, edges, tutorialActive]); |
|
|
|
|
|
|
|
|
const openConnectionMenuFromHandle = useCallback((request: NodeHandleMenuRequest) => { |
|
|
const openConnectionMenuFromHandle = useCallback((request: NodeHandleMenuRequest) => { |
|
|
if (isModalOpen) return; |
|
|
if (isModalOpen) return; |
|
|
@ -896,11 +916,12 @@ export function WorkflowCanvas() { |
|
|
if (!handleType) return; |
|
|
if (!handleType) return; |
|
|
|
|
|
|
|
|
const { width } = getNodeDimensions(node); |
|
|
const { width } = getNodeDimensions(node); |
|
|
|
|
|
const absolutePosition = absoluteNodePositions.get(node.id) ?? node.position; |
|
|
setConnectionDrop({ |
|
|
setConnectionDrop({ |
|
|
position: request.clientPosition, |
|
|
position: request.clientPosition, |
|
|
flowPosition: { |
|
|
flowPosition: { |
|
|
x: node.position.x + width + 160, |
|
|
x: absolutePosition.x + width + 160, |
|
|
y: node.position.y, |
|
|
y: absolutePosition.y, |
|
|
}, |
|
|
}, |
|
|
intent: { |
|
|
intent: { |
|
|
...intent, |
|
|
...intent, |
|
|
@ -913,15 +934,17 @@ export function WorkflowCanvas() { |
|
|
sourceNodeType: node.type as NodeType, |
|
|
sourceNodeType: node.type as NodeType, |
|
|
sourceNodeId: node.id, |
|
|
sourceNodeId: node.id, |
|
|
sourceHandleId: request.handleId || source?.handleId || SINGLE_OUTPUT_HANDLE_ID, |
|
|
sourceHandleId: request.handleId || source?.handleId || SINGLE_OUTPUT_HANDLE_ID, |
|
|
|
|
|
sourceGroupId: getNodeGroupId(node), |
|
|
}); |
|
|
}); |
|
|
} else { |
|
|
} else { |
|
|
const { width } = getNodeDimensions(node); |
|
|
const { width } = getNodeDimensions(node); |
|
|
const handleType = intent.mediaTypes[0] ?? null; |
|
|
const handleType = intent.mediaTypes[0] ?? null; |
|
|
|
|
|
const absolutePosition = absoluteNodePositions.get(node.id) ?? node.position; |
|
|
setConnectionDrop({ |
|
|
setConnectionDrop({ |
|
|
position: request.clientPosition, |
|
|
position: request.clientPosition, |
|
|
flowPosition: { |
|
|
flowPosition: { |
|
|
x: node.position.x - width - 120, |
|
|
x: absolutePosition.x - width - 120, |
|
|
y: node.position.y, |
|
|
y: absolutePosition.y, |
|
|
}, |
|
|
}, |
|
|
intent, |
|
|
intent, |
|
|
handleType, |
|
|
handleType, |
|
|
@ -929,13 +952,14 @@ export function WorkflowCanvas() { |
|
|
mode: "drag", |
|
|
mode: "drag", |
|
|
sourceNodeId: node.id, |
|
|
sourceNodeId: node.id, |
|
|
sourceHandleId: request.handleId || SINGLE_INPUT_HANDLE_ID, |
|
|
sourceHandleId: request.handleId || SINGLE_INPUT_HANDLE_ID, |
|
|
|
|
|
sourceGroupId: getNodeGroupId(node), |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (tutorialActive) { |
|
|
if (tutorialActive) { |
|
|
useFTUXStore.getState().setConnectionMenuShown(true); |
|
|
useFTUXStore.getState().setConnectionMenuShown(true); |
|
|
} |
|
|
} |
|
|
}, [edges, isModalOpen, nodes, tutorialActive]); |
|
|
}, [absoluteNodePositions, edges, isModalOpen, nodes, tutorialActive]); |
|
|
|
|
|
|
|
|
const nodeHandleMenuValue = useMemo(() => ({ |
|
|
const nodeHandleMenuValue = useMemo(() => ({ |
|
|
openConnectionMenuFromHandle, |
|
|
openConnectionMenuFromHandle, |
|
|
@ -1053,7 +1077,7 @@ export function WorkflowCanvas() { |
|
|
|
|
|
|
|
|
// Get current groupId of the node
|
|
|
// Get current groupId of the node
|
|
|
const currentNode = nodes.find((n) => n.id === node.id); |
|
|
const currentNode = nodes.find((n) => n.id === node.id); |
|
|
const currentGroupId = currentNode?.groupId; |
|
|
const currentGroupId = currentNode ? getNodeGroupId(currentNode) : undefined; |
|
|
|
|
|
|
|
|
// Update groupId if it changed
|
|
|
// Update groupId if it changed
|
|
|
if (targetGroupId !== currentGroupId) { |
|
|
if (targetGroupId !== currentGroupId) { |
|
|
@ -1316,6 +1340,7 @@ export function WorkflowCanvas() { |
|
|
mode: "drag", |
|
|
mode: "drag", |
|
|
sourceNodeId: connectionState.fromNode.id, |
|
|
sourceNodeId: connectionState.fromNode.id, |
|
|
sourceHandleId: fromHandleId, |
|
|
sourceHandleId: fromHandleId, |
|
|
|
|
|
sourceGroupId: getNodeGroupId(connectionState.fromNode), |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
// Tutorial tracking
|
|
|
// Tutorial tracking
|
|
|
@ -1328,7 +1353,7 @@ export function WorkflowCanvas() { |
|
|
|
|
|
|
|
|
// Handle the splitGrid action - uses automated grid detection
|
|
|
// Handle the splitGrid action - uses automated grid detection
|
|
|
const handleSplitGridAction = useCallback( |
|
|
const handleSplitGridAction = useCallback( |
|
|
async (sourceNodeId: string, flowPosition: { x: number; y: number }) => { |
|
|
async (sourceNodeId: string, flowPosition: { x: number; y: number }, sourceGroupId?: string) => { |
|
|
const sourceNode = getNodeById(sourceNodeId); |
|
|
const sourceNode = getNodeById(sourceNodeId); |
|
|
if (!sourceNode) return; |
|
|
if (!sourceNode) return; |
|
|
|
|
|
|
|
|
@ -1373,10 +1398,14 @@ export function WorkflowCanvas() { |
|
|
const row = Math.floor(index / grid.cols); |
|
|
const row = Math.floor(index / grid.cols); |
|
|
const col = index % grid.cols; |
|
|
const col = index % grid.cols; |
|
|
|
|
|
|
|
|
const nodeId = addNode("imageInput", { |
|
|
const nodeId = addNodeWithOptionalGroup( |
|
|
x: flowPosition.x + col * (nodeWidth + gap), |
|
|
"imageInput", |
|
|
y: flowPosition.y + row * (nodeHeight + gap), |
|
|
{ |
|
|
}); |
|
|
x: flowPosition.x + col * (nodeWidth + gap), |
|
|
|
|
|
y: flowPosition.y + row * (nodeHeight + gap), |
|
|
|
|
|
}, |
|
|
|
|
|
sourceGroupId |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
// Get dimensions from the split image
|
|
|
// Get dimensions from the split image
|
|
|
const img = new Image(); |
|
|
const img = new Image(); |
|
|
@ -1397,7 +1426,7 @@ export function WorkflowCanvas() { |
|
|
setIsSplitting(false); |
|
|
setIsSplitting(false); |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
[getNodeById, edges, addNode, updateNodeData, t] |
|
|
[getNodeById, edges, addNodeWithOptionalGroup, updateNodeData, t] |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
// Helper to get image from a node
|
|
|
// Helper to get image from a node
|
|
|
@ -1520,12 +1549,12 @@ export function WorkflowCanvas() { |
|
|
async (selection: { type: NodeType | MenuAction; isAction: boolean }) => { |
|
|
async (selection: { type: NodeType | MenuAction; isAction: boolean }) => { |
|
|
if (!connectionDrop) return; |
|
|
if (!connectionDrop) return; |
|
|
|
|
|
|
|
|
const { flowPosition, sourceNodeId, sourceHandleId, connectionType, handleType } = connectionDrop; |
|
|
const { flowPosition, sourceNodeId, sourceHandleId, sourceGroupId, connectionType, handleType } = connectionDrop; |
|
|
|
|
|
|
|
|
// Handle actions differently from node creation
|
|
|
// Handle actions differently from node creation
|
|
|
if (selection.isAction) { |
|
|
if (selection.isAction) { |
|
|
if (selection.type === "splitGridImmediate" && sourceNodeId) { |
|
|
if (selection.type === "splitGridImmediate" && sourceNodeId) { |
|
|
handleSplitGridAction(sourceNodeId, flowPosition); |
|
|
handleSplitGridAction(sourceNodeId, flowPosition, sourceGroupId); |
|
|
} else if ( |
|
|
} else if ( |
|
|
selection.type === "multiAngleImage" && |
|
|
selection.type === "multiAngleImage" && |
|
|
sourceNodeId && |
|
|
sourceNodeId && |
|
|
@ -1537,6 +1566,7 @@ export function WorkflowCanvas() { |
|
|
sourceNodeId, |
|
|
sourceNodeId, |
|
|
sourceHandleId, |
|
|
sourceHandleId, |
|
|
flowPosition, |
|
|
flowPosition, |
|
|
|
|
|
sourceGroupId, |
|
|
settings: { ...DEFAULT_MULTI_ANGLE_SETTINGS }, |
|
|
settings: { ...DEFAULT_MULTI_ANGLE_SETTINGS }, |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
@ -1548,7 +1578,12 @@ export function WorkflowCanvas() { |
|
|
const nodeType = selection.type as NodeType; |
|
|
const nodeType = selection.type as NodeType; |
|
|
|
|
|
|
|
|
// Create the new node at the drop position (empty - tutorial will populate after connection)
|
|
|
// Create the new node at the drop position (empty - tutorial will populate after connection)
|
|
|
const newNodeId = addNode(nodeType, flowPosition, await buildDefaultGenerationNodeData(nodeType)); |
|
|
const newNodeId = addNodeWithOptionalGroup( |
|
|
|
|
|
nodeType, |
|
|
|
|
|
flowPosition, |
|
|
|
|
|
sourceGroupId, |
|
|
|
|
|
await buildDefaultGenerationNodeData(nodeType) |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
// Tutorial tracking
|
|
|
// Tutorial tracking
|
|
|
if (tutorialActive && nodeType === "nanoBanana") { |
|
|
if (tutorialActive && nodeType === "nanoBanana") { |
|
|
@ -1787,7 +1822,7 @@ export function WorkflowCanvas() { |
|
|
selectSingleNode(newNodeId); |
|
|
selectSingleNode(newNodeId); |
|
|
setConnectionDrop(null); |
|
|
setConnectionDrop(null); |
|
|
}, |
|
|
}, |
|
|
[connectionDrop, addNode, onConnect, nodes, edges, handleSplitGridAction, getImageFromNode, updateNodeData, tutorialActive, selectSingleNode] |
|
|
[connectionDrop, addNodeWithOptionalGroup, onConnect, nodes, edges, handleSplitGridAction, getImageFromNode, updateNodeData, tutorialActive, selectSingleNode] |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
const handleCloseDropMenu = useCallback(() => { |
|
|
const handleCloseDropMenu = useCallback(() => { |
|
|
@ -1859,9 +1894,10 @@ export function WorkflowCanvas() { |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const newNodeId = addNode( |
|
|
const newNodeId = addNodeWithOptionalGroup( |
|
|
"nanoBanana", |
|
|
"nanoBanana", |
|
|
multiAngleDialog.flowPosition, |
|
|
multiAngleDialog.flowPosition, |
|
|
|
|
|
multiAngleDialog.sourceGroupId, |
|
|
buildMultiAngleNodeData(sourceImage, multiAngleDialog.settings) |
|
|
buildMultiAngleNodeData(sourceImage, multiAngleDialog.settings) |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
@ -1874,7 +1910,7 @@ export function WorkflowCanvas() { |
|
|
selectSingleNode(newNodeId); |
|
|
selectSingleNode(newNodeId); |
|
|
setMultiAngleDialog(null); |
|
|
setMultiAngleDialog(null); |
|
|
await regenerateNode(newNodeId); |
|
|
await regenerateNode(newNodeId); |
|
|
}, [addNode, getImageFromNode, multiAngleDialog, onConnect, regenerateNode, selectSingleNode, showToast, t]); |
|
|
}, [addNodeWithOptionalGroup, getImageFromNode, multiAngleDialog, onConnect, regenerateNode, selectSingleNode, showToast, t]); |
|
|
|
|
|
|
|
|
// Get copy/paste functions and clipboard from store
|
|
|
// Get copy/paste functions and clipboard from store
|
|
|
const copySelectedNodes = useWorkflowStore((state) => state.copySelectedNodes); |
|
|
const copySelectedNodes = useWorkflowStore((state) => state.copySelectedNodes); |
|
|
|