From c82c8a1bf2931e4e406ce89651288a84de298284 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 15 Apr 2026 14:32:22 +1200 Subject: [PATCH] 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 --- src/components/nodes/LLMFallbackPopover.tsx | 2 +- src/store/workflowStore.ts | 44 ++++++++++++++++----- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/components/nodes/LLMFallbackPopover.tsx b/src/components/nodes/LLMFallbackPopover.tsx index 66cb1bbf..13646457 100644 --- a/src/components/nodes/LLMFallbackPopover.tsx +++ b/src/components/nodes/LLMFallbackPopover.tsx @@ -83,7 +83,7 @@ export function LLMFallbackPopover({ nodeId, onClose }: LLMFallbackPopoverProps) modelId: model, displayName: label, }; - updateNodeData(nodeId, { fallbackModel }); + updateNodeData(nodeId, { fallbackModel, fallbackParameters: {} }); onClose(); }; diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 63f00de4..bd0277ae 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -1622,11 +1622,14 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ const nanoBananaNode = nodes.find((n) => n.type === "nanoBanana"); if (!nanoBananaNode) return; + const controller = new AbortController(); + const { signal } = controller; + // Set execution state set({ isRunning: true, currentNodeIds: [nanoBananaNode.id], - _abortController: new AbortController(), + _abortController: controller, }); // Set loading state (triggers edge animations) @@ -1635,18 +1638,36 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ error: null, }); - // Wait 5 seconds (realistic generation time) - await new Promise((resolve) => setTimeout(resolve, 5000)); + // Cancellable 5-second delay + try { + await new Promise((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 const mockImageUrl = "/tutorial/owl-aviator.png"; try { - const response = await fetch(mockImageUrl); + const response = await fetch(mockImageUrl, { signal }); const blob = await response.blob(); const reader = new FileReader(); - const base64Image = await new Promise((resolve) => { + const base64Image = await new Promise((resolve, reject) => { + signal.addEventListener("abort", () => { + reader.abort(); + reject(new DOMException("Aborted", "AbortError")); + }, { once: true }); reader.onloadend = () => resolve(reader.result as string); + reader.onerror = () => reject(reader.error); reader.readAsDataURL(blob); }); @@ -1658,11 +1679,14 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ selectedHistoryIndex: 0, }); } catch (error) { - // Fallback to error state if image not found - updateNodeData(nanoBananaNode.id, { - status: "error", - error: "Failed to load tutorial image", - }); + if (error instanceof DOMException && error.name === "AbortError") { + updateNodeData(nanoBananaNode.id, { status: "idle", error: null }); + } else { + updateNodeData(nanoBananaNode.id, { + status: "error", + error: "Failed to load tutorial image", + }); + } } // Clear execution state