Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 80 additions & 3 deletions app/thumbnails/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const thumbnailFaqItems: { question: string; answer: string }[] = [
export default function Home() {
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
// AbortController for cancelling in-flight generation and suggestion requests
const abortControllerRef = useRef<AbortController | null>(null);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since abortControllerRef is now central to request lifecycle, consider aborting it in a component unmount cleanup as well, so in-flight fetch calls can’t resolve and attempt state updates after navigation/unmount.

🤖 Was this useful? React with 👍 or 👎


const [videoUrl, setVideoUrl] = useState<string | null>(null);
const [videoReady, setVideoReady] = useState(false);
Expand Down Expand Up @@ -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 {
Expand All @@ -172,20 +180,32 @@ 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,
[index]: data.suggestions,
}));
}
} 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 }));
}
}
};

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If “Start Over” can be clicked while generation or suggested-refinement fetches are still in flight, those async completions can still update results/suggestedRefinements (index-keyed) after this reset, potentially causing stale suggestions or a stuck loading state. Consider adding a guard/cancellation mechanism for outstanding async work when starting over.

🤖 Was this useful? React with 👍 or 👎

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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleStartOver clears generation state but doesn’t reset refinementState (e.g., isRefinementMode) or close related UI like the history browser. If a user clicks Start Over while refining, this can leave the app “stuck” in refinement mode and also prevent suggested refinements from being fetched on the next run due to the !refinementState.isRefinementMode guard.

🤖 Was this useful? React with 👍 or 👎

};

const generate = async () => {
// Get all valid template IDs (custom + curated)
const allValidTemplateIds = new Set([
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
};

Expand Down Expand Up @@ -2125,7 +2202,7 @@ export default function Home() {

{/* Compact nav */}
<div className={styles.navRow} style={{ marginTop: 12 }}>
<button onClick={() => goTo(1)} style={{ padding: '6px 12px', fontSize: 13 }}>← Start Over</button>
<button onClick={handleStartOver} style={{ padding: '6px 12px', fontSize: 13 }}>← Start Over</button>
</div>
</>
)}
Expand Down