From f8407d4aadc013335dfb366f403fb6979dec271d Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 6 Mar 2026 22:25:34 +1300 Subject: [PATCH] feat: add Run/Apply buttons to ControlPanel settings panels Add execution buttons at the bottom of each configurable node's settings panel so users can trigger generation directly from where they configure settings, instead of needing keyboard shortcuts or external run buttons. - GenerateImage, GenerateVideo, Generate3D, GenerateAudio, LLM: "Run" button - EaseCurve: "Apply" button - ConditionalSwitch: no button (passive routing node) - Buttons disable and show running state while workflow executes Co-Authored-By: Claude Opus 4.6 --- src/components/nodes/ControlPanel.tsx | 68 +++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/src/components/nodes/ControlPanel.tsx b/src/components/nodes/ControlPanel.tsx index 67c680d7..eedfed71 100644 --- a/src/components/nodes/ControlPanel.tsx +++ b/src/components/nodes/ControlPanel.tsx @@ -183,6 +183,8 @@ function getNodeTypeTitle(type: NodeType): string { function GenerateImageControls({ node }: { node: Node }) { const nodeData = node.data as NanoBananaNodeData; const updateNodeData = useWorkflowStore((state) => state.updateNodeData); + const regenerateNode = useWorkflowStore((state) => state.regenerateNode); + const isRunning = useWorkflowStore((state) => state.isRunning); const { replicateApiKey, falApiKey, kieApiKey, replicateEnabled, kieEnabled } = useProviderApiKeys(); const [externalModels, setExternalModels] = useState([]); const [isLoadingModels, setIsLoadingModels] = useState(false); @@ -480,6 +482,14 @@ function GenerateImageControls({ node }: { node: Node }) { )} + + {isBrowseDialogOpen && ( state.updateNodeData); + const regenerateNode = useWorkflowStore((state) => state.regenerateNode); + const isRunning = useWorkflowStore((state) => state.isRunning); const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false); const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal"; @@ -571,6 +583,14 @@ function GenerateVideoControls({ node }: { node: Node }) { )} + + {isBrowseDialogOpen && ( state.updateNodeData); + const regenerateNode = useWorkflowStore((state) => state.regenerateNode); + const isRunning = useWorkflowStore((state) => state.isRunning); const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false); const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal"; @@ -664,6 +686,14 @@ function Generate3DControls({ node }: { node: Node }) { )} + + {isBrowseDialogOpen && ( state.regenerateNode); + const isRunning = useWorkflowStore((state) => state.isRunning); + return ( -
- Audio controls coming in full implementation +
+
+ Audio generation settings +
+
); } @@ -689,6 +731,8 @@ function GenerateAudioControls({ node }: { node: Node }) { function LLMControls({ node }: { node: Node }) { const nodeData = node.data as LLMGenerateNodeData; const updateNodeData = useWorkflowStore((state) => state.updateNodeData); + const regenerateNode = useWorkflowStore((state) => state.regenerateNode); + const isRunning = useWorkflowStore((state) => state.isRunning); const handleProviderChange = useCallback( (e: React.ChangeEvent) => { @@ -787,6 +831,14 @@ function LLMControls({ node }: { node: Node }) { className="nodrag nopan w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-blue-500" />
+ + ); } @@ -795,6 +847,8 @@ function LLMControls({ node }: { node: Node }) { function EaseCurveControls({ node }: { node: Node }) { const nodeData = node.data as EaseCurveNodeData; const updateNodeData = useWorkflowStore((state) => state.updateNodeData); + const regenerateNode = useWorkflowStore((state) => state.regenerateNode); + const isRunning = useWorkflowStore((state) => state.isRunning); const [showPresets, setShowPresets] = useState(false); const presetsButtonRef = useRef(null); @@ -878,6 +932,14 @@ function EaseCurveControls({ node }: { node: Node }) { /> + + {showPresets && typeof document !== 'undefined' && createPortal(