diff --git a/apps/web/src/components/chat/message-deliverables.tsx b/apps/web/src/components/chat/message-deliverables.tsx index 5581448..f1d107b 100644 --- a/apps/web/src/components/chat/message-deliverables.tsx +++ b/apps/web/src/components/chat/message-deliverables.tsx @@ -354,18 +354,27 @@ function useLazyImagePreview( getToken: () => Promise, ): ImagePreview { const hostRef = useRef(null); + const getTokenRef = useRef(getToken); const isNearViewport = useNearViewport(hostRef); const [preview, setPreview] = useState>({ message: null, status: "idle", url: null, }); + useEffect(() => { + getTokenRef.current = getToken; + }, [getToken]); useEffect(() => { if (!isNearViewport) return; const controller = new AbortController(); let objectUrl: string | null = null; setPreview({ message: null, status: "loading", url: null }); - void loadOutputImagePreview(getToken, data.outputId, data.sizeBytes, controller.signal) + void loadOutputImagePreview( + () => getTokenRef.current(), + data.outputId, + data.sizeBytes, + controller.signal, + ) .then((blob) => { objectUrl = URL.createObjectURL(blob); setPreview({ message: null, status: "ready", url: objectUrl }); @@ -382,7 +391,7 @@ function useLazyImagePreview( controller.abort(); if (objectUrl) URL.revokeObjectURL(objectUrl); }; - }, [data.outputId, data.sizeBytes, getToken, isNearViewport]); + }, [data.outputId, data.sizeBytes, isNearViewport]); return { ...preview, hostRef }; } diff --git a/apps/web/src/lib/api/outputs.ts b/apps/web/src/lib/api/outputs.ts index 5349de1..e301773 100644 --- a/apps/web/src/lib/api/outputs.ts +++ b/apps/web/src/lib/api/outputs.ts @@ -13,6 +13,7 @@ import { } from "@/lib/api/authorized-fetch"; const OUTPUT_IMAGE_PREVIEW_MAX_BYTES = 20 * 1024 * 1024; +const OUTPUT_IMAGE_PREVIEW_TIMEOUT_MS = 30_000; export async function createOutputDownloadUrl( getToken: () => Promise, @@ -39,10 +40,14 @@ export async function loadOutputImagePreview( if (sizeBytes <= 0 || sizeBytes > OUTPUT_IMAGE_PREVIEW_MAX_BYTES) { throw new Error("This image is too large to preview here. Download it to view the original."); } - const capability = await createOutputDownloadUrl(getToken, outputId, signal); + const requestSignal = AbortSignal.any([ + signal, + AbortSignal.timeout(OUTPUT_IMAGE_PREVIEW_TIMEOUT_MS), + ]); + const capability = await createOutputDownloadUrl(getToken, outputId, requestSignal); const response = await fetch(capability.downloadUrl, { referrerPolicy: "no-referrer", - signal, + signal: requestSignal, }); if (!response.ok) { throw new Error(`Image preview returned HTTP ${response.status}`);