Browse Source
- 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 selectionhandoff-20260429-1057
2 changed files with 152 additions and 0 deletions
@ -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…
Reference in new issue