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") && ( + { + e.stopPropagation(); + gtagEvent("recreate_clicked"); + onRecreateClicked({ + prompt, + mediaClass: derivedMediaClass, + modelType, + contextImages, + }); + }} + > + Recreate + + )} + {onEditClicked && actionUrl && derivedMediaClass === "image" && ( { @@ -951,7 +989,7 @@ export function LightboxModal({ actionUrl && derivedMediaClass === "image" && ( { @@ -971,7 +1009,7 @@ export function LightboxModal({ actionUrl && derivedMediaClass === "image" && ( { @@ -993,7 +1031,7 @@ export function LightboxModal({ actionUrl && derivedMediaClass === "image" && ( { @@ -1015,7 +1053,7 @@ export function LightboxModal({ actionUrl && derivedMediaClass === "image" && ( { @@ -1037,8 +1075,8 @@ export function LightboxModal({ { e.stopPropagation(); @@ -1070,7 +1108,7 @@ export function LightboxModal({ {selectedMediaToken && ( { @@ -1103,7 +1141,7 @@ export function LightboxModal({ {selectedMediaToken && onDeleteClicked && ( { e.stopPropagation();