diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/BaseImageSelector.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/BaseImageSelector.tsx index 6e36664c53..f733d3f757 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/BaseImageSelector.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/BaseImageSelector.tsx @@ -1,20 +1,170 @@ import { faImage } from "@fortawesome/pro-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { downloadFileFromUrl } from "libs/api/src/lib/LocalApi"; +import Button from "node_modules/@storyteller/ui-button/src/lib/button"; +import GalleryModal, { GalleryItem } from "node_modules/@storyteller/ui-gallery-modal/src/lib/gallery-modal"; +import LoadingSpinner from "node_modules/@storyteller/ui-loading-spinner/src/lib/loading-spinner"; +import { ReferenceImage } from "node_modules/@storyteller/ui-promptbox/src/lib/PromptBox2D"; +import { useRef, useState } from "react"; +import toast from "react-hot-toast"; +import { uploadImage } from "~/components/reusable/UploadModalMedia/uploadImage"; +import { UploaderStates } from "~/enums"; -export type BaseImageSelectorProps = { - onImageSelect: (imageUrl: string) => void; +export interface BaseSelectorImage { + id: string; + url: string; + mediaToken: string; } -const BaseImageSelector = ({ +export type BaseImageSelectorProps = { + onImageSelect: (imageUrl: BaseSelectorImage) => void; +} + +const MAX_GALLERY_SELECTIONS = 1; + +export const BaseImageSelector = ({ onImageSelect }: BaseImageSelectorProps) => { + const fileInputRef = useRef(null); + const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false); + const [selectedGalleryImages, setSelectedGalleryImages] = useState( + [] + ); + const [isLoading, setIsLoading] = useState(false); + + const handleGalleryClick = () => setIsGalleryModalOpen(true); + + const handleGalleryClose = () => { + setIsGalleryModalOpen(false); + setSelectedGalleryImages([]); + }; + + const handleImageSelect = (id: string) => { + // If already selected, deselect it + // Else if not selected and under max limit, select it + if (selectedGalleryImages.includes(id)) { + setSelectedGalleryImages([]); + } else { + setSelectedGalleryImages([id]); + } + } + + const handleUseGalleryImages = (selectedItems: GalleryItem[]) => { + // We only want one file + if (selectedItems.length !== 1) { + return; + } + + const item = selectedItems[0]; + if (!item.fullImage) return; + const referenceImage: BaseSelectorImage = { + id: Math.random().toString(36).substring(7), + url: item.fullImage, + mediaToken: item.id, + }; + sendImageEvent(referenceImage); + } + + const handleUploadClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + } + + const handleFileUpload = (event: React.ChangeEvent) => { + const files = event.target.files; + if (files) { + setIsLoading(true); + + Array.from(files).forEach((file) => { + const reader = new FileReader(); reader.onloadend = () => { + uploadImage({ + title: `reference-image-${Math.random() + .toString(36) + .substring(2, 15)}`, + assetFile: file, + progressCallback: (newState) => { + console.debug("Upload progress:", newState.data); + if (newState.status === UploaderStates.success && newState.data) { + const referenceImage: BaseSelectorImage = { + id: Math.random().toString(36).substring(7), + url: reader.result as string, + mediaToken: newState.data || "", + }; + + toast.success("Image uploaded successfully!"); + sendImageEvent(referenceImage); + } else if ( + newState.status === UploaderStates.assetError || + newState.status === UploaderStates.imageCreateError + ) { + toast.error("Upload failed. Please try again."); + setIsLoading(false); + } + }, + }); + }; + + reader.readAsDataURL(file); + }); + } + }; + + const sendImageEvent = (image: ReferenceImage) => { + handleGalleryClose(); + onImageSelect(image); + } + return ( -
- - Click to upload or drag and drop an image here to edit -
+ <> +
+ {isLoading ? ( +
+ Uploading image... + +
+ ) : +
+ + Click to upload or drag and drop an image here to edit +
+ + + + +
+
+ }
-
+ + ) } diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx index c8be5b88ff..a24e1ec537 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx @@ -21,6 +21,7 @@ import { AspectRatioType } from "../PageDraw/stores/SceneState"; import { ActiveEditTool, useEditStore } from "./stores/EditState"; import { EditPaintSurface } from "./EditPaintSurface"; import { normalizeCanvas } from "~/Helpers/CanvasHelpers"; +import { BaseImageSelector, BaseSelectorImage } from "./BaseImageSelector"; const PageEdit = () => { //useStateSceneLoader(); @@ -36,6 +37,7 @@ const PageEdit = () => { const leftPanelRef = useRef({} as Konva.Layer); const rectRef = useRef({} as Konva.Rect); const transformerRefs = useRef<{ [key: string]: Konva.Transformer }>({}); + const [baseImage, setBaseImage] = useState(null); // Use the Zustand store const store = useEditStore(); @@ -223,6 +225,19 @@ const PageEdit = () => { fittedCanvas.toBlob(downloadCallback, "image/png", 1.0); }; + // Display image selector on launch, otherwise hide it + if (!baseImage) { + return ( +
+ { + setBaseImage(image); + }} + /> +
+ ) + } + return ( <>
{ }`} > { - console.log("Prompt changed:", prompt); - // Handle prompt changes here - }} - onRandomize={() => { - console.log("Randomize clicked"); - // Handle randomize action here - }} - onVary={() => { - console.log("Vary clicked"); - // Handle vary action here - }} onModeChange={(mode: string) => { store.setActiveTool(mode as ActiveEditTool) }} selectedMode={store.activeTool} - onEnqueuePressed={onEnqueuedPressed} onGenerateClick={downloadLeftPanelBitmap} />
- { - store.setActiveTool("select"); - }} - onAddShape={(shape: "rectangle" | "circle" | "triangle"): void => { - }} - onPaintBrush={(hex: string, size: number, opacity: number): void => { - // store.setActiveTool("draw"); - // store.setBrushColor(hex); - // store.setBrushSize(size); - // store.setBrushOpacity(opacity); - }} - onEraser={(size: number): void => { - // store.setActiveTool("eraser"); - // store.setBrushSize(size); - }} - onCanvasBackground={(hex: string): void => { - console.log("Canvas background activated", { color: hex }); - // Add background change logic here - // TODO: minor bug needs to update the preview panel - // Debounce also causes issues with real time color change. - store.setFillColor(hex); - }} - onGenerateImage={(): void => { - console.log("Generate image activated"); - // Add image generation logic here - }} - onUploadImage={(): void => { - // Create input element dynamically like in PromptEditor - console.log("Upload image activated"); - const input = document.createElement("input"); - input.type = "file"; - input.accept = "image/*"; - input.multiple = true; - input.style.display = "none"; // Hide the input - - // Attach to DOM - document.body.appendChild(input); - - input.onchange = (e: Event) => { - console.log("File change event triggered"); - const target = e.target as HTMLInputElement; - if (target.files) { - const files = Array.from(target.files); - console.log("Selected files:", files); - const imageFiles = files.filter((file) => - file.type.startsWith("image/"), - ); - console.log("Filtered image files:", imageFiles); - - if (imageFiles.length > 0) { - console.log("Uploading images:", imageFiles); - handleImageUpload(imageFiles); - } else { - console.log("No valid image files selected"); - } - } else { - console.log("No files selected"); - } - // Clean up: remove input from DOM after use - document.body.removeChild(input); - }; - - // Reset value before click (for same file selection) - input.value = ""; - input.click(); - }} - onDelete={(): void => { - // This onDelete prop for SideToolbar might still be needed for the button - store.deleteSelectedItems(); - }} - activeToolId={store.activeTool} - />
{ diff --git a/frontend/apps/editor3d/app/src/pages/Stores/TabState.ts b/frontend/apps/editor3d/app/src/pages/Stores/TabState.ts index cd2c2d07d0..d6f1fd7964 100644 --- a/frontend/apps/editor3d/app/src/pages/Stores/TabState.ts +++ b/frontend/apps/editor3d/app/src/pages/Stores/TabState.ts @@ -18,7 +18,7 @@ interface TabState { } export const useTabStore = create((set, get) => ({ - activeTabId: '2D', + activeTabId: 'EDIT', tabData: {}, setActiveTab: async (newTabId) => { diff --git a/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx b/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx index e2b2e7c459..b4678f72fb 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx @@ -559,8 +559,8 @@ export const PromptBox2D = ({ onChange={handleChange} onPaste={handlePaste} onKeyDown={handleKeyDown} - onFocus={() => {}} - onBlur={() => {}} + onFocus={() => { }} + onBlur={() => { }} />