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} +
+ ); +}