mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
reprompt (recreate) button (#1194)
<img width="1219" height="790"
alt="{A15AF77F-C555-4C85-BC15-B4B8D44D2248}"
src="https://github.com/user-attachments/assets/ec4b52a2-68c4-42b0-88fc-00ee3b322321"
/>
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.
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
<ProviderSetupModal />
|
||||
|
||||
@@ -249,6 +249,19 @@ interface GalleryModalProps {
|
||||
url: string,
|
||||
media_id?: string,
|
||||
) => Promise<void> | 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}
|
||||
|
||||
@@ -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> | 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 && (
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
<div className="mt-4 grid grid-cols-2 gap-1.5">
|
||||
{onRecreateClicked &&
|
||||
hasPromptToken &&
|
||||
(derivedMediaClass === "image" ||
|
||||
derivedMediaClass === "video") && (
|
||||
<Button
|
||||
className="w-full col-span-2 py-1.5 text-[13px]"
|
||||
variant="primary"
|
||||
icon={faArrowRotateRight}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
gtagEvent("recreate_clicked");
|
||||
onRecreateClicked({
|
||||
prompt,
|
||||
mediaClass: derivedMediaClass,
|
||||
modelType,
|
||||
contextImages,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Recreate
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onEditClicked &&
|
||||
actionUrl &&
|
||||
derivedMediaClass === "image" && (
|
||||
<Button
|
||||
className="w-full"
|
||||
className="w-full py-1.5 text-[13px]"
|
||||
variant="primary"
|
||||
icon={faPencil}
|
||||
onClick={async (e) => {
|
||||
@@ -951,7 +989,7 @@ export function LightboxModal({
|
||||
actionUrl &&
|
||||
derivedMediaClass === "image" && (
|
||||
<Button
|
||||
className="w-full"
|
||||
className="w-full py-1.5 text-[13px]"
|
||||
variant="primary"
|
||||
icon={faVideo}
|
||||
onClick={async (e) => {
|
||||
@@ -971,7 +1009,7 @@ export function LightboxModal({
|
||||
actionUrl &&
|
||||
derivedMediaClass === "image" && (
|
||||
<Button
|
||||
className="w-full"
|
||||
className="w-full py-1.5 text-[13px]"
|
||||
variant="secondary"
|
||||
icon={faWandMagicSparkles}
|
||||
onClick={async (e) => {
|
||||
@@ -993,7 +1031,7 @@ export function LightboxModal({
|
||||
actionUrl &&
|
||||
derivedMediaClass === "image" && (
|
||||
<Button
|
||||
className="w-full"
|
||||
className="w-full py-1.5 text-[13px]"
|
||||
variant="secondary"
|
||||
icon={faCube}
|
||||
onClick={async (e) => {
|
||||
@@ -1015,7 +1053,7 @@ export function LightboxModal({
|
||||
actionUrl &&
|
||||
derivedMediaClass === "image" && (
|
||||
<Button
|
||||
className="w-full"
|
||||
className="w-full py-1.5 text-[13px]"
|
||||
variant="secondary"
|
||||
icon={faGlobe}
|
||||
onClick={async (e) => {
|
||||
@@ -1037,8 +1075,8 @@ export function LightboxModal({
|
||||
<Button
|
||||
className={
|
||||
derivedMediaClass === "image"
|
||||
? "w-full"
|
||||
: "w-full col-span-2"
|
||||
? "w-full py-1.5 text-[13px]"
|
||||
: "w-full col-span-2 py-1.5 text-[13px]"
|
||||
}
|
||||
variant="secondary"
|
||||
icon={faDownToLine}
|
||||
@@ -1054,7 +1092,7 @@ export function LightboxModal({
|
||||
|
||||
{onAddToSceneClicked && actionUrl && (
|
||||
<Button
|
||||
className="w-full"
|
||||
className="w-full py-1.5 text-[13px]"
|
||||
variant="secondary"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -1070,7 +1108,7 @@ export function LightboxModal({
|
||||
|
||||
{selectedMediaToken && (
|
||||
<Button
|
||||
className="w-full"
|
||||
className="w-full py-1.5 text-[13px]"
|
||||
variant="secondary"
|
||||
icon={shareCopied ? faCheck : faLink}
|
||||
onClick={async (e) => {
|
||||
@@ -1103,7 +1141,7 @@ export function LightboxModal({
|
||||
{selectedMediaToken && onDeleteClicked && (
|
||||
<Button
|
||||
icon={faTrashCan}
|
||||
className="w-full"
|
||||
className="w-full py-1.5 text-[13px]"
|
||||
variant="destructive"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
Reference in New Issue
Block a user