Write base image uploader logic

This commit is contained in:
Tanish Manku committed 2025-07-28 16:18:38 -07:00
1 parent 2081e1fa30
commit 34b8ac81ec
4 files changed
+176 -99

No files matched your search

@@ -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<HTMLInputElement>(null);
const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false);
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
[]
);
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<HTMLInputElement>) => {
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 (
<div className="flex items-center justify-center bg-ui-panel border-ui-panel-border border-4">
<FontAwesomeIcon icon={faImage} className="text-6xl text-ui-text-secondary" />
<span className="text-ui-text-secondary text-lg ml-2">Click to upload or drag and drop an image here to edit</span>
<div className="mt-4">
<>
<div className="h-1/2 w-1/2 flex items-center justify-center bg-ui-background border-ui-panel-border border-8">
{isLoading ? (
<div className="flex flex-col items-center gap-4">
<span>Uploading image...</span>
<LoadingSpinner />
</div>
) :
<div className="flex flex-col gap-8">
<FontAwesomeIcon icon={faImage} className="text-6xl text-ui-controls" />
<span className="text-ui-controls text-lg ml-2">Click to upload or drag and drop an image here to edit</span>
<div className="mt-4 flex justify-center gap-2">
<input
type="file"
ref={fileInputRef}
className="hidden"
accept="image/*"
onChange={handleFileUpload}
multiple
/>
<Button
variant="primary"
onClick={handleUploadClick}
>
Upload an image
</Button>
<Button
variant="secondary"
onClick={handleGalleryClick}
>
Select from library
</Button>
</div>
</div>
}
</div>
</div>
<GalleryModal
isOpen={!!isGalleryModalOpen}
onClose={handleGalleryClose}
mode="select"
selectedItemIds={selectedGalleryImages}
onSelectItem={handleImageSelect}
maxSelections={MAX_GALLERY_SELECTIONS}
onUseSelected={handleUseGalleryImages}
onDownloadClicked={downloadFileFromUrl}
forceFilter="image"
/>
</>
)
}
@@ -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<Konva.Layer>({} as Konva.Layer);
const rectRef = useRef<Konva.Rect>({} as Konva.Rect);
const transformerRefs = useRef<{ [key: string]: Konva.Transformer }>({});
const [baseImage, setBaseImage] = useState<BaseSelectorImage | null>(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 (
<div className={"h-screen w-full flex items-center justify-center bg-ui-panel"}>
<BaseImageSelector
onImageSelect={(image: BaseSelectorImage) => {
setBaseImage(image);
}}
/>
</div>
)
}
return (
<>
<div
@@ -230,99 +245,11 @@ const PageEdit = () => {
}`}
>
<PromptEditor
initialPrompt=""
onPromptChange={(prompt: string) => {
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}
/>
</div>
<SideToolbar
className="fixed left-0 top-1/2 z-10 -translate-y-1/2 transform"
onSelect={(): void => {
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}
/>
<div className="relative z-0">
<ContextMenuContainer
onAction={(e, action) => {
@@ -18,7 +18,7 @@ interface TabState {
}
export const useTabStore = create<TabState>((set, get) => ({
activeTabId: '2D',
activeTabId: 'EDIT',
tabData: {},
setActiveTab: async (newTabId) => {
@@ -559,8 +559,8 @@ export const PromptBox2D = ({
onChange={handleChange}
onPaste={handlePaste}
onKeyDown={handleKeyDown}
onFocus={() => {}}
onBlur={() => {}}
onFocus={() => { }}
onBlur={() => { }}
/>
</div>
<div className="mt-2 flex items-center justify-between gap-2">