@@ -299,38 +417,42 @@ const FailedCard = ({
{task.subtitle}
)}
-
-
+
+
{statusLabel}
{task.failureReason && (
-
-
- {task.failureReason}
-
-
+
+
+
+ {task.failureReason}
+
+
+
)}
{task.failureMessage && (
-
-
- {task.failureMessage}
-
-
+
+
+
+ {task.failureMessage}
+
+
+
)}
{onDismiss && (
@@ -380,7 +502,7 @@ export const TaskQueue = () => {
primaryActionText: "",
primaryActionIcon: faTrashAlt,
primaryActionBtnClassName: "",
- onConfirm: async () => {},
+ onConfirm: async () => { },
});
const handleClearCompleted = (onSuccess?: () => void) => {
@@ -538,8 +660,8 @@ export const TaskQueue = () => {
if (!duration) {
const actualModel = t.model_type
? ALL_MODELS_LIST.find(
- (m) => m.tauriId === t.model_type || m.id === t.model_type,
- )
+ (m) => m.tauriId === t.model_type || m.id === t.model_type,
+ )
: undefined;
duration =
actualModel?.progressBarTime ??
@@ -555,6 +677,11 @@ export const TaskQueue = () => {
const estimatedTimeLeftMs = Math.max(0, duration - elapsed);
const parts = formatTitleParts(t);
const canDismiss = now - createdMs > 5 * 60 * 1000; // 5 minutes
+ const meta = getMetaForTask(
+ t.id,
+ t.model_type ? String(t.model_type) : undefined,
+ createdMs,
+ );
return {
id: t.id,
title: `Generating ${parts.kind || "Task"}...`,
@@ -564,6 +691,8 @@ export const TaskQueue = () => {
canDismiss,
estimatedTimeLeftMs,
modelType: t.model_type ? String(t.model_type) : undefined,
+ prompt: meta?.prompt,
+ refImageUrls: meta?.refImageUrls,
};
});
@@ -594,10 +723,17 @@ export const TaskQueue = () => {
? coverImageCache.get(mediaToken)
: undefined;
+ const meta = getMetaForTask(
+ t.id,
+ t.model_type ? String(t.model_type) : undefined,
+ t.created_at.getTime(),
+ );
+
return {
id: t.id,
...formatTitleParts(t),
thumbnailUrl: serverThumbnail || cachedThumbnail || undefined,
+ prompt: meta?.prompt,
imageUrls: t.completed_item?.primary_media_file?.cdn_url
? [t.completed_item?.primary_media_file?.cdn_url]
: [],
@@ -636,6 +772,11 @@ export const TaskQueue = () => {
fr?.failure_message && fr.failure_type !== "unknown"
? fr.failure_message
: undefined;
+ const meta = getMetaForTask(
+ t.id,
+ t.model_type ? String(t.model_type) : undefined,
+ t.created_at.getTime(),
+ );
return {
id: t.id,
title: parts.title || "Task",
@@ -644,6 +785,8 @@ export const TaskQueue = () => {
status: t.task_status,
failureReason: failureReason || fr?.failure_message || undefined,
failureMessage,
+ prompt: meta?.prompt,
+ refImageUrls: meta?.refImageUrls,
};
});
@@ -670,6 +813,7 @@ export const TaskQueue = () => {
}
};
+ cleanupOldEntries();
load();
const id = setInterval(load, 5000);
diff --git a/frontend/apps/artcraft/app/src/components/signaled/TopBar/taskEnqueueMeta.ts b/frontend/apps/artcraft/app/src/components/signaled/TopBar/taskEnqueueMeta.ts
new file mode 100644
index 0000000000..7ef5a0d64b
--- /dev/null
+++ b/frontend/apps/artcraft/app/src/components/signaled/TopBar/taskEnqueueMeta.ts
@@ -0,0 +1,210 @@
+/**
+ * Hacky and temporary lightweight cache that stores enqueue metadata (prompt, ref image thumbnails)
+ * so failed/in-progress tasks in the TaskQueue can display them.
+ *
+ * Keyed by approximate timestamp + model type because the Rust backend
+ * does not expose the frontend_subscriber_id on TaskQueueItem responses.
+ *
+ * Ref images are converted to small base64 thumbnails at store time so they
+ * survive page refreshes (blob URLs do not).
+ */
+
+const STORAGE_KEY = "task_enqueue_meta";
+const MAX_ENTRIES = 100;
+const MATCH_WINDOW_MS = 30_000; // 30-second window for timestamp matching
+const THUMB_SIZE = 144; // px – 2x the 72px card thumbnail for retina
+
+export interface EnqueueMeta {
+ prompt?: string;
+ refImageUrls?: string[];
+ modelType?: string;
+ timestamp: number; // Date.now() at enqueue time
+}
+
+interface StoredEntry {
+ id: string;
+ prompt?: string;
+ refImageUrls?: string[]; // base64 data-URLs after conversion
+ modelType?: string;
+ timestamp: number;
+ matchedTaskId?: string; // set once matched to a specific task
+}
+
+interface TaskMatch {
+ prompt?: string;
+ refImageUrls?: string[];
+}
+
+// In-memory cache of task_id → matched metadata
+const matchedTasks = new Map
();
+
+function readEntries(): StoredEntry[] {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ return raw ? JSON.parse(raw) : [];
+ } catch {
+ return [];
+ }
+}
+
+function writeEntries(entries: StoredEntry[]) {
+ const trimmed = entries.slice(-MAX_ENTRIES);
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed));
+ } catch {
+ // localStorage full – drop oldest half and retry
+ const half = trimmed.slice(Math.floor(trimmed.length / 2));
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(half));
+ } catch {
+ // give up
+ }
+ }
+}
+
+let entryCounter = 0;
+
+/**
+ * Convert a blob/object URL to a small base64 data-URL thumbnail.
+ * Returns the original URL if conversion fails (e.g. CORS, non-image).
+ */
+function toBase64Thumb(url: string): Promise {
+ return new Promise((resolve) => {
+ // Already a data URL — keep as-is
+ if (url.startsWith("data:")) {
+ resolve(url);
+ return;
+ }
+ const img = new Image();
+ img.crossOrigin = "anonymous";
+ img.onload = () => {
+ try {
+ const canvas = document.createElement("canvas");
+ const scale = Math.min(THUMB_SIZE / img.width, THUMB_SIZE / img.height, 1);
+ canvas.width = Math.round(img.width * scale);
+ canvas.height = Math.round(img.height * scale);
+ const ctx = canvas.getContext("2d");
+ if (!ctx) {
+ resolve(url);
+ return;
+ }
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
+ resolve(canvas.toDataURL("image/jpeg", 0.6));
+ } catch {
+ resolve(url);
+ }
+ };
+ img.onerror = () => resolve(url);
+ img.src = url;
+ });
+}
+
+/**
+ * Call this at enqueue time to store the prompt + ref image URLs.
+ * Blob URLs are asynchronously converted to base64 thumbnails for persistence.
+ */
+export function storeEnqueueMeta(meta: EnqueueMeta) {
+ const entryId = `${Date.now()}_${entryCounter++}`;
+
+ // Write immediately with original URLs so same-session lookups work
+ const entry: StoredEntry = {
+ id: entryId,
+ prompt: meta.prompt,
+ refImageUrls: meta.refImageUrls,
+ modelType: meta.modelType,
+ timestamp: meta.timestamp,
+ };
+ const entries = readEntries();
+ entries.push(entry);
+ writeEntries(entries);
+
+ // Then convert blob URLs to base64 in the background for persistence
+ if (meta.refImageUrls && meta.refImageUrls.length > 0) {
+ Promise.all(meta.refImageUrls.map(toBase64Thumb)).then((dataUrls) => {
+ const current = readEntries();
+ const found = current.find((e) => e.id === entryId);
+ if (found) {
+ found.refImageUrls = dataUrls;
+ writeEntries(current);
+ }
+ });
+ }
+}
+
+/**
+ * Try to match a task to stored enqueue metadata.
+ * Uses timestamp proximity + model type matching.
+ * Once matched, the entry stores the taskId so it survives page refresh.
+ */
+export function getMetaForTask(
+ taskId: string,
+ modelType: string | undefined,
+ createdAtMs: number,
+): TaskMatch | undefined {
+ // Check in-memory cache first
+ const cached = matchedTasks.get(taskId);
+ if (cached) return cached;
+
+ const entries = readEntries();
+
+ // First check if this task was already matched in a previous session
+ const previousMatch = entries.find((e) => e.matchedTaskId === taskId);
+ if (previousMatch) {
+ const result: TaskMatch = {
+ prompt: previousMatch.prompt,
+ refImageUrls: previousMatch.refImageUrls,
+ };
+ matchedTasks.set(taskId, result);
+ return result;
+ }
+
+ // Otherwise find the best unmatched entry by timestamp + model proximity
+ let bestIdx = -1;
+ let bestDiff = Infinity;
+
+ for (let i = 0; i < entries.length; i++) {
+ const entry = entries[i];
+ if (entry.matchedTaskId) continue; // already matched to another task
+
+ const entryModel = entry.modelType?.toLowerCase();
+ const taskModel = modelType?.toLowerCase();
+ if (entryModel !== taskModel) continue;
+
+ const diff = Math.abs(entry.timestamp - createdAtMs);
+ if (diff < MATCH_WINDOW_MS && diff < bestDiff) {
+ bestDiff = diff;
+ bestIdx = i;
+ }
+ }
+
+ if (bestIdx === -1) return undefined;
+
+ // Mark as matched with this taskId (persists across refresh)
+ entries[bestIdx].matchedTaskId = taskId;
+ writeEntries(entries);
+
+ const matched = entries[bestIdx];
+ const result: TaskMatch = {
+ prompt: matched.prompt,
+ refImageUrls: matched.refImageUrls,
+ };
+ matchedTasks.set(taskId, result);
+ return result;
+}
+
+/**
+ * Clean up old entries (older than 24 hours).
+ */
+export function cleanupOldEntries() {
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
+ const entries = readEntries().filter((e) => e.timestamp > cutoff);
+ writeEntries(entries);
+}
+
+// Expose globally so libs/ packages can call without cross-package imports.
+declare global {
+ interface Window {
+ __storeTaskEnqueueMeta?: (meta: EnqueueMeta) => void;
+ }
+}
+window.__storeTaskEnqueueMeta = storeEnqueueMeta;
diff --git a/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx b/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx
index f26ac38a1c..5a32b4a895 100644
--- a/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx
+++ b/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx
@@ -272,6 +272,12 @@ export const Angles = () => {
const subscriberId = uuidv4();
state.startGeneration(subscriberId);
+ window.__storeTaskEnqueueMeta?.({
+ prompt: "",
+ modelType: (selectedModel as any)?.tauriId || "flux_2_lora_angles",
+ timestamp: Date.now(),
+ });
+
try {
await EnqueueEditImage({
model: selectedModel ?? ("flux_2_lora_angles" as any),
diff --git a/frontend/apps/artcraft/app/src/pages/PageDraw/PageDraw.tsx b/frontend/apps/artcraft/app/src/pages/PageDraw/PageDraw.tsx
index 958f2a2667..24407bf4b5 100644
--- a/frontend/apps/artcraft/app/src/pages/PageDraw/PageDraw.tsx
+++ b/frontend/apps/artcraft/app/src/pages/PageDraw/PageDraw.tsx
@@ -796,6 +796,16 @@ const PageDraw = () => {
return;
}
+ window.__storeTaskEnqueueMeta?.({
+ prompt,
+ refImageUrls: (options?.images || [])
+ .map((img) => img.url)
+ .filter(Boolean),
+ modelType:
+ (selectedImageModel as any)?.tauriId || String(selectedImageModel),
+ timestamp: Date.now(),
+ });
+
try {
let result;
diff --git a/frontend/apps/artcraft/app/src/pages/PageEdit/PageEdit.tsx b/frontend/apps/artcraft/app/src/pages/PageEdit/PageEdit.tsx
index 6448257cc9..f3c743774b 100644
--- a/frontend/apps/artcraft/app/src/pages/PageEdit/PageEdit.tsx
+++ b/frontend/apps/artcraft/app/src/pages/PageEdit/PageEdit.tsx
@@ -390,6 +390,16 @@ const PageEdit = () => {
crypto?.randomUUID?.() ??
`inpaint-${Date.now()}-${Math.random().toString(36).slice(2)}`;
+ window.__storeTaskEnqueueMeta?.({
+ prompt,
+ refImageUrls: (options?.images || [])
+ .map((img) => img.url)
+ .filter(Boolean),
+ modelType:
+ (selectedImageModel as any)?.tauriId || String(selectedImageModel),
+ timestamp: Date.now(),
+ });
+
try {
let result;
diff --git a/frontend/libs/components/promptbox/src/lib/PromptBox3D.tsx b/frontend/libs/components/promptbox/src/lib/PromptBox3D.tsx
index 46fc1d2b9c..3fbe1f8211 100644
--- a/frontend/libs/components/promptbox/src/lib/PromptBox3D.tsx
+++ b/frontend/libs/components/promptbox/src/lib/PromptBox3D.tsx
@@ -395,6 +395,17 @@ export const PromptBox3D = ({
request.common_aspect_ratio = commonAspectRatio;
}
+ window.__storeTaskEnqueueMeta?.({
+ prompt,
+ refImageUrls: referenceImages
+ ?.map((image) => image.url)
+ .filter(Boolean),
+ modelType:
+ (selectedImageModel as any)?.tauriId ||
+ String(selectedImageModel),
+ timestamp: Date.now(),
+ });
+
const generateResponse = await EnqueueEditImage(request);
console.log("generateResponse", generateResponse);
diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx
index e9dc5792e2..d6caeaad21 100644
--- a/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx
+++ b/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx
@@ -297,6 +297,15 @@ export const PromptBoxImage = ({
.filter((t) => t.length > 0);
}
+ window.__storeTaskEnqueueMeta?.({
+ prompt,
+ refImageUrls: referenceImages
+ ?.map((img) => img.url)
+ .filter(Boolean),
+ modelType: (selectedModel as any)?.tauriId || String(selectedModel),
+ timestamp: Date.now(),
+ });
+
const generateResponse = await EnqueueTextToImage(request);
console.debug("PromptBoxImage - generateResponse", generateResponse);
diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx
index e96fae6913..c9b87d8b32 100644
--- a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx
+++ b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx
@@ -596,6 +596,15 @@ export const PromptBoxVideo = ({
return request;
};
+ window.__storeTaskEnqueueMeta?.({
+ prompt,
+ refImageUrls: referenceImages
+ ?.map((img) => img.url)
+ .filter(Boolean),
+ modelType: (selectedModel as any)?.tauriId || String(selectedModel),
+ timestamp: Date.now(),
+ });
+
const subscriberIds: string[] = [];
const enqueuePromises: Promise[] = [];