Browse Source

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
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
4908f2b370
  1. 94
      src/components/WorkflowCanvas.tsx

94
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<string, string> = {
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 <ProviderBadge provider={provider} />;
}
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() {
}
}}
/>
<ViewportPortal>
{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 (
<FloatingNodeHeader
key={`header-${node.id}`}
id={node.id}
type={node.type as NodeType}
data={node.data}
position={node.position}
width={nodeWidth}
selected={!!node.selected}
title={getNodeTitle(node)}
customTitle={node.data?.customTitle}
comment={node.data?.comment}
titlePrefix={getNodeTitlePrefix(node)}
onCustomTitleChange={(title) => handleCustomTitleChange(node.id, title)}
onCommentChange={(comment) => handleCommentChange(node.id, comment)}
/>
);
})}
</ViewportPortal>
</ReactFlow>
{/* Connection drop menu */}

Loading…
Cancel
Save