Browse Source

feat(46-02): create ControlPanel component with node-type routing

- Created ControlPanel component rendering on right side of viewport
- Fixed position with pointer-events-none wrapper, auto scrolling
- Filters to single selected configurable node (7 types)
- Placeholder controls for each node type (Task 2 will implement)
- Integrated into WorkflowCanvas after ChatPanel
- Panel appears/disappears based on node selection
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
324c58d58c
  1. 4
      src/components/WorkflowCanvas.tsx
  2. 148
      src/components/nodes/ControlPanel.tsx

4
src/components/WorkflowCanvas.tsx

@ -58,6 +58,7 @@ import { NodeType, NanoBananaNodeData, HandleType } from "@/types";
import { defaultNodeDimensions } from "@/store/utils/nodeDefaults";
import { FloatingNodeHeader } from "./nodes/FloatingNodeHeader";
import { ProviderBadge } from "./nodes/ProviderBadge";
import { ControlPanel } from "./nodes/ControlPanel";
import { detectAndSplitGrid } from "@/utils/gridSplitter";
import { logger } from "@/utils/logger";
import { WelcomeModal } from "./quickstart";
@ -2080,6 +2081,9 @@ export function WorkflowCanvas() {
workflowState={chatWorkflowState}
selectedNodeIds={selectedNodeIds}
/>
{/* Control panel - renders on right side when a configurable node is selected */}
<ControlPanel />
</div>
);
}

148
src/components/nodes/ControlPanel.tsx

@ -0,0 +1,148 @@
"use client";
import { useMemo } from "react";
import { Node } from "@xyflow/react";
import { useWorkflowStore } from "@/store/workflowStore";
import { NodeType } from "@/types";
// List of node types that have configurable parameters
const CONFIGURABLE_NODE_TYPES: NodeType[] = [
"nanoBanana",
"generateVideo",
"generate3d",
"generateAudio",
"llmGenerate",
"easeCurve",
"conditionalSwitch",
];
/**
* Fixed-position control panel on the right side of viewport
* Displays controls for the currently selected node
*/
export function ControlPanel() {
const nodes = useWorkflowStore((state) => state.nodes);
// Get the single selected node
const selectedNode = useMemo(() => {
const selected = nodes.filter((n) => n.selected);
if (selected.length !== 1) return null;
return selected[0];
}, [nodes]);
// Check if the selected node is configurable
const isConfigurable = selectedNode && CONFIGURABLE_NODE_TYPES.includes(selectedNode.type as NodeType);
// If no single node selected or not configurable, hide panel
if (!selectedNode || !isConfigurable) {
return null;
}
return (
<div className="fixed top-0 right-6 h-screen z-[50] flex items-center pointer-events-none">
<div className="w-80 bg-neutral-800 border border-neutral-700 rounded-xl shadow-xl max-h-[80vh] overflow-y-auto pointer-events-auto transition-opacity duration-200 nowheel">
<div className="p-4">
{/* Header */}
<h3 className="text-sm font-medium text-neutral-200 mb-4">
{getNodeTypeTitle(selectedNode.type as NodeType)}
</h3>
{/* Node-specific controls */}
<div className="space-y-4">
{selectedNode.type === "nanoBanana" && (
<GenerateImageControls node={selectedNode} />
)}
{selectedNode.type === "generateVideo" && (
<GenerateVideoControls node={selectedNode} />
)}
{selectedNode.type === "generate3d" && (
<Generate3DControls node={selectedNode} />
)}
{selectedNode.type === "generateAudio" && (
<GenerateAudioControls node={selectedNode} />
)}
{selectedNode.type === "llmGenerate" && (
<LLMControls node={selectedNode} />
)}
{selectedNode.type === "easeCurve" && (
<EaseCurveControls node={selectedNode} />
)}
{selectedNode.type === "conditionalSwitch" && (
<ConditionalSwitchControls node={selectedNode} />
)}
</div>
</div>
</div>
</div>
);
}
function getNodeTypeTitle(type: NodeType): string {
const titles: Record<string, string> = {
nanoBanana: "Generate Image Settings",
generateVideo: "Generate Video Settings",
generate3d: "Generate 3D Settings",
generateAudio: "Generate Audio Settings",
llmGenerate: "LLM Settings",
easeCurve: "Ease Curve Settings",
conditionalSwitch: "Conditional Switch Settings",
};
return titles[type] || "Settings";
}
// Placeholder components for each node type (Task 2 will implement these fully)
function GenerateImageControls({ node }: { node: Node }) {
return (
<div className="text-xs text-neutral-400">
Settings for {node.type}
</div>
);
}
function GenerateVideoControls({ node }: { node: Node }) {
return (
<div className="text-xs text-neutral-400">
Settings for {node.type}
</div>
);
}
function Generate3DControls({ node }: { node: Node }) {
return (
<div className="text-xs text-neutral-400">
Settings for {node.type}
</div>
);
}
function GenerateAudioControls({ node }: { node: Node }) {
return (
<div className="text-xs text-neutral-400">
Settings for {node.type}
</div>
);
}
function LLMControls({ node }: { node: Node }) {
return (
<div className="text-xs text-neutral-400">
Settings for {node.type}
</div>
);
}
function EaseCurveControls({ node }: { node: Node }) {
return (
<div className="text-xs text-neutral-400">
Settings for {node.type}
</div>
);
}
function ConditionalSwitchControls({ node }: { node: Node }) {
return (
<div className="text-xs text-neutral-400">
Settings for {node.type}
</div>
);
}
Loading…
Cancel
Save