From bb3fe2df2d6cc07f2ed7610cb42cf9d589f7b4e1 Mon Sep 17 00:00:00 2001 From: BFlat Date: Mon, 23 Mar 2026 11:14:01 +0700 Subject: [PATCH] reprompt (recreate) button (#1194) {A15AF77F-C555-4C85-BC15-B4B8D44D2248} A "recreate" button in the media lightbox modal. On clicked, it will prefill the image or video promptbox with the same model, prompt and ref images that was used to generate that media the button was clicked from. Temporary and doesnt support seedance video/audio ref yet. --- .../src/components/signaled/TopBar/TopBar.tsx | 61 ++++++++++++++++++- .../gallery-modal/src/lib/gallery-modal.tsx | 15 +++++ .../lightbox-modal/src/lib/lightbox-modal.tsx | 60 ++++++++++++++---- 3 files changed, 123 insertions(+), 13 deletions(-) diff --git a/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx b/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx index 4271ebe9ac..5e772d5fac 100644 --- a/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx +++ b/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx @@ -16,7 +16,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { signal } from "@preact/signals-react"; import { useSignals } from "@preact/signals-react/runtime"; import { FilterMediaClasses } from "@storyteller/api"; -import { getCreatorIcon, ModelCreator } from "@storyteller/model-list"; +import { getCreatorIcon, ModelCreator, IMAGE_MODELS_BY_ID, VIDEO_MODELS_BY_ID } from "@storyteller/model-list"; +import { useClassyModelSelectorStore, ModelPage } from "@storyteller/ui-model-selector"; import { useCreditsState } from "@storyteller/credits"; import { gtagEvent } from "@storyteller/google-analytics"; import { ProviderBillingModal } from "@storyteller/provider-billing-modal"; @@ -50,7 +51,7 @@ import { useCostBreakdownModalStore, CreditsModal, } from "@storyteller/ui-pricing-modal"; -import { RefImage, usePromptVideoStore } from "@storyteller/ui-promptbox"; +import { RefImage, usePromptImageStore, usePromptVideoStore } from "@storyteller/ui-promptbox"; import { LoadingSpinner } from "@storyteller/ui-loading-spinner"; import { SettingsModal } from "@storyteller/ui-settings-modal"; import { Tooltip } from "@storyteller/ui-tooltip"; @@ -263,6 +264,61 @@ export const TopBar = ({ pageName }: Props) => { } }; + const handleRecreateFromGallery = (data: { + prompt: string | null; + mediaClass: string | undefined; + modelType: string | null; + contextImages: Array<{ + media_links: { + cdn_url: string; + maybe_thumbnail_template: string; + }; + media_token: string; + semantic: string; + }> | null; + }) => { + try { + const { prompt: recreatePrompt, mediaClass: recreateMediaClass, modelType: recreateModelType, contextImages: recreateContextImages } = data; + + // Build reference images from context images + const refImages: RefImage[] = (recreateContextImages || []).map((ci) => ({ + id: Math.random().toString(36).substring(7), + url: ci.media_links.cdn_url, + file: new File([], "recreate-ref"), + mediaToken: ci.media_token, + })); + + const modelStore = useClassyModelSelectorStore.getState(); + + if (recreateMediaClass === "video") { + const videoStore = usePromptVideoStore.getState(); + if (recreatePrompt) videoStore.setPrompt(recreatePrompt); + if (refImages.length > 0) videoStore.setReferenceImages(refImages); + if (recreateModelType) { + const model = VIDEO_MODELS_BY_ID.get(recreateModelType); + if (model) modelStore.setSelectedModel(ModelPage.ImageToVideo, model); + } + useTabStore.getState().setActiveTab("VIDEO"); + } else { + // Default to image + const imageStore = usePromptImageStore.getState(); + if (recreatePrompt) imageStore.setPrompt(recreatePrompt); + if (refImages.length > 0) imageStore.setReferenceImages(refImages); + if (recreateModelType) { + const model = IMAGE_MODELS_BY_ID.get(recreateModelType); + if (model) modelStore.setSelectedModel(ModelPage.TextToImage, model); + } + useTabStore.getState().setActiveTab("IMAGE"); + } + + galleryModalVisibleViewMode.value = false; + galleryModalVisibleDuringDrag.value = false; + galleryModalLightboxVisible.value = false; + } catch (e) { + // no-op + } + }; + const handleRemoveBackgroundFromGallery = async (url: string) => { try { useRemoveBackgroundStore.getState().setPendingExternalUrl(url); @@ -640,6 +696,7 @@ export const TopBar = ({ pageName }: Props) => { onRemoveBackgroundClicked={handleRemoveBackgroundFromGallery} onMake3DObjectClicked={handleMake3DObjectFromGallery} onMake3DWorldClicked={handleMake3DWorldFromGallery} + onRecreateClicked={handleRecreateFromGallery} /> diff --git a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx index f94734d7af..94a62c05d2 100644 --- a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx +++ b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx @@ -249,6 +249,19 @@ interface GalleryModalProps { url: string, media_id?: string, ) => Promise | void; + onRecreateClicked?: (data: { + prompt: string | null; + mediaClass: string | undefined; + modelType: string | null; + contextImages: Array<{ + media_links: { + cdn_url: string; + maybe_thumbnail_template: string; + }; + media_token: string; + semantic: string; + }> | null; + }) => void; } // --- Constants (never re-created) --- @@ -362,6 +375,7 @@ export const GalleryModal = React.memo( onRemoveBackgroundClicked, onMake3DObjectClicked, onMake3DWorldClicked, + onRecreateClicked, }: GalleryModalProps) => { const [loading, setLoading] = useState(false); // Separate state for pagination spinner (bottom of list) — not shared with background refresh @@ -1296,6 +1310,7 @@ export const GalleryModal = React.memo( onRemoveBackgroundClicked={onRemoveBackgroundClicked} onMake3DObjectClicked={onMake3DObjectClicked} onMake3DWorldClicked={onMake3DWorldClicked} + onRecreateClicked={onRecreateClicked} onNavigatePrev={handleNavigatePrev} onNavigateNext={handleNavigateNext} onNavigateToMedia={handleNavigateToMedia} diff --git a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx index 88f5133d3f..219e9368f7 100644 --- a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx +++ b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx @@ -14,6 +14,7 @@ import { faTrashCan, faVideo, faWandMagicSparkles, + faArrowRotateRight, } from "@fortawesome/pro-solid-svg-icons"; import { MediaFileDelete } from "@storyteller/tauri-api"; import { LoadingSpinner } from "@storyteller/ui-loading-spinner"; @@ -90,6 +91,19 @@ interface LightboxModalProps { url: string, media_id?: string, ) => Promise | void; + onRecreateClicked?: (data: { + prompt: string | null; + mediaClass: string | undefined; + modelType: string | null; + contextImages: Array<{ + media_links: { + cdn_url: string; + maybe_thumbnail_template: string; + }; + media_token: string; + semantic: string; + }> | null; + }) => void; batchImageToken?: string; onNavigatePrev?: () => void; onNavigateNext?: () => void; @@ -119,6 +133,7 @@ export function LightboxModal({ onRemoveBackgroundClicked, onMake3DObjectClicked, onMake3DWorldClicked, + onRecreateClicked, batchImageToken, onNavigatePrev, onNavigateNext, @@ -929,12 +944,35 @@ export function LightboxModal({ {/* buttons with spacing */} {actionUrl && ( -
+
+ {onRecreateClicked && + hasPromptToken && + (derivedMediaClass === "image" || + derivedMediaClass === "video") && ( + + )} + {onEditClicked && actionUrl && derivedMediaClass === "image" && (