Skip to content

Commit f5a775a

Browse files
feat(ui): toast on queue item errors, improved error descriptions
Show error toasts on queue item error events instead of invocation error events. This allows errors that occurred outside node execution to be surfaced to the user. The error description component is updated to show the new error message if available. Commercial handling is retained, but local now uses the same component to display the error message itself.
1 parent 50dd569 commit f5a775a

File tree

4 files changed

+86
-75
lines changed

4 files changed

+86
-75
lines changed

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,16 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
33
import { deepClone } from 'common/util/deepClone';
44
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
55
import { zNodeStatus } from 'features/nodes/types/invocation';
6-
import { toast } from 'features/toast/toast';
7-
import ToastWithSessionRefDescription from 'features/toast/ToastWithSessionRefDescription';
8-
import { t } from 'i18next';
9-
import { startCase } from 'lodash-es';
106
import { socketInvocationError } from 'services/events/actions';
117

128
const log = logger('socketio');
139

14-
const getTitle = (errorType: string) => {
15-
if (errorType === 'OutOfMemoryError') {
16-
return t('toast.outOfMemoryError');
17-
}
18-
return t('toast.serverError');
19-
};
20-
21-
const getDescription = (errorType: string, sessionId: string, isLocal?: boolean) => {
22-
if (!isLocal) {
23-
if (errorType === 'OutOfMemoryError') {
24-
return ToastWithSessionRefDescription({
25-
message: t('toast.outOfMemoryDescription'),
26-
sessionId,
27-
});
28-
}
29-
return ToastWithSessionRefDescription({
30-
message: errorType,
31-
sessionId,
32-
});
33-
}
34-
return errorType;
35-
};
36-
3710
export const addInvocationErrorEventListener = (startAppListening: AppStartListening) => {
3811
startAppListening({
3912
actionCreator: socketInvocationError,
40-
effect: (action, { getState }) => {
13+
effect: (action) => {
4114
log.error(action.payload, `Invocation error (${action.payload.data.node.type})`);
42-
const { source_node_id, error_type, error_message, error_traceback, graph_execution_state_id } =
43-
action.payload.data;
15+
const { source_node_id, error_type, error_message, error_traceback } = action.payload.data;
4416
const nes = deepClone($nodeExecutionStates.get()[source_node_id]);
4517
if (nes) {
4618
nes.status = zNodeStatus.enum.FAILED;
@@ -53,19 +25,6 @@ export const addInvocationErrorEventListener = (startAppListening: AppStartListe
5325
};
5426
upsertExecutionState(nes.nodeId, nes);
5527
}
56-
57-
const errorType = startCase(error_type);
58-
const sessionId = graph_execution_state_id;
59-
const { isLocal } = getState().config;
60-
61-
toast({
62-
id: `INVOCATION_ERROR_${errorType}`,
63-
title: getTitle(errorType),
64-
status: 'error',
65-
duration: null,
66-
description: getDescription(errorType, sessionId, isLocal),
67-
updateDescription: isLocal ? true : false,
68-
});
6928
},
7029
});
7130
};
Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
33
import { deepClone } from 'common/util/deepClone';
44
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
55
import { zNodeStatus } from 'features/nodes/types/invocation';
6+
import ErrorToastDescription, { getTitleFromErrorType } from 'features/toast/ErrorToastDescription';
7+
import { toast } from 'features/toast/toast';
68
import { forEach } from 'lodash-es';
79
import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue';
810
import { socketQueueItemStatusChanged } from 'services/events/actions';
@@ -12,7 +14,7 @@ const log = logger('socketio');
1214
export const addSocketQueueItemStatusChangedEventListener = (startAppListening: AppStartListening) => {
1315
startAppListening({
1416
actionCreator: socketQueueItemStatusChanged,
15-
effect: async (action, { dispatch }) => {
17+
effect: async (action, { dispatch, getState }) => {
1618
// we've got new status for the queue item, batch and queue
1719
const { queue_item, batch_status, queue_status } = action.payload.data;
1820

@@ -54,7 +56,7 @@ export const addSocketQueueItemStatusChangedEventListener = (startAppListening:
5456
])
5557
);
5658

57-
if (['in_progress'].includes(action.payload.data.queue_item.status)) {
59+
if (queue_item.status === 'in_progress') {
5860
forEach($nodeExecutionStates.get(), (nes) => {
5961
if (!nes) {
6062
return;
@@ -67,6 +69,26 @@ export const addSocketQueueItemStatusChangedEventListener = (startAppListening:
6769
clone.outputs = [];
6870
$nodeExecutionStates.setKey(clone.nodeId, clone);
6971
});
72+
} else if (queue_item.status === 'failed' && queue_item.error_type) {
73+
const { error_type, error_message, session_id } = queue_item;
74+
const isLocal = getState().config.isLocal ?? true;
75+
const sessionId = session_id;
76+
77+
toast({
78+
id: `INVOCATION_ERROR_${error_type}`,
79+
title: getTitleFromErrorType(error_type),
80+
status: 'error',
81+
duration: null,
82+
description: (
83+
<ErrorToastDescription
84+
errorType={error_type}
85+
errorMessage={error_message}
86+
sessionId={sessionId}
87+
isLocal={false}
88+
/>
89+
),
90+
updateDescription: isLocal ? true : false,
91+
});
7092
}
7193
},
7294
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
2+
import { t } from 'i18next';
3+
import { upperFirst } from 'lodash-es';
4+
import { useMemo } from 'react';
5+
import { useTranslation } from 'react-i18next';
6+
import { PiCopyBold } from 'react-icons/pi';
7+
8+
function onCopy(sessionId: string) {
9+
navigator.clipboard.writeText(sessionId);
10+
}
11+
12+
const ERROR_TYPE_TO_TITLE: Record<string, string> = {
13+
OutOfMemoryError: 'toast.outOfMemoryError',
14+
};
15+
16+
const COMMERCIAL_ERROR_TYPE_TO_DESC: Record<string, string> = {
17+
OutOfMemoryError: 'toast.outOfMemoryErrorDesc',
18+
};
19+
20+
export const getTitleFromErrorType = (errorType: string) => {
21+
return t(ERROR_TYPE_TO_TITLE[errorType] ?? 'toast.serverError');
22+
};
23+
24+
type Props = { errorType: string; errorMessage?: string | null; sessionId: string; isLocal: boolean };
25+
26+
export default function ErrorToastDescription({ errorType, errorMessage, sessionId, isLocal }: Props) {
27+
const { t } = useTranslation();
28+
const description = useMemo(() => {
29+
// Special handling for commercial error types
30+
const descriptionTKey = isLocal ? null : COMMERCIAL_ERROR_TYPE_TO_DESC[errorType];
31+
if (descriptionTKey) {
32+
return t(descriptionTKey);
33+
}
34+
if (errorMessage) {
35+
return upperFirst(errorMessage);
36+
}
37+
}, [errorMessage, errorType, isLocal, t]);
38+
return (
39+
<Flex flexDir="column">
40+
{description && <Text fontSize="md">{description}</Text>}
41+
{!isLocal && (
42+
<Flex gap="2" alignItems="center">
43+
<Text fontSize="sm" fontStyle="italic">
44+
{t('toast.sessionRef', { sessionId })}
45+
</Text>
46+
<IconButton
47+
size="sm"
48+
aria-label="Copy"
49+
icon={<PiCopyBold />}
50+
onClick={onCopy.bind(null, sessionId)}
51+
variant="ghost"
52+
sx={sx}
53+
/>
54+
</Flex>
55+
)}
56+
</Flex>
57+
);
58+
}
59+
60+
const sx = { svg: { fill: 'base.50' } };

invokeai/frontend/web/src/features/toast/ToastWithSessionRefDescription.tsx

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)