diff --git a/app/thumbnails/page.tsx b/app/thumbnails/page.tsx index ec8b6ed..2bbc2a4 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,17 @@ export default function Home() { })); } } catch (error) { + // Silently ignore abort errors + // 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); } finally { - setLoadingSuggestions(prev => ({ ...prev, [index]: false })); + // Only update loading state if not aborted + if (!currentAbortSignal?.aborted) { + setLoadingSuggestions(prev => ({ ...prev, [index]: false })); + } } }; @@ -551,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 () => { @@ -849,6 +879,35 @@ export default function Home() { setBlobUrls([]); }; + // 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); + // 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); + }; + const generate = async () => { // Get all valid template IDs (custom + curated) const allValidTemplateIds = new Set([ @@ -886,6 +945,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); @@ -1013,6 +1079,7 @@ export default function Home() { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), + signal: abortSignal, }); if (res.status === 401) { setAuthRequired(true); @@ -1156,9 +1223,19 @@ export default function Home() { } } } catch (err: unknown) { + // Don't show error if request was aborted (e.g., user clicked Start Over) + // 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"); } 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); + } } }; @@ -2125,7 +2202,7 @@ export default function Home() { {/* Compact nav */}
- +
)}