Skip to content
Open
Changes from 3 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
59 changes: 56 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,31 @@ 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
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 }));
}
}
};

Expand Down Expand Up @@ -849,6 +868,23 @@ 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);
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 +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);
Expand Down Expand Up @@ -1013,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);
Expand Down Expand Up @@ -1156,9 +1200,18 @@ 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') {
Copy link

Choose a reason for hiding this comment

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

fetch() aborts often reject with a DOMException (name AbortError) which may not satisfy error instanceof Error in all browsers, so this guard could miss aborts and still surface a "Failed to generate" error. Consider broadening the check to rely on the name rather than the instanceof constraint (also applies to the similar abort check in the suggested-refinements fetch).

🤖 Was this useful? React with 👍 or 👎

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 +2178,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