mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 01:12:40 -04:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85819578ff | ||
|
|
2516534b3f | ||
|
|
b221aa5df6 |
Generated
+22
-55
@@ -9,13 +9,17 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@devnomic/marquee": "^1.0.3",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@headlessui/react": "^2.1.3",
|
||||
"@preact/signals-react": "^2.2.0",
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"@techstark/opencv-js": "^4.10.0-release.1",
|
||||
"@zip.js/zip.js": "^2.7.52",
|
||||
"dayjs": "^1.11.13",
|
||||
"fortawesome": "^0.0.1-security",
|
||||
"konva": "^9.3.14",
|
||||
"onnxruntime-web": "^1.19.2",
|
||||
@@ -28,14 +32,10 @@
|
||||
"react-router-dom": "^6.26.1",
|
||||
"sonner": "^2.0.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"uuid": "^10.0.0",
|
||||
"yarn": "^1.22.22"
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@preact/signals-react-transform": "^0.4.0",
|
||||
"@tauri-apps/cli": "^2.4.0",
|
||||
"@types/deep-equal": "^1.0.4",
|
||||
@@ -590,33 +590,30 @@
|
||||
"integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA=="
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/6.6.0/fontawesome-common-types-6.6.0.tgz",
|
||||
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
|
||||
"license": "MIT",
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/6.7.2/fontawesome-common-types-6.7.2.tgz",
|
||||
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-svg-core/-/6.6.0/fontawesome-svg-core-6.6.0.tgz",
|
||||
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-svg-core/-/6.7.2/fontawesome-svg-core-6.7.2.tgz",
|
||||
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/free-brands-svg-icons/-/6.6.0/free-brands-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==",
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/free-brands-svg-icons/-/6.7.2/free-brands-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -634,20 +631,10 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/pro-regular-svg-icons/node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/6.7.2/fontawesome-common-types-6.7.2.tgz",
|
||||
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/pro-solid-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/pro-solid-svg-icons/-/6.7.2/pro-solid-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-SY2aP8d7uBlg1rNYznA9w9SVnnQhu3x/C7CCKQYcOGOC1YtPPHgIJhxKGbSzm6RkeJs1HVEfAkORX3J0XB5K9Q==",
|
||||
"dev": true,
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
@@ -656,22 +643,10 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/pro-solid-svg-icons/node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/6.7.2/fontawesome-common-types-6.7.2.tgz",
|
||||
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/react-fontawesome/-/0.2.2/react-fontawesome-0.2.2.tgz",
|
||||
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
@@ -2470,6 +2445,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||
@@ -9358,19 +9338,6 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/yarn": {
|
||||
"version": "1.22.22",
|
||||
"resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz",
|
||||
"integrity": "sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"yarn": "bin/yarn.js",
|
||||
"yarnpkg": "bin/yarn.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@@ -12,13 +12,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@devnomic/marquee": "^1.0.3",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/pro-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@headlessui/react": "^2.1.3",
|
||||
"@preact/signals-react": "^2.2.0",
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"@techstark/opencv-js": "^4.10.0-release.1",
|
||||
"@zip.js/zip.js": "^2.7.52",
|
||||
"dayjs": "^1.11.13",
|
||||
"fortawesome": "^0.0.1-security",
|
||||
"konva": "^9.3.14",
|
||||
"onnxruntime-web": "^1.19.2",
|
||||
@@ -31,14 +35,10 @@
|
||||
"react-router-dom": "^6.26.1",
|
||||
"sonner": "^2.0.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"uuid": "^10.0.0",
|
||||
"yarn": "^1.22.22"
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@preact/signals-react-transform": "^0.4.0",
|
||||
"@tauri-apps/cli": "^2.4.0",
|
||||
"@types/deep-equal": "^1.0.4",
|
||||
|
||||
@@ -790,7 +790,7 @@ export class RealTimeDrawEngine {
|
||||
const base64Bitmap = await this.imageBitmapToBase64(
|
||||
this.lastRenderedBitmap,
|
||||
);
|
||||
|
||||
|
||||
const generateResponse = await invoke("image_generation_command", {
|
||||
image: base64Bitmap,
|
||||
prompt: this.currentPrompt,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import {
|
||||
UploaderStates,
|
||||
UploaderState,
|
||||
// UploaderState,
|
||||
uploadImage,
|
||||
} from "~/components/UploadImage";
|
||||
|
||||
@@ -13,11 +13,8 @@ import { Button } from "~/components/reusable/Button";
|
||||
import { ToggleButton } from "~/components/reusable/ToggleButton";
|
||||
|
||||
import {
|
||||
faCamera,
|
||||
faMessageXmark,
|
||||
faMessageCheck,
|
||||
faDownload,
|
||||
faSave,
|
||||
faSparkles,
|
||||
faSpinnerThird,
|
||||
faTimes,
|
||||
@@ -31,11 +28,10 @@ import {
|
||||
faSquare,
|
||||
faRectangle,
|
||||
} from "@fortawesome/pro-regular-svg-icons";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverItem } from "~/components/reusable/Popover";
|
||||
import { getCanvasRenderBitmap } from "~/signal/canvasRenderBitmap";
|
||||
import { EncodeImageBitmapToBase64 } from "~/utilities/EncodeImageBitmapToBase64";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverItem } from "~/components/reusable/Popover";
|
||||
|
||||
interface ReferenceImage {
|
||||
id: string;
|
||||
@@ -44,11 +40,6 @@ interface ReferenceImage {
|
||||
mediaToken: string;
|
||||
}
|
||||
|
||||
interface ExtendedPopoverItem extends PopoverItem {
|
||||
id: string;
|
||||
focalLength: number;
|
||||
}
|
||||
|
||||
export const PromptBox = () => {
|
||||
useSignals();
|
||||
//const { lastRenderedBitmap } = useCanvasSignal();
|
||||
@@ -60,7 +51,6 @@ export const PromptBox = () => {
|
||||
const [uploadingImages, setUploadingImages] = useState<
|
||||
{ id: string; file: File }[]
|
||||
>([]);
|
||||
const [isCameraSettingsOpen, setIsCameraSettingsOpen] = useState(false);
|
||||
const [aspectRatioList, setAspectRatioList] = useState<PopoverItem[]>([
|
||||
{
|
||||
label: "3:2",
|
||||
@@ -83,22 +73,6 @@ export const PromptBox = () => {
|
||||
icon: <FontAwesomeIcon icon={faRectangleWide} className="h-4 w-4" />,
|
||||
},
|
||||
]);
|
||||
const [cameraList, setCameraList] = useState<ExtendedPopoverItem[]>([
|
||||
{
|
||||
id: "main",
|
||||
label: "Main View",
|
||||
selected: true,
|
||||
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
|
||||
focalLength: 35,
|
||||
},
|
||||
{
|
||||
id: "cam2",
|
||||
label: "Camera 2",
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
|
||||
focalLength: 35,
|
||||
},
|
||||
]);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -110,41 +84,6 @@ export const PromptBox = () => {
|
||||
}
|
||||
}, [prompt]);
|
||||
|
||||
// Update aspect ratio list based on the current cameraAspectRatio signal
|
||||
useEffect(() => {
|
||||
setAspectRatioList((prev) => prev);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleCameraSelect = (selectedItem: PopoverItem) => {};
|
||||
|
||||
const handleAddCamera = () => {
|
||||
const newIndex = cameraList.length + 1;
|
||||
const newId = `cam${newIndex}`;
|
||||
setCameraList((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: newId,
|
||||
label: `Camera ${newIndex}`,
|
||||
selected: false,
|
||||
icon: <FontAwesomeIcon icon={faCamera} className="h-4 w-4" />,
|
||||
focalLength: 35,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleCameraNameChange = (id: string, newName: string) => {
|
||||
setCameraList((prev) =>
|
||||
prev.map((cam) => (cam.id === id ? { ...cam, label: newName } : cam)),
|
||||
);
|
||||
};
|
||||
|
||||
const handleCameraFocalLengthChange = (id: string, value: number) => {
|
||||
setCameraList((prev) =>
|
||||
prev.map((cam) => (cam.id === id ? { ...cam, focalLength: value } : cam)),
|
||||
);
|
||||
};
|
||||
|
||||
const handleAspectRatioSelect = (selectedItem: PopoverItem) => {
|
||||
setAspectRatioList((prev) =>
|
||||
prev.map((item) => ({
|
||||
@@ -152,15 +91,6 @@ export const PromptBox = () => {
|
||||
selected: item.label === selectedItem.label,
|
||||
})),
|
||||
);
|
||||
|
||||
// Map the selected label to the corresponding CameraAspectRatio enum value
|
||||
|
||||
// Publish the change to the engine
|
||||
// Queue.publish({
|
||||
// queueName: QueueNames.TO_ENGINE,
|
||||
// action: toEngineActions.CHANGE_CAMERA_ASPECT_RATIO,
|
||||
// data: newRatio,
|
||||
// });
|
||||
};
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -251,7 +181,6 @@ export const PromptBox = () => {
|
||||
setisEnqueueing(true);
|
||||
|
||||
try {
|
||||
// Here we would pass both the prompt and reference images to the generation
|
||||
console.log(
|
||||
"Enqueuing with prompt:",
|
||||
prompt,
|
||||
@@ -278,12 +207,16 @@ export const PromptBox = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Get the current aspect ratio icon based on the cameraAspectRatio signal
|
||||
const getCurrentAspectRatioIcon = () => {};
|
||||
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 handleSaveFrame = async () => {};
|
||||
// const handleSaveFrame = async () => {};
|
||||
|
||||
const handleDownloadFrame = () => {};
|
||||
// const handleDownloadFrame = () => {};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
@@ -408,13 +341,6 @@ export const PromptBox = () => {
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content="Camera"
|
||||
position="top"
|
||||
className="z-50"
|
||||
delay={300}
|
||||
closeOnClick={true}
|
||||
></Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
useSystemPrompt
|
||||
@@ -434,7 +360,7 @@ export const PromptBox = () => {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip
|
||||
{/* <Tooltip
|
||||
content="Download frame"
|
||||
position="top"
|
||||
className="z-50"
|
||||
@@ -447,18 +373,18 @@ export const PromptBox = () => {
|
||||
icon={faDownload}
|
||||
onClick={handleDownloadFrame}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Tooltip> */}
|
||||
|
||||
<Button
|
||||
{/* <Button
|
||||
className="flex items-center border-none bg-[#5F5F68]/60 px-3 text-sm text-white backdrop-blur-lg hover:bg-[#5F5F68]/90"
|
||||
variant="secondary"
|
||||
icon={faSave}
|
||||
onClick={handleSaveFrame}
|
||||
>
|
||||
Save frame
|
||||
</Button>
|
||||
</Button> */}
|
||||
<Button
|
||||
className="bg-brand-primary flex items-center border-none px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||
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()}
|
||||
|
||||
@@ -1,50 +1,15 @@
|
||||
import {
|
||||
faRectangle,
|
||||
faRectanglePortrait,
|
||||
faSave,
|
||||
faSquare,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { toolbarMain } from "~/signals/uiAccess/toolbarMain";
|
||||
import { dispatchers } from "~/signals/uiEvents/toolbarMain";
|
||||
import { ToolbarMainButtonNames } from "../ToolbarMain/enum";
|
||||
import { ToggleButton } from "~/components/ui/ToggleButton";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Activity } from "~/components/ui/Activity/Activity";
|
||||
// import { paperWrapperStyles } from "~/components/styles";
|
||||
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
|
||||
// import { ToolbarButton } from "~/components/features/ToolbarButton";
|
||||
|
||||
export const ToolbarTopRight = () => {
|
||||
const saveButtonState =
|
||||
toolbarMain.signal.value.buttonStates[ToolbarMainButtonNames.SAVE];
|
||||
const [aspectRatio, setAspectRatio] = useState("1:1");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge("relative z-50 flex h-fit w-fit items-center gap-2")}
|
||||
>
|
||||
{import.meta.env.DEV && ( // only show in dev mode for now
|
||||
<ToggleButton
|
||||
label="Ratio"
|
||||
value={aspectRatio}
|
||||
onChange={setAspectRatio}
|
||||
options={["1:1", "3:2", "2:3"]}
|
||||
icons={{
|
||||
"1:1": faSquare,
|
||||
"3:2": faRectangle,
|
||||
"2:3": faRectanglePortrait,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
icon={faSave}
|
||||
onClick={dispatchers[ToolbarMainButtonNames.SAVE]}
|
||||
disabled={saveButtonState?.disabled}
|
||||
>
|
||||
Save Image
|
||||
</Button>
|
||||
<Activity />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
Transition,
|
||||
} from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "../Button";
|
||||
|
||||
@@ -83,11 +88,11 @@ export const PopoverMenu = ({
|
||||
<Popover className="relative">
|
||||
{({ close }) => (
|
||||
<>
|
||||
<Popover.Button className={className}>
|
||||
<PopoverButton className={className}>
|
||||
{triggerIcon}
|
||||
{mode === "toggle" && selectedItem ? selectedItem.label : null}
|
||||
{mode === "default" && triggerLabel ? triggerLabel : null}
|
||||
</Popover.Button>
|
||||
</PopoverButton>
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
@@ -112,7 +117,7 @@ export const PopoverMenu = ({
|
||||
: "-translate-y-2 opacity-0"
|
||||
}
|
||||
>
|
||||
<Popover.Panel
|
||||
<PopoverPanel
|
||||
className={twMerge(
|
||||
position === "bottom" ? "origin-top" : "origin-bottom",
|
||||
)}
|
||||
@@ -189,7 +194,7 @@ export const PopoverMenu = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</PopoverPanel>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const ToggleButton = ({
|
||||
className={twMerge(
|
||||
"flex h-9 w-9 items-center border-2 border-transparent text-sm text-white backdrop-blur-lg transition-all",
|
||||
isActive
|
||||
? "bg-brand-primary/40 hover:bg-brand-primary/40 border-white/20 hover:border-white/30"
|
||||
? "border-white/20 bg-primary/40 hover:border-white/30 hover:bg-primary/40"
|
||||
: "bg-[#5F5F68]/60 hover:bg-[#5F5F68]/90",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faBell, faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverMenu } from "~/components/reusable/Popover";
|
||||
import { CompletedCard } from "./CompletedCard";
|
||||
import { InProgressCard } from "./InProgressCard";
|
||||
|
||||
// Dummy data types
|
||||
interface CompletedItem {
|
||||
token: string;
|
||||
maybe_title: string;
|
||||
public_bucket_path: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ActiveJob {
|
||||
job_token: string;
|
||||
request: {
|
||||
maybe_model_title: string;
|
||||
};
|
||||
status: {
|
||||
status: string;
|
||||
progress_percentage: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Dummy data for testing
|
||||
const dummyCompletedItems: CompletedItem[] = [
|
||||
{
|
||||
token: "1",
|
||||
maybe_title: "Completed Item 1",
|
||||
public_bucket_path: "/test1",
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
token: "2",
|
||||
maybe_title: "Completed Item 2",
|
||||
public_bucket_path: "/test2",
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const dummyJobs: ActiveJob[] = [
|
||||
{
|
||||
job_token: "job1",
|
||||
request: {
|
||||
maybe_model_title: "In Progress Job 1",
|
||||
},
|
||||
status: {
|
||||
status: "STARTED",
|
||||
progress_percentage: 45,
|
||||
},
|
||||
},
|
||||
{
|
||||
job_token: "job2",
|
||||
request: {
|
||||
maybe_model_title: "In Progress Job 2",
|
||||
},
|
||||
status: {
|
||||
status: "PENDING",
|
||||
progress_percentage: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function Activity() {
|
||||
const [completedItems, setCompletedItems] = useState<CompletedItem[]>([]);
|
||||
const [jobs, setJobs] = useState<ActiveJob[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Dummy polling function
|
||||
useEffect(() => {
|
||||
const pollData = () => {
|
||||
// Simulate API calls
|
||||
setCompletedItems(dummyCompletedItems);
|
||||
setJobs(dummyJobs);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// Initial load
|
||||
pollData();
|
||||
|
||||
// Set up polling interval (every 5 seconds)
|
||||
const interval = setInterval(pollData, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PopoverMenu
|
||||
mode="default"
|
||||
buttonClassName="h-[42px] px-4"
|
||||
panelClassName="w-[360px] p-2"
|
||||
position="bottom"
|
||||
align="end"
|
||||
triggerIcon={
|
||||
<div>
|
||||
<span className="text-md flex items-center gap-2">
|
||||
<>
|
||||
{jobs.length > 0 ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin"
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faBell} />
|
||||
)}
|
||||
Activity
|
||||
</>
|
||||
</span>
|
||||
{jobs.length > 0 && (
|
||||
<div className="absolute -right-1 -top-1 h-3 w-3 animate-pulse rounded-full bg-blue-500" />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="max-h-[480px] overflow-y-auto">
|
||||
{/* In progress */}
|
||||
{jobs.length > 0 && (
|
||||
<div>
|
||||
{jobs.map((job) => (
|
||||
<InProgressCard key={job.job_token} job={job} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
spin
|
||||
size="2x"
|
||||
className="text-gray-400"
|
||||
/>
|
||||
<h3 className="text-gray-300">Retrieving Activities</h3>
|
||||
</div>
|
||||
) : completedItems.length === 0 && jobs.length === 0 ? (
|
||||
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
|
||||
<h3 className="text-gray-300">No activities yet</h3>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{/* Completed */}
|
||||
<div className="flex flex-col">
|
||||
{completedItems.map((item) => (
|
||||
<CompletedCard key={item.token} job={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { BaseDialog } from "../BaseDialog";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface Props {
|
||||
job: {
|
||||
token: string;
|
||||
maybe_title: string;
|
||||
public_bucket_path: string;
|
||||
updated_at: string;
|
||||
maybe_style_name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function CompletedCard({ job }: Props) {
|
||||
const [loadError, setLoadError] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const imageUrl = job.public_bucket_path + "-thumb.gif";
|
||||
const fullImageUrl = job.public_bucket_path;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex w-full items-center justify-between rounded-lg p-2 text-start transition-all duration-150 hover:cursor-pointer hover:bg-white/10"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex aspect-square h-14 w-14 justify-center overflow-hidden rounded-lg border border-[#A9A9A9]/50 bg-black/60">
|
||||
<img
|
||||
src={
|
||||
loadError ? "/resources/images/movie-placeholder.png" : imageUrl
|
||||
}
|
||||
className="h-full w-full object-cover"
|
||||
alt={job.maybe_title ?? "unknown"}
|
||||
crossOrigin="anonymous"
|
||||
onError={() => setLoadError(true)}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center gap-1">
|
||||
<div className="font-medium">{job.maybe_title || "Untitled"}</div>
|
||||
<div className="text-sm text-white/60">
|
||||
{dayjs(job.updated_at).format("MMM D, YYYY HH:mm:ss")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseDialog
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
className="h-[80vh] w-[75vw] max-w-[75vw] p-0"
|
||||
>
|
||||
<div className="relative grid h-full grid-cols-3 gap-6">
|
||||
<button
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="absolute right-4 top-4 z-10 flex h-7 w-7 items-center justify-center rounded-full bg-black/40 text-white/60 transition-all hover:bg-black/70 hover:text-white"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="text-lg" />
|
||||
</button>
|
||||
<div className="col-span-2 flex h-full items-center justify-center overflow-hidden rounded-l-xl bg-black/40">
|
||||
<img
|
||||
src={fullImageUrl}
|
||||
className="h-full w-full object-contain"
|
||||
alt={job.maybe_title ?? "unknown"}
|
||||
crossOrigin="anonymous"
|
||||
onError={() => setLoadError(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 py-5">
|
||||
<div className="text-xl font-medium">
|
||||
{job.maybe_title || "Untitled"}
|
||||
</div>
|
||||
<div className="text-sm text-white/60">
|
||||
Created: {dayjs(job.updated_at).format("MMM D, YYYY HH:mm:ss")}
|
||||
</div>
|
||||
{job.maybe_style_name && (
|
||||
<div className="text-sm text-white/60">
|
||||
Style: {job.maybe_style_name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useCallback } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faClose,
|
||||
faCircleExclamation,
|
||||
faSpinnerThird,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface Props {
|
||||
job: {
|
||||
job_token: string;
|
||||
request: {
|
||||
maybe_model_title: string;
|
||||
};
|
||||
status: {
|
||||
status: string;
|
||||
progress_percentage: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function getPercent(status: string, percentage: number) {
|
||||
if (["COMPLETE_FAILURE", "ATTEMPT_FAILED", "DEAD"].includes(status)) {
|
||||
return 0;
|
||||
} else {
|
||||
return percentage;
|
||||
}
|
||||
}
|
||||
|
||||
export function InProgressCard({ job }: Props) {
|
||||
const completePercent = getPercent(
|
||||
job.status.status,
|
||||
job.status.progress_percentage,
|
||||
);
|
||||
|
||||
const deleteJob = useCallback(async (jobToDelete: Props["job"]) => {
|
||||
// Simulate API call
|
||||
console.log("Deleting job:", jobToDelete.job_token);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between rounded-lg p-2 text-start transition-all duration-150 hover:bg-white/10">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex aspect-square h-14 w-14 items-center justify-center overflow-hidden rounded-lg border border-[#A9A9A9]/50 bg-black/60">
|
||||
{(job.status.status === "STARTED" ||
|
||||
job.status.status === "PENDING") && (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} spin size={"lg"} />
|
||||
)}
|
||||
{(job.status.status === "COMPLETE_FAILURE" ||
|
||||
job.status.status === "ATTEMPT_FAILED") && (
|
||||
<FontAwesomeIcon icon={faCircleExclamation} size={"lg"} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col justify-center gap-1">
|
||||
<div className="font-medium">
|
||||
{job.request.maybe_model_title || "Untitled"}
|
||||
</div>
|
||||
|
||||
<div className="text-sm capitalize text-white/60">
|
||||
{job.status.status.split("_").join(" ")}... {completePercent}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{job.status.status !== "STARTED" && (
|
||||
<div className="pr-5">
|
||||
<button
|
||||
onClick={() => deleteJob(job)}
|
||||
className="text-[15px] font-medium text-white/50 transition-all duration-150 hover:text-white/100"
|
||||
title="Cancel"
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Activity } from "./Activity";
|
||||
@@ -33,7 +33,7 @@ export const BaseDialog = ({
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
|
||||
<div className="fixed inset-0 bg-black/60" aria-hidden="true" />
|
||||
</TransitionChild>
|
||||
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
|
||||
Reference in New Issue
Block a user