diff --git a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx
index 2ab2ca2ba6..45d8e79eba 100644
--- a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx
+++ b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TopBar.tsx
@@ -22,6 +22,7 @@ import {
GalleryModal,
galleryModalVisibleViewMode,
galleryModalVisibleDuringDrag,
+ galleryModalLightboxVisible,
} from "@storyteller/ui-gallery-modal";
import { SettingsModal } from "@storyteller/ui-settings-modal";
import { Tooltip } from "@storyteller/ui-tooltip";
@@ -48,6 +49,8 @@ import {
useTauriWindowControls,
useTauriPlatform,
} from "@storyteller/tauri-utils";
+import { useEditStore } from "../../../pages/PageEdit/stores/EditState";
+import { BaseSelectorImage } from "../../../pages/PageEdit/BaseImageSelector";
interface Props {
pageName: string;
@@ -137,6 +140,30 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
}
};
+ const handleEditFromGallery = async (url: string, mediaId?: string) => {
+ try {
+ // Reset editor state
+ useEditStore.getState().RESET();
+
+ // Switch to EDIT tab
+ useTabStore.getState().setActiveTab("EDIT");
+
+ // Select the image for editing
+ const baseImage: BaseSelectorImage = {
+ url,
+ mediaToken: mediaId || "",
+ };
+ useEditStore.getState().setBaseImageInfo(baseImage);
+
+ // Close gallery modal and lightbox if open
+ galleryModalVisibleViewMode.value = false;
+ galleryModalVisibleDuringDrag.value = false;
+ galleryModalLightboxVisible.value = false;
+ } catch (e) {
+ // no-op
+ }
+ };
+
const getPageTitle = (): string => {
switch (tabStore.activeTabId) {
case "2D":
@@ -171,7 +198,9 @@ export const TopBar = ({ pageName, loginSignUpPressed }: Props) => {
>
-
ArtCraft
+
+ ArtCraft
+

{
globalAccountLogoutCallback={() => setLogoutStates()}
/>
-
+
>
);
};
diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/HistoryStack.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/HistoryStack.tsx
index 14445f795c..993f74c578 100644
--- a/frontend/apps/editor3d/app/src/pages/PageEdit/HistoryStack.tsx
+++ b/frontend/apps/editor3d/app/src/pages/PageEdit/HistoryStack.tsx
@@ -1,7 +1,8 @@
import { Button } from "@storyteller/ui-button";
-import { EditedImage, useImageEditCompleteEvent } from "@storyteller/tauri-events";
+import { useImageEditCompleteEvent } from "@storyteller/tauri-events";
import { faTrash } from "@fortawesome/pro-solid-svg-icons";
import { useState } from "react";
+import { twMerge } from "tailwind-merge";
import { BaseSelectorImage } from "./BaseImageSelector";
export interface ImageBundle {
@@ -16,43 +17,74 @@ interface HistoryStackProps {
export const HistoryStack = ({
onClear,
- onImageSelect = () => { },
+ onImageSelect = () => {},
startingBundles = [],
}: HistoryStackProps) => {
-
- const [imageBundles, setImageBundles] = useState
(startingBundles);
+ const [imageBundles, setImageBundles] =
+ useState(startingBundles);
+ const [selectedImageToken, setSelectedImageToken] = useState(
+ startingBundles.length > 0 && startingBundles[0].images.length > 0
+ ? startingBundles[0].images[0].mediaToken
+ : null,
+ );
useImageEditCompleteEvent(async (event) => {
const newBundle: ImageBundle = {
- images: event.edited_images.map((editedImage) => ({
- url: editedImage.cdn_url,
- mediaToken: editedImage.media_token,
- } as BaseSelectorImage)),
+ images: event.edited_images.map(
+ (editedImage) =>
+ ({
+ url: editedImage.cdn_url,
+ mediaToken: editedImage.media_token,
+ }) as BaseSelectorImage,
+ ),
};
- imageBundles.push(newBundle);
- setImageBundles(imageBundles);
+ setImageBundles([...imageBundles, newBundle]);
+ const newlySelected = newBundle.images[0];
+ if (newlySelected) {
+ setSelectedImageToken(newlySelected.mediaToken);
+ onImageSelect(newlySelected);
+ }
});
const handleClear = () => {
setImageBundles(startingBundles);
+ setSelectedImageToken(null);
onClear();
- }
+ };
return (
-
-
-
+
+
+
{imageBundles.map((bundle) => (
<>
-
+
{bundle.images.map((image, imgIndex) => (
-
);
-}
+};
diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/LeftResetStack.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/LeftResetStack.tsx
new file mode 100644
index 0000000000..cbca4328fb
--- /dev/null
+++ b/frontend/apps/editor3d/app/src/pages/PageEdit/LeftResetStack.tsx
@@ -0,0 +1,61 @@
+import React from "react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faXmark, faTrashXmark } from "@fortawesome/pro-solid-svg-icons";
+import { Tooltip } from "@storyteller/ui-tooltip";
+import {
+ showActionReminder,
+ isActionReminderOpen,
+} from "@storyteller/ui-action-reminder-modal";
+
+interface LeftResetStackProps {
+ onReset: () => void;
+ className?: string;
+}
+
+const LeftResetStack: React.FC
= ({
+ onReset,
+ className = "",
+}) => {
+ return (
+
+
+
+
+ showActionReminder({
+ reminderType: "default",
+ title: "Reset All",
+ primaryActionIcon: faTrashXmark,
+ primaryActionBtnClassName: "bg-red hover:bg-red/80",
+ message: (
+
+ Are you sure you want to reset all? This will clear all your
+ work and cannot be undone.
+
+ ),
+ primaryActionText: "Reset all",
+ onPrimaryAction: () => {
+ onReset();
+ isActionReminderOpen.value = false;
+ },
+ })
+ }
+ >
+
+
+
+
+
+ );
+};
+
+export default LeftResetStack;
diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx
index 1ca9f7b3b9..bcb39f9b70 100644
--- a/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx
+++ b/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx
@@ -23,6 +23,7 @@ import {
import { ModelInfo } from "@storyteller/model-list";
import { useImageEditCompleteEvent } from "@storyteller/tauri-events";
import { HistoryStack, ImageBundle } from "./HistoryStack";
+import LeftResetStack from "./LeftResetStack";
const PAGE_ID: ModelPage = ModelPage.ImageEditor;
@@ -140,7 +141,6 @@ const PageEdit = () => {
};
}, [store]);
-
const onFitPressed = useCallback(async () => {
// Get the stage and its container dimensions
const stage = stageRef.current;
@@ -188,7 +188,6 @@ const PageEdit = () => {
// When baseImageBitmap changes from null to an actual image, call onFitPressed
// Use a slight delay to ensure the image dimensions are properly loaded
onFitPressed();
-
}, [onFitPressed, store.baseImageBitmap]);
// Create a function to use the left layer ref and download the bitmap from it
@@ -228,37 +227,45 @@ const PageEdit = () => {
return new Uint8Array(arrayBuffer);
};
- const handleGenerate = useCallback(async (prompt: string) => {
- if (isEnqueuing) {
- console.warn("Already enqueuing an image, please wait.");
- return;
- }
+ const handleGenerate = useCallback(
+ async (prompt: string) => {
+ if (isEnqueuing) {
+ console.warn("Already enqueuing an image, please wait.");
+ return;
+ }
- setIsEnqueuing(true);
+ setIsEnqueuing(true);
- const editedImageToken = store.baseImageInfo?.mediaToken;
+ const editedImageToken = store.baseImageInfo?.mediaToken;
- if (!editedImageToken) {
- console.error("Base image is not available");
- return;
- }
+ if (!editedImageToken) {
+ console.error("Base image is not available");
+ return;
+ }
- // TODO: Call inference API here
- const arrayBuffer = await getMaskArrayBuffer();
+ // TODO: Call inference API here
+ const arrayBuffer = await getMaskArrayBuffer();
- try {
- await EnqueueImageInpaint({
- model: selectedModelInfo,
- image_media_token: editedImageToken,
- mask_image_raw_bytes: arrayBuffer,
- prompt: prompt,
- image_count: generationCount,
- frontend_caller: "image_editor",
- });
- } finally {
- setIsEnqueuing(false);
- }
- }, [generationCount, isEnqueuing, selectedModelInfo, store.baseImageInfo?.mediaToken]);
+ try {
+ await EnqueueImageInpaint({
+ model: selectedModelInfo,
+ image_media_token: editedImageToken,
+ mask_image_raw_bytes: arrayBuffer,
+ prompt: prompt,
+ image_count: generationCount,
+ frontend_caller: "image_editor",
+ });
+ } finally {
+ setIsEnqueuing(false);
+ }
+ },
+ [
+ generationCount,
+ isEnqueuing,
+ selectedModelInfo,
+ store.baseImageInfo?.mediaToken,
+ ],
+ );
// Display image selector on launch, otherwise hide it
// Also show loading state if info is set but image is loading
@@ -284,8 +291,9 @@ const PageEdit = () => {
return (
<>
{
/>
+ store.RESET()} />
+
+
{ store.RESET(); }}
+ onClear={() => {
+ store.RESET();
+ }}
startingBundles={[{ images: [store.baseImageInfo] } as ImageBundle]}
onImageSelect={(baseImage) => {
store.clearLineNodes();
- store.setBaseImageInfo(baseImage)
+ store.setBaseImageInfo(baseImage);
}}
/>
Promise;
isOpen?: boolean;
forceFilter?: string;
+ onEditClicked?: (url: string, media_id?: string) => Promise | void;
}
export const GalleryModal = React.memo(
@@ -86,6 +87,7 @@ export const GalleryModal = React.memo(
onAddToSceneClicked,
isOpen,
forceFilter,
+ onEditClicked,
}: GalleryModalProps) => {
const [loading, setLoading] = useState(false);
// Lightbox state is now handled via signals
@@ -658,6 +660,7 @@ export const GalleryModal = React.memo(
onDownloadClicked={onDownloadClicked}
onAddToSceneClicked={onAddToSceneClicked}
mediaClass={lightboxImageSignal.value?.mediaClass}
+ onEditClicked={onEditClicked}
/>
)}
>
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 acd1a97b61..7365c75144 100644
--- a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx
+++ b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx
@@ -1,7 +1,7 @@
import { Modal } from "@storyteller/ui-modal";
import { Button } from "@storyteller/ui-button";
import dayjs from "dayjs";
-import { faDownToLine } from "@fortawesome/pro-solid-svg-icons";
+import { faDownToLine, faPencil } from "@fortawesome/pro-solid-svg-icons";
import {
EnqueueImageTo3dObject,
EnqueueImageTo3dObjectModel,
@@ -40,6 +40,7 @@ interface LightboxModalProps {
) => Promise;
mediaClass?: string;
onPromptCopy?: (prompt: string) => void;
+ onEditClicked?: (url: string, media_id?: string) => Promise | void;
}
export function LightboxModal({
@@ -57,6 +58,7 @@ export function LightboxModal({
onDownloadClicked,
onAddToSceneClicked,
mediaClass,
+ onEditClicked,
}: LightboxModalProps) {
// NB(bt,2025-06-14): We add ?cors=1 to the image url to prevent caching "sec-fetch-mode: no-cors" from
// the tag request from being cached. If we then drag it into the canvas after it's been cached,
@@ -402,6 +404,19 @@ export function LightboxModal({
3D
+ {onEditClicked && downloadUrl && (
+ {
+ e.stopPropagation();
+ gtagEvent("edit_image_clicked");
+ await onEditClicked(downloadUrl, mediaId);
+ }}
+ >
+ Edit
+
+ )}
+
{onAddToSceneClicked && downloadUrl && (
{