Compare commits

...
Author SHA1 Message Date
Michael Chung 384ffa7cad Jobs should add the image to the list
Jobs also show completed images in the gallery.
2025-04-15 02:48:08 -04:00
6 changed files with 208 additions and 20 deletions
@@ -105,8 +105,8 @@ export function Activity() {
public_bucket_path: job.data.result.image_url || "",
updated_at: new Date().toISOString(),
};
setCompletedItems((prev) => [newCompletedItem, ...prev]);
toast.success("Done Processing Image");
setCompletedItems((prev) => [newCompletedItem, ...prev]);
}
} else {
// Store the job in our map
@@ -40,7 +40,7 @@ export class JobsApi extends ApiManager {
public ListRecentJobs(): Promise<ApiResponse<Job[]>> {
const endpoint = `${this.ApiTargets.BaseApi}/v1/jobs/session`;
return this.get<{
success: boolean;
jobs: Job[];
+1
View File
@@ -33,4 +33,5 @@ export interface JobStatusResponse {
maybe_first_started_at: string;
requires_keepalive: boolean;
status: JobStatus;
progress_percentage: number;
}
@@ -5,10 +5,161 @@ import { PopoverMenu } from "~/components/reusable/Popover/Popover";
import { CompletedCard } from "./CompletedCard";
import { InProgressCard } from "./InProgressCard";
import { H3, P, Tooltip } from "~/components";
import { activeImageGenerationJobs, userMovies } from "~/signals";
import { activeImageGenerationJobs, toasts, userMovies } from "~/signals";
import { useEffect, useState } from "react";
import { JobsApi } from "~/Classes/ApiManager/JobsApi";
import { toast } from "sonner";
// TODO ensure we de-dupe all this extra code.
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;
};
}
// {
// "job_token": "jinf_421nnr6rj8wy2sj0s11v9fns32s",
// "request": {
// "inference_category": "image_generation",
// "maybe_model_type": "image_gen_api",
// "maybe_model_token": null,
// "maybe_model_title": "Image Generation",
// "maybe_raw_inference_text": null,
// "maybe_style_name": null,
// "maybe_live_portrait_details": null,
// "maybe_lipsync_details": null
// },
// "status": {
// "status": "complete_success",
// "maybe_extra_status_description": null,
// "maybe_assigned_worker": "inference-job-4o-image-848c55d5dd-555kn",
// "maybe_assigned_cluster": "storyteller-eks-1",
// "maybe_first_started_at": "2025-04-15T04:41:01Z",
// "maybe_current_execution_duration_seconds": null,
// "attempt_count": 1,
// "requires_keepalive": false,
// "maybe_failure_category": null,
// "progress_percentage": 100
// },
// "maybe_result": {
// "entity_type": "media_file",
// "entity_token": "m_ytrjp1fcd5tb43ebcs3ewhq6y05tfb",
// "maybe_public_bucket_media_path": "/media/p/t/7/y/g/pt7yg2xfv4ynemssfv1mwb7yxe26e2nb/image_pt7yg2xfv4ynemssfv1mwb7yxe26e2nb.png",
// "media_links": {
// "cdn_url": "https://cdn-2.fakeyou.com/media/p/t/7/y/g/pt7yg2xfv4ynemssfv1mwb7yxe26e2nb/image_pt7yg2xfv4ynemssfv1mwb7yxe26e2nb.png",
// "maybe_thumbnail_template": "https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/p/t/7/y/g/pt7yg2xfv4ynemssfv1mwb7yxe26e2nb/image_pt7yg2xfv4ynemssfv1mwb7yxe26e2nb.png",
// "maybe_video_previews": null
// },
// "maybe_successfully_completed_at": "2025-04-15T04:42:14Z"
// },
// "created_at": "2025-04-15T04:41:01Z",
// "updated_at": "2025-04-15T04:42:14Z"
// }
export function Activity() {
useSignals();
const [completedItems, setCompletedItems] = useState<CompletedItem[]>([]);
const [loading, setLoading] = useState(true);
const [jobs, setJobs] = useState<ActiveJob[]>([]);
useEffect(() => {
const pollData = async () => {
const fetchJobs = async () => {
try {
const jobsApi = new JobsApi();
const jobsResponse = await jobsApi.ListRecentJobs();
if (jobsResponse.success && jobsResponse.data) {
// Process active jobs
const activeJobs = jobsResponse.data
.filter(job => {
const status = job.status?.status?.toLowerCase();
return status && status !== "complete_success" && status !== "failed";
})
.map(job => ({
job_token: job.job_token,
request: {
maybe_model_title: job.request?.maybe_model_title || "Image Generation"
},
status: {
status: job.status?.status?.toUpperCase() || "",
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");
// Create a set of existing tokens for faster lookup
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 => ({
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()
}));
// 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`;
toast.success(message);
}
// Update completed items with a maximum limit of 50 items
setCompletedItems(prev => {
const combined = [...newCompletedItems, ...prev];
// Remove duplicates based on token
const uniqueItems = Array.from(
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())
.slice(0, 50);
});
setJobs(activeJobs);
setLoading(false);
}
} catch (error) {
toast.error("Error Fetching Recent Jobs");
console.error("Failed to fetch recent jobs:", error);
}
};
await fetchJobs();
};
// Initial load
// Initial call to pollData
pollData();
// Set up polling interval (every 5 seconds)
const interval = setInterval(async () => {
await pollData();
}, 5000);
return () => clearInterval(interval);
}, []);
return (
<Tooltip content="Activity" position="bottom" closeOnClick={true}>
@@ -30,11 +181,10 @@ export function Activity() {
>
<div className="max-h-[480px] overflow-y-auto">
{/* In progress */}
{activeImageGenerationJobs.value &&
activeImageGenerationJobs.value.length > 0 && (
{jobs && jobs.length > 0 && (
<div>
{activeImageGenerationJobs.value.map((movieJob) => (
<InProgressCard key={movieJob.job_token} movie={movieJob} />
{jobs.map((job) => (
<InProgressCard key={job.job_token} movie={job} />
))}
</div>
)}
@@ -62,11 +212,17 @@ export function Activity() {
<div>
{/* Completed */}
<div className="flex flex-col">
{userMovies.value?.map((movie) => (
<CompletedCard key={movie.token} movie={movie} />
{completedItems.map((item) => (
<CompletedCard
key={item.token}
movie={item}
setMovieId={(item_id) => {
console.log("item_id", item_id);
}}
/>
))}
</div>
</div>
</div>
)}
</div>
</PopoverMenu>
@@ -6,7 +6,7 @@ import { BucketConfig } from "~/api/BucketConfig";
import dayjs from "dayjs";
import environmentVariables from "~/Classes/EnvironmentVariables";
import { useSignals } from "@preact/signals-react/runtime";
import { downloadFile } from "~/pages/PageEnigma/comps/GenerateModals/utils/downloadFile";
import { downloadFileImage } from "~/pages/PageEnigma/comps/GenerateModals/utils/downloadFile";
import { styleList } from "~/pages/PageEnigma/styleList";
import { GetCdnOrigin } from "~/api/GetCdnOrigin";
@@ -27,13 +27,20 @@ export function CompletedCard({ movie, setMovieId }: Props) {
const bucketConfig = useRef<BucketConfig>(new BucketConfig());
const [loadError, setLoadError] = useState(false);
const cdnOrigin = GetCdnOrigin();
const downloadLink = `https://${cdnOrigin}${movie.public_bucket_path}`;
const imageUrl = bucketConfig.current.getCdnUrl(
movie.public_bucket_path + "-thumb.gif",
360,
20,
);
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);
@@ -71,21 +78,21 @@ export function CompletedCard({ movie, setMovieId }: Props) {
</div>
</div>
</div>
{/* <div className="pr-5">
<div className="pr-5">
<button
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
const title =
movie.maybe_title !== null ? movie.maybe_title : "Untitled";
downloadFile({ url: downloadLink, title });
downloadFileImage({ url: downloadLink, title });
}}
className="text-[15px] font-medium text-white/50 transition-all duration-150 hover:text-white/100"
>
<FontAwesomeIcon icon={faArrowDownToLine} className="mr-2" />
Download
</button>
</div> */}
</div>
</div>
);
}
@@ -24,4 +24,28 @@ export const downloadFile = ({
.catch(() => addToast(ToastTypes.ERROR, "Could not download file."));
};
export const downloadFileImage = ({
url,
title,
}: {
url: string;
title: string;
}) => {
// console.log(url);
fetch(url)
.then((resp) => resp.blob())
.then((blob) => {
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = blobUrl;
a.download = `${title}.png`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
})
.catch(() => addToast(ToastTypes.ERROR, "Could not download file."));
};
//check