mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 16:38:14 +00:00
temporary prompt info and image ref for task queue generations (#1220)
<img width="490" height="683"
alt="{68D2BE71-2E3F-4AB3-BD40-8A1BED5561F6}"
src="https://github.com/user-attachments/assets/edcc6090-3f78-4512-8314-474116f93898"
/>
makes it easier to differentiate which generation task is which by
prompt hover and image ref thumbnails
This commit is contained in:
1 parent
338b0b6313
commit
68f8a339af
9 files changed
+489
-60
No files matched your search
@@ -406,6 +406,18 @@ export const ImageTo3DExperience = ({
|
||||
worldStartGeneration("image", note, firstPreview, subscriberId);
|
||||
setSelectedResultId(subscriberId);
|
||||
|
||||
window.__storeTaskEnqueueMeta?.({
|
||||
prompt: worldPrompt.trim() || undefined,
|
||||
refImageUrls: worldImages
|
||||
.map((img) => img.preview)
|
||||
.filter(Boolean) as string[],
|
||||
modelType:
|
||||
(selectedWorldModel as any)?.tauriId ||
|
||||
(SPLAT_MODELS[0] as any)?.tauriId ||
|
||||
String(selectedWorldModel ?? SPLAT_MODELS[0]),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await EnqueueImageToGaussian({
|
||||
image_media_tokens: readyTokens,
|
||||
prompt: worldPrompt.trim() || undefined,
|
||||
@@ -436,6 +448,14 @@ export const ImageTo3DExperience = ({
|
||||
);
|
||||
setSelectedResultId(subscriberId);
|
||||
|
||||
window.__storeTaskEnqueueMeta?.({
|
||||
prompt: snapshotPrompt || undefined,
|
||||
refImageUrls: snapshotPreview ? [snapshotPreview] : undefined,
|
||||
modelType:
|
||||
(selectedModel as any)?.tauriId || String(selectedModel),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const result = await EnqueueImageTo3dObject({
|
||||
image_media_token: uploadedMediaToken || undefined,
|
||||
model: selectedModel,
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
} from "@storyteller/common";
|
||||
import { coverImageCache } from "~/pages/PageImageTo3DObject/ImageTo3DStore";
|
||||
import { useCreditsState } from "@storyteller/credits";
|
||||
import { getMetaForTask, cleanupOldEntries } from "./taskEnqueueMeta";
|
||||
|
||||
type InProgressTask = {
|
||||
id: string;
|
||||
@@ -58,6 +59,8 @@ type InProgressTask = {
|
||||
canDismiss?: boolean;
|
||||
estimatedTimeLeftMs?: number;
|
||||
modelType?: string;
|
||||
prompt?: string;
|
||||
refImageUrls?: string[];
|
||||
};
|
||||
|
||||
type CompletedTask = {
|
||||
@@ -71,6 +74,7 @@ type CompletedTask = {
|
||||
mediaTokens?: string[];
|
||||
mediaFileClass?: TaskMediaFileClass;
|
||||
batchImageToken?: string;
|
||||
prompt?: string;
|
||||
};
|
||||
|
||||
type FailedTask = {
|
||||
@@ -81,6 +85,8 @@ type FailedTask = {
|
||||
status: string;
|
||||
failureReason?: string;
|
||||
failureMessage?: string;
|
||||
prompt?: string;
|
||||
refImageUrls?: string[];
|
||||
};
|
||||
|
||||
const formatTimeLeft = (ms: number): string => {
|
||||
@@ -110,16 +116,67 @@ const InProgressCard = ({
|
||||
? formatTimeLeft(task.estimatedTimeLeftMs)
|
||||
: null;
|
||||
const isSeedance2 = task.modelType === "seedance_2p0";
|
||||
return (
|
||||
<div className="rounded-md p-2 transition-colors hover:bg-ui-controls/40">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-[72px] w-[72px] shrink-0 items-center justify-center overflow-hidden rounded bg-ui-controls">
|
||||
const hasRefImages =
|
||||
task.refImageUrls && task.refImageUrls.length > 0;
|
||||
|
||||
const thumbnailContent = hasRefImages ? (
|
||||
<Tooltip
|
||||
content={task.prompt || ""}
|
||||
position="bottom"
|
||||
strategy="fixed"
|
||||
className="max-w-[320px] text-wrap text-xs"
|
||||
zIndex={50}
|
||||
delay={200}
|
||||
disabled={!task.prompt}
|
||||
>
|
||||
<div className="relative h-[72px] w-[72px] shrink-0 overflow-hidden rounded">
|
||||
<img
|
||||
src={task.refImageUrls![0]}
|
||||
alt="Reference"
|
||||
className="h-full w-full object-cover opacity-50"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/30" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin text-base-fg/60"
|
||||
className="animate-spin text-white/80"
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
{task.refImageUrls!.length > 1 && (
|
||||
<div className="absolute bottom-0.5 right-0.5 rounded bg-black/60 px-1 text-[9px] text-white/80">
|
||||
+{task.refImageUrls!.length - 1}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
content={task.prompt || ""}
|
||||
position="bottom"
|
||||
strategy="fixed"
|
||||
className="max-w-[320px] text-wrap text-xs"
|
||||
zIndex={50}
|
||||
delay={200}
|
||||
disabled={!task.prompt}
|
||||
>
|
||||
<div className="flex h-[72px] w-[72px] shrink-0 items-center justify-center overflow-hidden rounded bg-ui-controls">
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin text-base-fg/60"
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="rounded-md p-2 transition-colors hover:bg-ui-controls/40">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{thumbnailContent}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-1.5 truncate font-medium text-base-fg/90">
|
||||
@@ -196,28 +253,38 @@ const CompletedCard = ({
|
||||
role={onClick ? "button" : undefined}
|
||||
tabIndex={onClick ? 0 : -1}
|
||||
>
|
||||
<div className="h-[72px] w-[72px] shrink-0 overflow-hidden rounded bg-ui-controls">
|
||||
{task.thumbnailUrl ? (
|
||||
<img
|
||||
src={task.thumbnailUrl}
|
||||
alt={task.title}
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = getPlaceholderForMediaClass(
|
||||
task.mediaFileClass,
|
||||
);
|
||||
e.currentTarget.style.opacity = "0.3";
|
||||
// Set the `data-brokenurl` property for debugging the broken images:
|
||||
(e.currentTarget as HTMLImageElement).dataset.brokenurl =
|
||||
task.thumbnailUrl;
|
||||
}}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center text-[10px] text-base-fg/40">
|
||||
Done
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip
|
||||
content={task.prompt || ""}
|
||||
position="bottom"
|
||||
strategy="fixed"
|
||||
className="max-w-[320px] text-wrap text-xs"
|
||||
zIndex={50}
|
||||
delay={200}
|
||||
disabled={!task.prompt}
|
||||
>
|
||||
<div className="h-[72px] w-[72px] shrink-0 overflow-hidden rounded bg-ui-controls">
|
||||
{task.thumbnailUrl ? (
|
||||
<img
|
||||
src={task.thumbnailUrl}
|
||||
alt={task.title}
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = getPlaceholderForMediaClass(
|
||||
task.mediaFileClass,
|
||||
);
|
||||
e.currentTarget.style.opacity = "0.3";
|
||||
// Set the `data-brokenurl` property for debugging the broken images:
|
||||
(e.currentTarget as HTMLImageElement).dataset.brokenurl =
|
||||
task.thumbnailUrl;
|
||||
}}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center text-[10px] text-base-fg/40">
|
||||
Done
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium text-base-fg/90">
|
||||
{task.title}
|
||||
@@ -278,16 +345,67 @@ const FailedCard = ({
|
||||
onDismiss?: () => void;
|
||||
}) => {
|
||||
const statusLabel = FAILED_STATUS_LABEL[task.status] || "Failed";
|
||||
return (
|
||||
<div className="rounded-md p-2 transition-colors hover:bg-ui-controls/40">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-[72px] w-[72px] shrink-0 items-center justify-center overflow-hidden rounded bg-red-500/10">
|
||||
const hasRefImages =
|
||||
task.refImageUrls && task.refImageUrls.length > 0;
|
||||
|
||||
const thumbnailContent = hasRefImages ? (
|
||||
<Tooltip
|
||||
content={task.prompt || ""}
|
||||
position="bottom"
|
||||
strategy="fixed"
|
||||
className="max-w-[320px] text-wrap text-xs"
|
||||
zIndex={50}
|
||||
delay={200}
|
||||
disabled={!task.prompt}
|
||||
>
|
||||
<div className="relative h-[72px] w-[72px] shrink-0 overflow-hidden rounded">
|
||||
<img
|
||||
src={task.refImageUrls![0]}
|
||||
alt="Reference"
|
||||
className="h-full w-full object-cover opacity-50"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-red-900/40" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleExclamation}
|
||||
className="text-red-400"
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
{task.refImageUrls!.length > 1 && (
|
||||
<div className="absolute bottom-0.5 right-0.5 rounded bg-black/60 px-1 text-[9px] text-white/80">
|
||||
+{task.refImageUrls!.length - 1}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
content={task.prompt || ""}
|
||||
position="bottom"
|
||||
strategy="fixed"
|
||||
className="max-w-[320px] text-wrap text-xs"
|
||||
zIndex={50}
|
||||
delay={200}
|
||||
disabled={!task.prompt}
|
||||
>
|
||||
<div className="flex h-[72px] w-[72px] shrink-0 items-center justify-center overflow-hidden rounded bg-red-500/10">
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleExclamation}
|
||||
className="text-red-400"
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="rounded-md p-2 transition-colors hover:bg-ui-controls/40">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{thumbnailContent}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="truncate font-medium text-base-fg/90">
|
||||
@@ -299,38 +417,42 @@ const FailedCard = ({
|
||||
{task.subtitle}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 flex items-center gap-1.5">
|
||||
<span className="rounded bg-red-500/15 px-1.5 py-0 text-[11px] font-medium text-red-400">
|
||||
<div className="mt-1 flex min-w-0 items-center gap-1.5 overflow-hidden">
|
||||
<span className="shrink-0 rounded bg-red-500/15 px-1.5 py-0 text-[11px] font-medium text-red-400">
|
||||
{statusLabel}
|
||||
</span>
|
||||
{task.failureReason && (
|
||||
<Tooltip
|
||||
content={task.failureReason}
|
||||
position="bottom"
|
||||
strategy="fixed"
|
||||
className="max-w-[280px] text-wrap bg-danger text-xs"
|
||||
zIndex={50}
|
||||
delay={300}
|
||||
>
|
||||
<span className="truncate text-[11px] text-red-400/60">
|
||||
{task.failureReason}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<Tooltip
|
||||
content={task.failureReason}
|
||||
position="bottom"
|
||||
strategy="fixed"
|
||||
className="max-w-[280px] text-wrap bg-danger text-xs"
|
||||
zIndex={50}
|
||||
delay={300}
|
||||
>
|
||||
<div className="truncate text-[11px] text-red-400/60">
|
||||
{task.failureReason}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{task.failureMessage && (
|
||||
<Tooltip
|
||||
content={task.failureMessage}
|
||||
position="bottom"
|
||||
strategy="fixed"
|
||||
className="max-w-[280px] text-wrap text-xs"
|
||||
zIndex={50}
|
||||
delay={200}
|
||||
>
|
||||
<div className="mt-0.5 cursor-default truncate text-[11px] text-base-fg/40">
|
||||
{task.failureMessage}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="overflow-hidden">
|
||||
<Tooltip
|
||||
content={task.failureMessage}
|
||||
position="bottom"
|
||||
strategy="fixed"
|
||||
className="max-w-[280px] text-wrap text-xs"
|
||||
zIndex={50}
|
||||
delay={200}
|
||||
>
|
||||
<div className="mt-0.5 cursor-default truncate text-[11px] text-base-fg/40">
|
||||
{task.failureMessage}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{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);
|
||||
|
||||
|
||||
@@ -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<string, TaskMatch>();
|
||||
|
||||
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<string> {
|
||||
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;
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<void>[] = [];
|
||||
|
||||
|
||||
Reference in new issue
Block a user