From 3082e8f4f1766de73b0fa340fbcb6e6857ce17eb Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 14 Jul 2026 06:51:58 +0000 Subject: [PATCH 1/7] Add preview dialog and refine actions to generation results --- .../generation/GenerationResults.tsx | 432 ++++++++++++------ 1 file changed, 298 insertions(+), 134 deletions(-) diff --git a/templates/assets/app/components/generation/GenerationResults.tsx b/templates/assets/app/components/generation/GenerationResults.tsx index 8a1b0d0fa6..e2a2236e07 100644 --- a/templates/assets/app/components/generation/GenerationResults.tsx +++ b/templates/assets/app/components/generation/GenerationResults.tsx @@ -5,9 +5,16 @@ import { useActionQuery, useT, } from "@agent-native/core/client"; -import { IconMessageCircle, IconPhoto, IconTrash } from "@tabler/icons-react"; +import { + IconDeviceFloppy, + IconMessageCircle, + IconPhoto, + IconTrash, + IconX, +} from "@tabler/icons-react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useRef, useState } from "react"; +import { Link } from "react-router"; import { toast } from "sonner"; import { @@ -22,7 +29,20 @@ import { } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/components/ui/dialog"; import { Spinner } from "@/components/ui/spinner"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { assetPreviewSources } from "@/lib/asset-preview-sources"; import type { @@ -34,6 +54,8 @@ type LibraryListResult = { libraries?: ImageLibrarySummary[]; }; +type VariantSlot = AssetVariantState["slots"][number]; + function variantStateKey(threadId: string | null) { return threadId ? `asset-variants:${threadId}` : "asset-variants"; } @@ -46,15 +68,13 @@ function variantStateKey(threadId: string | null) { // its finished image arrives. const STALE_PENDING_RUN_MS = 10 * 60 * 1000; -function slotTime(slot: AssetVariantState["slots"][number]): number { +function slotTime(slot: VariantSlot): number { const raw = slot.createdAt ?? slot.updatedAt ?? ""; const time = Date.parse(raw); return Number.isNaN(time) ? 0 : time; } -function stalePendingRunId( - slot: AssetVariantState["slots"][number], -): string | null { +function stalePendingRunId(slot: VariantSlot): string | null { if (slot.status !== "pending") return null; if (!slot.runId) return null; const timestamp = slotTime(slot); @@ -66,6 +86,7 @@ export function GenerationResults({ threadId }: { threadId: string | null }) { const t = useT(); const queryClient = useQueryClient(); const [clearAllOpen, setClearAllOpen] = useState(false); + const [previewSlotId, setPreviewSlotId] = useState(null); const stateKey = variantStateKey(threadId); const stateQueryKey = useMemo(() => ["app-state", stateKey], [stateKey]); const { data: variants } = useQuery({ @@ -137,6 +158,11 @@ export function GenerationResults({ threadId }: { threadId: string | null }) { ); }, [belongsToThread, queryClient, refreshGeneration, slots, stateQueryKey]); + const previewSlot = useMemo( + () => slots.find((slot) => slot.slotId === previewSlotId) ?? null, + [previewSlotId, slots], + ); + if (!belongsToThread || !variants) return null; if (slots.length === 0) return null; @@ -169,6 +195,61 @@ export function GenerationResults({ threadId }: { threadId: string | null }) { ); } + function saveSlot(slot: VariantSlot, onDone?: () => void) { + if (!slot.assetId && !slot.slotId) return; + saveGenerated.mutate( + { + ...(slot.assetId ? { assetId: slot.assetId } : {}), + ...(slot.slotId ? { slotId: slot.slotId } : {}), + threadId, + }, + { + onSuccess: () => { + toast.success(t("library.savedGeneratedAsset")); + void queryClient.invalidateQueries({ queryKey: stateQueryKey }); + void queryClient.invalidateQueries({ + queryKey: ["action", "get-library"], + }); + onDone?.(); + }, + onError: (error) => + toast.error(error.message || t("library.couldNotSaveCandidate")), + }, + ); + } + + function dismissSlotById(slot: VariantSlot, onDone?: () => void) { + dismissSlot.mutate( + { slotId: slot.slotId, threadId }, + { + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: stateQueryKey }); + onDone?.(); + }, + onError: (error) => + toast.error(error.message || t("library.couldNotDismissCandidate")), + }, + ); + } + + function refineSlot(slot: VariantSlot) { + sendToAgentChat({ + message: `Refine generated asset ${slot.assetId}: `, + context: [ + "## Assets candidate", + `Asset ID: ${slot.assetId}`, + `Run ID: ${slot.runId ?? "unknown"}`, + `Prompt: ${variants?.prompt ?? ""}`, + libraryTitle ? `Brand kit: ${libraryTitle}` : "", + "Use refine-image with assetId when the user describes the change.", + ] + .filter(Boolean) + .join("\n"), + submit: false, + openSidebar: true, + }); + } + return ( <> @@ -206,6 +287,18 @@ export function GenerationResults({ threadId }: { threadId: string | null }) { + { + if (!open) setPreviewSlotId(null); + }} + onSave={(slot) => saveSlot(slot, () => setPreviewSlotId(null))} + onRefine={refineSlot} + /> +
@@ -244,64 +337,19 @@ export function GenerationResults({ threadId }: { threadId: string | null }) {
-
- {slots.map((slot, index) => ( - + {slots.map((slot) => ( + { - if (!slot.assetId && !slot.slotId) return; - saveGenerated.mutate( - { - ...(slot.assetId ? { assetId: slot.assetId } : {}), - ...(slot.slotId ? { slotId: slot.slotId } : {}), - threadId, - }, - { - onSuccess: () => { - toast.success(t("library.savedGeneratedAsset")); - void queryClient.invalidateQueries({ - queryKey: stateQueryKey, - }); - void queryClient.invalidateQueries({ - queryKey: ["action", "get-library"], - }); - }, - onError: (error) => - toast.error( - error.message || t("library.couldNotSaveCandidate"), - ), - }, - ); - }} - onDismiss={() => { - dismissSlot.mutate( - { slotId: slot.slotId, threadId }, - { - onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: stateQueryKey, - }); - }, - onError: (error) => - toast.error( - error.message || - t("library.couldNotDismissCandidate"), - ), - }, - ); - }} + onPreview={() => setPreviewSlotId(slot.slotId)} + onSave={() => saveSlot(slot)} + onRefine={() => refineSlot(slot)} + onDismiss={() => dismissSlotById(slot)} /> ))}
@@ -312,117 +360,233 @@ export function GenerationResults({ threadId }: { threadId: string | null }) { ); } -function GenerationResultItem({ +function GenerationDraftCard({ slot, - index, prompt, libraryTitle, isSaving, isDismissing, + onPreview, onSave, + onRefine, onDismiss, }: { - slot: AssetVariantState["slots"][number]; - index: number; + slot: VariantSlot; prompt: string; libraryTitle: string | null; isSaving: boolean; isDismissing: boolean; + onPreview: () => void; onSave: () => void; + onRefine: () => void; onDismiss: () => void; }) { - const ready = slot.status === "ready" && Boolean(slot.assetId); const t = useT(); + const ready = slot.status === "ready" && Boolean(slot.assetId); + const title = libraryTitle + ? `${libraryTitle}${prompt ? ` / ${prompt}` : ""}` + : prompt || t("library.readyToSave"); return ( -
-
- -
+
+ + + {slot.status !== "ready" ? ( +
{slot.status === "pending" ? : null} {slot.status === "pending" ? t("library.generating") - : slot.status === "ready" - ? t("library.candidateWithNumber", { number: index + 1 }) - : t("library.failed")} + : t("library.failed")}
-
-
-
-
- {slot.status === "ready" - ? t("library.readyToSave") - : slot.status === "pending" - ? t("library.stillRendering") - : t("library.generationFailed")} -
-
- {libraryTitle || t("library.noBrandKit")} - {prompt ? ` / ${prompt}` : null} + ) : null} + + {ready ? ( + +
+ + + + + {t("library.save")} + + + + + + {t("library.refine")} + + + + + + {t("library.deleteCandidate")} +
-
-
- - - -
-
-
+ + ) : ( + + )} +
); } -function GenerationSlotPreview({ +function GenerationPreviewDialog({ slot, + prompt, + libraryTitle, + isSaving, + onOpenChange, + onSave, + onRefine, }: { - slot: AssetVariantState["slots"][number]; + slot: VariantSlot | null; + prompt: string; + libraryTitle: string | null; + isSaving: boolean; + onOpenChange: (open: boolean) => void; + onSave: (slot: VariantSlot) => void; + onRefine: (slot: VariantSlot) => void; }) { + const t = useT(); + const sources = slot ? assetPreviewSources(slot, "preview") : []; + const src = sources[0]; + return ( + + {slot ? ( + + + {libraryTitle || t("library.noBrandKit")} + + + {prompt || t("library.readyToSave")} + +
+
+ + + {slot.assetId ? ( + + ) : null} + + + +
+ {src ? ( + {prompt + ) : ( +
+ +
+ )} +
+
+ ) : null} +
+ ); +} + +function GenerationSlotPreview({ slot }: { slot: VariantSlot }) { const t = useT(); const sources = assetPreviewSources(slot, "thumbnail"); const src = sources[0]; if (src) { - return ; + return ( + + ); } if (slot.status === "failed") { return ( From 07e1b4ab4447a505945b049c3a7843138362bf02 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 14 Jul 2026 08:42:12 +0000 Subject: [PATCH 2/7] Add image navigation controls to generation preview dialog --- .../generation/GenerationResults.tsx | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/templates/assets/app/components/generation/GenerationResults.tsx b/templates/assets/app/components/generation/GenerationResults.tsx index e2a2236e07..463c3b87d1 100644 --- a/templates/assets/app/components/generation/GenerationResults.tsx +++ b/templates/assets/app/components/generation/GenerationResults.tsx @@ -6,6 +6,8 @@ import { useT, } from "@agent-native/core/client"; import { + IconChevronLeft, + IconChevronRight, IconDeviceFloppy, IconMessageCircle, IconPhoto, @@ -289,12 +291,14 @@ export function GenerationResults({ threadId }: { threadId: string | null }) { { if (!open) setPreviewSlotId(null); }} + onSelect={setPreviewSlotId} onSave={(slot) => saveSlot(slot, () => setPreviewSlotId(null))} onRefine={refineSlot} /> @@ -492,22 +496,45 @@ function GenerationDraftCard({ function GenerationPreviewDialog({ slot, + slots, prompt, libraryTitle, isSaving, onOpenChange, + onSelect, onSave, onRefine, }: { slot: VariantSlot | null; + slots: VariantSlot[]; prompt: string; libraryTitle: string | null; isSaving: boolean; onOpenChange: (open: boolean) => void; + onSelect: (slotId: string) => void; onSave: (slot: VariantSlot) => void; onRefine: (slot: VariantSlot) => void; }) { const t = useT(); + const previewableSlots = useMemo( + () => + slots.filter( + (item) => item.status === "ready" && Boolean(item.assetId), + ), + [slots], + ); + const previewIndex = slot + ? previewableSlots.findIndex((item) => item.slotId === slot.slotId) + : -1; + const hasPrev = previewIndex > 0; + const hasNext = + previewIndex >= 0 && previewIndex < previewableSlots.length - 1; + const showPreviousSlot = () => { + if (hasPrev) onSelect(previewableSlots[previewIndex - 1].slotId); + }; + const showNextSlot = () => { + if (hasNext) onSelect(previewableSlots[previewIndex + 1].slotId); + }; const sources = slot ? assetPreviewSources(slot, "preview") : []; const src = sources[0]; return ( @@ -515,6 +542,10 @@ function GenerationPreviewDialog({ {slot ? ( { + if (event.key === "ArrowLeft") showPreviousSlot(); + if (event.key === "ArrowRight") showNextSlot(); + }} className="max-w-4xl border-0 bg-transparent p-0 shadow-none" > @@ -569,6 +600,28 @@ function GenerationPreviewDialog({
)} + {hasPrev || hasNext ? ( +
+ + +
+ ) : null} ) : null} From 4a6f61f40f66fc8528d2d040b744aa3a5f69d2fd Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 14 Jul 2026 08:49:38 +0000 Subject: [PATCH 3/7] refactor(assets): reorganize preview dialog layout with top actions and bottom navigation --- .../generation/GenerationResults.tsx | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/templates/assets/app/components/generation/GenerationResults.tsx b/templates/assets/app/components/generation/GenerationResults.tsx index 463c3b87d1..e2307b065a 100644 --- a/templates/assets/app/components/generation/GenerationResults.tsx +++ b/templates/assets/app/components/generation/GenerationResults.tsx @@ -537,6 +537,9 @@ function GenerationPreviewDialog({ }; const sources = slot ? assetPreviewSources(slot, "preview") : []; const src = sources[0]; + const title = libraryTitle + ? `${libraryTitle}${prompt ? ` / ${prompt}` : ""}` + : prompt || t("library.readyToSave"); return ( {slot ? ( @@ -546,7 +549,7 @@ function GenerationPreviewDialog({ if (event.key === "ArrowLeft") showPreviousSlot(); if (event.key === "ArrowRight") showNextSlot(); }} - className="max-w-4xl border-0 bg-transparent p-0 shadow-none" + className="flex max-h-[85vh] w-[calc(100vw-24px)] max-w-4xl flex-col gap-0 overflow-hidden p-0" > {libraryTitle || t("library.noBrandKit")} @@ -554,10 +557,13 @@ function GenerationPreviewDialog({ {prompt || t("library.readyToSave")} -
-
+ +
+

+ {title} +

+
+
+ +
{src ? ( {prompt ) : (
@@ -600,14 +609,15 @@ function GenerationPreviewDialog({
)}
+ {hasPrev || hasNext ? ( -
+
@@ -616,7 +626,7 @@ function GenerationPreviewDialog({ aria-label={t("library.nextImage")} onClick={showNextSlot} disabled={!hasNext} - className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white transition hover:bg-black/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white disabled:cursor-not-allowed disabled:opacity-40" + className="inline-flex h-9 w-9 items-center justify-center rounded-full text-foreground transition hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-40" > From efaab2400618b02e1b76c51d398cfe3e1dde150b Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 14 Jul 2026 08:56:25 +0000 Subject: [PATCH 4/7] refactor(assets): use setAgentChatContextItem for asset refinement --- .../app/components/generation/GenerationResults.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/templates/assets/app/components/generation/GenerationResults.tsx b/templates/assets/app/components/generation/GenerationResults.tsx index e2307b065a..d36ea502f0 100644 --- a/templates/assets/app/components/generation/GenerationResults.tsx +++ b/templates/assets/app/components/generation/GenerationResults.tsx @@ -1,6 +1,6 @@ import { readClientAppState, - sendToAgentChat, + setAgentChatContextItem, useActionMutation, useActionQuery, useT, @@ -235,8 +235,10 @@ export function GenerationResults({ threadId }: { threadId: string | null }) { } function refineSlot(slot: VariantSlot) { - sendToAgentChat({ - message: `Refine generated asset ${slot.assetId}: `, + if (!slot.assetId) return; + setAgentChatContextItem({ + key: `refine-asset:${slot.assetId}`, + title: t("library.refine"), context: [ "## Assets candidate", `Asset ID: ${slot.assetId}`, @@ -247,7 +249,6 @@ export function GenerationResults({ threadId }: { threadId: string | null }) { ] .filter(Boolean) .join("\n"), - submit: false, openSidebar: true, }); } From 65fe44b49b350df68f721f1d1f0d31e47150af9e Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 14 Jul 2026 09:05:31 +0000 Subject: [PATCH 5/7] Close preview when refining slot in full-screen view --- .../assets/app/components/generation/GenerationResults.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/assets/app/components/generation/GenerationResults.tsx b/templates/assets/app/components/generation/GenerationResults.tsx index d36ea502f0..1b54e996a1 100644 --- a/templates/assets/app/components/generation/GenerationResults.tsx +++ b/templates/assets/app/components/generation/GenerationResults.tsx @@ -301,7 +301,10 @@ export function GenerationResults({ threadId }: { threadId: string | null }) { }} onSelect={setPreviewSlotId} onSave={(slot) => saveSlot(slot, () => setPreviewSlotId(null))} - onRefine={refineSlot} + onRefine={(slot) => { + refineSlot(slot); + setPreviewSlotId(null); + }} />
From cef50d8f644557f3f74f00e5dd1b808683caf895 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 14 Jul 2026 09:21:26 +0000 Subject: [PATCH 6/7] Add variant numbering labels to image thumbnails and refine --- .../generation/GenerationResults.tsx | 250 +++++++++--------- templates/assets/app/i18n-data.ts | 1 + 2 files changed, 128 insertions(+), 123 deletions(-) diff --git a/templates/assets/app/components/generation/GenerationResults.tsx b/templates/assets/app/components/generation/GenerationResults.tsx index 1b54e996a1..62279d13e3 100644 --- a/templates/assets/app/components/generation/GenerationResults.tsx +++ b/templates/assets/app/components/generation/GenerationResults.tsx @@ -234,11 +234,14 @@ export function GenerationResults({ threadId }: { threadId: string | null }) { ); } - function refineSlot(slot: VariantSlot) { + function refineSlot(slot: VariantSlot, variantNumber: number) { if (!slot.assetId) return; + const variantLabel = t("library.variantWithNumber", { + number: variantNumber, + }); setAgentChatContextItem({ - key: `refine-asset:${slot.assetId}`, - title: t("library.refine"), + key: "refine-asset:" + slot.assetId, + title: t("library.refine") + " : " + variantLabel, context: [ "## Assets candidate", `Asset ID: ${slot.assetId}`, @@ -301,8 +304,8 @@ export function GenerationResults({ threadId }: { threadId: string | null }) { }} onSelect={setPreviewSlotId} onSave={(slot) => saveSlot(slot, () => setPreviewSlotId(null))} - onRefine={(slot) => { - refineSlot(slot); + onRefine={(slot, variantNumber) => { + refineSlot(slot, variantNumber); setPreviewSlotId(null); }} /> @@ -346,17 +349,16 @@ export function GenerationResults({ threadId }: { threadId: string | null }) {
- {slots.map((slot) => ( + {slots.map((slot, index) => ( setPreviewSlotId(slot.slotId)} onSave={() => saveSlot(slot)} - onRefine={() => refineSlot(slot)} + onRefine={() => refineSlot(slot, index + 1)} onDismiss={() => dismissSlotById(slot)} /> ))} @@ -370,8 +372,7 @@ export function GenerationResults({ threadId }: { threadId: string | null }) { function GenerationDraftCard({ slot, - prompt, - libraryTitle, + variantNumber, isSaving, isDismissing, onPreview, @@ -380,8 +381,7 @@ function GenerationDraftCard({ onDismiss, }: { slot: VariantSlot; - prompt: string; - libraryTitle: string | null; + variantNumber: number; isSaving: boolean; isDismissing: boolean; onPreview: () => void; @@ -391,109 +391,114 @@ function GenerationDraftCard({ }) { const t = useT(); const ready = slot.status === "ready" && Boolean(slot.assetId); - const title = libraryTitle - ? `${libraryTitle}${prompt ? ` / ${prompt}` : ""}` - : prompt || t("library.readyToSave"); + const variantLabel = t("library.variantWithNumber", { + number: variantNumber, + }); return ( -
- - - {slot.status !== "ready" ? ( -
- {slot.status === "pending" ? : null} - - {slot.status === "pending" - ? t("library.generating") - : t("library.failed")} - -
- ) : null} - - {ready ? ( - -
- - - - - {t("library.save")} - - - - - - {t("library.refine")} - - - - - - {t("library.deleteCandidate")} - -
-
- ) : ( +
+
- )} + + {slot.status !== "ready" ? ( +
+ {slot.status === "pending" ? : null} + + {slot.status === "pending" + ? t("library.generating") + : t("library.failed")} + +
+ ) : null} + + {ready ? ( + +
+ + + + + {t("library.save")} + + + + + + {t("library.refine")} + + + + + + {t("library.deleteCandidate")} + +
+
+ ) : ( + + )} +
+
+ {variantLabel} +
); } @@ -517,14 +522,12 @@ function GenerationPreviewDialog({ onOpenChange: (open: boolean) => void; onSelect: (slotId: string) => void; onSave: (slot: VariantSlot) => void; - onRefine: (slot: VariantSlot) => void; + onRefine: (slot: VariantSlot, variantNumber: number) => void; }) { const t = useT(); const previewableSlots = useMemo( () => - slots.filter( - (item) => item.status === "ready" && Boolean(item.assetId), - ), + slots.filter((item) => item.status === "ready" && Boolean(item.assetId)), [slots], ); const previewIndex = slot @@ -541,9 +544,12 @@ function GenerationPreviewDialog({ }; const sources = slot ? assetPreviewSources(slot, "preview") : []; const src = sources[0]; - const title = libraryTitle - ? `${libraryTitle}${prompt ? ` / ${prompt}` : ""}` - : prompt || t("library.readyToSave"); + const variantNumber = slot + ? slots.findIndex((item) => item.slotId === slot.slotId) + 1 + : 0; + const variantLabel = t("library.variantWithNumber", { + number: variantNumber, + }); return ( {slot ? ( @@ -555,16 +561,14 @@ function GenerationPreviewDialog({ }} className="flex max-h-[85vh] w-[calc(100vw-24px)] max-w-4xl flex-col gap-0 overflow-hidden p-0" > - - {libraryTitle || t("library.noBrandKit")} - + {variantLabel} {prompt || t("library.readyToSave")}
-

- {title} +

+ {variantLabel}

@@ -509,20 +513,24 @@ function GenerationPreviewDialog({ prompt, libraryTitle, isSaving, + isDismissing, onOpenChange, onSelect, onSave, onRefine, + onDismiss, }: { slot: VariantSlot | null; slots: VariantSlot[]; prompt: string; libraryTitle: string | null; isSaving: boolean; + isDismissing: boolean; onOpenChange: (open: boolean) => void; onSelect: (slotId: string) => void; onSave: (slot: VariantSlot) => void; onRefine: (slot: VariantSlot, variantNumber: number) => void; + onDismiss: (slot: VariantSlot) => void; }) { const t = useT(); const previewableSlots = useMemo( @@ -590,11 +598,28 @@ function GenerationPreviewDialog({ {slot.assetId ? ( ) : null} +