From 08490f0f2f11f537739612b75f7995753646626e Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sun, 8 Mar 2026 23:16:39 +1300 Subject: [PATCH] feat(47-02): wire inline parameters into GenerateImageNode and GenerateVideoNode - Import useInlineParameters hook and InlineParameterPanel component - Add collapse state computation and toggle handler for both nodes - Render InlineParameterPanel after content area when inline enabled - GenerateImageNode includes Gemini-specific controls (model, aspect ratio, resolution, search toggles) with unified styling - GenerateImageNode includes ModelParameters for external providers - GenerateVideoNode includes model selector and ModelParameters - Add node height management effects that measure parameter panel and resize nodes - Conditional rendering: inline params only show when global setting enabled --- src/components/nodes/GenerateImageNode.tsx | 178 +++++++++++++++++++++ src/components/nodes/GenerateVideoNode.tsx | 91 ++++++++++- 2 files changed, 267 insertions(+), 2 deletions(-) diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index bebe042e..13eab26c 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -13,6 +13,8 @@ import { useToast } from "@/components/Toast"; import { getImageDimensions, calculateNodeSizePreservingHeight } from "@/utils/nodeDimensions"; import { ProviderBadge } from "./ProviderBadge"; import { getModelPageUrl, getProviderDisplayName } from "@/utils/providerUrls"; +import { useInlineParameters } from "@/hooks/useInlineParameters"; +import { InlineParameterPanel } from "./InlineParameterPanel"; // Base 10 aspect ratios (all Gemini image models) const BASE_ASPECT_RATIOS: AspectRatio[] = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"]; @@ -48,6 +50,10 @@ export function GenerateImageNode({ id, data, selected }: NodeProps(null); const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false); + // Inline parameters infrastructure + const { inlineParametersEnabled } = useInlineParameters(); + const parameterPanelRef = useRef(null); + // Get the current selected provider (default to gemini) const currentProvider: ProviderType = nodeData.selectedModel?.provider || "gemini"; @@ -142,6 +148,16 @@ export function GenerateImageNode({ id, data, selected }: NodeProps { + const nodes = useWorkflowStore.getState().nodes; + const node = nodes.find(n => n.id === id); + const currentExpanded = (node?.data as NanoBananaNodeData)?.parametersExpanded ?? true; + updateNodeData(id, { parametersExpanded: !currentExpanded }); + }, [id, updateNodeData]); + // Handle provider change const handleProviderChange = useCallback( (e: React.ChangeEvent) => { @@ -479,6 +495,39 @@ export function GenerateImageNode({ id, data, selected }: NodeProps { + if (!inlineParametersEnabled) return; + + // Use RAF to wait for render + requestAnimationFrame(() => { + requestAnimationFrame(() => { + const currentNode = useWorkflowStore.getState().nodes.find(n => n.id === id); + if (!currentNode) return; + + const baseHeight = typeof currentNode.style?.height === 'number' + ? currentNode.style.height + : 300; + + let paramHeight = 0; + if (isParamsExpanded && parameterPanelRef.current) { + // Measure the actual rendered height of parameters + const panelContent = parameterPanelRef.current.querySelector('#params-' + id); + if (panelContent) { + paramHeight = (panelContent as HTMLElement).scrollHeight; + } + } + // Add chevron button height (~28px) + const chevronHeight = 28; + const totalHeight = baseHeight + chevronHeight + paramHeight; + + setNodes(nodes => nodes.map(n => + n.id === id ? { ...n, style: { ...n.style, height: totalHeight } } : n + )); + }); + }); + }, [id, isParamsExpanded, inlineParametersEnabled, setNodes]); + return ( <> )} + + {/* Inline parameter panel */} + {inlineParametersEnabled && ( + + {/* Model selector: Browse button + current model display */} +
+
+
+ {displayTitle} +
+
+ {enabledProviders.find(p => p.id === currentProvider)?.name || currentProvider} +
+
+ +
+ + {/* Gemini-specific controls - unified styling to match ModelParameters */} + {isGeminiProvider && currentModelId && ( +
+ {/* Model selector */} +
+ + +
+ + {/* Aspect Ratio */} +
+ + +
+ + {/* Resolution (if supported) */} + {supportsResolution && ( +
+ + +
+ )} + + {/* Google Search toggle */} + {(currentModelId === "nano-banana-pro" || currentModelId === "nano-banana-2") && ( + + )} + + {/* Image Search toggle (NB2 only) */} + {currentModelId === "nano-banana-2" && ( + + )} +
+ )} + + {/* External provider parameters - reuse ModelParameters component */} + {!isGeminiProvider && nodeData.selectedModel?.modelId && ( + + )} +
+ )}
+ + {/* Model browse dialog */} + {isBrowseDialogOpen && ( + setIsBrowseDialogOpen(false)} + onModelSelected={handleBrowseModelSelect} + capabilities={IMAGE_CAPABILITIES} + /> + )} ); } diff --git a/src/components/nodes/GenerateVideoNode.tsx b/src/components/nodes/GenerateVideoNode.tsx index b56ab2f5..b27f3afa 100644 --- a/src/components/nodes/GenerateVideoNode.tsx +++ b/src/components/nodes/GenerateVideoNode.tsx @@ -14,6 +14,8 @@ import { getVideoDimensions, calculateNodeSizePreservingHeight } from "@/utils/n import { ProviderBadge } from "./ProviderBadge"; import { useVideoBlobUrl } from "@/hooks/useVideoBlobUrl"; import { getModelPageUrl, getProviderDisplayName } from "@/utils/providerUrls"; +import { useInlineParameters } from "@/hooks/useInlineParameters"; +import { InlineParameterPanel } from "./InlineParameterPanel"; // Video generation capabilities const VIDEO_CAPABILITIES: ModelCapability[] = ["text-to-video", "image-to-video"]; @@ -53,6 +55,10 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps(null); + const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal"; // Get enabled providers @@ -122,6 +128,16 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps { + const nodes = useWorkflowStore.getState().nodes; + const node = nodes.find(n => n.id === id); + const currentExpanded = (node?.data as GenerateVideoNodeData)?.parametersExpanded ?? true; + updateNodeData(id, { parametersExpanded: !currentExpanded }); + }, [id, updateNodeData]); + // Handle provider change const handleProviderChange = useCallback( (e: React.ChangeEvent) => { @@ -387,6 +403,39 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps { + if (!inlineParametersEnabled) return; + + // Use RAF to wait for render + requestAnimationFrame(() => { + requestAnimationFrame(() => { + const currentNode = useWorkflowStore.getState().nodes.find(n => n.id === id); + if (!currentNode) return; + + const baseHeight = typeof currentNode.style?.height === 'number' + ? currentNode.style.height + : 300; + + let paramHeight = 0; + if (isParamsExpanded && parameterPanelRef.current) { + // Measure the actual rendered height of parameters + const panelContent = parameterPanelRef.current.querySelector('#params-' + id); + if (panelContent) { + paramHeight = (panelContent as HTMLElement).scrollHeight; + } + } + // Add chevron button height (~28px) + const chevronHeight = 28; + const totalHeight = baseHeight + chevronHeight + paramHeight; + + setNodes(nodes => nodes.map(n => + n.id === id ? { ...n, style: { ...n.style, height: totalHeight } } : n + )); + }); + }); + }, [id, isParamsExpanded, inlineParametersEnabled, setNodes]); + return ( <> )} + + {/* Inline parameter panel */} + {inlineParametersEnabled && ( + + {/* Model selector: Browse button + current model display */} +
+
+
+ {displayTitle} +
+
+ {enabledProviders.find(p => p.id === currentProvider)?.name || currentProvider} +
+
+ +
+ + {/* External provider parameters - reuse ModelParameters component */} + {nodeData.selectedModel?.modelId && !isVeoModel(nodeData.selectedModel.modelId) && ( + + )} +
+ )}
- {/* Hidden ModelParameters — only for schema-loading side effect (dynamic handles) */} - {nodeData.selectedModel?.modelId && !isVeoModel(nodeData.selectedModel.modelId) && ( + {/* Hidden ModelParameters — only for schema-loading side effect (dynamic handles) when inline disabled */} + {!inlineParametersEnabled && nodeData.selectedModel?.modelId && !isVeoModel(nodeData.selectedModel.modelId) && (