diff --git a/frontend/apps/artcraft/app/src/pages/PageImage/TextToImage.tsx b/frontend/apps/artcraft/app/src/pages/PageImage/TextToImage.tsx
index 8a66448847..0b89ef9aec 100644
--- a/frontend/apps/artcraft/app/src/pages/PageImage/TextToImage.tsx
+++ b/frontend/apps/artcraft/app/src/pages/PageImage/TextToImage.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
JobContextType,
getThumbnailUrl,
@@ -31,6 +31,8 @@ import {
galleryModalLightboxImage,
galleryModalLightboxMediaId,
galleryModalLightboxVisible,
+ galleryModalLightboxNavPrev,
+ galleryModalLightboxNavNext,
} from "@storyteller/ui-gallery-modal";
import { Badge } from "@storyteller/ui-badge";
import { twMerge } from "tailwind-merge";
@@ -125,6 +127,63 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
// Like Midjourney instead of a "chat history" style.
const inverseBatch = [...batches].reverse();
+ // Keep a ref so nav callbacks always see the latest batch list without
+ // needing to be recreated on every render.
+ const inverseBatchRef = useRef(inverseBatch);
+ inverseBatchRef.current = inverseBatch;
+
+ const openBatchInLightbox = useCallback(
+ (targetBatch: (typeof inverseBatch)[0], startIndex = 0) => {
+ const images = targetBatch.images;
+ if (!images.length) return;
+
+ const imageUrls = images.map(
+ (img) =>
+ getThumbnailUrl(img.maybe_thumbnail_template, { width: 3200 }) ??
+ img.cdn_url,
+ );
+ const mediaTokens = images.map((img) => img.media_token);
+ const actionUrls = images.map((img) => img.cdn_url);
+
+ const clickedImg = images[startIndex];
+ const batchIndex = inverseBatchRef.current.findIndex(
+ (b) => b.id === targetBatch.id,
+ );
+
+ galleryModalLightboxNavPrev.value =
+ batchIndex > 0
+ ? () => openBatchInLightbox(inverseBatchRef.current[batchIndex - 1])
+ : null;
+ galleryModalLightboxNavNext.value =
+ batchIndex < inverseBatchRef.current.length - 1
+ ? () => openBatchInLightbox(inverseBatchRef.current[batchIndex + 1])
+ : null;
+
+ galleryModalLightboxMediaId.value = clickedImg.media_token;
+ galleryModalLightboxImage.value = {
+ id: clickedImg.media_token,
+ label: targetBatch.prompt || "Generated Image",
+ thumbnail:
+ getThumbnailUrl(clickedImg.maybe_thumbnail_template, {
+ width: THUMBNAIL_SIZES.MEDIUM,
+ }) ?? clickedImg.cdn_url,
+ fullImage:
+ getThumbnailUrl(clickedImg.maybe_thumbnail_template, {
+ width: 2048,
+ }) ?? clickedImg.cdn_url,
+ createdAt: new Date(targetBatch.createdAt).toISOString(),
+ mediaClass: "image" as const,
+ mediaTokens,
+ imageUrls,
+ actionUrls,
+ initialIndex: startIndex,
+ thumbnailUrlTemplate: clickedImg.maybe_thumbnail_template,
+ };
+ galleryModalLightboxVisible.value = true;
+ },
+ [],
+ );
+
return (
{