Browse Source

feat(47-02): wire inline parameters into LLMGenerateNode, GenerateAudioNode, and Generate3DNode

- Import useInlineParameters hook and InlineParameterPanel component in all 3 nodes
- Add LLM_PROVIDERS and LLM_MODELS constants to LLMGenerateNode
- Add collapse state computation and toggle handlers for all 3 nodes
- LLMGenerateNode: render inline controls for provider, model, temperature, max tokens with compact styling
- GenerateAudioNode: render model selector and ModelParameters inline
- Generate3DNode: render model selector and ModelParameters inline
- Hide top ModelParameters when inline enabled (prevents duplication)
- All nodes use unified compact styling matching GenerateImageNode Gemini controls
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
5929c34fad
  1. 57
      src/components/nodes/Generate3DNode.tsx
  2. 57
      src/components/nodes/GenerateAudioNode.tsx
  3. 155
      src/components/nodes/LLMGenerateNode.tsx

57
src/components/nodes/Generate3DNode.tsx

@ -11,6 +11,8 @@ import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog";
import { useToast } from "@/components/Toast";
import { ProviderBadge } from "./ProviderBadge";
import { getModelPageUrl, getProviderDisplayName } from "@/utils/providerUrls";
import { useInlineParameters } from "@/hooks/useInlineParameters";
import { InlineParameterPanel } from "./InlineParameterPanel";
// 3D generation capabilities
const THREE_D_CAPABILITIES: ModelCapability[] = ["text-to-3d", "image-to-3d"];
@ -23,6 +25,9 @@ export function Generate3DNode({ id, data, selected }: NodeProps<Generate3DNodeT
const { replicateApiKey, falApiKey, kieApiKey } = useProviderApiKeys();
const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false);
// Inline parameters infrastructure
const { inlineParametersEnabled } = useInlineParameters();
// Get the current selected provider (default to fal since most 3D models are there)
const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal";
@ -86,6 +91,16 @@ export function Generate3DNode({ id, data, selected }: NodeProps<Generate3DNodeT
return "Select 3D model...";
}, [nodeData.selectedModel?.displayName, nodeData.selectedModel?.modelId]);
// Inline parameters: compute collapse state and toggle handler
const isParamsExpanded = nodeData.parametersExpanded ?? true; // default expanded
const handleToggleParams = useCallback(() => {
const nodes = useWorkflowStore.getState().nodes;
const node = nodes.find(n => n.id === id);
const currentExpanded = (node?.data as Generate3DNodeData)?.parametersExpanded ?? true;
updateNodeData(id, { parametersExpanded: !currentExpanded });
}, [id, updateNodeData]);
// Track previous status to detect error transitions
const prevStatusRef = useRef(nodeData.status);
@ -397,8 +412,8 @@ export function Generate3DNode({ id, data, selected }: NodeProps<Generate3DNodeT
</div>
)}
{/* Model-specific parameters */}
{nodeData.selectedModel?.modelId && (
{/* Model-specific parameters (hidden when inline enabled - shown in panel below) */}
{!inlineParametersEnabled && nodeData.selectedModel?.modelId && (
<ModelParameters
modelId={nodeData.selectedModel.modelId}
provider={currentProvider}
@ -408,6 +423,44 @@ export function Generate3DNode({ id, data, selected }: NodeProps<Generate3DNodeT
onInputsLoaded={handleInputsLoaded}
/>
)}
{/* Inline parameter panel */}
{inlineParametersEnabled && (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
>
{/* 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>

57
src/components/nodes/GenerateAudioNode.tsx

@ -11,6 +11,8 @@ import { ProviderModel } from "@/lib/providers/types";
import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog";
import { useAudioVisualization } from "@/hooks/useAudioVisualization";
import { useAudioPlayback } from "@/hooks/useAudioPlayback";
import { useInlineParameters } from "@/hooks/useInlineParameters";
import { InlineParameterPanel } from "./InlineParameterPanel";
type GenerateAudioNodeType = Node<GenerateAudioNodeData, "generateAudio">;
@ -21,6 +23,9 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps<GenerateAudi
const [isBrowseDialogOpen, setIsBrowseDialogOpen] = useState(false);
const [isLoadingCarouselAudio, setIsLoadingCarouselAudio] = useState(false);
// Inline parameters infrastructure
const { inlineParametersEnabled } = useInlineParameters();
// Get the current selected provider (default to fal)
const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal";
@ -189,6 +194,16 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps<GenerateAudi
return "Generate Audio";
}, [nodeData.selectedModel?.displayName, nodeData.selectedModel?.modelId]);
// Inline parameters: compute collapse state and toggle handler
const isParamsExpanded = nodeData.parametersExpanded ?? true; // default expanded
const handleToggleParams = useCallback(() => {
const nodes = useWorkflowStore.getState().nodes;
const node = nodes.find(n => n.id === id);
const currentExpanded = (node?.data as GenerateAudioNodeData)?.parametersExpanded ?? true;
updateNodeData(id, { parametersExpanded: !currentExpanded });
}, [id, updateNodeData]);
// Dynamic handles based on inputSchema
const dynamicHandles = useMemo(() => {
if (!nodeData.inputSchema || nodeData.inputSchema.length === 0) return null;
@ -222,8 +237,8 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps<GenerateAudi
minWidth={300}
minHeight={250}
>
{/* Model parameters */}
{nodeData.selectedModel?.modelId && (
{/* Model parameters (hidden when inline enabled - shown in panel below) */}
{!inlineParametersEnabled && nodeData.selectedModel?.modelId && (
<ModelParameters
provider={currentProvider}
modelId={nodeData.selectedModel.modelId}
@ -368,6 +383,44 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps<GenerateAudi
data-handletype="audio"
style={{ background: "rgb(167, 139, 250)" }}
/>
{/* Inline parameter panel */}
{inlineParametersEnabled && (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
>
{/* 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 */}

155
src/components/nodes/LLMGenerateNode.tsx

@ -4,7 +4,34 @@ import { useCallback, useState } from "react";
import { Handle, Position, NodeProps, Node } from "@xyflow/react";
import { BaseNode } from "./BaseNode";
import { useWorkflowStore } from "@/store/workflowStore";
import { LLMGenerateNodeData } from "@/types";
import { LLMGenerateNodeData, LLMProvider, LLMModelType } from "@/types";
import { useInlineParameters } from "@/hooks/useInlineParameters";
import { InlineParameterPanel } from "./InlineParameterPanel";
// LLM providers and models
const LLM_PROVIDERS: { value: LLMProvider; label: string }[] = [
{ value: "google", label: "Google" },
{ value: "openai", label: "OpenAI" },
{ value: "anthropic", label: "Anthropic" },
];
const LLM_MODELS: Record<LLMProvider, { value: LLMModelType; label: string }[]> = {
google: [
{ value: "gemini-3-flash-preview", label: "Gemini 3 Flash" },
{ value: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
{ value: "gemini-3-pro-preview", label: "Gemini 3.0 Pro" },
{ value: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro" },
],
openai: [
{ value: "gpt-4.1-mini", label: "GPT-4.1 Mini" },
{ value: "gpt-4.1-nano", label: "GPT-4.1 Nano" },
],
anthropic: [
{ value: "claude-sonnet-4.5", label: "Claude Sonnet 4.5" },
{ value: "claude-haiku-4.5", label: "Claude Haiku 4.5" },
{ value: "claude-opus-4.6", label: "Claude Opus 4.6" },
],
};
type LLMGenerateNodeType = Node<LLMGenerateNodeData, "llmGenerate">;
@ -15,6 +42,9 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
const regenerateNode = useWorkflowStore((state) => state.regenerateNode);
const isRunning = useWorkflowStore((state) => state.isRunning);
// Inline parameters infrastructure
const { inlineParametersEnabled } = useInlineParameters();
const handleRegenerate = useCallback(() => {
regenerateNode(id);
}, [id, regenerateNode]);
@ -37,6 +67,57 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
}
}, [nodeData.outputText]);
// Inline parameters: compute collapse state and toggle handler
const isParamsExpanded = nodeData.parametersExpanded ?? true; // default expanded
const handleToggleParams = useCallback(() => {
const nodes = useWorkflowStore.getState().nodes;
const node = nodes.find(n => n.id === id);
const currentExpanded = (node?.data as LLMGenerateNodeData)?.parametersExpanded ?? true;
updateNodeData(id, { parametersExpanded: !currentExpanded });
}, [id, updateNodeData]);
// LLM parameter handlers
const handleProviderChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const newProvider = e.target.value as LLMProvider;
const firstModelForProvider = LLM_MODELS[newProvider][0].value;
const updates: Partial<LLMGenerateNodeData> = {
provider: newProvider,
model: firstModelForProvider,
};
if (newProvider === "anthropic" && (nodeData.temperature || 0.7) > 1) {
updates.temperature = 1;
}
updateNodeData(id, updates);
},
[id, nodeData.temperature, updateNodeData]
);
const handleModelChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
updateNodeData(id, { model: e.target.value as LLMModelType });
},
[id, updateNodeData]
);
const handleTemperatureChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
updateNodeData(id, { temperature: parseFloat(e.target.value) });
},
[id, updateNodeData]
);
const handleMaxTokensChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
updateNodeData(id, { maxTokens: parseInt(e.target.value, 10) });
},
[id, updateNodeData]
);
const provider = nodeData.provider || "google";
const availableModels = LLM_MODELS[provider] || LLM_MODELS.google;
return (
<BaseNode
id={id}
@ -155,6 +236,78 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
</div>
)}
</div>
{/* Inline parameter panel */}
{inlineParametersEnabled && (
<InlineParameterPanel
expanded={isParamsExpanded}
onToggle={handleToggleParams}
nodeId={id}
>
{/* LLM-specific controls with compact styling */}
<div className="space-y-1.5">
{/* Provider */}
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide">Provider</label>
<select
value={provider}
onChange={handleProviderChange}
className="nodrag nopan w-full text-[10px] py-0.5 px-1.5 bg-neutral-800/50 border border-neutral-700 rounded focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300"
>
{LLM_PROVIDERS.map(p => (
<option key={p.value} value={p.value}>{p.label}</option>
))}
</select>
</div>
{/* Model */}
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-neutral-400 uppercase tracking-wide">Model</label>
<select
value={nodeData.model || availableModels[0].value}
onChange={handleModelChange}
className="nodrag nopan w-full text-[10px] py-0.5 px-1.5 bg-neutral-800/50 border border-neutral-700 rounded focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300"
>
{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-[9px] text-neutral-400 uppercase tracking-wide">
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-[9px] text-neutral-400 uppercase tracking-wide">
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