mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
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:
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 = ({
|
||||
|
||||
+7
-2
@@ -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") {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Generated
+3
-4729
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.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Generated
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
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
Reference in new issue
Block a user