Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54d5c53c79 | ||
|
|
b507c7a6ba | ||
|
|
77f4ef02f2 | ||
|
|
ad8b81845a |
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
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 |