Feature/video addition (#615)

Co-authored-by: Michael Chung <michaelchung@Michaels-MacBook-Pro-2.local>
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
This commit is contained in:
authored and GitHub committed 2025-05-18 20:38:12 -04:00
1 parent 76cd00cc13
commit f7c9ebfaa3
542 files changed
+679 -4783

No files matched your search

@@ -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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 542 files, more files were not shown because too many files have changed in this diff. Show more