Browse Source

fix: clear stale fallback params on provider switch and make tutorial abort-aware

Reset fallbackParameters when saving a new fallback model provider to
prevent stale parameter values (e.g. temperature=1.5) being sent to a
provider with different valid ranges (e.g. Anthropic max=1).

Wire AbortController signal through mockTutorialExecution so clicking
Stop actually cancels the 5s delay, fetch request, and FileReader,
returning the node to idle state instead of completing silently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 3 months ago
parent
commit
c82c8a1bf2
  1. 2
      src/components/nodes/LLMFallbackPopover.tsx
  2. 44
      src/store/workflowStore.ts

2
src/components/nodes/LLMFallbackPopover.tsx

@ -83,7 +83,7 @@ export function LLMFallbackPopover({ nodeId, onClose }: LLMFallbackPopoverProps)
modelId: model, modelId: model,
displayName: label, displayName: label,
}; };
updateNodeData(nodeId, { fallbackModel }); updateNodeData(nodeId, { fallbackModel, fallbackParameters: {} });
onClose(); onClose();
}; };

44
src/store/workflowStore.ts

@ -1622,11 +1622,14 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
const nanoBananaNode = nodes.find((n) => n.type === "nanoBanana"); const nanoBananaNode = nodes.find((n) => n.type === "nanoBanana");
if (!nanoBananaNode) return; if (!nanoBananaNode) return;
const controller = new AbortController();
const { signal } = controller;
// Set execution state // Set execution state
set({ set({
isRunning: true, isRunning: true,
currentNodeIds: [nanoBananaNode.id], currentNodeIds: [nanoBananaNode.id],
_abortController: new AbortController(), _abortController: controller,
}); });
// Set loading state (triggers edge animations) // Set loading state (triggers edge animations)
@ -1635,18 +1638,36 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
error: null, error: null,
}); });
// Wait 5 seconds (realistic generation time) // Cancellable 5-second delay
await new Promise((resolve) => setTimeout(resolve, 5000)); try {
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(resolve, 5000);
signal.addEventListener("abort", () => {
clearTimeout(timer);
reject(new DOMException("Aborted", "AbortError"));
}, { once: true });
});
} catch {
// Aborted during wait — clean up and exit
updateNodeData(nanoBananaNode.id, { status: "idle", error: null });
set({ isRunning: false, currentNodeIds: [], _abortController: null });
return;
}
// Load the mock output image // Load the mock output image
const mockImageUrl = "/tutorial/owl-aviator.png"; const mockImageUrl = "/tutorial/owl-aviator.png";
try { try {
const response = await fetch(mockImageUrl); const response = await fetch(mockImageUrl, { signal });
const blob = await response.blob(); const blob = await response.blob();
const reader = new FileReader(); const reader = new FileReader();
const base64Image = await new Promise<string>((resolve) => { const base64Image = await new Promise<string>((resolve, reject) => {
signal.addEventListener("abort", () => {
reader.abort();
reject(new DOMException("Aborted", "AbortError"));
}, { once: true });
reader.onloadend = () => resolve(reader.result as string); reader.onloadend = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
}); });
@ -1658,11 +1679,14 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
selectedHistoryIndex: 0, selectedHistoryIndex: 0,
}); });
} catch (error) { } catch (error) {
// Fallback to error state if image not found if (error instanceof DOMException && error.name === "AbortError") {
updateNodeData(nanoBananaNode.id, { updateNodeData(nanoBananaNode.id, { status: "idle", error: null });
status: "error", } else {
error: "Failed to load tutorial image", updateNodeData(nanoBananaNode.id, {
}); status: "error",
error: "Failed to load tutorial image",
});
}
} }
// Clear execution state // Clear execution state

Loading…
Cancel
Save