diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index e591b914..af8abd49 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -23,6 +23,8 @@ interface BaseNodeProps { aspectFitMedia?: string | null; /** When true, bottom corners lose rounding so the selection ring connects to the settings panel below */ settingsExpanded?: boolean; + /** Settings panel rendered outside the bordered area so it shares the node's full width */ + settingsPanel?: ReactNode; } /** @@ -65,6 +67,7 @@ export function BaseNode({ fullBleed = false, aspectFitMedia, settingsExpanded = false, + settingsPanel, }: BaseNodeProps) { const currentNodeIds = useWorkflowStore((state) => state.currentNodeIds); const setHoveredNodeId = useWorkflowStore((state) => state.setHoveredNodeId); @@ -129,19 +132,31 @@ export function BaseNode({ />
setHoveredNodeId(id)} onMouseLeave={() => setHoveredNodeId(null)} > + {/* When settings panel is expanded, render the selection ring as a separate clipped overlay + so it doesn't show a line at the bottom — the InlineParameterPanel draws the bottom half */} + {settingsExpanded && selected && ( +
+ )}
{children}
+ {settingsPanel}
); } diff --git a/src/components/nodes/Generate3DNode.tsx b/src/components/nodes/Generate3DNode.tsx index d61f258b..8cba4a51 100644 --- a/src/components/nodes/Generate3DNode.tsx +++ b/src/components/nodes/Generate3DNode.tsx @@ -124,6 +124,43 @@ export function Generate3DNode({ id, data, selected }: NodeProps + {/* Model selector: Browse button + current model display */} +
+
+
+ {displayTitle} +
+
+ {currentProvider} +
+
+ +
+ + {/* External provider parameters - reuse ModelParameters component */} + {nodeData.selectedModel?.modelId && ( + + )} + + ) : undefined} > {/* Dynamic input handles based on model schema */} {nodeData.inputSchema && nodeData.inputSchema.length > 0 ? ( @@ -425,44 +462,6 @@ export function Generate3DNode({ id, data, selected }: NodeProps )} - {/* Inline parameter panel */} - {inlineParametersEnabled && ( - - {/* Model selector: Browse button + current model display */} -
-
-
- {displayTitle} -
-
- {currentProvider} -
-
- -
- - {/* External provider parameters - reuse ModelParameters component */} - {nodeData.selectedModel?.modelId && ( - - )} -
- )} diff --git a/src/components/nodes/GenerateAudioNode.tsx b/src/components/nodes/GenerateAudioNode.tsx index 80e7f05d..b353766b 100644 --- a/src/components/nodes/GenerateAudioNode.tsx +++ b/src/components/nodes/GenerateAudioNode.tsx @@ -237,6 +237,43 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps + {/* Model selector: Browse button + current model display */} +
+
+
+ {displayTitle} +
+
+ {currentProvider} +
+
+ +
+ + {/* External provider parameters - reuse ModelParameters component */} + {nodeData.selectedModel?.modelId && ( + + )} + + ) : undefined} > {/* Model parameters (hidden when inline enabled - shown in panel below) */} {!inlineParametersEnabled && nodeData.selectedModel?.modelId && ( @@ -385,44 +422,6 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps - {/* Inline parameter panel */} - {inlineParametersEnabled && ( - - {/* Model selector: Browse button + current model display */} -
-
-
- {displayTitle} -
-
- {currentProvider} -
-
- -
- - {/* External provider parameters - reuse ModelParameters component */} - {nodeData.selectedModel?.modelId && ( - - )} -
- )} {/* Browse dialog */} diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index aac513a0..f8e922db 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -538,6 +538,106 @@ export function GenerateImageNode({ id, data, selected }: NodeProps + {/* Gemini-specific controls */} + {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 && ( + + )} + + ) : undefined} > {/* Input handles - ALWAYS use same IDs and positions for connection stability */} {/* Image input at 35%, Text input at 65% - never changes regardless of model */} @@ -602,7 +702,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps -
+
{/* Preview area */} {nodeData.outputImage ? ( <> @@ -752,107 +852,6 @@ export function GenerateImageNode({ id, data, selected }: NodeProps - {/* Inline parameter panel */} - {inlineParametersEnabled && ( - - {/* Gemini-specific controls */} - {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 */} diff --git a/src/components/nodes/GenerateVideoNode.tsx b/src/components/nodes/GenerateVideoNode.tsx index 6aeedca7..1c4c70e4 100644 --- a/src/components/nodes/GenerateVideoNode.tsx +++ b/src/components/nodes/GenerateVideoNode.tsx @@ -446,6 +446,25 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps + {/* External provider parameters - reuse ModelParameters component */} + {nodeData.selectedModel?.modelId && !isVeoModel(nodeData.selectedModel.modelId) && ( + + )} + + ) : undefined} > {/* Dynamic input handles based on model schema */} {nodeData.inputSchema && nodeData.inputSchema.length > 0 ? ( @@ -660,7 +679,7 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps -
+
{/* Preview area */} {nodeData.outputVideo ? ( <> @@ -815,26 +834,6 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps - {/* Inline parameter panel */} - {inlineParametersEnabled && ( - - {/* External provider parameters - reuse ModelParameters component */} - {nodeData.selectedModel?.modelId && !isVeoModel(nodeData.selectedModel.modelId) && ( - - )} - - )} {/* Hidden ModelParameters — only for schema-loading side effect (dynamic handles) when inline disabled */} diff --git a/src/components/nodes/InlineParameterPanel.tsx b/src/components/nodes/InlineParameterPanel.tsx index fc2e3b3d..1f167612 100644 --- a/src/components/nodes/InlineParameterPanel.tsx +++ b/src/components/nodes/InlineParameterPanel.tsx @@ -21,14 +21,17 @@ export function InlineParameterPanel({ nodeId, selected = false, }: InlineParameterPanelProps) { - // When expanded + selected, overlap with BaseNode ring using negative margin - // so the two rings merge into one continuous outline + // When expanded + selected, draw ring on bottom half and clip the top edge + // so it merges seamlessly with BaseNode's ring (which clips its bottom edge) const ringClass = expanded && selected - ? "ring-2 ring-blue-500/40 rounded-b-lg -mt-[3px] pt-[3px]" + ? "ring-2 ring-blue-500/40 rounded-b-lg" : ""; + const ringStyle = expanded && selected + ? { clipPath: 'inset(0 -20px -20px -20px)' } + : undefined; return ( -
+
{/* Settings toggle button — no background when collapsed, floats below node edge */}