mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 22:04:51 -05:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
384ffa7cad |
@@ -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[];
|
||||
|
||||
@@ -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>
|
||||
|
||||
+17
-10
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user