From 89186f8be1bcf29f233e691caa45176785af4a99 Mon Sep 17 00:00:00 2001 From: AJ Date: Sun, 14 Dec 2025 20:40:38 -0800 Subject: [PATCH 1/6] fix: Start Over button now properly resets state to allow new generations Fixes #55 The Start Over button previously only navigated to step 1 without clearing the generated results. This caused the old results to persist when users went back through the steps, preventing them from generating new thumbnails. Changes: - Added handleStartOver function that clears results, labeled results, suggestions, blob URLs, and errors before navigating to step 1 - Updated Start Over button to use handleStartOver instead of just goTo(1) --- app/thumbnails/page.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/thumbnails/page.tsx b/app/thumbnails/page.tsx index ec8b6ed..c30c98b 100644 --- a/app/thumbnails/page.tsx +++ b/app/thumbnails/page.tsx @@ -849,6 +849,17 @@ export default function Home() { setBlobUrls([]); }; + // Start over: clear results and go back to step 1 + const handleStartOver = () => { + cleanupBlobUrls(); + setResults([]); + setLabeledResults([]); + setSuggestedRefinements({}); + setLoadingSuggestions({}); + setError(null); + goTo(1); + }; + const generate = async () => { // Get all valid template IDs (custom + curated) const allValidTemplateIds = new Set([ @@ -2125,7 +2136,7 @@ export default function Home() { {/* Compact nav */}
- +
)} From e56b852ef909edb0f1f2527b446f741a1700e600 Mon Sep 17 00:00:00 2001 From: AJ Date: Sun, 14 Dec 2025 20:47:21 -0800 Subject: [PATCH 2/6] fix: add cancellation mechanism for in-flight requests on Start Over - Add AbortController ref to cancel pending async operations - Update handleStartOver to abort in-flight requests before clearing state - Pass abort signal to /api/generate fetch calls - Pass abort signal to suggested refinements fetch calls - Handle AbortError gracefully without showing error messages - Also reset loading state when starting over Addresses PR review comment about race conditions when Start Over is clicked while generation or suggestion fetches are still in progress. --- app/thumbnails/page.tsx | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/app/thumbnails/page.tsx b/app/thumbnails/page.tsx index c30c98b..4d17dc2 100644 --- a/app/thumbnails/page.tsx +++ b/app/thumbnails/page.tsx @@ -46,6 +46,8 @@ const thumbnailFaqItems: { question: string; answer: string }[] = [ export default function Home() { const videoRef = useRef(null); const canvasRef = useRef(null); + // AbortController for cancelling in-flight generation and suggestion requests + const abortControllerRef = useRef(null); const [videoUrl, setVideoUrl] = useState(null); const [videoReady, setVideoReady] = useState(false); @@ -145,11 +147,17 @@ export default function Home() { // Fetch suggested refinements when results are generated useEffect(() => { if (results.length > 0 && !refinementState.isRefinementMode) { + // Capture current abort signal to use in async callbacks + const currentAbortSignal = abortControllerRef.current?.signal; + // Fetch suggestions for each result results.forEach((thumbnailUrl, index) => { if (thumbnailUrl && !suggestedRefinements[index] && !loadingSuggestions[index]) { // Call the fetch function inline to avoid dependency issues const fetchSuggestions = async () => { + // Check if aborted before starting + if (currentAbortSignal?.aborted) return; + setLoadingSuggestions(prev => ({ ...prev, [index]: true })); try { @@ -172,10 +180,14 @@ export default function Home() { originalPrompt, templateId: selectedIds[0] || "default", }), + signal: currentAbortSignal, }); const data = await response.json(); + // Check if aborted before updating state + if (currentAbortSignal?.aborted) return; + if (data.success && data.suggestions) { setSuggestedRefinements(prev => ({ ...prev, @@ -183,9 +195,16 @@ export default function Home() { })); } } catch (error) { + // Silently ignore abort errors + if (error instanceof Error && error.name === 'AbortError') { + return; + } console.error("Failed to fetch suggested refinements:", error); } finally { - setLoadingSuggestions(prev => ({ ...prev, [index]: false })); + // Only update loading state if not aborted + if (!currentAbortSignal?.aborted) { + setLoadingSuggestions(prev => ({ ...prev, [index]: false })); + } } }; @@ -851,11 +870,17 @@ export default function Home() { // Start over: clear results and go back to step 1 const handleStartOver = () => { + // Abort any in-flight generation or suggestion requests + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } cleanupBlobUrls(); setResults([]); setLabeledResults([]); setSuggestedRefinements({}); setLoadingSuggestions({}); + setLoading(false); setError(null); goTo(1); }; @@ -897,6 +922,13 @@ export default function Home() { setError(needed <= 0 ? "Please select at least one template." : `You need ${needed} credit${needed === 1 ? '' : 's'} to run this. You have ${credits}.`); return; } + // Abort any previous in-flight requests before starting new generation + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + abortControllerRef.current = new AbortController(); + const abortSignal = abortControllerRef.current.signal; + setAuthRequired(false); setShowAuthModal(false); setError(null); @@ -1024,6 +1056,7 @@ export default function Home() { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), + signal: abortSignal, }); if (res.status === 401) { setAuthRequired(true); @@ -1167,6 +1200,10 @@ export default function Home() { } } } catch (err: unknown) { + // Don't show error if request was aborted (e.g., user clicked Start Over) + if (err instanceof Error && err.name === 'AbortError') { + return; + } setError(err instanceof Error ? err.message : "Failed to generate"); } finally { setLoading(false); From 7554da973a84c792f7e5070904b573a525abef46 Mon Sep 17 00:00:00 2001 From: AJ Date: Sun, 14 Dec 2025 20:52:44 -0800 Subject: [PATCH 3/6] fix: guard setLoading in finally block to prevent race condition When a generation request is aborted (e.g., user clicks Start Over), the finally block was still calling setLoading(false). This caused a race condition where the aborted request could flip loading to false while a newer request was still in progress. Now we check if the abort signal was triggered before updating loading state. Addresses PR review comment about guarding against aborted requests updating the loading state. --- app/thumbnails/page.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/thumbnails/page.tsx b/app/thumbnails/page.tsx index 4d17dc2..8659ba8 100644 --- a/app/thumbnails/page.tsx +++ b/app/thumbnails/page.tsx @@ -1206,7 +1206,12 @@ export default function Home() { } setError(err instanceof Error ? err.message : "Failed to generate"); } finally { - setLoading(false); + // Only update loading state if this request wasn't aborted + // This prevents an aborted request from flipping loading to false + // while a newer request is still in progress + if (!abortSignal.aborted) { + setLoading(false); + } } }; From 352917828b2e84cab7988379e4a895244ee0bc22 Mon Sep 17 00:00:00 2001 From: AJ Date: Sun, 14 Dec 2025 20:59:25 -0800 Subject: [PATCH 4/6] fix: Reset refinement state and close history browser on Start Over Addresses PR review feedback: handleStartOver now properly resets refinementState (exits refinement mode) and closes the history browser to prevent the app from getting stuck in refinement mode when a user clicks Start Over while refining. --- app/thumbnails/page.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/thumbnails/page.tsx b/app/thumbnails/page.tsx index 8659ba8..09be0f7 100644 --- a/app/thumbnails/page.tsx +++ b/app/thumbnails/page.tsx @@ -882,6 +882,18 @@ export default function Home() { setLoadingSuggestions({}); setLoading(false); setError(null); + // Exit refinement mode if active to prevent getting stuck + setRefinementState(prev => ({ + ...prev, + isRefinementMode: false, + selectedThumbnailIndex: undefined, + selectedThumbnailUrl: undefined, + currentHistory: undefined, + feedbackPrompt: "", + refinementError: undefined, + })); + // Close the history browser if open + setShowHistoryBrowser(false); goTo(1); }; From e16c34f8fe15af501fefe83401827065319085bf Mon Sep 17 00:00:00 2001 From: AJ Date: Sun, 14 Dec 2025 21:05:52 -0800 Subject: [PATCH 5/6] fix: use duck typing for AbortError check to handle DOMException in all browsers The previous check using 'instanceof Error' may fail in some browsers where DOMException (thrown by fetch() when aborted) does not extend Error. This change uses duck typing to check for the 'name' property being 'AbortError' which works reliably across all browsers. Addresses review comment about AbortError detection in catch blocks. --- app/thumbnails/page.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/thumbnails/page.tsx b/app/thumbnails/page.tsx index 09be0f7..e3bf70c 100644 --- a/app/thumbnails/page.tsx +++ b/app/thumbnails/page.tsx @@ -196,7 +196,8 @@ export default function Home() { } } catch (error) { // Silently ignore abort errors - if (error instanceof Error && error.name === 'AbortError') { + // Use duck typing to check for AbortError since DOMException may not satisfy instanceof Error in all browsers + if (error && typeof error === 'object' && 'name' in error && error.name === 'AbortError') { return; } console.error("Failed to fetch suggested refinements:", error); @@ -1213,7 +1214,8 @@ export default function Home() { } } catch (err: unknown) { // Don't show error if request was aborted (e.g., user clicked Start Over) - if (err instanceof Error && err.name === 'AbortError') { + // Use duck typing to check for AbortError since DOMException may not satisfy instanceof Error in all browsers + if (err && typeof err === 'object' && 'name' in err && err.name === 'AbortError') { return; } setError(err instanceof Error ? err.message : "Failed to generate"); From 8e58537d09b7f1fb5b2aed97ab33f06e8c64a8e0 Mon Sep 17 00:00:00 2001 From: AJ Date: Sun, 14 Dec 2025 21:10:45 -0800 Subject: [PATCH 6/6] fix: abort in-flight requests on component unmount Address PR review comment to abort abortControllerRef in component unmount cleanup to prevent state updates after navigation/unmount. --- app/thumbnails/page.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/thumbnails/page.tsx b/app/thumbnails/page.tsx index e3bf70c..2bbc2a4 100644 --- a/app/thumbnails/page.tsx +++ b/app/thumbnails/page.tsx @@ -571,6 +571,16 @@ export default function Home() { }; }, [videoUrl]); + // Abort in-flight requests on component unmount to prevent state updates after unmount + useEffect(() => { + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + }; + }, []); + // Revoke blob URLs created for results to prevent memory leaks useEffect(() => { return () => {