diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemCircularProgress.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemCircularProgress.tsx
index ebf32f8082..80da7d250d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemCircularProgress.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemCircularProgress.tsx
@@ -1,6 +1,6 @@
import type { CircularProgressProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { CircularProgress, Tooltip } from '@invoke-ai/ui-library';
-import { useCanvasSessionContext,useProgressData } from 'features/controlLayers/components/SimpleSession/context';
+import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
import { getProgressMessage } from 'features/controlLayers/components/SimpleSession/shared';
import { memo } from 'react';
import type { S } from 'services/api/types';
@@ -15,32 +15,28 @@ const circleStyles: SystemStyleObject = {
right: 2,
};
-export const QueueItemCircularProgress = memo(
- ({
- session_id,
- status,
- ...rest
- }: { session_id: string; status: S['SessionQueueItem']['status'] } & CircularProgressProps) => {
- const { $progressData } = useCanvasSessionContext();
- const { progressEvent } = useProgressData($progressData, session_id);
+type Props = { itemId: number; status: S['SessionQueueItem']['status'] } & CircularProgressProps;
- if (status !== 'in_progress') {
- return null;
- }
+export const QueueItemCircularProgress = memo(({ itemId, status, ...rest }: Props) => {
+ const { $progressData } = useCanvasSessionContext();
+ const { progressEvent } = useProgressData($progressData, itemId);
- return (
-
-
-
- );
+ if (status !== 'in_progress') {
+ return null;
}
-);
+
+ return (
+
+
+
+ );
+});
QueueItemCircularProgress.displayName = 'QueueItemCircularProgress';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
index 0c867db89a..11a3904bd9 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewFull.tsx
@@ -38,23 +38,11 @@ export const QueueItemPreviewFull = memo(({ item, number }: Props) => {
{imageDTO && }
- {!imageLoaded && }
+ {!imageLoaded && }
{imageDTO && }
-
-
+
+
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
index ffecb9c9b8..2578731bfa 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx
@@ -1,5 +1,6 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
+import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { QueueItemCircularProgress } from 'features/controlLayers/components/SimpleSession/QueueItemCircularProgress';
import { QueueItemNumber } from 'features/controlLayers/components/SimpleSession/QueueItemNumber';
import { QueueItemProgressImage } from 'features/controlLayers/components/SimpleSession/QueueItemProgressImage';
@@ -34,25 +35,25 @@ type Props = {
item: S['SessionQueueItem'];
number: number;
isSelected: boolean;
- onSelectItemId: (item_id: number) => void;
- onChangeAutoSwitch: (autoSwitch: boolean) => void;
};
-export const QueueItemPreviewMini = memo(({ item, isSelected, number, onSelectItemId, onChangeAutoSwitch }: Props) => {
+export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) => {
+ const ctx = useCanvasSessionContext();
const [imageLoaded, setImageLoaded] = useState(false);
const imageDTO = useOutputImageDTO(item);
const onClick = useCallback(() => {
- onSelectItemId(item.item_id);
- }, [item.item_id, onSelectItemId]);
+ ctx.$selectedItemId.set(item.item_id);
+ }, [ctx.$selectedItemId, item.item_id]);
const onDoubleClick = useCallback(() => {
- onChangeAutoSwitch(item.status === 'in_progress');
- }, [item.status, onChangeAutoSwitch]);
+ ctx.$autoSwitch.set(item.status === 'in_progress');
+ }, [ctx.$autoSwitch, item.status]);
const onLoad = useCallback(() => {
setImageLoaded(true);
- }, []);
+ ctx.$lastLoadedItemId.set(item.item_id);
+ }, [ctx.$lastLoadedItemId, item.item_id]);
return (
- {imageDTO && }
- {!imageLoaded && }
+ {imageDTO && }
+ {!imageLoaded && }
-
+
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
index 55e8d836b7..2ea3dd827e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressImage.tsx
@@ -1,12 +1,13 @@
import type { ImageProps } from '@invoke-ai/ui-library';
import { Image } from '@invoke-ai/ui-library';
-import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
import { memo } from 'react';
-import { useProgressData } from 'services/events/stores';
-export const QueueItemProgressImage = memo(({ session_id, ...rest }: { session_id: string } & ImageProps) => {
- const { $progressData } = useCanvasSessionContext();
- const { progressImage } = useProgressData($progressData, session_id);
+type Props = { itemId: number } & ImageProps;
+
+export const QueueItemProgressImage = memo(({ itemId, ...rest }: Props) => {
+ const ctx = useCanvasSessionContext();
+ const { progressImage } = useProgressData(ctx.$progressData, itemId);
if (!progressImage) {
return null;
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressMessage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressMessage.tsx
index d287811fb3..fc60a93acf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressMessage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemProgressMessage.tsx
@@ -1,34 +1,33 @@
/* eslint-disable i18next/no-literal-string */
import type { TextProps } from '@invoke-ai/ui-library';
import { Text } from '@invoke-ai/ui-library';
-import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
+import { useCanvasSessionContext, useProgressData } from 'features/controlLayers/components/SimpleSession/context';
import { DROP_SHADOW, getProgressMessage } from 'features/controlLayers/components/SimpleSession/shared';
import { memo } from 'react';
import type { S } from 'services/api/types';
-import { useProgressData } from 'services/events/stores';
-export const QueueItemProgressMessage = memo(
- ({ session_id, status, ...rest }: { session_id: string; status: S['SessionQueueItem']['status'] } & TextProps) => {
- const { $progressData } = useCanvasSessionContext();
- const { progressEvent } = useProgressData($progressData, session_id);
+type Props = { itemId: number; status: S['SessionQueueItem']['status'] } & TextProps;
- if (status === 'completed' || status === 'failed' || status === 'canceled') {
- return null;
- }
+export const QueueItemProgressMessage = memo(({ itemId, status, ...rest }: Props) => {
+ const ctx = useCanvasSessionContext();
+ const { progressEvent } = useProgressData(ctx.$progressData, itemId);
- if (status === 'pending') {
- return (
-
- Waiting to start...
-
- );
- }
+ if (status === 'completed' || status === 'failed' || status === 'canceled') {
+ return null;
+ }
+ if (status === 'pending') {
return (
- {getProgressMessage(progressEvent)}
+ Waiting to start...
);
}
-);
+
+ return (
+
+ {getProgressMessage(progressEvent)}
+
+ );
+});
QueueItemProgressMessage.displayName = 'QueueItemProgressMessage';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
index 5924b63981..56cb4c92c0 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemStatusLabel.tsx
@@ -4,39 +4,39 @@ import { Text } from '@invoke-ai/ui-library';
import { memo } from 'react';
import type { S } from 'services/api/types';
-export const QueueItemStatusLabel = memo(
- ({ status, ...rest }: { status: S['SessionQueueItem']['status'] } & TextProps) => {
- if (status === 'pending') {
- return (
-
- Pending
-
- );
- }
- if (status === 'canceled') {
- return (
-
- Canceled
-
- );
- }
- if (status === 'failed') {
- return (
-
- Failed
-
- );
- }
+type Props = { status: S['SessionQueueItem']['status'] } & TextProps;
- if (status === 'in_progress') {
- return (
-
- In Progress
-
- );
- }
-
- return null;
+export const QueueItemStatusLabel = memo(({ status, ...rest }: Props) => {
+ if (status === 'pending') {
+ return (
+
+ Pending
+
+ );
}
-);
+ if (status === 'canceled') {
+ return (
+
+ Canceled
+
+ );
+ }
+ if (status === 'failed') {
+ return (
+
+ Failed
+
+ );
+ }
+
+ if (status === 'in_progress') {
+ return (
+
+ In Progress
+
+ );
+ }
+
+ return null;
+});
QueueItemStatusLabel.displayName = 'QueueItemStatusLabel';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
index 1146119933..d679835707 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaItemsList.tsx
@@ -20,8 +20,6 @@ export const StagingAreaItemsList = memo(() => {
item={item}
number={i + 1}
isSelected={selectedItemId === item.item_id}
- onSelectItemId={ctx.$selectedItemId.set}
- onChangeAutoSwitch={ctx.$autoSwitch.set}
/>
))}
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
index 64eae0b8f4..c278e38c84 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx
@@ -17,23 +17,21 @@ import { $socket } from 'services/events/stores';
import { assert } from 'tsafe';
export type ProgressData = {
- sessionId: string;
+ itemId: number;
progressEvent: S['InvocationProgressEvent'] | null;
progressImage: ProgressImage | null;
};
-export const buildProgressDataAtom = () => atom>({});
-
export const useProgressData = (
- $progressData: WritableAtom>,
- sessionId: string
+ $progressData: WritableAtom>,
+ itemId: number
): ProgressData => {
const [value, setValue] = useState(() => {
- return $progressData.get()[sessionId] ?? { sessionId, progressEvent: null, progressImage: null };
+ return $progressData.get()[itemId] ?? { itemId, progressEvent: null, progressImage: null };
});
useEffect(() => {
const unsub = $progressData.subscribe((data) => {
- const progressData = data[sessionId];
+ const progressData = data[itemId];
if (!progressData) {
return;
}
@@ -42,35 +40,35 @@ export const useProgressData = (
return () => {
unsub();
};
- }, [$progressData, sessionId]);
+ }, [$progressData, itemId]);
return value;
};
export const useHasProgressImage = (
- $progressData: WritableAtom>,
- sessionId: string
+ $progressData: WritableAtom>,
+ itemId: number
): boolean => {
const [value, setValue] = useState(false);
useEffect(() => {
const unsub = $progressData.subscribe((data) => {
- const progressData = data[sessionId];
+ const progressData = data[itemId];
setValue(Boolean(progressData?.progressImage));
});
return () => {
unsub();
};
- }, [$progressData, sessionId]);
+ }, [$progressData, itemId]);
return value;
};
export const setProgress = (
- $progressData: WritableAtom>,
+ $progressData: WritableAtom>,
data: S['InvocationProgressEvent']
) => {
const progressData = $progressData.get();
- const current = progressData[data.session_id];
+ const current = progressData[data.item_id];
if (current) {
const next = { ...current };
next.progressEvent = data;
@@ -79,13 +77,13 @@ export const setProgress = (
}
$progressData.set({
...progressData,
- [data.session_id]: next,
+ [data.item_id]: next,
});
} else {
$progressData.set({
...progressData,
- [data.session_id]: {
- sessionId: data.session_id,
+ [data.item_id]: {
+ itemId: data.item_id,
progressEvent: data,
progressImage: data.image ?? null,
},
@@ -93,9 +91,9 @@ export const setProgress = (
}
};
-export const clearProgressEvent = ($progressData: WritableAtom>, sessionId: string) => {
+export const clearProgressEvent = ($progressData: WritableAtom>, itemId: number) => {
const progressData = $progressData.get();
- const current = progressData[sessionId];
+ const current = progressData[itemId];
if (!current) {
return;
}
@@ -103,13 +101,13 @@ export const clearProgressEvent = ($progressData: WritableAtom>, sessionId: string) => {
+export const clearProgressImage = ($progressData: WritableAtom>, itemId: number) => {
const progressData = $progressData.get();
- const current = progressData[sessionId];
+ const current = progressData[itemId];
if (!current) {
return;
}
@@ -117,7 +115,7 @@ export const clearProgressImage = ($progressData: WritableAtom;
$selectedItemIndex: Atom;
$autoSwitch: WritableAtom;
+ $lastLoadedItemId: WritableAtom;
};
const CanvasSessionContext = createContext(null);
@@ -159,10 +158,16 @@ export const CanvasSessionContextProvider = memo(
*/
const $autoSwitch = useState(() => atom(true))[0];
+ /**
+ * An internal flag used to work around race conditions with auto-switch switching to queue items before their
+ * output images have fully loaded.
+ */
+ const $lastLoadedItemId = useState(() => atom(null))[0];
+
/**
* An ephemeral store of progress events and images for all items in the current session.
*/
- const $progressData = useState(() => atom>({}))[0];
+ const $progressData = useState(() => atom>({}))[0];
/**
* The currently selected queue item's ID, or null if one is not selected.
@@ -231,21 +236,10 @@ export const CanvasSessionContextProvider = memo(
setProgress($progressData, data);
};
- const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
- if (data.destination !== session.id) {
- return;
- }
- if (data.status === 'completed' && $autoSwitch.get()) {
- $selectedItemId.set(data.item_id);
- }
- };
-
socket.on('invocation_progress', onProgress);
- socket.on('queue_item_status_changed', onQueueItemStatusChanged);
return () => {
socket.off('invocation_progress', onProgress);
- socket.off('queue_item_status_changed', onQueueItemStatusChanged);
};
}, [$autoSwitch, $progressData, $selectedItemId, session.id, socket]);
@@ -285,23 +279,37 @@ export const CanvasSessionContextProvider = memo(
// Clean up the progress data when a queue item is discarded.
const unsubCleanUpProgressData = effect([$items, $progressData], (items, progressData) => {
- const toDelete: string[] = [];
+ const toDelete: number[] = [];
for (const datum of Object.values(progressData)) {
- if (items.findIndex(({ session_id }) => session_id === datum.sessionId) === -1) {
- toDelete.push(datum.sessionId);
+ if (items.findIndex(({ item_id }) => item_id === datum.itemId) === -1) {
+ toDelete.push(datum.itemId);
}
}
if (toDelete.length === 0) {
return;
}
const newProgressData = { ...progressData };
- for (const sessionId of toDelete) {
- delete newProgressData[sessionId];
+ for (const itemId of toDelete) {
+ delete newProgressData[itemId];
}
// This will re-trigger the effect - maybe this could just be a listener on $items? Brain hurt
$progressData.set(newProgressData);
});
+ // We only want to auto-switch to completed queue items once their images have fully loaded to prevent flashes
+ // of fallback content and/or progress images. The only surefire way to determine when images have fully loaded
+ // is via the image elements' `onLoad` callback. Images set `$lastLoadedItemId` to their queue item ID in their
+ // `onLoad` handler, and we listen for that here. If auto-switch is enabled, we then switch the to the item.
+ const unsubHandleAutoSwitch = $lastLoadedItemId.listen((lastLoadedItemId) => {
+ if (lastLoadedItemId === null) {
+ return;
+ }
+ if ($autoSwitch.get()) {
+ $selectedItemId.set(lastLoadedItemId);
+ }
+ $lastLoadedItemId.set(null);
+ });
+
// Create an RTK Query subscription. Without this, the query cache selector will never return anything bc RTK
// doesn't know we care about it.
const { unsubscribe: unsubQueueItemsQuery } = store.dispatch(
@@ -310,6 +318,7 @@ export const CanvasSessionContextProvider = memo(
// Clean up all subscriptions and top-level (i.e. non-computed/derived state)
return () => {
+ unsubHandleAutoSwitch();
unsubQueueItemsQuery();
unsubReduxSyncToItemsAtom();
unsubEnsureSelectedItemIdExists();
@@ -318,7 +327,7 @@ export const CanvasSessionContextProvider = memo(
$progressData.set({});
$selectedItemId.set(null);
};
- }, [$items, $progressData, $selectedItemId, selectQueueItems, session.id, store]);
+ }, [$autoSwitch, $items, $lastLoadedItemId, $progressData, $selectedItemId, selectQueueItems, session.id, store]);
const value = useMemo(
() => ({
@@ -330,8 +339,19 @@ export const CanvasSessionContextProvider = memo(
$autoSwitch,
$selectedItem,
$selectedItemIndex,
+ $lastLoadedItemId,
}),
- [$autoSwitch, $hasItems, $items, $progressData, $selectedItem, $selectedItemId, $selectedItemIndex, session]
+ [
+ $autoSwitch,
+ $hasItems,
+ $items,
+ $lastLoadedItemId,
+ $progressData,
+ $selectedItem,
+ $selectedItemId,
+ $selectedItemIndex,
+ session,
+ ]
);
return {children};
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
index 6736e2c306..1723b4bebf 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/shared.ts
@@ -21,7 +21,7 @@ export const getProgressMessage = (data?: S['InvocationProgressEvent'] | null) =
export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.3))';
-export const getQueueItemElementId = (item_id: number) => `queue-item-status-card-${item_id}`;
+export const getQueueItemElementId = (itemId: number) => `queue-item-status-card-${itemId}`;
const getOutputImageName = (item: S['SessionQueueItem']) => {
const nodeId = Object.entries(item.session.source_prepared_mapping).find(([nodeId]) =>
diff --git a/invokeai/frontend/web/src/services/events/stores.ts b/invokeai/frontend/web/src/services/events/stores.ts
index b6c8305bfe..0d62110efd 100644
--- a/invokeai/frontend/web/src/services/events/stores.ts
+++ b/invokeai/frontend/web/src/services/events/stores.ts
@@ -1,9 +1,7 @@
import type { EphemeralProgressImage } from 'features/controlLayers/store/types';
import type { ProgressImage } from 'features/nodes/types/common';
import { round } from 'lodash-es';
-import type { WritableAtom } from 'nanostores';
import { atom, computed, map } from 'nanostores';
-import { useEffect, useState } from 'react';
import type { ImageDTO, S } from 'services/api/types';
import type { AppSocket } from 'services/events/types';
import type { ManagerOptions, SocketOptions } from 'socket.io-client';
@@ -27,103 +25,6 @@ export type ProgressData = {
progressImage: ProgressImage | null;
};
-export const useProgressData = (
- $progressData: WritableAtom>,
- sessionId: string
-): ProgressData => {
- const [value, setValue] = useState(() => {
- return $progressData.get()[sessionId] ?? { sessionId, progressEvent: null, progressImage: null };
- });
- useEffect(() => {
- const unsub = $progressData.subscribe((data) => {
- const progressData = data[sessionId];
- if (!progressData) {
- return;
- }
- setValue(progressData);
- });
- return () => {
- unsub();
- };
- }, [$progressData, sessionId]);
-
- return value;
-};
-
-export const useHasProgressImage = (
- $progressData: WritableAtom>,
- sessionId: string
-): boolean => {
- const [value, setValue] = useState(false);
- useEffect(() => {
- const unsub = $progressData.subscribe((data) => {
- const progressData = data[sessionId];
- setValue(Boolean(progressData?.progressImage));
- });
- return () => {
- unsub();
- };
- }, [$progressData, sessionId]);
-
- return value;
-};
-
-export const setProgress = (
- $progressData: WritableAtom>,
- data: S['InvocationProgressEvent']
-) => {
- const progressData = $progressData.get();
- const current = progressData[data.session_id];
- if (current) {
- const next = { ...current };
- next.progressEvent = data;
- if (data.image) {
- next.progressImage = data.image;
- }
- $progressData.set({
- ...progressData,
- [data.session_id]: next,
- });
- } else {
- $progressData.set({
- ...progressData,
- [data.session_id]: {
- sessionId: data.session_id,
- progressEvent: data,
- progressImage: data.image ?? null,
- },
- });
- }
-};
-
-export const clearProgressEvent = ($progressData: WritableAtom>, sessionId: string) => {
- const progressData = $progressData.get();
- const current = progressData[sessionId];
- if (!current) {
- return;
- }
- const next = { ...current };
- next.progressEvent = null;
- $progressData.set({
- ...progressData,
- [sessionId]: next,
- });
-};
-
-export const clearProgressImage = ($progressData: WritableAtom>, sessionId: string) => {
- const progressData = $progressData.get();
- const current = progressData[sessionId];
- if (!current) {
- return;
- }
- const next = { ...current };
- next.progressImage = null;
- $progressData.set({
- ...progressData,
- [sessionId]: next,
- });
-};
-
export const $lastCanvasProgressEvent = atom(null);
export const $lastCanvasProgressImage = atom(null);
export const $lastWorkflowsProgressEvent = atom(null);