mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
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.
This commit is contained in:
parent
50dd569411
commit
f5a775ae4e
@ -3,44 +3,16 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
|
|||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||||
import { toast } from 'features/toast/toast';
|
|
||||||
import ToastWithSessionRefDescription from 'features/toast/ToastWithSessionRefDescription';
|
|
||||||
import { t } from 'i18next';
|
|
||||||
import { startCase } from 'lodash-es';
|
|
||||||
import { socketInvocationError } from 'services/events/actions';
|
import { socketInvocationError } from 'services/events/actions';
|
||||||
|
|
||||||
const log = logger('socketio');
|
const log = logger('socketio');
|
||||||
|
|
||||||
const getTitle = (errorType: string) => {
|
|
||||||
if (errorType === 'OutOfMemoryError') {
|
|
||||||
return t('toast.outOfMemoryError');
|
|
||||||
}
|
|
||||||
return t('toast.serverError');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDescription = (errorType: string, sessionId: string, isLocal?: boolean) => {
|
|
||||||
if (!isLocal) {
|
|
||||||
if (errorType === 'OutOfMemoryError') {
|
|
||||||
return ToastWithSessionRefDescription({
|
|
||||||
message: t('toast.outOfMemoryDescription'),
|
|
||||||
sessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return ToastWithSessionRefDescription({
|
|
||||||
message: errorType,
|
|
||||||
sessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return errorType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addInvocationErrorEventListener = (startAppListening: AppStartListening) => {
|
export const addInvocationErrorEventListener = (startAppListening: AppStartListening) => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: socketInvocationError,
|
actionCreator: socketInvocationError,
|
||||||
effect: (action, { getState }) => {
|
effect: (action) => {
|
||||||
log.error(action.payload, `Invocation error (${action.payload.data.node.type})`);
|
log.error(action.payload, `Invocation error (${action.payload.data.node.type})`);
|
||||||
const { source_node_id, error_type, error_message, error_traceback, graph_execution_state_id } =
|
const { source_node_id, error_type, error_message, error_traceback } = action.payload.data;
|
||||||
action.payload.data;
|
|
||||||
const nes = deepClone($nodeExecutionStates.get()[source_node_id]);
|
const nes = deepClone($nodeExecutionStates.get()[source_node_id]);
|
||||||
if (nes) {
|
if (nes) {
|
||||||
nes.status = zNodeStatus.enum.FAILED;
|
nes.status = zNodeStatus.enum.FAILED;
|
||||||
@ -53,19 +25,6 @@ export const addInvocationErrorEventListener = (startAppListening: AppStartListe
|
|||||||
};
|
};
|
||||||
upsertExecutionState(nes.nodeId, nes);
|
upsertExecutionState(nes.nodeId, nes);
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorType = startCase(error_type);
|
|
||||||
const sessionId = graph_execution_state_id;
|
|
||||||
const { isLocal } = getState().config;
|
|
||||||
|
|
||||||
toast({
|
|
||||||
id: `INVOCATION_ERROR_${errorType}`,
|
|
||||||
title: getTitle(errorType),
|
|
||||||
status: 'error',
|
|
||||||
duration: null,
|
|
||||||
description: getDescription(errorType, sessionId, isLocal),
|
|
||||||
updateDescription: isLocal ? true : false,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,8 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
|
|||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
|
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
|
||||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||||
|
import ErrorToastDescription, { getTitleFromErrorType } from 'features/toast/ErrorToastDescription';
|
||||||
|
import { toast } from 'features/toast/toast';
|
||||||
import { forEach } from 'lodash-es';
|
import { forEach } from 'lodash-es';
|
||||||
import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue';
|
import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue';
|
||||||
import { socketQueueItemStatusChanged } from 'services/events/actions';
|
import { socketQueueItemStatusChanged } from 'services/events/actions';
|
||||||
@ -12,7 +14,7 @@ const log = logger('socketio');
|
|||||||
export const addSocketQueueItemStatusChangedEventListener = (startAppListening: AppStartListening) => {
|
export const addSocketQueueItemStatusChangedEventListener = (startAppListening: AppStartListening) => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: socketQueueItemStatusChanged,
|
actionCreator: socketQueueItemStatusChanged,
|
||||||
effect: async (action, { dispatch }) => {
|
effect: async (action, { dispatch, getState }) => {
|
||||||
// we've got new status for the queue item, batch and queue
|
// we've got new status for the queue item, batch and queue
|
||||||
const { queue_item, batch_status, queue_status } = action.payload.data;
|
const { queue_item, batch_status, queue_status } = action.payload.data;
|
||||||
|
|
||||||
@ -54,7 +56,7 @@ export const addSocketQueueItemStatusChangedEventListener = (startAppListening:
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
if (['in_progress'].includes(action.payload.data.queue_item.status)) {
|
if (queue_item.status === 'in_progress') {
|
||||||
forEach($nodeExecutionStates.get(), (nes) => {
|
forEach($nodeExecutionStates.get(), (nes) => {
|
||||||
if (!nes) {
|
if (!nes) {
|
||||||
return;
|
return;
|
||||||
@ -67,6 +69,26 @@ export const addSocketQueueItemStatusChangedEventListener = (startAppListening:
|
|||||||
clone.outputs = [];
|
clone.outputs = [];
|
||||||
$nodeExecutionStates.setKey(clone.nodeId, clone);
|
$nodeExecutionStates.setKey(clone.nodeId, clone);
|
||||||
});
|
});
|
||||||
|
} else if (queue_item.status === 'failed' && queue_item.error_type) {
|
||||||
|
const { error_type, error_message, session_id } = queue_item;
|
||||||
|
const isLocal = getState().config.isLocal ?? true;
|
||||||
|
const sessionId = session_id;
|
||||||
|
|
||||||
|
toast({
|
||||||
|
id: `INVOCATION_ERROR_${error_type}`,
|
||||||
|
title: getTitleFromErrorType(error_type),
|
||||||
|
status: 'error',
|
||||||
|
duration: null,
|
||||||
|
description: (
|
||||||
|
<ErrorToastDescription
|
||||||
|
errorType={error_type}
|
||||||
|
errorMessage={error_message}
|
||||||
|
sessionId={sessionId}
|
||||||
|
isLocal={false}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
updateDescription: isLocal ? true : false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
@ -0,0 +1,60 @@
|
|||||||
|
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { upperFirst } from 'lodash-es';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiCopyBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
function onCopy(sessionId: string) {
|
||||||
|
navigator.clipboard.writeText(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ERROR_TYPE_TO_TITLE: Record<string, string> = {
|
||||||
|
OutOfMemoryError: 'toast.outOfMemoryError',
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMMERCIAL_ERROR_TYPE_TO_DESC: Record<string, string> = {
|
||||||
|
OutOfMemoryError: 'toast.outOfMemoryErrorDesc',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTitleFromErrorType = (errorType: string) => {
|
||||||
|
return t(ERROR_TYPE_TO_TITLE[errorType] ?? 'toast.serverError');
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = { errorType: string; errorMessage?: string | null; sessionId: string; isLocal: boolean };
|
||||||
|
|
||||||
|
export default function ErrorToastDescription({ errorType, errorMessage, sessionId, isLocal }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const description = useMemo(() => {
|
||||||
|
// Special handling for commercial error types
|
||||||
|
const descriptionTKey = isLocal ? null : COMMERCIAL_ERROR_TYPE_TO_DESC[errorType];
|
||||||
|
if (descriptionTKey) {
|
||||||
|
return t(descriptionTKey);
|
||||||
|
}
|
||||||
|
if (errorMessage) {
|
||||||
|
return upperFirst(errorMessage);
|
||||||
|
}
|
||||||
|
}, [errorMessage, errorType, isLocal, t]);
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column">
|
||||||
|
{description && <Text fontSize="md">{description}</Text>}
|
||||||
|
{!isLocal && (
|
||||||
|
<Flex gap="2" alignItems="center">
|
||||||
|
<Text fontSize="sm" fontStyle="italic">
|
||||||
|
{t('toast.sessionRef', { sessionId })}
|
||||||
|
</Text>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
aria-label="Copy"
|
||||||
|
icon={<PiCopyBold />}
|
||||||
|
onClick={onCopy.bind(null, sessionId)}
|
||||||
|
variant="ghost"
|
||||||
|
sx={sx}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sx = { svg: { fill: 'base.50' } };
|
@ -1,30 +0,0 @@
|
|||||||
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
|
||||||
import { t } from 'i18next';
|
|
||||||
import { PiCopyBold } from 'react-icons/pi';
|
|
||||||
|
|
||||||
function onCopy(sessionId: string) {
|
|
||||||
navigator.clipboard.writeText(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = { message: string; sessionId: string };
|
|
||||||
|
|
||||||
export default function ToastWithSessionRefDescription({ message, sessionId }: Props) {
|
|
||||||
return (
|
|
||||||
<Flex flexDir="column">
|
|
||||||
<Text fontSize="md">{message}</Text>
|
|
||||||
<Flex gap="2" alignItems="center">
|
|
||||||
<Text fontSize="sm">{t('toast.sessionRef', { sessionId })}</Text>
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
aria-label="Copy"
|
|
||||||
icon={<PiCopyBold />}
|
|
||||||
onClick={onCopy.bind(null, sessionId)}
|
|
||||||
variant="ghost"
|
|
||||||
sx={sx}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sx = { svg: { fill: 'base.50' } };
|
|
Loading…
Reference in New Issue
Block a user