From 4908f2b3706af3ab95021b632764a39d6af379ef Mon Sep 17 00:00:00 2001 From: shrimbly Date: Mon, 2 Mar 2026 22:02:21 +1300 Subject: [PATCH] feat(46-01): render FloatingNodeHeaders via ViewportPortal - Import ViewportPortal from @xyflow/react - Add NODE_TITLES mapping for all node types - Create getNodeTitle helper (checks selectedModel.displayName for generate nodes) - Create getNodeTitlePrefix helper (returns ProviderBadge for nodes with provider) - Wire up handleCustomTitleChange and handleCommentChange callbacks - Render FloatingNodeHeader for each node inside ViewportPortal - Headers positioned above nodes, width matches node width - onExpand/onRun callbacks not wired yet (will be added in Plan 03) - Comment editing and title editing fully functional --- src/components/WorkflowCanvas.tsx | 94 +++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 2064f614..c149c599 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -14,6 +14,7 @@ import { OnConnectEnd, Node, OnSelectionChangeParams, + ViewportPortal, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; @@ -55,6 +56,8 @@ import { GlobalImageHistory } from "./GlobalImageHistory"; import { GroupBackgroundsPortal, GroupControlsOverlay } from "./GroupsOverlay"; import { NodeType, NanoBananaNodeData, HandleType } from "@/types"; import { defaultNodeDimensions } from "@/store/utils/nodeDefaults"; +import { FloatingNodeHeader } from "./nodes/FloatingNodeHeader"; +import { ProviderBadge } from "./nodes/ProviderBadge"; import { detectAndSplitGrid } from "@/utils/gridSplitter"; import { logger } from "@/utils/logger"; import { WelcomeModal } from "./quickstart"; @@ -310,6 +313,69 @@ export function WorkflowCanvas() { }); }, [nodes, dimmedNodeIds]); + // Node title mapping for FloatingNodeHeaders + const NODE_TITLES: Record = { + imageInput: 'Image Input', + audioInput: 'Audio Input', + annotation: 'Annotation', + prompt: 'Prompt', + array: 'Array', + promptConstructor: 'Prompt Constructor', + nanoBanana: 'Generate Image', + generateVideo: 'Generate Video', + generate3d: 'Generate 3D', + generateAudio: 'Generate Audio', + llmGenerate: 'LLM Generate', + splitGrid: 'Split Grid', + output: 'Output', + outputGallery: 'Output Gallery', + imageCompare: 'Image Compare', + videoStitch: 'Video Stitch', + easeCurve: 'Ease Curve', + videoTrim: 'Video Trim', + videoFrameGrab: 'Frame Grab', + router: 'Router', + switch: 'Switch', + conditionalSwitch: 'Conditional Switch', + glbViewer: '3D Viewer', + }; + + // Helper to get node title (used for FloatingNodeHeader) + const getNodeTitle = useCallback((node: Node) => { + // For generate nodes, check for selectedModel display name + if (node.type === "nanoBanana" || node.type === "generateVideo" || node.type === "generate3d" || node.type === "generateAudio") { + const model = node.data?.selectedModel; + if (model?.displayName) return model.displayName; + } + + // For LLM nodes, check for selectedLLMModel or selectedModel + if (node.type === "llmGenerate") { + const model = node.data?.selectedLLMModel || node.data?.selectedModel; + if (model?.displayName) return model.displayName; + if (model?.name) return model.name; + } + + return NODE_TITLES[node.type || ""] || "Node"; + }, []); + + // Helper to get title prefix (provider badge for generate/LLM nodes) + const getNodeTitlePrefix = useCallback((node: Node): React.ReactNode => { + const provider = node.data?.selectedModel?.provider; + if (provider) { + return ; + } + return null; + }, []); + + // Wire comment/title change callbacks for FloatingNodeHeaders + const handleCustomTitleChange = useCallback((nodeId: string, title: string) => { + updateNodeData(nodeId, { customTitle: title || undefined }); + }, [updateNodeData]); + + const handleCommentChange = useCallback((nodeId: string, comment: string) => { + updateNodeData(nodeId, { comment: comment || undefined }); + }, [updateNodeData]); + // Check if a node was dropped into a group and add it to that group const handleNodeDragStop = useCallback( @@ -1952,6 +2018,34 @@ export function WorkflowCanvas() { } }} /> + + {allNodes.map((node) => { + // Groups don't get floating headers + if (node.type === "group") return null; + + const nodeWidth = typeof node.style?.width === "number" + ? node.style.width + : (defaultNodeDimensions[node.type as NodeType]?.width ?? 250); + + return ( + handleCustomTitleChange(node.id, title)} + onCommentChange={(comment) => handleCommentChange(node.id, comment)} + /> + ); + })} + {/* Connection drop menu */}