diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx
index c149c599..5c3ea050 100644
--- a/src/components/WorkflowCanvas.tsx
+++ b/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 */}
+
);
}
diff --git a/src/components/nodes/ControlPanel.tsx b/src/components/nodes/ControlPanel.tsx
new file mode 100644
index 00000000..919ed7a1
--- /dev/null
+++ b/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 (
+
+
+
+ {/* Header */}
+
+ {getNodeTypeTitle(selectedNode.type as NodeType)}
+
+
+ {/* Node-specific controls */}
+
+ {selectedNode.type === "nanoBanana" && (
+
+ )}
+ {selectedNode.type === "generateVideo" && (
+
+ )}
+ {selectedNode.type === "generate3d" && (
+
+ )}
+ {selectedNode.type === "generateAudio" && (
+
+ )}
+ {selectedNode.type === "llmGenerate" && (
+
+ )}
+ {selectedNode.type === "easeCurve" && (
+
+ )}
+ {selectedNode.type === "conditionalSwitch" && (
+
+ )}
+
+
+
+
+ );
+}
+
+function getNodeTypeTitle(type: NodeType): string {
+ const titles: Record = {
+ 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 (
+
+ Settings for {node.type}
+
+ );
+}
+
+function GenerateVideoControls({ node }: { node: Node }) {
+ return (
+
+ Settings for {node.type}
+
+ );
+}
+
+function Generate3DControls({ node }: { node: Node }) {
+ return (
+
+ Settings for {node.type}
+
+ );
+}
+
+function GenerateAudioControls({ node }: { node: Node }) {
+ return (
+
+ Settings for {node.type}
+
+ );
+}
+
+function LLMControls({ node }: { node: Node }) {
+ return (
+
+ Settings for {node.type}
+
+ );
+}
+
+function EaseCurveControls({ node }: { node: Node }) {
+ return (
+
+ Settings for {node.type}
+
+ );
+}
+
+function ConditionalSwitchControls({ node }: { node: Node }) {
+ return (
+
+ Settings for {node.type}
+
+ );
+}