Skip to content

Commit 17665da

Browse files
authored
fix(web): stabilize image preview loading (#150)
## Summary - Keep the current auth-token reader behind a ref so preview loading does not restart on render. - Bound both capability exchange and object download to a 30-second preview timeout. ## Production finding The initial deployment correctly opened the generated JPEG in Files, but its inline card remained on Preparing preview because the loading render changed the auth callback dependency and aborted the request before network dispatch. ## Checks - [x] Targeted Biome check - [x] Web TypeScript check - [ ] Production inline preview, modal, and download validation after deployment
1 parent 010b31a commit 17665da

2 files changed

Lines changed: 18 additions & 4 deletions

File tree

apps/web/src/components/chat/message-deliverables.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,18 +354,27 @@ function useLazyImagePreview(
354354
getToken: () => Promise<null | string>,
355355
): ImagePreview {
356356
const hostRef = useRef<HTMLDivElement | null>(null);
357+
const getTokenRef = useRef(getToken);
357358
const isNearViewport = useNearViewport(hostRef);
358359
const [preview, setPreview] = useState<Omit<ImagePreview, "hostRef">>({
359360
message: null,
360361
status: "idle",
361362
url: null,
362363
});
364+
useEffect(() => {
365+
getTokenRef.current = getToken;
366+
}, [getToken]);
363367
useEffect(() => {
364368
if (!isNearViewport) return;
365369
const controller = new AbortController();
366370
let objectUrl: string | null = null;
367371
setPreview({ message: null, status: "loading", url: null });
368-
void loadOutputImagePreview(getToken, data.outputId, data.sizeBytes, controller.signal)
372+
void loadOutputImagePreview(
373+
() => getTokenRef.current(),
374+
data.outputId,
375+
data.sizeBytes,
376+
controller.signal,
377+
)
369378
.then((blob) => {
370379
objectUrl = URL.createObjectURL(blob);
371380
setPreview({ message: null, status: "ready", url: objectUrl });
@@ -382,7 +391,7 @@ function useLazyImagePreview(
382391
controller.abort();
383392
if (objectUrl) URL.revokeObjectURL(objectUrl);
384393
};
385-
}, [data.outputId, data.sizeBytes, getToken, isNearViewport]);
394+
}, [data.outputId, data.sizeBytes, isNearViewport]);
386395
return { ...preview, hostRef };
387396
}
388397

apps/web/src/lib/api/outputs.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "@/lib/api/authorized-fetch";
1414

1515
const OUTPUT_IMAGE_PREVIEW_MAX_BYTES = 20 * 1024 * 1024;
16+
const OUTPUT_IMAGE_PREVIEW_TIMEOUT_MS = 30_000;
1617

1718
export async function createOutputDownloadUrl(
1819
getToken: () => Promise<null | string>,
@@ -39,10 +40,14 @@ export async function loadOutputImagePreview(
3940
if (sizeBytes <= 0 || sizeBytes > OUTPUT_IMAGE_PREVIEW_MAX_BYTES) {
4041
throw new Error("This image is too large to preview here. Download it to view the original.");
4142
}
42-
const capability = await createOutputDownloadUrl(getToken, outputId, signal);
43+
const requestSignal = AbortSignal.any([
44+
signal,
45+
AbortSignal.timeout(OUTPUT_IMAGE_PREVIEW_TIMEOUT_MS),
46+
]);
47+
const capability = await createOutputDownloadUrl(getToken, outputId, requestSignal);
4348
const response = await fetch(capability.downloadUrl, {
4449
referrerPolicy: "no-referrer",
45-
signal,
50+
signal: requestSignal,
4651
});
4752
if (!response.ok) {
4853
throw new Error(`Image preview returned HTTP ${response.status}`);

0 commit comments

Comments
 (0)