mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 17:58:55 +00:00
failed tasks inside task queue and image generation session (#1084)
- failed tasks to show in image generation session view - failed tasks now show in task queue dropdown - completed and failed tasks clearable on "clear all" click - styling updates
This commit is contained in:
1 parent
367f4fb675
commit
2a2e1ddf30
4 files changed
+341
-43
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
faTasks,
|
||||
faBroom,
|
||||
faBomb,
|
||||
faCircleExclamation,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import {
|
||||
@@ -69,6 +70,14 @@ type CompletedTask = {
|
||||
batchImageToken?: string;
|
||||
};
|
||||
|
||||
type FailedTask = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
failedAt?: Date;
|
||||
status: string;
|
||||
};
|
||||
|
||||
const formatTimeLeft = (ms: number): string => {
|
||||
const totalSeconds = Math.ceil(ms / 1000);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
@@ -219,10 +228,70 @@ const CompletedCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
const FAILED_STATUS_LABEL: Record<string, string> = {
|
||||
complete_failure: "Failed",
|
||||
attempt_failed: "Failed",
|
||||
dead: "Failed",
|
||||
cancelled_by_user: "Cancelled",
|
||||
cancelled_by_provider: "Cancelled by provider",
|
||||
cancelled_by_us: "Cancelled",
|
||||
};
|
||||
|
||||
const FailedCard = ({
|
||||
task,
|
||||
onDismiss,
|
||||
}: {
|
||||
task: FailedTask;
|
||||
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">
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleExclamation}
|
||||
className="text-red-400"
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
{task.title}
|
||||
</div>
|
||||
</div>
|
||||
{task.subtitle && (
|
||||
<div className="mt-0.5 truncate text-xs text-base-fg opacity-60">
|
||||
{task.subtitle}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 text-xs font-medium text-red-400">
|
||||
{statusLabel}
|
||||
</div>
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="ml-auto h-6 w-6 rounded-full p-1 text-base-fg/60 hover:bg-ui-controls"
|
||||
aria-label="Dismiss"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDismiss();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TaskQueue = () => {
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const [inProgress, setInProgress] = useState<InProgressTask[]>([]);
|
||||
const [completed, setCompleted] = useState<CompletedTask[]>([]);
|
||||
const [failed, setFailed] = useState<FailedTask[]>([]);
|
||||
const [lastReadAt, setLastReadAt] = useState<number>(() => {
|
||||
const stored = localStorage.getItem("taskQueueLastReadAt");
|
||||
return stored ? parseInt(stored, 10) : 0;
|
||||
@@ -290,6 +359,25 @@ export const TaskQueue = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearFailed = () => {
|
||||
setConfirmationConfig({
|
||||
isOpen: true,
|
||||
title: "Clear failed tasks?",
|
||||
message: (
|
||||
<span className="text-sm text-white/80">
|
||||
This will remove all failed/cancelled tasks from the queue.
|
||||
</span>
|
||||
),
|
||||
primaryActionText: "Clear failed",
|
||||
primaryActionIcon: faTrashAlt,
|
||||
primaryActionBtnClassName:
|
||||
"bg-red-500/10 hover:bg-red-500/20 text-red-500",
|
||||
onConfirm: async () => {
|
||||
await dismissFailed();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveAll = () => {
|
||||
setConfirmationConfig({
|
||||
isOpen: true,
|
||||
@@ -452,8 +540,32 @@ export const TaskQueue = () => {
|
||||
};
|
||||
});
|
||||
|
||||
const FAILED_STATUSES = new Set([
|
||||
"complete_failure",
|
||||
"attempt_failed",
|
||||
"dead",
|
||||
"cancelled_by_user",
|
||||
"cancelled_by_provider",
|
||||
"cancelled_by_us",
|
||||
]);
|
||||
|
||||
const failedTasks = tasks
|
||||
.filter((t) => FAILED_STATUSES.has(t.task_status))
|
||||
.sort((a, b) => b.updated_at.getTime() - a.updated_at.getTime())
|
||||
.map((t: TaskQueueItem) => {
|
||||
const parts = formatTitleParts(t);
|
||||
return {
|
||||
id: t.id,
|
||||
title: parts.title || "Task",
|
||||
subtitle: parts.subtitle,
|
||||
failedAt: t.completed_at || t.updated_at,
|
||||
status: t.task_status,
|
||||
};
|
||||
});
|
||||
|
||||
setInProgress(inProg);
|
||||
setCompleted(done);
|
||||
setFailed(failedTasks);
|
||||
|
||||
// Track newly completed IDs when popover is closed
|
||||
const newCompletedIdSet = new Set(done.map((d) => d.id));
|
||||
@@ -486,10 +598,17 @@ export const TaskQueue = () => {
|
||||
};
|
||||
window.addEventListener("cover-image-uploaded", handleCoverUploaded);
|
||||
|
||||
let unlisten: Promise<UnlistenFn> | null = null;
|
||||
let unlistenComplete: Promise<UnlistenFn> | null = null;
|
||||
let unlistenFailed: Promise<UnlistenFn> | null = null;
|
||||
(async () => {
|
||||
// Update immediately when Tauri signals a generation completion
|
||||
unlisten = listen("generation-complete-event", () => {
|
||||
unlistenComplete = listen("generation-complete-event", () => {
|
||||
if (!cancelled) {
|
||||
load();
|
||||
}
|
||||
});
|
||||
// Also update immediately when a generation fails
|
||||
unlistenFailed = listen("generation-failed-event", () => {
|
||||
if (!cancelled) {
|
||||
load();
|
||||
}
|
||||
@@ -499,8 +618,11 @@ export const TaskQueue = () => {
|
||||
cancelled = true;
|
||||
clearInterval(id);
|
||||
window.removeEventListener("cover-image-uploaded", handleCoverUploaded);
|
||||
if (unlisten) {
|
||||
unlisten.then((f) => f());
|
||||
if (unlistenComplete) {
|
||||
unlistenComplete.then((f) => f());
|
||||
}
|
||||
if (unlistenFailed) {
|
||||
unlistenFailed.then((f) => f());
|
||||
}
|
||||
};
|
||||
}, [
|
||||
@@ -511,8 +633,9 @@ export const TaskQueue = () => {
|
||||
]);
|
||||
|
||||
const hasNothing = useMemo(
|
||||
() => inProgress.length === 0 && completed.length === 0,
|
||||
[inProgress.length, completed.length],
|
||||
() =>
|
||||
inProgress.length === 0 && completed.length === 0 && failed.length === 0,
|
||||
[inProgress.length, completed.length, failed.length],
|
||||
);
|
||||
|
||||
const inProgressCount = inProgress.length;
|
||||
@@ -533,6 +656,7 @@ export const TaskQueue = () => {
|
||||
await MarkTaskAsDismissed(id);
|
||||
setInProgress((prev) => prev.filter((t) => t.id !== id));
|
||||
setCompleted((prev) => prev.filter((t) => t.id !== id));
|
||||
setFailed((prev) => prev.filter((t) => t.id !== id));
|
||||
setUnreadCompletedIds((prev) => (prev ?? []).filter((x) => x !== id));
|
||||
taskDurationRef.current.delete(id);
|
||||
} catch (_) {
|
||||
@@ -552,6 +676,17 @@ export const TaskQueue = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const dismissFailed = async () => {
|
||||
const ids = failed.map((t) => t.id);
|
||||
try {
|
||||
await Promise.all(ids.map((id) => MarkTaskAsDismissed(id)));
|
||||
} catch (_) {
|
||||
// ignore
|
||||
} finally {
|
||||
setFailed([]);
|
||||
}
|
||||
};
|
||||
|
||||
const dismissStale = async () => {
|
||||
const staleIds = inProgress.filter((t) => t.canDismiss).map((t) => t.id);
|
||||
try {
|
||||
@@ -575,6 +710,7 @@ export const TaskQueue = () => {
|
||||
} finally {
|
||||
setCompleted([]);
|
||||
setInProgress([]);
|
||||
setFailed([]);
|
||||
setUnreadCompletedIds([]);
|
||||
taskDurationRef.current.clear();
|
||||
}
|
||||
@@ -609,8 +745,8 @@ export const TaskQueue = () => {
|
||||
>
|
||||
{(close) => (
|
||||
<>
|
||||
<div className="flex max-h-[480px] flex-col">
|
||||
<div className="max-h-[420px] overflow-y-auto p-1">
|
||||
<div className="flex max-h-[80vh] flex-col">
|
||||
<div className="max-h-[80vh] overflow-y-auto p-1">
|
||||
{hasNothing ? (
|
||||
<div className="flex w-full flex-col items-center justify-center p-5 text-base-fg/60">
|
||||
<div className="flex items-center gap-2.5 text-sm opacity-60">
|
||||
@@ -637,10 +773,48 @@ export const TaskQueue = () => {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{failed.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-1 flex items-center justify-between px-1">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-red-400/70">
|
||||
Failed
|
||||
</div>
|
||||
<button
|
||||
className="text-xs tracking-wide text-red-400/70 transition-colors hover:text-red-300"
|
||||
onClick={() => handleClearFailed()}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faXmark}
|
||||
className="mr-1"
|
||||
/>
|
||||
Clear failed
|
||||
</button>
|
||||
</div>
|
||||
{failed.map((t) => (
|
||||
<FailedCard
|
||||
key={t.id}
|
||||
task={t}
|
||||
onDismiss={() => dismissTask(t.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{completed.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-1 px-1 text-xs uppercase tracking-wide text-base-fg/50">
|
||||
Completed
|
||||
<div className="mb-1 flex items-center justify-between px-1">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-base-fg/50">
|
||||
Completed
|
||||
</div>
|
||||
<button
|
||||
className="text-xs tracking-wide text-base-fg/50 transition-colors hover:text-base-fg/100"
|
||||
onClick={() => handleClearCompleted()}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faXmark}
|
||||
className="mr-1"
|
||||
/>
|
||||
Clear completed
|
||||
</button>
|
||||
</div>
|
||||
{completed.map((t) => (
|
||||
<CompletedCard
|
||||
@@ -680,11 +854,11 @@ export const TaskQueue = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="pt-3">
|
||||
<div className="flex items-center justify-center">
|
||||
<Button
|
||||
className="grow"
|
||||
variant="secondary"
|
||||
className="grow border-none bg-white/5 text-white/70 hover:bg-white/10"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setModalOpen(true);
|
||||
close();
|
||||
@@ -692,19 +866,6 @@ export const TaskQueue = () => {
|
||||
>
|
||||
Show all
|
||||
</Button>
|
||||
<Tooltip
|
||||
content="Clear completed"
|
||||
position="bottom"
|
||||
closeOnClick={true}
|
||||
>
|
||||
<Button
|
||||
className="flex h-9 w-9 items-center justify-center rounded-md bg-green-500/10 text-green-500 hover:bg-green-500/20"
|
||||
aria-label="Clear completed"
|
||||
onClick={() => handleClearCompleted(() => close())}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBroom} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -738,6 +899,13 @@ export const TaskQueue = () => {
|
||||
<FontAwesomeIcon icon={faTrashAlt} className="mr-1.5" />
|
||||
Clear stale
|
||||
</Button>
|
||||
<Button
|
||||
className="flex h-9 items-center justify-center bg-red-500/10 px-3 text-red-400 hover:bg-red-500/20"
|
||||
onClick={() => handleClearFailed()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt} className="mr-1.5" />
|
||||
Clear failed
|
||||
</Button>
|
||||
<Button
|
||||
className="flex h-9 items-center justify-center bg-red-500/10 px-3 text-red-500 hover:bg-red-500/20"
|
||||
onClick={() => handleRemoveAll()}
|
||||
@@ -775,6 +943,20 @@ export const TaskQueue = () => {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{failed.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 px-1 text-xs uppercase tracking-wide text-red-400/70">
|
||||
Failed
|
||||
</div>
|
||||
{failed.map((t) => (
|
||||
<FailedCard
|
||||
key={t.id}
|
||||
task={t}
|
||||
onDismiss={() => dismissTask(t.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{completed.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-2 px-1 text-xs uppercase tracking-wide text-base-fg/50">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { JobContextType } from "@storyteller/common";
|
||||
import { PromptBoxImage } from "@storyteller/ui-promptbox";
|
||||
import { uploadImage } from "../../components/reusable/UploadModalMedia/uploadImage";
|
||||
import BackgroundGallery from "./BackgroundGallery";
|
||||
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import {
|
||||
TEXT_TO_IMAGE_PAGE_MODEL_LIST,
|
||||
ModelPage,
|
||||
@@ -13,11 +14,13 @@ import {
|
||||
//PROVIDER_LOOKUP_BY_PAGE,
|
||||
} from "@storyteller/ui-model-selector";
|
||||
import { ImageModel } from "@storyteller/model-list";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCircleExclamation, faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
interface TextToImageProps {
|
||||
imageMediaId?: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
import { useTextToImageGenerationCompleteEvent } from "@storyteller/tauri-events";
|
||||
//import { useTextToImageGenerationCompleteEvent } from "@storyteller/tauri-events";
|
||||
import { useTextToImageStore } from "./TextToImageStore";
|
||||
import { animated, useSpring } from "@react-spring/web";
|
||||
import {
|
||||
@@ -40,7 +43,9 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const batches = useTextToImageStore((s) => s.batches);
|
||||
const startBatch = useTextToImageStore((s) => s.startBatch);
|
||||
const completeBatch = useTextToImageStore((s) => s.completeBatch);
|
||||
//const completeBatch = useTextToImageStore((s) => s.completeBatch);
|
||||
const failBatch = useTextToImageStore((s) => s.failBatch);
|
||||
const dismissBatch = useTextToImageStore((s) => s.dismissBatch);
|
||||
const resetBatches = useTextToImageStore((s) => s.reset);
|
||||
const [imageRowVisible, setImageRowVisible] = useState(false);
|
||||
const promptContentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -95,6 +100,23 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
||||
config: { tension: 200, friction: 28, mass: 1.1 },
|
||||
});
|
||||
|
||||
// Listen for generation-failed-event and mark the oldest pending image batch as failed
|
||||
useEffect(() => {
|
||||
let unlisten: Promise<UnlistenFn> | null = null;
|
||||
unlisten = listen<{ data: { action: string; reason?: string } }>(
|
||||
"generation-failed-event",
|
||||
(event) => {
|
||||
const { action, reason } = event.payload.data;
|
||||
if (action === "generate_image") {
|
||||
failBatch(reason || "Generation failed");
|
||||
}
|
||||
},
|
||||
);
|
||||
return () => {
|
||||
if (unlisten) unlisten.then((f) => f());
|
||||
};
|
||||
}, [failBatch]);
|
||||
|
||||
// Show the batches in reverse order, with the newest items at top.
|
||||
// Like Midjourney instead of a "chat history" style.
|
||||
const inverseBatch = [...batches].reverse();
|
||||
@@ -129,10 +151,40 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
||||
>
|
||||
<div className="mx-auto flex max-w-screen-2xl flex-col gap-8 pr-2">
|
||||
{inverseBatch.map((batch) => (
|
||||
<div key={batch.id} className="flex items-start gap-4">
|
||||
<div
|
||||
key={batch.id}
|
||||
className="relative flex items-stretch gap-4"
|
||||
>
|
||||
<div className="grid flex-1 grid-cols-4 gap-4">
|
||||
{batch.status === "pending" && batch.images.length === 0
|
||||
? Array.from({
|
||||
{batch.status === "failed" ? (
|
||||
<>
|
||||
<div className="flex aspect-square w-full flex-col items-center justify-center gap-3 rounded-lg bg-red-500/10 text-red-400">
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleExclamation}
|
||||
size="2x"
|
||||
/>
|
||||
<span className="px-4 text-center text-sm font-medium">
|
||||
{batch.failureReason || "Generation failed"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => dismissBatch(batch.id)}
|
||||
className="mt-1 flex items-center gap-1.5 rounded-md bg-white/5 px-3 py-1.5 text-xs text-white/50 transition-colors hover:bg-white/10 hover:text-white/70"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div
|
||||
key={`empty-${batch.id}-${i}`}
|
||||
className="aspect-square w-full rounded-lg bg-white/[0.02]"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : batch.status === "pending" &&
|
||||
batch.images.length === 0 ? (
|
||||
<>
|
||||
{Array.from({
|
||||
length: Math.max(
|
||||
1,
|
||||
Math.min(4, batch.requestedCount ?? 4),
|
||||
@@ -140,10 +192,30 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
||||
}).map((_, i) => (
|
||||
<div
|
||||
key={`sk-${batch.id}-${i}`}
|
||||
className="aspect-square w-full animate-pulse rounded-lg bg-white/5"
|
||||
className="aspect-square w-full overflow-hidden rounded-lg bg-white/[0.03]"
|
||||
>
|
||||
<div className="animate-shimmer h-full w-full bg-gradient-to-r from-transparent via-white/10 to-transparent bg-[length:200%_100%]" />
|
||||
</div>
|
||||
))}
|
||||
{Array.from({
|
||||
length: Math.max(
|
||||
0,
|
||||
4 -
|
||||
Math.max(
|
||||
1,
|
||||
Math.min(4, batch.requestedCount ?? 4),
|
||||
),
|
||||
),
|
||||
}).map((_, i) => (
|
||||
<div
|
||||
key={`filler-sk-${batch.id}-${i}`}
|
||||
className="aspect-square w-full rounded-lg bg-white/[0.02]"
|
||||
/>
|
||||
))
|
||||
: batch.images.slice(0, 4).map((img) => (
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{batch.images.slice(0, 4).map((img) => (
|
||||
<button
|
||||
key={img.media_token}
|
||||
onClick={() => {
|
||||
@@ -170,21 +242,38 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
||||
galleryModalLightboxImage.value = lightboxItem;
|
||||
galleryModalLightboxVisible.value = true;
|
||||
}}
|
||||
className="aspect-square w-full overflow-hidden rounded-lg"
|
||||
className="aspect-square w-full overflow-hidden rounded-lg transition-opacity duration-200 hover:cursor-pointer hover:opacity-75"
|
||||
>
|
||||
<img
|
||||
src={img.cdn_url}
|
||||
alt="Generated"
|
||||
className="h-full w-full object-cover"
|
||||
className="h-full w-full bg-black/10 object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
{Array.from({
|
||||
length: Math.max(
|
||||
0,
|
||||
4 - batch.images.slice(0, 4).length,
|
||||
),
|
||||
}).map((_, i) => (
|
||||
<div
|
||||
key={`filler-${batch.id}-${i}`}
|
||||
className="aspect-square w-full rounded-lg bg-white/[0.02]"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="glass inline-block w-[320px] shrink-0 rounded-xl px-4 py-3 text-left text-sm text-base-fg/90">
|
||||
<div
|
||||
className="flex w-[320px] shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="absolute bottom-0 right-0 top-0 flex w-[320px] flex-col">
|
||||
<div className="glass min-h-0 overflow-y-auto rounded-xl px-4 py-3 text-left text-sm text-base-fg/90">
|
||||
<div>{batch.prompt}</div>
|
||||
</div>
|
||||
<div className="mt-2 flex justify-end">
|
||||
<div className="flex justify-end pt-2">
|
||||
<Badge
|
||||
label={batch.modelLabel}
|
||||
className="px-2 py-1 text-xs opacity-70"
|
||||
|
||||
@@ -4,12 +4,13 @@ import type { GeneratedImage } from "@storyteller/tauri-events";
|
||||
export type TextToImageBatch = {
|
||||
id: string;
|
||||
prompt: string;
|
||||
status: "pending" | "complete";
|
||||
status: "pending" | "complete" | "failed";
|
||||
images: GeneratedImage[];
|
||||
createdAt: number;
|
||||
requestedCount: number;
|
||||
modelLabel: string;
|
||||
subscriberId: string;
|
||||
failureReason?: string;
|
||||
};
|
||||
|
||||
type TextToImageState = {
|
||||
@@ -25,6 +26,8 @@ type TextToImageState = {
|
||||
maybeSubscriberId?: string,
|
||||
maybePrompt?: string,
|
||||
) => void;
|
||||
failBatch: (reason?: string) => void;
|
||||
dismissBatch: (id: string) => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
@@ -62,8 +65,8 @@ export const useTextToImageStore = create<TextToImageState>((set, get) => ({
|
||||
const pending = maybeSubscriberId
|
||||
? get().batches.find((b) => b.subscriberId === maybeSubscriberId)
|
||||
: get().batches.find((b) => b.status === "pending");
|
||||
const prompt = pending?.prompt ?? maybePrompt ?? "";
|
||||
const modelLabel = pending?.modelLabel ?? "";
|
||||
//const prompt = pending?.prompt ?? maybePrompt ?? "";
|
||||
//const modelLabel = pending?.modelLabel ?? "";
|
||||
// Mark the most recent pending batch complete, or create one if none exists
|
||||
set((s) => {
|
||||
const idx = pending
|
||||
@@ -81,5 +84,24 @@ export const useTextToImageStore = create<TextToImageState>((set, get) => ({
|
||||
return { batches: updated };
|
||||
});
|
||||
},
|
||||
failBatch: (reason?: string) => {
|
||||
set((s) => {
|
||||
// Mark the oldest pending batch as failed
|
||||
const idx = s.batches.findIndex((b) => b.status === "pending");
|
||||
if (idx === -1) return { batches: s.batches };
|
||||
const updated = [...s.batches];
|
||||
updated[idx] = {
|
||||
...updated[idx],
|
||||
status: "failed",
|
||||
failureReason: reason,
|
||||
};
|
||||
return { batches: updated };
|
||||
});
|
||||
},
|
||||
dismissBatch: (id: string) => {
|
||||
set((s) => ({
|
||||
batches: s.batches.filter((b) => b.id !== id),
|
||||
}));
|
||||
},
|
||||
reset: () => set({ batches: [] }),
|
||||
}));
|
||||
@@ -18,9 +18,14 @@ export default {
|
||||
"0%": { opacity: "0" },
|
||||
"100%": { opacity: "1" },
|
||||
},
|
||||
shimmer: {
|
||||
"0%": { backgroundPosition: "200% 0" },
|
||||
"100%": { backgroundPosition: "-200% 0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
fadeIn: "fadeIn 0.2s ease-in-out",
|
||||
shimmer: "shimmer 2s ease-in-out infinite",
|
||||
},
|
||||
},
|
||||
colors: {
|
||||
|
||||
Reference in new issue
Block a user