Compare commits

...
Author SHA1 Message Date
bflatastic 85819578ff restore signal 2025-04-09 01:43:27 +07:00
bflatastic 2516534b3f modal backdrop darker 2025-04-09 01:37:37 +07:00
bflatastic b221aa5df6 jobs activity with dummy polling for now 2025-04-09 01:37:37 +07:00
12 changed files with 381 additions and 196 deletions
+22 -55
View File
@@ -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",
+6 -6
View File
@@ -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,
+16 -90
View File
@@ -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">