Browse Source

feat(46-04): wire onRun/onExpand callbacks and add expansion modals

- Add regenerateNode to store imports for onRun callbacks
- Create getOnRun callback factory for runnable nodes (nanoBanana, generateVideo, generate3d, generateAudio, llmGenerate)
- Create getOnExpand callback factory for expandable nodes (prompt, promptConstructor, annotation, splitGrid)
- Add expandingNode state to manage modal display
- Import and render PromptEditorModal, AnnotationModal, SplitGridSettingsModal globally
- AnnotationModal uses annotationStore.openModal (already globally rendered)
- Pass onRun and onExpand callbacks to FloatingNodeHeader in ViewportPortal
- Modals rendered via createPortal to escape React Flow stacking context
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
7231ea270e
  1. 90
      src/components/WorkflowCanvas.tsx

90
src/components/WorkflowCanvas.tsx

@ -66,6 +66,11 @@ import { ProjectSetupModal } from "./ProjectSetupModal";
import { ChatPanel } from "./ChatPanel"; import { ChatPanel } from "./ChatPanel";
import { EditOperation } from "@/lib/chat/editOperations"; import { EditOperation } from "@/lib/chat/editOperations";
import { stripBinaryData } from "@/lib/chat/contextBuilder"; import { stripBinaryData } from "@/lib/chat/contextBuilder";
import { PromptEditorModal } from "./modals/PromptEditorModal";
import { AnnotationModal } from "./AnnotationModal";
import { SplitGridSettingsModal } from "./SplitGridSettingsModal";
import { createPortal } from "react-dom";
import { useAnnotationStore } from "@/store/annotationStore";
const nodeTypes: NodeTypes = { const nodeTypes: NodeTypes = {
imageInput: ImageInputNode, imageInput: ImageInputNode,
@ -260,8 +265,9 @@ const findScrollableAncestor = (target: HTMLElement, deltaX: number, deltaY: num
}; };
export function WorkflowCanvas() { export function WorkflowCanvas() {
const { nodes, edges, groups, onNodesChange, onEdgesChange, onConnect, addNode, updateNodeData, loadWorkflow, getNodeById, addToGlobalHistory, setNodeGroupId, executeWorkflow, isModalOpen, showQuickstart, setShowQuickstart, navigationTarget, setNavigationTarget, captureSnapshot, applyEditOperations, setWorkflowMetadata, canvasNavigationSettings, setShortcutsDialogOpen, dimmedNodeIds } = const { nodes, edges, groups, onNodesChange, onEdgesChange, onConnect, addNode, updateNodeData, loadWorkflow, getNodeById, addToGlobalHistory, setNodeGroupId, executeWorkflow, isModalOpen, showQuickstart, setShowQuickstart, navigationTarget, setNavigationTarget, captureSnapshot, applyEditOperations, setWorkflowMetadata, canvasNavigationSettings, setShortcutsDialogOpen, dimmedNodeIds, regenerateNode } =
useWorkflowStore(); useWorkflowStore();
const openAnnotationModal = useAnnotationStore((state) => state.openModal);
const { screenToFlowPosition, getViewport, zoomIn, zoomOut, setViewport, setCenter } = useReactFlow(); const { screenToFlowPosition, getViewport, zoomIn, zoomOut, setViewport, setCenter } = useReactFlow();
const { show: showToast } = useToast(); const { show: showToast } = useToast();
const [isDragOver, setIsDragOver] = useState(false); const [isDragOver, setIsDragOver] = useState(false);
@ -271,6 +277,7 @@ export function WorkflowCanvas() {
const [isChatOpen, setIsChatOpen] = useState(false); const [isChatOpen, setIsChatOpen] = useState(false);
const [isBuildingWorkflow, setIsBuildingWorkflow] = useState(false); const [isBuildingWorkflow, setIsBuildingWorkflow] = useState(false);
const [showNewProjectSetup, setShowNewProjectSetup] = useState(false); const [showNewProjectSetup, setShowNewProjectSetup] = useState(false);
const [expandingNode, setExpandingNode] = useState<{ id: string; type: string } | null>(null);
const reactFlowWrapper = useRef<HTMLDivElement>(null); const reactFlowWrapper = useRef<HTMLDivElement>(null);
// Detect if canvas is empty for showing quickstart // Detect if canvas is empty for showing quickstart
@ -377,6 +384,35 @@ export function WorkflowCanvas() {
updateNodeData(nodeId, { comment: comment || undefined }); updateNodeData(nodeId, { comment: comment || undefined });
}, [updateNodeData]); }, [updateNodeData]);
// Create onRun callback for runnable nodes
const getOnRun = useCallback((nodeId: string, nodeType: string) => {
const runnableTypes = ['nanoBanana', 'generateVideo', 'generate3d', 'generateAudio', 'llmGenerate'];
if (runnableTypes.includes(nodeType)) {
return () => regenerateNode(nodeId);
}
return undefined;
}, [regenerateNode]);
// Create onExpand callback for expandable nodes
const getOnExpand = useCallback((nodeId: string, nodeType: string) => {
if (nodeType === 'annotation') {
// Annotation uses annotationStore's openModal
return () => {
const node = getNodeById(nodeId);
if (!node) return;
const imageToEdit = node.data?.outputImage || node.data?.image;
if (!imageToEdit) return;
openAnnotationModal(nodeId, imageToEdit, node.data?.annotations);
};
}
const expandableTypes = ['prompt', 'promptConstructor', 'splitGrid'];
if (expandableTypes.includes(nodeType)) {
return () => setExpandingNode({ id: nodeId, type: nodeType });
}
return undefined;
}, [getNodeById, openAnnotationModal]);
// Check if a node was dropped into a group and add it to that group // Check if a node was dropped into a group and add it to that group
const handleNodeDragStop = useCallback( const handleNodeDragStop = useCallback(
@ -2043,6 +2079,8 @@ export function WorkflowCanvas() {
titlePrefix={getNodeTitlePrefix(node)} titlePrefix={getNodeTitlePrefix(node)}
onCustomTitleChange={(title) => handleCustomTitleChange(node.id, title)} onCustomTitleChange={(title) => handleCustomTitleChange(node.id, title)}
onCommentChange={(comment) => handleCommentChange(node.id, comment)} onCommentChange={(comment) => handleCommentChange(node.id, comment)}
onRun={getOnRun(node.id, node.type as string)}
onExpand={getOnExpand(node.id, node.type as string)}
/> />
); );
})} })}
@ -2084,6 +2122,56 @@ export function WorkflowCanvas() {
{/* Control panel - renders on right side when a configurable node is selected */} {/* Control panel - renders on right side when a configurable node is selected */}
<ControlPanel /> <ControlPanel />
{/* Expansion modals - rendered via portal when expand button is clicked */}
{expandingNode && expandingNode.type === 'prompt' && (() => {
const node = getNodeById(expandingNode.id);
if (!node) return null;
return createPortal(
<PromptEditorModal
isOpen={true}
initialPrompt={node.data?.prompt || ''}
onSubmit={(prompt) => {
updateNodeData(expandingNode.id, { prompt });
setExpandingNode(null);
}}
onClose={() => setExpandingNode(null)}
/>,
document.body
);
})()}
{expandingNode && expandingNode.type === 'promptConstructor' && (() => {
const node = getNodeById(expandingNode.id);
if (!node) return null;
return createPortal(
<PromptEditorModal
isOpen={true}
initialPrompt={node.data?.template || ''}
onSubmit={(template) => {
updateNodeData(expandingNode.id, { template });
setExpandingNode(null);
}}
onClose={() => setExpandingNode(null)}
/>,
document.body
);
})()}
{expandingNode && expandingNode.type === 'splitGrid' && (() => {
const node = getNodeById(expandingNode.id);
if (!node) return null;
return (
<SplitGridSettingsModal
nodeId={expandingNode.id}
nodeData={node.data}
onClose={() => setExpandingNode(null)}
/>
);
})()}
{/* AnnotationModal is globally managed by annotationStore */}
<AnnotationModal />
</div> </div>
); );
} }

Loading…
Cancel
Save