Browse Source

feat: add fallback settings tab to LLM generate node

Adds SettingsTabBar to LLMGenerateNode with temperature and max tokens
controls for the fallback model. Updates llmGenerateExecutor to use
fallbackParameters override when running the fallback model.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 3 months ago
parent
commit
0da1f84e4e
  1. 181
      src/components/nodes/LLMGenerateNode.tsx
  2. 11
      src/store/execution/llmGenerateExecutor.ts
  3. 1
      src/types/nodes.ts

181
src/components/nodes/LLMGenerateNode.tsx

@ -7,6 +7,7 @@ import { useWorkflowStore } from "@/store/workflowStore";
import { LLMGenerateNodeData, LLMProvider, LLMModelType } from "@/types"; import { LLMGenerateNodeData, LLMProvider, LLMModelType } from "@/types";
import { useInlineParameters } from "@/hooks/useInlineParameters"; import { useInlineParameters } from "@/hooks/useInlineParameters";
import { InlineParameterPanel } from "./InlineParameterPanel"; import { InlineParameterPanel } from "./InlineParameterPanel";
import { SettingsTabBar } from "./SettingsTabBar";
// LLM providers and models // LLM providers and models
const LLM_PROVIDERS: { value: LLMProvider; label: string }[] = [ const LLM_PROVIDERS: { value: LLMProvider; label: string }[] = [
@ -54,6 +55,7 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
}, [id, updateNodeData]); }, [id, updateNodeData]);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [settingsTab, setSettingsTab] = useState<"primary" | "fallback">("primary");
const handleCopyOutput = useCallback(async () => { const handleCopyOutput = useCallback(async () => {
if (nodeData.outputText) { if (nodeData.outputText) {
@ -129,68 +131,131 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps<LLMGenerateNod
onToggle={handleToggleParams} onToggle={handleToggleParams}
nodeId={id} nodeId={id}
> >
{/* LLM-specific controls */} {/* Tab bar for primary/fallback settings */}
<div className="space-y-1.5 max-w-[280px]"> {nodeData.fallbackModel && (
{/* Provider */} <SettingsTabBar
<div className="flex items-center gap-2"> activeTab={settingsTab}
<label className="text-[11px] text-neutral-400 shrink-0">Provider</label> onTabChange={setSettingsTab}
<select primaryLabel={nodeData.model || "Primary"}
value={provider} fallbackLabel={nodeData.fallbackModel.displayName}
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 */} {/* Primary tab: LLM-specific controls */}
<div className="flex items-center gap-2"> {settingsTab === "primary" && (
<label className="text-[11px] text-neutral-400 shrink-0">Model</label> <div className="space-y-1.5 max-w-[280px]">
<select {/* Provider */}
value={nodeData.model || availableModels[0].value} <div className="flex items-center gap-2">
onChange={handleModelChange} <label className="text-[11px] text-neutral-400 shrink-0">Provider</label>
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" <select
> value={provider}
{availableModels.map(m => ( onChange={handleProviderChange}
<option key={m.value} value={m.value}>{m.label}</option> 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"
))} >
</select> {LLM_PROVIDERS.map(p => (
</div> <option key={p.value} value={p.value}>{p.label}</option>
))}
</select>
</div>
{/* Temperature */} {/* Model */}
<div className="flex flex-col gap-0.5"> <div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400"> <label className="text-[11px] text-neutral-400 shrink-0">Model</label>
Temperature: {(nodeData.temperature ?? 0.7).toFixed(2)} <select
</label> value={nodeData.model || availableModels[0].value}
<input onChange={handleModelChange}
type="range" 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"
min="0" >
max={provider === "anthropic" ? "1" : "2"} {availableModels.map(m => (
step="0.01" <option key={m.value} value={m.value}>{m.label}</option>
value={nodeData.temperature ?? 0.7} ))}
onChange={handleTemperatureChange} </select>
className="nodrag nopan w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-blue-500" </div>
/>
</div>
{/* Max Tokens */} {/* Temperature */}
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<label className="text-[11px] text-neutral-400"> <label className="text-[11px] text-neutral-400">
Max Tokens: {(nodeData.maxTokens || 2048).toLocaleString()} Temperature: {(nodeData.temperature ?? 0.7).toFixed(2)}
</label> </label>
<input <input
type="range" type="range"
min="256" min="0"
max="16384" max={provider === "anthropic" ? "1" : "2"}
step="256" step="0.01"
value={nodeData.maxTokens || 2048} value={nodeData.temperature ?? 0.7}
onChange={handleMaxTokensChange} onChange={handleTemperatureChange}
className="nodrag nopan w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-blue-500" 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> </div>
</div> )}
{/* Fallback tab: temperature + maxTokens for fallback model */}
{settingsTab === "fallback" && nodeData.fallbackModel && (() => {
const fbParams = nodeData.fallbackParameters || {};
const fbTemp = (fbParams.temperature as number | undefined) ?? 0.7;
const fbMaxTokens = (fbParams.maxTokens as number | undefined) ?? 2048;
const fbProvider = nodeData.fallbackModel!.provider;
const isAnthropicFb = fbProvider === "anthropic";
return (
<div className="space-y-1.5 max-w-[280px]">
{/* Read-only model display */}
<div className="flex items-center gap-2">
<label className="text-[11px] text-neutral-400 shrink-0">Model</label>
<span className="text-[11px] text-neutral-200 truncate">{nodeData.fallbackModel!.displayName}</span>
</div>
{/* Temperature */}
<div className="flex flex-col gap-0.5">
<label className="text-[11px] text-neutral-400">
Temperature: {fbTemp.toFixed(2)}
</label>
<input
type="range"
min="0"
max={isAnthropicFb ? "1" : "2"}
step="0.01"
value={fbTemp}
onChange={(e) => updateNodeData(id, { fallbackParameters: { ...fbParams, temperature: parseFloat(e.target.value) } })}
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: {fbMaxTokens.toLocaleString()}
</label>
<input
type="range"
min="256"
max="16384"
step="256"
value={fbMaxTokens}
onChange={(e) => updateNodeData(id, { fallbackParameters: { ...fbParams, maxTokens: parseInt(e.target.value, 10) } })}
className="nodrag nopan w-full h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
/>
</div>
</div>
);
})()}
</InlineParameterPanel> </InlineParameterPanel>
) : undefined} ) : undefined}
> >

11
src/store/execution/llmGenerateExecutor.ts

@ -77,11 +77,14 @@ export async function executeLlmGenerate(
error: null, error: null,
}); });
const runOnce = async (modelToUse: SelectedModel): Promise<void> => { const runOnce = async (modelToUse: SelectedModel, parametersOverride?: Record<string, unknown>): Promise<void> => {
const llmProvider = providerTypeToLlmProvider(modelToUse.provider); const llmProvider = providerTypeToLlmProvider(modelToUse.provider);
const llmModel = modelToUse.modelId as LLMModelType; const llmModel = modelToUse.modelId as LLMModelType;
const headers = buildLlmHeaders(llmProvider, providerSettings); const headers = buildLlmHeaders(llmProvider, providerSettings);
const temperature = (parametersOverride?.temperature as number | undefined) ?? nodeData.temperature;
const maxTokens = (parametersOverride?.maxTokens as number | undefined) ?? nodeData.maxTokens;
try { try {
const response = await fetch("/api/llm", { const response = await fetch("/api/llm", {
method: "POST", method: "POST",
@ -91,8 +94,8 @@ export async function executeLlmGenerate(
...(images.length > 0 && { images }), ...(images.length > 0 && { images }),
provider: llmProvider, provider: llmProvider,
model: llmModel, model: llmModel,
temperature: nodeData.temperature, temperature,
maxTokens: nodeData.maxTokens, maxTokens,
}), }),
...(signal ? { signal } : {}), ...(signal ? { signal } : {}),
}); });
@ -165,7 +168,9 @@ export async function executeLlmGenerate(
nodeId: node.id, nodeId: node.id,
primary: primaryModel, primary: primaryModel,
fallback: nodeData.fallbackModel, fallback: nodeData.fallbackModel,
fallbackParameters: nodeData.fallbackParameters,
updateNodeData, updateNodeData,
runOnce, runOnce,
clearOutput: { outputText: null },
}); });
} }

1
src/types/nodes.ts

@ -305,6 +305,7 @@ export interface LLMGenerateNodeData extends BaseNodeData {
model: LLMModelType; model: LLMModelType;
temperature: number; temperature: number;
maxTokens: number; maxTokens: number;
fallbackParameters?: Record<string, unknown>; // Parameters for fallback model (temperature, maxTokens)
parametersExpanded?: boolean; // Collapse state for inline parameter display parametersExpanded?: boolean; // Collapse state for inline parameter display
_settingsPanelHeight?: number; // Measured settings panel height for reload correction _settingsPanelHeight?: number; // Measured settings panel height for reload correction
status: NodeStatus; status: NodeStatus;

Loading…
Cancel
Save