Moved the 2d editor to away to the old frontend_old deprecated folder.

Reinstalled konva and react-colorful.
Added a new video promptbox.
This commit is contained in:
Michael Chung committed 2025-05-18 15:18:36 -04:00
1 parent 9827a3efc5
commit ad8b81845a
542 files changed
+706 -4785

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;
@@ -37,7 +36,7 @@ interface Props {
const appTabs: TabItem[] = [
{ id: "2D", label: "2D" },
{ id: "3D", label: "3D" },
// { id: "VIDEO", label: "Video" },
{ id: "VIDEO", label: "Video" },
];
export const TopBar = ({
@@ -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,66 @@
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
uploadImage={async (file: File) => {
// TODO: Implement file upload logic
console.log("Uploading file:", file);
return { url: "", mediaToken: "" };
}}
getCanvasRenderBitmap={() => {}}
EncodeImageBitmapToBase64={async (bitmap: ImageBitmap) => {
// TODO: Implement bitmap encoding
console.log("Encoding bitmap to base64");
return "";
}}
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,6 +1,6 @@
import { signal } from "@preact/signals-react";
export const appTabId = signal("3D");
export const appTabId = signal("VIDEO");
export const setAppTabId = (newId: string) => {
if (newId != "2D" && newId != "3D" && newId != "VIDEO") {
+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,629 @@
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 { lastRenderedBitmap } = useCanvasSignal();
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 [aspectRatioList, setAspectRatioList] = useState<PopoverItem[]>([
{
label: "3:2",
selected: true,
icon: <FontAwesomeIcon icon={faRectangle} className="h-4 w-4" />,
},
{
label: "2:3",
selected: false,
icon: <FontAwesomeIcon icon={faRectangleVertical} className="h-4 w-4" />,
},
{
label: "1:1",
selected: false,
icon: <FontAwesomeIcon icon={faSquare} 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 handleAspectRatioSelect = (selectedItem: PopoverItem) => {
setAspectRatioList((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 prev.filter((imageId) => imageId !== id);
}
if (prev.length >= 4) {
return prev;
}
return [...prev, id];
});
};
const handleGalleryImages = (selectedItems: GalleryItem[]) => {
selectedItems.forEach((item) => {
if (!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((prev) => [...prev, referenceImage]);
});
setIsGalleryModalOpen(false);
setSelectedGalleryImages([]);
};
const handleAction = (action: string) => {
switch (action) {
case "upload":
handleUploadClick();
break;
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 getCurrentAspectRatioIcon = () => {
const selected = aspectRatioList.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: "Upload from device",
selected: false,
icon: <FontAwesomeIcon icon={faUpload} className="h-4 w-4" />,
action: "upload",
},
{
label: "Choose from library",
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="Describe your image..."
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="Aspect ratio"
position="top"
className="z-50"
closeOnClick={true}
>
<PopoverMenu
items={aspectRatioList}
onSelect={handleAspectRatioSelect}
mode="toggle"
panelTitle="Aspect Ratio"
showIconsInList
triggerIcon={
<FontAwesomeIcon
icon={getCurrentAspectRatioIcon()}
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