Compare commits

...
Author SHA1 Message Date
Michael Chung 54d5c53c79 Making video tab not available for merge into main. 2025-05-18 20:37:21 -04:00
Michael Chung b507c7a6ba Adding Image to Video Panel 2025-05-18 20:33:16 -04:00
Michael Chung 77f4ef02f2 Merge branch 'main' into feature/video-addition 2025-05-18 15:46:09 -04:00
Michael Chung ad8b81845a Moved the 2d editor to away to the old frontend_old deprecated folder.
Reinstalled konva and react-colorful.
Added a new video promptbox.
2025-05-18 15:18:36 -04:00
542 changed files with 679 additions and 4783 deletions
@@ -19,7 +19,6 @@ import { Signal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime";
import { setLogoutStates } from "~/signals/authentication/utilities";
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
import { useAppUiContext } from "~/pages/Page2d/contextSignals/appUi";
function isEditorPath(path: string) {
if (path === "/") return true;
@@ -13,11 +13,13 @@ export const DialogAddImage = ({
isOpen,
closeCallback,
onAddImage,
isProcessing = false,
}: {
stagedImage?: File | null;
isOpen: boolean;
closeCallback: () => void;
onAddImage: (file: File) => void;
isProcessing?: boolean;
}) => {
const [assetFile, setAssetFile] = useState<File | null>(null);
const previouslyStagedImageRef = useRef<File | null>(null);
@@ -37,10 +39,13 @@ export const DialogAddImage = ({
closeCallback();
}
// Show dialog if no image is set (currFile is null) and not processing
const shouldShowDialog = isOpen && (!currFile || !isProcessing);
return (
<Modal isOpen={isOpen} onClose={closeCallback}>
<Modal isOpen={shouldShowDialog} onClose={closeCallback}>
<div className="flex flex-col gap-3">
<DialogTitle className="text-2xl font-bold">Upload Image</DialogTitle>
<h1 className="text-2xl font-bold">Upload Image</h1>
<div className="flex flex-col rounded-lg border-2 border-dashed border-white/20">
<FileUploader
fileTypes={Object.values(IMAGE_FILE_TYPE)}
@@ -1,59 +1,55 @@
import { useState, useEffect, useRef } from "react";
import { PromptBox2D } from "@storyteller/ui-promptbox";
import { UploaderState } from "libs/common/src/lib/interfaces";
import {
MaybeCanvasRenderBitmapType,
JobContextType,
} from "libs/common/src/lib/types";
import { useState, useRef } from "react";
import { JobContextType } from "libs/common/src/lib/types";
import { PromptBoxVideo } from "@storyteller/ui-promptbox";
const ImageToVideo = () => {
const containerRef = useRef<HTMLDivElement>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isImageBeingProcessed, setIsImageBeingProcessed] = useState(false);
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const [imagePreviewUrl, setImagePreviewUrl] = useState<string>("");
const uploadImage = ({
title,
assetFile,
progressCallback,
}: {
title: string;
assetFile: File;
progressCallback: (newState: UploaderState) => void;
}): Promise<void> => {
throw new Error("Function not implemented.");
const handleClose = () => {
setIsDialogOpen(false);
};
const getCanvasRenderBitmap = (): MaybeCanvasRenderBitmapType => {
throw new Error("Function not implemented.");
const handleAddImage = (image: File) => {
setSelectedImage(image);
setIsImageBeingProcessed(true);
setImagePreviewUrl(URL.createObjectURL(image));
// TODO: Implement actual image processing here
// For now, we'll just simulate processing
setTimeout(() => {
setIsImageBeingProcessed(false);
}, 3000);
};
const EncodeImageBitmapToBase64 = (
imageBitmap: ImageBitmap,
): Promise<string> => {
throw new Error("Function not implemented.");
};
const useJobContext = (): JobContextType => {
throw new Error("Function not implemented.");
const jobContext: JobContextType = {
jobTokens: [],
addJobToken: () => {},
removeJobToken: () => {},
clearJobTokens: () => {},
};
return (
<div
ref={containerRef}
className="flex h-[calc(100vh-56px)] w-full bg-ui-background"
className="flex h-[calc(100vh-56px)] w-full rounded-lg bg-ui-background"
>
<div className="h-full w-1/2 p-4">
<div className="flex h-full w-full items-center justify-center rounded-md bg-gray-800">
<div className="text-white text-opacity-50">Image Display</div>
<div className="relative flex h-full w-full items-center justify-center">
<img
src=""
alt="Preview"
className="max-h-full max-w-full object-contain"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = "none";
}}
/>
</div>
<div className="h-full w-full p-4">
<div className="flex h-full w-full flex-col items-center justify-center rounded-md bg-gray-800">
<PromptBoxVideo
useJobContext={() => {
// TODO: Implement job context logic
console.log("Using job context");
return jobContext;
}}
onEnqueuePressed={async () => {
// TODO: Implement enqueue logic
console.log("Enqueue pressed");
}}
/>
</div>
</div>
</div>
@@ -1,5 +1,6 @@
import { signal } from "@preact/signals-react";
// change starting tab
export const appTabId = signal("3D");
export const setAppTabId = (newId: string) => {
+2
View File
@@ -58,9 +58,11 @@
"detect-gpu": "^5.0.38",
"http-proxy-middleware": "^3.0.0",
"isbot": "^4.1.0",
"konva": "^9.3.20",
"lodash.debounce": "^4.0.8",
"posthog-js": "^1.141.0",
"react": "^18.2.0",
"react-colorful": "^5.6.1",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
"react-drag-drop-files": "^2.3.10",
@@ -1,2 +1,3 @@
export * from "./lib/PromptBox2D";
export * from "./lib/PromptBox3D";
export * from "./lib/PromptBoxVideo";
@@ -32,7 +32,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
import { PromptsApi } from "@storyteller/api";
import { CommandSuccessStatus, GetAppPreferences, SoraImageRemix, SoraImageRemixErrorType } from "@storyteller/tauri-api";
import {
CommandSuccessStatus,
GetAppPreferences,
SoraImageRemix,
SoraImageRemixErrorType,
} from "@storyteller/tauri-api";
import { SoundRegistry } from "@storyteller/soundboard";
interface ReferenceImage {
@@ -291,9 +296,7 @@ export const PromptBox2D = ({
snapshot_media_token: snapshotMediaToken.data,
disable_system_prompt: !useSystemPrompt,
prompt: prompt,
maybe_additional_images: referenceImages.map(
(image) => image.mediaToken,
),
maybe_additional_images: referenceImages.map((image) => image.mediaToken),
maybe_number_of_samples: 1,
});
@@ -307,7 +310,6 @@ export const PromptBox2D = ({
registry.playSound(soundName);
}
toast.success("Image added to queue");
} else {
const prefs = await GetAppPreferences();
const soundName = prefs.preferences?.enqueue_failure_sound;
@@ -321,16 +323,19 @@ export const PromptBox2D = ({
if ("error_type" in generateResponse) {
switch (generateResponse.error_type) {
case SoraImageRemixErrorType.SoraLoginRequired:
errorMessage = "You need to log into Sora to continue. See the settings menu.";
errorMessage =
"You need to log into Sora to continue. See the settings menu.";
break;
case SoraImageRemixErrorType.ServerError:
errorMessage = "Server error. Failed to enqueue image.";
break;
case SoraImageRemixErrorType.TooManyConcurrentTasks:
errorMessage = "You have too many Sora image generation tasks running. Please wait a moment.";
errorMessage =
"You have too many Sora image generation tasks running. Please wait a moment.";
break;
case SoraImageRemixErrorType.SoraUsernameNotYetCreated:
errorMessage = "Your Sora username is not yet created. Please visit the Sora.com website to create it first.";
errorMessage =
"Your Sora username is not yet created. Please visit the Sora.com website to create it first.";
break;
case SoraImageRemixErrorType.SoraIsHavingProblems:
errorMessage = "Sora is having problems. Please try again later.";
@@ -339,7 +344,6 @@ export const PromptBox2D = ({
}
toast.error(errorMessage);
}
};
const handleWebEnqueue = async () => {
@@ -0,0 +1,614 @@
import { useState, useRef, useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { toast } from "@storyteller/ui-toaster";
import {
JobContextType,
MaybeCanvasRenderBitmapType,
UploaderState,
UploaderStates,
} from "@storyteller/common";
import { downloadFileFromUrl } from "@storyteller/api";
import { PopoverMenu, PopoverItem } from "@storyteller/ui-popover";
import { Tooltip } from "@storyteller/ui-tooltip";
import { Button, ToggleButton } from "@storyteller/ui-button";
import { Modal } from "@storyteller/ui-modal";
import {
faMessageXmark,
faMessageCheck,
faSparkles,
faSpinnerThird,
faTimes,
faPlus,
faUpload,
faImages,
} from "@fortawesome/pro-solid-svg-icons";
import {
faRectangleVertical,
faSquare,
faRectangle,
} from "@fortawesome/pro-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
import { PromptsApi } from "@storyteller/api";
import {
CommandSuccessStatus,
GetAppPreferences,
SoraImageRemix,
SoraImageRemixErrorType,
} from "@storyteller/tauri-api";
import { SoundRegistry } from "@storyteller/soundboard";
interface ReferenceImage {
id: string;
url: string;
file: File;
mediaToken: string;
}
interface PromptBoxVideoProps {
uploadImage: ({
title,
assetFile,
progressCallback,
}: {
title: string;
assetFile: File;
progressCallback: (newState: UploaderState) => void;
}) => Promise<void>;
getCanvasRenderBitmap: () => MaybeCanvasRenderBitmapType;
EncodeImageBitmapToBase64: (imageBitmap: ImageBitmap) => Promise<string>;
useJobContext: () => JobContextType;
onEnqueuePressed?: () => void | Promise<void>;
}
export const PromptBoxVideo = ({
uploadImage,
getCanvasRenderBitmap,
EncodeImageBitmapToBase64,
useJobContext,
onEnqueuePressed,
}: PromptBoxVideoProps) => {
useSignals();
console.log("Is this a desktop app?", IsDesktopApp());
const [isModalOpen, setIsModalOpen] = useState(false);
const [content, setContent] = useState("");
const { jobTokens, addJobToken, removeJobToken, clearJobTokens } =
useJobContext();
const [prompt, setPrompt] = useState("");
const [isEnqueueing, setIsEnqueueing] = useState(false);
const [useSystemPrompt, setUseSystemPrompt] = useState(true);
const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false);
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
[]
);
const [activeGalleryTab, setActiveGalleryTab] = useState("my-media");
const [referenceImages, setReferenceImages] = useState<ReferenceImage[]>([]);
const [uploadingImages, setUploadingImages] = useState<
{ id: string; file: File }[]
>([]);
const [resolutionList, setResolutionList] = useState<PopoverItem[]>([
{
label: "720p",
selected: true,
icon: <FontAwesomeIcon icon={faRectangle} className="h-4 w-4" />,
},
{
label: "480p",
selected: false,
icon: <FontAwesomeIcon icon={faRectangle} className="h-4 w-4" />,
},
]);
const fileInputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
}
});
const handleResolutionSelect = (selectedItem: PopoverItem) => {
setResolutionList((prev) =>
prev.map((item) => ({
...item,
selected: item.label === selectedItem.label,
}))
);
};
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files) {
Array.from(files).forEach((file) => {
const uploadId = Math.random().toString(36).substring(7);
setUploadingImages((prev) => [...prev, { id: uploadId, 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: ReferenceImage = {
id: Math.random().toString(36).substring(7),
url: reader.result as string,
file,
mediaToken: newState.data || "",
};
setReferenceImages((prev) => [...prev, referenceImage]);
setUploadingImages((prev) =>
prev.filter((img) => img.id !== uploadId)
);
toast.success("Reference image added.");
} else if (
newState.status === UploaderStates.assetError ||
newState.status === UploaderStates.imageCreateError
) {
setUploadingImages((prev) =>
prev.filter((img) => img.id !== uploadId)
);
toast.error("Upload failed. Please try again.");
}
},
});
};
reader.readAsDataURL(file);
});
}
};
const handleRemoveReference = (id: string) => {
setReferenceImages((prev) => prev.filter((img) => img.id !== id));
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleUploadClick = () => {
fileInputRef.current?.click();
};
const handleGallerySelect = () => {
setIsGalleryModalOpen(true);
};
const handleGalleryClose = () => {
setIsGalleryModalOpen(false);
setSelectedGalleryImages([]);
};
const handleImageSelect = (id: string) => {
setSelectedGalleryImages((prev) => {
if (prev.includes(id)) {
return [];
}
return [id];
});
};
const handleGalleryImages = (selectedItems: GalleryItem[]) => {
// Clear existing reference images first
setReferenceImages([]);
// Only take the first selected item
const item = selectedItems[0];
if (!item || !item.fullImage) return;
const referenceImage: ReferenceImage = {
id: Math.random().toString(36).substring(7),
url: item.fullImage,
file: new File([], "library-image"),
mediaToken: item.id,
};
setReferenceImages([referenceImage]);
setIsGalleryModalOpen(false);
setSelectedGalleryImages([]);
};
const handleAction = (action: string) => {
switch (action) {
case "gallery":
handleGallerySelect();
break;
default:
console.log("Unknown action:", action);
}
};
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
e.preventDefault();
const pastedText = e.clipboardData.getData("text").trim();
setPrompt(pastedText);
};
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setPrompt(e.target.value);
};
const handleTauriEnqueue = async () => {
const api = new PromptsApi();
let image = getCanvasRenderBitmap();
if (image === undefined) {
console.log("image is undefined");
return;
}
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const byteString = atob(base64Bitmap);
const mimeString = "image/png";
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const uuid = crypto.randomUUID(); // Generate a new UUID
const file = new File([ab], `${uuid}.png`, { type: mimeString });
const snapshotMediaToken = await api.uploadSceneSnapshot({
screenshot: file,
});
if (snapshotMediaToken.data === undefined) {
toast.error("Error: Unable to upload scene snapshot Please try again.");
return;
}
console.log("useSystemPrompt", useSystemPrompt);
console.log("Snapshot media token:", snapshotMediaToken.data);
const generateResponse = await SoraImageRemix({
snapshot_media_token: snapshotMediaToken.data,
disable_system_prompt: !useSystemPrompt,
prompt: prompt,
maybe_additional_images: referenceImages.map((image) => image.mediaToken),
maybe_number_of_samples: 1,
});
console.log("generateResponse", generateResponse);
if (generateResponse.status === CommandSuccessStatus.Success) {
const prefs = await GetAppPreferences();
const soundName = prefs.preferences?.enqueue_success_sound;
if (soundName !== undefined) {
const registry = SoundRegistry.getInstance();
registry.playSound(soundName);
}
toast.success("Image added to queue");
} else {
const prefs = await GetAppPreferences();
const soundName = prefs.preferences?.enqueue_failure_sound;
if (soundName !== undefined) {
const registry = SoundRegistry.getInstance();
registry.playSound(soundName);
}
let errorMessage = "Failed to enqueue image";
if ("error_type" in generateResponse) {
switch (generateResponse.error_type) {
case SoraImageRemixErrorType.SoraLoginRequired:
errorMessage =
"You need to log into Sora to continue. See the settings menu.";
break;
case SoraImageRemixErrorType.ServerError:
errorMessage = "Server error. Failed to enqueue image.";
break;
case SoraImageRemixErrorType.TooManyConcurrentTasks:
errorMessage =
"You have too many Sora image generation tasks running. Please wait a moment.";
break;
case SoraImageRemixErrorType.SoraUsernameNotYetCreated:
errorMessage =
"Your Sora username is not yet created. Please visit the Sora.com website to create it first.";
break;
case SoraImageRemixErrorType.SoraIsHavingProblems:
errorMessage = "Sora is having problems. Please try again later.";
break;
}
}
toast.error(errorMessage);
}
};
const handleWebEnqueue = async () => {
const api = new PromptsApi();
// to take the snapshot of the canvas
// Call onEnqueuePressed if it exists
let image = getCanvasRenderBitmap();
if (image === undefined) {
toast.error(
"Error: Unable to generate image. Please check the input and try again."
);
return;
}
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const byteString = atob(base64Bitmap);
const mimeString = "image/png";
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const uuid = crypto.randomUUID(); // Generate a new UUID
const file = new File([ab], `${uuid}.png`, { type: mimeString });
const snapshotMediaToken = await api.uploadSceneSnapshot({
screenshot: file,
});
if (snapshotMediaToken.data === undefined) {
toast.error("Error: Unable to upload scene snapshot Please try again.");
return;
}
console.log("useSystemPrompt", useSystemPrompt);
console.log("Snapshot media token:", snapshotMediaToken.data);
const response = await api.enqueueImageGeneration({
disableSystemPrompt: !useSystemPrompt,
prompt: prompt,
snapshotMediaToken: snapshotMediaToken.data,
additionalImages: referenceImages.map((image) => image.mediaToken),
});
if (response.success === true) {
toast.success("Please wait while we process your image.");
if (response.data) {
addJobToken(response.data);
}
return;
} else {
toast.error("Failed to enqueue image generation. Please try again.");
}
};
const handleEnqueue = async () => {
console.log("Pressing Enqueue");
if (onEnqueuePressed) {
try {
await onEnqueuePressed();
} catch (error) {
console.error("Error in onEnqueuePressed callback:", error);
return;
}
}
if (isEnqueueing) return;
setIsEnqueueing(true);
if (!prompt.trim()) return;
try {
console.log(
"(4) Enqueuing with prompt:",
prompt,
"and reference images:",
referenceImages
);
const isDesktop = IsDesktopApp();
console.log("Is this a desktop app?", isDesktop);
if (isDesktop) {
await handleTauriEnqueue();
} else {
await handleWebEnqueue();
}
} catch (error) {
console.error("Error during image generation:", error);
toast.error(
"An error occurred while generating the image. Please try again."
);
} finally {
await new Promise((resolve) => setTimeout(resolve, 100));
setIsEnqueueing(false);
}
};
const getCurrentResolutionIcon = () => {
const selected = resolutionList.find((item) => item.selected);
if (!selected || !selected.icon) return faRectangle;
const iconElement = selected.icon as React.ReactElement;
return iconElement.props.icon;
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleEnqueue();
}
};
return (
<>
<Modal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setContent("");
}}
>
{content}
</Modal>
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col gap-3">
{(referenceImages.length > 0 || uploadingImages.length > 0) && (
<div className="flex w-full gap-2">
{referenceImages.map((image) => (
<div
key={image.id}
className="glass relative aspect-square w-20 rounded-lg"
>
<img
src={image.url}
alt="Reference"
className="h-full w-full rounded-lg object-cover"
/>
<button
onClick={() => handleRemoveReference(image.id)}
className="absolute right-[2px] top-[2px] flex h-5 w-5 items-center justify-center rounded-full bg-black/40 text-white backdrop-blur-md transition-colors hover:bg-black"
>
<FontAwesomeIcon icon={faTimes} className="h-2.5 w-2.5" />
</button>
</div>
))}
{uploadingImages.map(({ id, file }) => {
const previewUrl = URL.createObjectURL(file);
return (
<div
key={id}
className="glass relative aspect-square w-20 overflow-hidden rounded-lg"
>
<div className="absolute inset-0">
<img
src={previewUrl}
alt="Uploading preview"
className="h-full w-full object-cover blur-sm"
/>
</div>
<div className="absolute inset-0 flex items-center justify-center bg-black/20">
<FontAwesomeIcon
icon={faSpinnerThird}
className="h-6 w-6 animate-spin text-white"
/>
</div>
</div>
);
})}
</div>
)}
<div className="glass w-[730px] rounded-xl p-4">
<input
type="file"
ref={fileInputRef}
className="hidden"
accept="image/*"
onChange={handleFileUpload}
multiple
/>
<div className="flex justify-center gap-2">
<PopoverMenu
mode="button"
panelTitle="Add Image"
items={[
{
label: "Choose from Assets",
selected: false,
icon: <FontAwesomeIcon icon={faImages} className="h-4 w-4" />,
action: "gallery",
},
]}
onPanelAction={handleAction}
showIconsInList
buttonClassName="bg-transparent hover:bg-transparent py-1.5 px-0 pr-1 m-0 hover:opacity-50 transition-opacity duration-100 ring-0 border-none focus:ring-0 outline-none"
triggerIcon={
<FontAwesomeIcon icon={faPlus} className="text-xl" />
}
/>
<textarea
ref={textareaRef}
rows={1}
placeholder="Choose the image from assets (+) and describe your what you want to happen in the video..."
className="text-md mb-2 max-h-[5.5em] flex-1 resize-none overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 text-white placeholder-white placeholder:text-white/60 focus:outline-none"
value={prompt}
onChange={handleChange}
onPaste={handlePaste}
onKeyDown={handleKeyDown}
onFocus={() => {}}
onBlur={() => {}}
/>
</div>
<div className="mt-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Tooltip
content="Resolution"
position="top"
className="z-50"
closeOnClick={true}
>
<PopoverMenu
items={resolutionList}
onSelect={handleResolutionSelect}
mode="toggle"
panelTitle="Resolution"
showIconsInList
triggerIcon={
<FontAwesomeIcon
icon={getCurrentResolutionIcon()}
className="h-4 w-4"
/>
}
/>
</Tooltip>
<Tooltip
content={
useSystemPrompt
? "Use system prompt: ON"
: "Use system prompt: OFF"
}
position="top"
className="z-50"
delay={200}
>
<ToggleButton
isActive={useSystemPrompt}
icon={faMessageXmark}
activeIcon={faMessageCheck}
onClick={() => setUseSystemPrompt(!useSystemPrompt)}
/>
</Tooltip>
</div>
<div className="flex items-center gap-2">
<Button
className="flex items-center border-none bg-primary px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
icon={!isEnqueueing ? faSparkles : undefined}
onClick={handleEnqueue}
disabled={isEnqueueing || !prompt.trim()}
>
{isEnqueueing ? (
<FontAwesomeIcon
icon={faSpinnerThird}
className="animate-spin text-lg"
/>
) : (
"Generate"
)}
</Button>
</div>
</div>
</div>
</div>
<GalleryModal
isOpen={isGalleryModalOpen}
onClose={handleGalleryClose}
mode="select"
selectedItemIds={selectedGalleryImages}
onSelectItem={handleImageSelect}
maxSelections={4}
onUseSelected={handleGalleryImages}
tabs={[
{ id: "my-media", label: "My generations" },
{ id: "uploads", label: "My uploads" },
]}
activeTab={activeGalleryTab}
onTabChange={setActiveGalleryTab}
onDownloadClicked={downloadFileFromUrl}
/>
</>
);
};
+3 -4729
View File
File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before

Width:  |  Height:  |  Size: 596 B

After

Width:  |  Height:  |  Size: 596 B

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 351 KiB

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 267 KiB

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 271 KiB

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 320 KiB

Before

Width:  |  Height:  |  Size: 564 KiB

After

Width:  |  Height:  |  Size: 564 KiB

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 366 KiB

Before

Width:  |  Height:  |  Size: 452 KiB

After

Width:  |  Height:  |  Size: 452 KiB

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 337 KiB

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 378 KiB

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Before

Width:  |  Height:  |  Size: 446 KiB

After

Width:  |  Height:  |  Size: 446 KiB

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 344 KiB

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before

Width:  |  Height:  |  Size: 299 KiB

After

Width:  |  Height:  |  Size: 299 KiB

Before

Width:  |  Height:  |  Size: 396 KiB

After

Width:  |  Height:  |  Size: 396 KiB

Before

Width:  |  Height:  |  Size: 318 KiB

After

Width:  |  Height:  |  Size: 318 KiB

Before

Width:  |  Height:  |  Size: 521 KiB

After

Width:  |  Height:  |  Size: 521 KiB

Before

Width:  |  Height:  |  Size: 318 KiB

After

Width:  |  Height:  |  Size: 318 KiB

Before

Width:  |  Height:  |  Size: 396 KiB

After

Width:  |  Height:  |  Size: 396 KiB

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 316 KiB

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 378 KiB

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 341 KiB

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Before

Width:  |  Height:  |  Size: 462 KiB

After

Width:  |  Height:  |  Size: 462 KiB

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 615 B

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Before

Width:  |  Height:  |  Size: 428 KiB

After

Width:  |  Height:  |  Size: 428 KiB

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before

Width:  |  Height:  |  Size: 525 KiB

After

Width:  |  Height:  |  Size: 525 KiB

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 943 KiB

After

Width:  |  Height:  |  Size: 943 KiB

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 692 KiB

After

Width:  |  Height:  |  Size: 692 KiB

Before

Width:  |  Height:  |  Size: 704 KiB

After

Width:  |  Height:  |  Size: 704 KiB

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Some files were not shown because too many files have changed in this diff Show More