Browse Source

fix(47-02): render settings panel outside bordered div for seamless selection ring

Move InlineParameterPanel from BaseNode children to a settingsPanel prop
rendered as a sibling of the bordered div. This fixes:
- Notch between preview and settings (width mismatch from border inset)
- Selection ring overlap/gap at the junction (clip-path on separate overlay)
- Bottom border and rounding when settings expanded

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
08997aaa4d
  1. 23
      src/components/nodes/BaseNode.tsx
  2. 75
      src/components/nodes/Generate3DNode.tsx
  3. 75
      src/components/nodes/GenerateAudioNode.tsx
  4. 203
      src/components/nodes/GenerateImageNode.tsx
  5. 41
      src/components/nodes/GenerateVideoNode.tsx
  6. 11
      src/components/nodes/InlineParameterPanel.tsx
  7. 145
      src/components/nodes/LLMGenerateNode.tsx

23
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({
/>
<div
className={`
h-full w-full flex flex-col overflow-visible
${fullBleed ? `${settingsExpanded && selected ? "rounded-t-lg" : "rounded-lg"} bg-neutral-800/50 border border-neutral-700/40` : "bg-neutral-800 rounded-lg shadow-lg border"}
h-full w-full flex flex-col overflow-visible relative
${fullBleed
? `${settingsExpanded ? "rounded-t-lg border-b-0" : "rounded-lg"} bg-neutral-800/50 border border-neutral-700/40`
: `bg-neutral-800 ${settingsExpanded ? "rounded-t-lg border-b-0" : "rounded-lg"} shadow-lg border`}
${fullBleed ? "" : (isCurrentlyExecuting || isExecuting ? "border-blue-500 ring-1 ring-blue-500/20" : "border-neutral-700/60")}
${fullBleed ? "" : (hasError ? "border-red-500" : "")}
${fullBleed && selected ? "ring-2 ring-blue-500/40 shadow-lg shadow-blue-500/25" : ""}
${!fullBleed && selected ? "border-blue-500 ring-2 ring-blue-500/40 shadow-lg shadow-blue-500/25" : ""}
${fullBleed && selected && !settingsExpanded ? "ring-2 ring-blue-500/40 shadow-lg shadow-blue-500/25" : ""}
${!fullBleed && selected && !settingsExpanded ? "border-blue-500 ring-2 ring-blue-500/40 shadow-lg shadow-blue-500/25" : ""}
${!fullBleed && selected && settingsExpanded ? "border-blue-500" : ""}
${className}
`}
onMouseEnter={() => 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 && (
<div
className="absolute -inset-px rounded-t-lg ring-2 ring-blue-500/40 shadow-lg shadow-blue-500/25 pointer-events-none"
style={{ clipPath: 'inset(-20px -20px 1px -20px)' }}
/>
)}
<div className={contentClassName ?? (fullBleed ? "flex-1 min-h-0 relative" : "px-3 pb-4 flex-1 min-h-0 overflow-hidden flex flex-col")}>{children}</div>
</div>
{settingsPanel}
</div>
);
}

75
src/components/nodes/Generate3DNode.tsx

@ -124,6 +124,43 @@ export function Generate3DNode({ id, data, selected }: NodeProps<Generate3DNodeT
settingsExpanded={inlineParametersEnabled && isParamsExpanded}
isExecuting={isRunning}
hasError={nodeData.status === "error"}
settingsPanel={inlineParametersEnabled ? (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
selected={selected}
>
{/* Model selector: Browse button + current model display */}
<div className="flex items-center justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="text-[11px] text-neutral-200 truncate">
{displayTitle}
</div>
<div className="text-[9px] text-neutral-500">
{currentProvider}
</div>
</div>
<button
onClick={() => setIsBrowseDialogOpen(true)}
className="nodrag nopan shrink-0 px-2 py-1 text-[10px] bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
>
Browse
</button>
</div>
{/* External provider parameters - reuse ModelParameters component */}
{nodeData.selectedModel?.modelId && (
<ModelParameters
modelId={nodeData.selectedModel.modelId}
provider={currentProvider}
parameters={nodeData.parameters || {}}
onParametersChange={handleParametersChange}
onInputsLoaded={handleInputsLoaded}
/>
)}
</InlineParameterPanel>
) : 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<Generate3DNodeT
/>
)}
{/* Inline parameter panel */}
{inlineParametersEnabled && (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
selected={selected}
>
{/* Model selector: Browse button + current model display */}
<div className="flex items-center justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="text-[11px] text-neutral-200 truncate">
{displayTitle}
</div>
<div className="text-[9px] text-neutral-500">
{currentProvider}
</div>
</div>
<button
onClick={() => setIsBrowseDialogOpen(true)}
className="nodrag nopan shrink-0 px-2 py-1 text-[10px] bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
>
Browse
</button>
</div>
{/* External provider parameters - reuse ModelParameters component */}
{nodeData.selectedModel?.modelId && (
<ModelParameters
modelId={nodeData.selectedModel.modelId}
provider={currentProvider}
parameters={nodeData.parameters || {}}
onParametersChange={handleParametersChange}
onInputsLoaded={handleInputsLoaded}
/>
)}
</InlineParameterPanel>
)}
</div>
</BaseNode>

75
src/components/nodes/GenerateAudioNode.tsx

@ -237,6 +237,43 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps<GenerateAudi
hasError={nodeData.status === "error"}
minWidth={300}
minHeight={250}
settingsPanel={inlineParametersEnabled ? (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
selected={selected}
>
{/* Model selector: Browse button + current model display */}
<div className="flex items-center justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="text-[11px] text-neutral-200 truncate">
{displayTitle}
</div>
<div className="text-[9px] text-neutral-500">
{currentProvider}
</div>
</div>
<button
onClick={() => setIsBrowseDialogOpen(true)}
className="nodrag nopan shrink-0 px-2 py-1 text-[10px] bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
>
Browse
</button>
</div>
{/* External provider parameters - reuse ModelParameters component */}
{nodeData.selectedModel?.modelId && (
<ModelParameters
modelId={nodeData.selectedModel.modelId}
provider={currentProvider}
parameters={nodeData.parameters || {}}
onParametersChange={handleParametersChange}
onInputsLoaded={handleInputsLoaded}
/>
)}
</InlineParameterPanel>
) : 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<GenerateAudi
style={{ background: "rgb(167, 139, 250)" }}
/>
{/* Inline parameter panel */}
{inlineParametersEnabled && (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
selected={selected}
>
{/* Model selector: Browse button + current model display */}
<div className="flex items-center justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="text-[11px] text-neutral-200 truncate">
{displayTitle}
</div>
<div className="text-[9px] text-neutral-500">
{currentProvider}
</div>
</div>
<button
onClick={() => setIsBrowseDialogOpen(true)}
className="nodrag nopan shrink-0 px-2 py-1 text-[10px] bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
>
Browse
</button>
</div>
{/* External provider parameters - reuse ModelParameters component */}
{nodeData.selectedModel?.modelId && (
<ModelParameters
modelId={nodeData.selectedModel.modelId}
provider={currentProvider}
parameters={nodeData.parameters || {}}
onParametersChange={handleParametersChange}
onInputsLoaded={handleInputsLoaded}
/>
)}
</InlineParameterPanel>
)}
</BaseNode>
{/* Browse dialog */}

203
src/components/nodes/GenerateImageNode.tsx

@ -538,6 +538,106 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
fullBleed
settingsExpanded={inlineParametersEnabled && isParamsExpanded}
aspectFitMedia={nodeData.outputImage}
settingsPanel={inlineParametersEnabled ? (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
selected={selected}
>
{/* Gemini-specific controls */}
{isGeminiProvider && currentModelId && (
<div className="space-y-1.5">
{/* Model selector */}
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Model</label>
<select
value={currentModelId}
onChange={handleModelChange}
className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{GEMINI_IMAGE_MODELS.map((m) => (
<option key={m.value} value={m.value}>
{m.label}
</option>
))}
</select>
</div>
{/* Aspect Ratio */}
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Aspect Ratio</label>
<select
value={nodeData.aspectRatio || "1:1"}
onChange={handleAspectRatioChange}
className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{aspectRatios.map((ratio) => (
<option key={ratio} value={ratio}>
{ratio}
</option>
))}
</select>
</div>
{/* Resolution (if supported) */}
{supportsResolution && (
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Resolution</label>
<select
value={nodeData.resolution || "2K"}
onChange={handleResolutionChange}
className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{resolutions.map((res) => (
<option key={res} value={res}>
{res}
</option>
))}
</select>
</div>
)}
{/* Google Search toggle */}
{(currentModelId === "nano-banana-pro" || currentModelId === "nano-banana-2") && (
<label className="flex items-center gap-1.5 text-[11px] text-neutral-300 cursor-pointer">
<input
type="checkbox"
checked={nodeData.useGoogleSearch || false}
onChange={handleGoogleSearchToggle}
className="nodrag nopan w-3 h-3 rounded bg-[#1a1a1a] text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0"
/>
Google Search
</label>
)}
{/* Image Search toggle (NB2 only) */}
{currentModelId === "nano-banana-2" && (
<label className="flex items-center gap-1.5 text-[11px] text-neutral-300 cursor-pointer">
<input
type="checkbox"
checked={nodeData.useImageSearch || false}
onChange={handleImageSearchToggle}
className="nodrag nopan w-3 h-3 rounded bg-[#1a1a1a] text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0"
/>
Image Search
</label>
)}
</div>
)}
{/* External provider parameters - reuse ModelParameters component */}
{!isGeminiProvider && nodeData.selectedModel?.modelId && (
<ModelParameters
modelId={nodeData.selectedModel.modelId}
provider={currentProvider}
parameters={nodeData.parameters || {}}
onParametersChange={handleParametersChange}
onInputsLoaded={handleInputsLoaded}
/>
)}
</InlineParameterPanel>
) : 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<NanoBananaNo
Image
</div>
<div className={`relative w-full h-full min-h-0 overflow-hidden ${inlineParametersEnabled && isParamsExpanded ? "rounded-t-lg" : "rounded-lg"}`}>
<div className="relative w-full h-full min-h-0 overflow-hidden rounded-lg">
{/* Preview area */}
{nodeData.outputImage ? (
<>
@ -752,107 +852,6 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
)}
</div>
{/* Inline parameter panel */}
{inlineParametersEnabled && (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
selected={selected}
>
{/* Gemini-specific controls */}
{isGeminiProvider && currentModelId && (
<div className="space-y-1.5">
{/* Model selector */}
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Model</label>
<select
value={currentModelId}
onChange={handleModelChange}
className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{GEMINI_IMAGE_MODELS.map((m) => (
<option key={m.value} value={m.value}>
{m.label}
</option>
))}
</select>
</div>
{/* Aspect Ratio */}
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Aspect Ratio</label>
<select
value={nodeData.aspectRatio || "1:1"}
onChange={handleAspectRatioChange}
className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{aspectRatios.map((ratio) => (
<option key={ratio} value={ratio}>
{ratio}
</option>
))}
</select>
</div>
{/* Resolution (if supported) */}
{supportsResolution && (
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Resolution</label>
<select
value={nodeData.resolution || "2K"}
onChange={handleResolutionChange}
className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{resolutions.map((res) => (
<option key={res} value={res}>
{res}
</option>
))}
</select>
</div>
)}
{/* Google Search toggle */}
{(currentModelId === "nano-banana-pro" || currentModelId === "nano-banana-2") && (
<label className="flex items-center gap-1.5 text-[11px] text-neutral-300 cursor-pointer">
<input
type="checkbox"
checked={nodeData.useGoogleSearch || false}
onChange={handleGoogleSearchToggle}
className="nodrag nopan w-3 h-3 rounded bg-[#1a1a1a] text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0"
/>
Google Search
</label>
)}
{/* Image Search toggle (NB2 only) */}
{currentModelId === "nano-banana-2" && (
<label className="flex items-center gap-1.5 text-[11px] text-neutral-300 cursor-pointer">
<input
type="checkbox"
checked={nodeData.useImageSearch || false}
onChange={handleImageSearchToggle}
className="nodrag nopan w-3 h-3 rounded bg-[#1a1a1a] text-neutral-600 focus:ring-1 focus:ring-neutral-600 focus:ring-offset-0"
/>
Image Search
</label>
)}
</div>
)}
{/* External provider parameters - reuse ModelParameters component */}
{!isGeminiProvider && nodeData.selectedModel?.modelId && (
<ModelParameters
modelId={nodeData.selectedModel.modelId}
provider={currentProvider}
parameters={nodeData.parameters || {}}
onParametersChange={handleParametersChange}
onInputsLoaded={handleInputsLoaded}
/>
)}
</InlineParameterPanel>
)}
</BaseNode>
{/* Model browse dialog */}

41
src/components/nodes/GenerateVideoNode.tsx

@ -446,6 +446,25 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
fullBleed
settingsExpanded={inlineParametersEnabled && isParamsExpanded}
aspectFitMedia={nodeData.outputVideo}
settingsPanel={inlineParametersEnabled ? (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
selected={selected}
>
{/* External provider parameters - reuse ModelParameters component */}
{nodeData.selectedModel?.modelId && !isVeoModel(nodeData.selectedModel.modelId) && (
<ModelParameters
modelId={nodeData.selectedModel.modelId}
provider={currentProvider}
parameters={nodeData.parameters || {}}
onParametersChange={handleParametersChange}
onInputsLoaded={handleInputsLoaded}
/>
)}
</InlineParameterPanel>
) : 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<GenerateVide
Video
</div>
<div className={`relative w-full h-full min-h-0 overflow-hidden ${inlineParametersEnabled && isParamsExpanded ? "rounded-t-lg" : "rounded-lg"}`}>
<div className="relative w-full h-full min-h-0 overflow-hidden rounded-lg">
{/* Preview area */}
{nodeData.outputVideo ? (
<>
@ -815,26 +834,6 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
)}
</div>
{/* Inline parameter panel */}
{inlineParametersEnabled && (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
selected={selected}
>
{/* External provider parameters - reuse ModelParameters component */}
{nodeData.selectedModel?.modelId && !isVeoModel(nodeData.selectedModel.modelId) && (
<ModelParameters
modelId={nodeData.selectedModel.modelId}
provider={currentProvider}
parameters={nodeData.parameters || {}}
onParametersChange={handleParametersChange}
onInputsLoaded={handleInputsLoaded}
/>
)}
</InlineParameterPanel>
)}
</BaseNode>
{/* Hidden ModelParameters — only for schema-loading side effect (dynamic handles) when inline disabled */}

11
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 (
<div className={`w-full ${ringClass}`}>
<div className={`w-full ${ringClass}`} style={ringStyle}>
{/* Settings toggle button — no background when collapsed, floats below node edge */}
<button
type="button"

145
src/components/nodes/LLMGenerateNode.tsx

@ -126,6 +126,77 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
isExecuting={isRunning}
fullBleed
settingsExpanded={inlineParametersEnabled && isParamsExpanded}
settingsPanel={inlineParametersEnabled ? (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
selected={selected}
>
{/* LLM-specific controls */}
<div className="space-y-1.5">
{/* Provider */}
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Provider</label>
<select
value={provider}
onChange={handleProviderChange}
className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{LLM_PROVIDERS.map(p => (
<option key={p.value} value={p.value}>{p.label}</option>
))}
</select>
</div>
{/* Model */}
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Model</label>
<select
value={nodeData.model || availableModels[0].value}
onChange={handleModelChange}
className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{availableModels.map(m => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</select>
</div>
{/* Temperature */}
<div className="flex flex-col gap-0.5">
<label className="text-[11px] text-neutral-400">
Temperature: {(nodeData.temperature ?? 0.7).toFixed(2)}
</label>
<input
type="range"
min="0"
max={provider === "anthropic" ? "1" : "2"}
step="0.01"
value={nodeData.temperature ?? 0.7}
onChange={handleTemperatureChange}
className="nodrag nopan w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
/>
</div>
{/* Max Tokens */}
<div className="flex flex-col gap-0.5">
<label className="text-[11px] text-neutral-400">
Max Tokens: {(nodeData.maxTokens || 2048).toLocaleString()}
</label>
<input
type="range"
min="256"
max="16384"
step="256"
value={nodeData.maxTokens || 2048}
onChange={handleMaxTokensChange}
className="nodrag nopan w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
/>
</div>
</div>
</InlineParameterPanel>
) : undefined}
>
{/* Image input - optional */}
<Handle
@ -151,7 +222,7 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
data-handletype="text"
/>
<div className={`relative w-full h-full min-h-0 overflow-hidden ${inlineParametersEnabled && isParamsExpanded ? "rounded-t-lg" : "rounded-lg"}`}>
<div className="relative w-full h-full min-h-0 overflow-hidden rounded-lg">
{nodeData.status === "loading" ? (
<div className="w-full h-full bg-neutral-900/40 flex items-center justify-center">
<svg
@ -238,78 +309,6 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
)}
</div>
{/* Inline parameter panel */}
{inlineParametersEnabled && (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
selected={selected}
>
{/* LLM-specific controls */}
<div className="space-y-1.5">
{/* Provider */}
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Provider</label>
<select
value={provider}
onChange={handleProviderChange}
className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{LLM_PROVIDERS.map(p => (
<option key={p.value} value={p.value}>{p.label}</option>
))}
</select>
</div>
{/* Model */}
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Model</label>
<select
value={nodeData.model || availableModels[0].value}
onChange={handleModelChange}
className="nodrag nopan flex-1 min-w-0 text-[11px] py-1 px-2 bg-[#1a1a1a] rounded-md focus:outline-none focus:ring-1 focus:ring-neutral-600 text-white"
>
{availableModels.map(m => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</select>
</div>
{/* Temperature */}
<div className="flex flex-col gap-0.5">
<label className="text-[11px] text-neutral-400">
Temperature: {(nodeData.temperature ?? 0.7).toFixed(2)}
</label>
<input
type="range"
min="0"
max={provider === "anthropic" ? "1" : "2"}
step="0.01"
value={nodeData.temperature ?? 0.7}
onChange={handleTemperatureChange}
className="nodrag nopan w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
/>
</div>
{/* Max Tokens */}
<div className="flex flex-col gap-0.5">
<label className="text-[11px] text-neutral-400">
Max Tokens: {(nodeData.maxTokens || 2048).toLocaleString()}
</label>
<input
type="range"
min="256"
max="16384"
step="256"
value={nodeData.maxTokens || 2048}
onChange={handleMaxTokensChange}
className="nodrag nopan w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
/>
</div>
</div>
</InlineParameterPanel>
)}
</BaseNode>
);
}

Loading…
Cancel
Save