mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 09:22:41 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
557ec5a1f1 |
@@ -0,0 +1,122 @@
|
||||
import { CloseButton, Button } from "~/components";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { createPortal } from "react-dom";
|
||||
import dayjs from "dayjs";
|
||||
import { faDownToLine } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface LightboxModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
imageUrl?: string | null;
|
||||
imageAlt?: string;
|
||||
onImageError?: () => void;
|
||||
title?: string;
|
||||
createdAt?: string;
|
||||
additionalInfo?: React.ReactNode;
|
||||
downloadUrl?: string;
|
||||
}
|
||||
|
||||
export function LightboxModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
imageUrl,
|
||||
imageAlt = "",
|
||||
onImageError,
|
||||
title,
|
||||
createdAt,
|
||||
additionalInfo,
|
||||
downloadUrl,
|
||||
}: LightboxModalProps) {
|
||||
return createPortal(
|
||||
<Transition appear show={isOpen}>
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
<Transition.Child
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className="fixed inset-0 cursor-pointer bg-black/60"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Transition.Child>
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<Transition.Child
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
className="relative h-[90vh] w-[80vw] rounded-xl bg-[#2C2C2C]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<CloseButton
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 z-10"
|
||||
/>
|
||||
<div className="grid h-full grid-cols-3 gap-6">
|
||||
<div className="col-span-2 flex h-full items-center justify-center overflow-hidden rounded-l-xl bg-black/40">
|
||||
{!imageUrl ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-800">
|
||||
<span className="text-white/60">Image not available</span>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={imageAlt}
|
||||
className="h-full w-full object-contain"
|
||||
onError={onImageError}
|
||||
crossOrigin="anonymous"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex h-full flex-col py-5 pe-5">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="text-xl font-medium">
|
||||
{title || "Image Generation"}
|
||||
</div>
|
||||
{createdAt && (
|
||||
<div className="text-sm text-white/60">
|
||||
Created:{" "}
|
||||
{dayjs(createdAt).format("MMM D, YYYY HH:mm:ss")}
|
||||
</div>
|
||||
)}
|
||||
{additionalInfo}
|
||||
</div>
|
||||
{downloadUrl && (
|
||||
<div className="flex justify-end">
|
||||
<a
|
||||
href={downloadUrl}
|
||||
download
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="no-underline"
|
||||
>
|
||||
<Button
|
||||
icon={faDownToLine}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
@@ -27,16 +27,16 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="fixed left-0 top-0 z-30 w-full border-b border-white/5 bg-ui-background">
|
||||
<header className="fixed left-0 top-0 z-[60] w-full border-b border-white/5 bg-ui-background">
|
||||
<nav
|
||||
className="mx-auto grid h-[56px] w-screen grid-cols-3 items-center justify-between px-3"
|
||||
aria-label="Global"
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<a href="/" className="">
|
||||
<a href="/">
|
||||
<span className="sr-only">ArtCraft</span>
|
||||
<img
|
||||
className="h-[30px] w-auto"
|
||||
className="h-[28px] w-auto"
|
||||
src="/resources/images/artcraft-logo-3.png"
|
||||
alt="Logo ArtCraft"
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { activeImageGenerationJobs, toasts, userMovies } from "~/signals";
|
||||
import { useEffect, useState } from "react";
|
||||
import { JobsApi } from "~/Classes/ApiManager/JobsApi";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { ActiveJob } from "~/pages/PageEnigma/models";
|
||||
// TODO ensure we de-dupe all this extra code.
|
||||
interface CompletedItem {
|
||||
token: string;
|
||||
@@ -18,17 +18,6 @@ interface CompletedItem {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ActiveJob {
|
||||
job_token: string;
|
||||
request: {
|
||||
maybe_model_title: string;
|
||||
};
|
||||
status: {
|
||||
status: string;
|
||||
progress_percentage: number;
|
||||
};
|
||||
}
|
||||
|
||||
// {
|
||||
// "job_token": "jinf_421nnr6rj8wy2sj0s11v9fns32s",
|
||||
// "request": {
|
||||
@@ -80,65 +69,78 @@ export function Activity() {
|
||||
try {
|
||||
const jobsApi = new JobsApi();
|
||||
const jobsResponse = await jobsApi.ListRecentJobs();
|
||||
|
||||
|
||||
if (jobsResponse.success && jobsResponse.data) {
|
||||
// Process active jobs
|
||||
const activeJobs = jobsResponse.data
|
||||
.filter(job => {
|
||||
.filter((job) => {
|
||||
const status = job.status?.status?.toLowerCase();
|
||||
return status && status !== "complete_success" && status !== "failed";
|
||||
return (
|
||||
status && status !== "complete_success" && status !== "failed"
|
||||
);
|
||||
})
|
||||
.map(job => ({
|
||||
.map((job) => ({
|
||||
job_token: job.job_token,
|
||||
request: {
|
||||
maybe_model_title: job.request?.maybe_model_title || "Image Generation"
|
||||
maybe_model_title:
|
||||
job.request?.maybe_model_title || "Image Generation",
|
||||
},
|
||||
status: {
|
||||
status: job.status?.status?.toUpperCase() || "",
|
||||
progress_percentage: job.status?.progress_percentage || 0
|
||||
}
|
||||
progress_percentage: job.status?.progress_percentage || 0,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
// Process completed items
|
||||
// Get completed items and filter out any that are already in our list
|
||||
const successfulJobs = jobsResponse.data
|
||||
.filter(job => job.status?.status?.toLowerCase() === "complete_success");
|
||||
|
||||
const successfulJobs = jobsResponse.data.filter(
|
||||
(job) => job.status?.status?.toLowerCase() === "complete_success",
|
||||
);
|
||||
|
||||
// Create a set of existing tokens for faster lookup
|
||||
const existingTokens = new Set(completedItems.map(item => item.token));
|
||||
|
||||
const existingTokens = new Set(
|
||||
completedItems.map((item) => item.token),
|
||||
);
|
||||
|
||||
// Filter out jobs we already have in our list
|
||||
const newCompletedItems = successfulJobs
|
||||
.filter(job => !existingTokens.has(job.job_token))
|
||||
.map(job => ({
|
||||
.filter((job) => !existingTokens.has(job.job_token))
|
||||
.map((job) => ({
|
||||
token: job.job_token,
|
||||
maybe_title: job.request?.maybe_model_title || "Image Generation",
|
||||
public_bucket_path: job.maybe_result?.maybe_public_bucket_media_path || "",
|
||||
updated_at: job.updated_at || new Date().toISOString()
|
||||
maybe_title:
|
||||
job.request?.maybe_model_title || "Image Generation",
|
||||
public_bucket_path:
|
||||
job.maybe_result?.maybe_public_bucket_media_path || "",
|
||||
updated_at: job.updated_at || new Date().toISOString(),
|
||||
}));
|
||||
|
||||
|
||||
// Show toast notification for newly completed items
|
||||
if (newCompletedItems.length > 0) {
|
||||
const count = newCompletedItems.length;
|
||||
const message = count === 1
|
||||
? `${newCompletedItems[0].maybe_title} completed successfully`
|
||||
: `${count} images completed successfully`;
|
||||
const message =
|
||||
count === 1
|
||||
? `${newCompletedItems[0].maybe_title} completed successfully`
|
||||
: `${count} images completed successfully`;
|
||||
toast.success(message);
|
||||
}
|
||||
|
||||
// Update completed items with a maximum limit of 50 items
|
||||
setCompletedItems(prev => {
|
||||
setCompletedItems((prev) => {
|
||||
const combined = [...newCompletedItems, ...prev];
|
||||
// Remove duplicates based on token
|
||||
const uniqueItems = Array.from(
|
||||
new Map(combined.map(item => [item.token, item])).values()
|
||||
new Map(combined.map((item) => [item.token, item])).values(),
|
||||
);
|
||||
// Sort by updated_at in descending order and limit to 50 items
|
||||
return uniqueItems
|
||||
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updated_at).getTime() -
|
||||
new Date(a.updated_at).getTime(),
|
||||
)
|
||||
.slice(0, 50);
|
||||
});
|
||||
setJobs(activeJobs);
|
||||
setJobs(activeJobs as ActiveJob[]);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -158,6 +160,7 @@ export function Activity() {
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -170,23 +173,26 @@ export function Activity() {
|
||||
align="end"
|
||||
triggerIcon={
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faBell} />
|
||||
{activeImageGenerationJobs.value &&
|
||||
activeImageGenerationJobs.value.length > 0 && (
|
||||
<div className="bg-blue-500 absolute -right-1 -top-1 h-3 w-3 animate-pulse rounded-full" />
|
||||
)}
|
||||
{jobs && jobs.length > 0 ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faBell} />
|
||||
)}
|
||||
{jobs && jobs.length > 0 && (
|
||||
<div className="bg-blue-500 absolute -right-1 -top-1 h-3 w-3 animate-pulse rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="max-h-[480px] overflow-y-auto">
|
||||
{/* In progress */}
|
||||
{jobs && jobs.length > 0 && (
|
||||
<div>
|
||||
{jobs.map((job) => (
|
||||
<InProgressCard key={job.job_token} movie={job} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{jobs.map((job) => (
|
||||
<InProgressCard key={job.job_token} job={job} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!userMovies.value && !activeImageGenerationJobs.value ? (
|
||||
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
|
||||
@@ -212,16 +218,10 @@ export function Activity() {
|
||||
{/* Completed */}
|
||||
<div className="flex flex-col">
|
||||
{completedItems.map((item) => (
|
||||
<CompletedCard
|
||||
key={item.token}
|
||||
movie={item}
|
||||
setMovieId={(item_id) => {
|
||||
console.log("item_id", item_id);
|
||||
}}
|
||||
/>
|
||||
<CompletedCard key={item.token} job={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverMenu>
|
||||
|
||||
+65
-79
@@ -1,98 +1,84 @@
|
||||
import { MediaInfo } from "~/pages/PageEnigma/models/movies";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowDownToLine } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { useRef, useState } from "react";
|
||||
import { BucketConfig } from "~/api/BucketConfig";
|
||||
import { useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import environmentVariables from "~/Classes/EnvironmentVariables";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { downloadFileImage } from "~/pages/PageEnigma/comps/GenerateModals/utils/downloadFile";
|
||||
import { styleList } from "~/pages/PageEnigma/styleList";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faDownToLine } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { LightboxModal } from "~/components/reusable/LightboxModal";
|
||||
import { GetCdnOrigin } from "~/api/GetCdnOrigin";
|
||||
|
||||
export function getStyleName(typeInput: string) {
|
||||
const foundStyle = styleList.find((style) => {
|
||||
return style.type === typeInput;
|
||||
});
|
||||
return foundStyle ? foundStyle.label : "Unknown Style";
|
||||
}
|
||||
|
||||
interface Props {
|
||||
movie: MediaInfo;
|
||||
setMovieId?: (id: string) => void;
|
||||
job: {
|
||||
token: string;
|
||||
maybe_title: string;
|
||||
updated_at: string;
|
||||
public_bucket_path: string;
|
||||
maybe_style_name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function CompletedCard({ movie, setMovieId }: Props) {
|
||||
useSignals();
|
||||
const bucketConfig = useRef<BucketConfig>(new BucketConfig());
|
||||
export function CompletedCard({ job }: Props) {
|
||||
const [loadError, setLoadError] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const cdnOrigin = GetCdnOrigin();
|
||||
const mediaPath = job?.public_bucket_path || "";
|
||||
const downloadLink = `${cdnOrigin}${mediaPath}`;
|
||||
const imageUrl = downloadLink;
|
||||
const fullImageUrl = downloadLink;
|
||||
|
||||
|
||||
const downloadLink = `${cdnOrigin}${movie.public_bucket_path}`;
|
||||
|
||||
// console.log(downloadLink);
|
||||
// console.log("This is the completed item", movie);
|
||||
// console.log("item completed card", movie);
|
||||
|
||||
// to do use the thumbnail image
|
||||
const imageUrl = downloadLink; // bucketConfig.current.getCdnUrl(
|
||||
// movie.public_bucket_path + "-thumb.gif",
|
||||
// 360,
|
||||
// 20,
|
||||
// );
|
||||
const styleName =
|
||||
movie.maybe_style_name && getStyleName(movie.maybe_style_name);
|
||||
|
||||
const handleClick = () => {
|
||||
setMovieId?.(movie.token);
|
||||
};
|
||||
if (!job) return null;
|
||||
|
||||
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={handleClick}
|
||||
>
|
||||
<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={movie.maybe_title ?? "unknown"}
|
||||
crossOrigin="anonymous"
|
||||
onError={() => setLoadError(true)}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center gap-1">
|
||||
<div className="font-medium">{movie.maybe_title || "Untitled"}</div>
|
||||
<div>
|
||||
{/* {styleName && (
|
||||
<div className="text-sm text-white/60">{styleName}</div>
|
||||
)} */}
|
||||
<>
|
||||
<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(movie.updated_at).format("MMM D, YYYY HH:mm:ss")}
|
||||
{dayjs(job.updated_at).format("MMM D, YYYY HH:mm:ss")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pr-5">
|
||||
<button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const title =
|
||||
movie.maybe_title !== null ? movie.maybe_title : "Untitled";
|
||||
downloadFileImage({ url: downloadLink, title });
|
||||
}}
|
||||
className="text-[15px] font-medium text-white/50 transition-all duration-150 hover:text-white/100"
|
||||
<a
|
||||
href={downloadLink}
|
||||
download
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex h-9 w-9 items-center justify-center text-lg text-white/60 transition-all hover:text-white"
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowDownToLine} className="mr-2" />
|
||||
Download
|
||||
</button>
|
||||
<FontAwesomeIcon icon={faDownToLine} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LightboxModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
imageUrl={fullImageUrl}
|
||||
imageAlt={job.maybe_title ?? "unknown"}
|
||||
onImageError={() => setLoadError(true)}
|
||||
title={job.maybe_title}
|
||||
createdAt={job.updated_at}
|
||||
downloadUrl={downloadLink}
|
||||
additionalInfo={
|
||||
job.maybe_style_name && (
|
||||
<div className="text-sm text-white/60">
|
||||
Style: {job.maybe_style_name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+19
-25
@@ -1,22 +1,15 @@
|
||||
import { useCallback } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faClose,
|
||||
faCircleExclamation,
|
||||
faSpinnerThird,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { faClose, faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { ToastTypes } from "~/enums";
|
||||
import { addToast } from "~/signals";
|
||||
|
||||
import { JobState } from "~/pages/PageEnigma/enums";
|
||||
import { ActiveJob } from "~/pages/PageEnigma/models";
|
||||
|
||||
import { Tooltip } from "~/components";
|
||||
import { JobsApi } from "~/Classes/ApiManager";
|
||||
import { PollRecentJobs } from "~/hooks/useActiveJobs/utilities";
|
||||
interface Props {
|
||||
movie: ActiveJob;
|
||||
job: ActiveJob;
|
||||
}
|
||||
|
||||
function getPercent(status: JobState, percentage: number) {
|
||||
@@ -33,16 +26,16 @@ function getPercent(status: JobState, percentage: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export function InProgressCard({ movie }: Props) {
|
||||
export function InProgressCard({ job }: Props) {
|
||||
const completePercent = getPercent(
|
||||
movie.status.status as JobState,
|
||||
movie.status.progress_percentage,
|
||||
job.status.status as JobState,
|
||||
job.status.progress_percentage,
|
||||
);
|
||||
|
||||
const deleteJob = useCallback(async (movieJob: ActiveJob) => {
|
||||
const deleteJob = useCallback(async (job: ActiveJob) => {
|
||||
const jobsApi = new JobsApi();
|
||||
|
||||
const response = await jobsApi.DeleteJobByToken(movieJob.job_token);
|
||||
const response = await jobsApi.DeleteJobByToken(job.job_token);
|
||||
if (response.success) {
|
||||
PollRecentJobs();
|
||||
addToast(ToastTypes.SUCCESS, "File successfully deleted.");
|
||||
@@ -58,32 +51,33 @@ export function InProgressCard({ movie }: Props) {
|
||||
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">
|
||||
{(movie.status.status === JobState.STARTED ||
|
||||
movie.status.status === JobState.PENDING) && (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} spin size={"lg"} />
|
||||
<div className="flex aspect-square h-14 w-14 items-center justify-center rounded-lg border border-[#A9A9A9]/50 bg-black/60 text-white">
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
{/* {(job.status.status === JobState.STARTED ||
|
||||
job.status.status === JobState.PENDING) && (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
)}
|
||||
{(movie.status.status === JobState.COMPLETE_FAILURE ||
|
||||
movie.status.status === JobState.ATTEMPT_FAILED) && (
|
||||
{(job.status.status === JobState.COMPLETE_FAILURE ||
|
||||
job.status.status === JobState.ATTEMPT_FAILED) && (
|
||||
<FontAwesomeIcon icon={faCircleExclamation} size={"lg"} />
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
<div className="flex flex-col justify-center gap-1">
|
||||
<div className="font-medium">
|
||||
{movie.request.maybe_model_title || "Untitled"}
|
||||
{job.request.maybe_model_title || "Image Generation"}
|
||||
</div>
|
||||
|
||||
<div className="text-sm capitalize text-white/60">
|
||||
{movie.status.status.replaceAll("_", " ")}... {completePercent}%
|
||||
{job.status.status.replaceAll("_", " ")}... {completePercent}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{movie.status.status !== JobState.STARTED && (
|
||||
{job.status.status !== JobState.STARTED && (
|
||||
<div className="pr-5">
|
||||
<Tooltip content="Cancel" position="left">
|
||||
<button
|
||||
onClick={() => deleteJob(movie)}
|
||||
onClick={() => deleteJob(job)}
|
||||
className="text-[15px] font-medium text-white/50 transition-all duration-150 hover:text-white/100"
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} />
|
||||
|
||||
@@ -3,19 +3,12 @@ import { faCheck } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, LoadingSpinner, CloseButton } from "~/components";
|
||||
import { TabSelector } from "~/components/reusable/TabSelector";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
Fragment,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { LibraryModalApi } from "./LibraryModalApi";
|
||||
import { FilterMediaClasses } from "../../../../enums";
|
||||
import { BucketConfig } from "../../../../api/BucketConfig";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { LightboxModal } from "~/components/reusable/LightboxModal";
|
||||
import { GetCdnOrigin } from "~/api/GetCdnOrigin";
|
||||
|
||||
export interface LibraryItem {
|
||||
id: string;
|
||||
@@ -64,7 +57,8 @@ export const LibraryModal = React.memo(
|
||||
);
|
||||
const [isLightboxVisible, setIsLightboxVisible] = useState(false);
|
||||
const [failedImageUrls] = useState<Set<string>>(new Set());
|
||||
const bucketConfig = useMemo(() => new BucketConfig(), []);
|
||||
const cdnOrigin = GetCdnOrigin();
|
||||
const imageUrl = lightboxImage?.fullImage || "";
|
||||
const api = useMemo(() => new LibraryModalApi(), []);
|
||||
|
||||
const formatDate = useCallback((date: string) => {
|
||||
@@ -109,10 +103,11 @@ export const LibraryModal = React.memo(
|
||||
const getImageUrl = useCallback(
|
||||
(path: string | undefined | null) => {
|
||||
if (!path) return null;
|
||||
const url = bucketConfig.getGcsUrl(path);
|
||||
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||
const url = `${cdnOrigin}${normalizedPath}`;
|
||||
return failedImageUrls.has(url) ? null : url;
|
||||
},
|
||||
[bucketConfig, failedImageUrls],
|
||||
[cdnOrigin, failedImageUrls],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -125,11 +120,8 @@ export const LibraryModal = React.memo(
|
||||
if (response.success && response.data) {
|
||||
const newItems = response.data.map((item) => ({
|
||||
id: item.token,
|
||||
label: item.maybe_title || "Untitled",
|
||||
thumbnail: getImageUrl(
|
||||
item.cover_image.maybe_cover_image_public_bucket_path ||
|
||||
item.public_bucket_path,
|
||||
),
|
||||
label: item.maybe_title || "Image Generation",
|
||||
thumbnail: getImageUrl(item.public_bucket_path),
|
||||
fullImage: getImageUrl(item.public_bucket_path),
|
||||
createdAt: item.created_at,
|
||||
}));
|
||||
@@ -318,74 +310,16 @@ export const LibraryModal = React.memo(
|
||||
</div>
|
||||
</TransitionDialogue>
|
||||
|
||||
{/* Lightbox Modal */}
|
||||
{lightboxImage &&
|
||||
createPortal(
|
||||
<Transition appear show={isLightboxVisible} as={Fragment}>
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
<Transition.Child
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className="fixed inset-0 cursor-pointer bg-black/60"
|
||||
onClick={handleCloseLightbox}
|
||||
/>
|
||||
</Transition.Child>
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center p-4"
|
||||
onClick={handleCloseLightbox}
|
||||
>
|
||||
<Transition.Child
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
className="relative h-[90vh] w-[80vw] rounded-xl bg-[#2C2C2C]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<CloseButton
|
||||
onClick={handleCloseLightbox}
|
||||
className="absolute right-4 top-4 z-10"
|
||||
/>
|
||||
<div className="grid h-full grid-cols-3 gap-6">
|
||||
<div className="col-span-2 flex h-full items-center justify-center overflow-hidden bg-black/40">
|
||||
{!lightboxImage.fullImage ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-800">
|
||||
<span className="text-white/60">
|
||||
Image not available
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={lightboxImage.fullImage}
|
||||
alt={lightboxImage.label}
|
||||
className="h-full w-full object-contain"
|
||||
onError={() =>
|
||||
handleImageError(lightboxImage.fullImage!)
|
||||
}
|
||||
crossOrigin="anonymous"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="py-5">Prompt</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>,
|
||||
document.body,
|
||||
)}
|
||||
<LightboxModal
|
||||
isOpen={isLightboxVisible}
|
||||
onClose={handleCloseLightbox}
|
||||
imageUrl={imageUrl}
|
||||
imageAlt={lightboxImage?.label || ""}
|
||||
onImageError={() => imageUrl && handleImageError(imageUrl)}
|
||||
title={lightboxImage?.label}
|
||||
createdAt={lightboxImage?.createdAt}
|
||||
downloadUrl={imageUrl}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user