Skip to content

Commit

Permalink
feat(ui): toast on queue item errors, improved error descriptions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
psychedelicious committed May 24, 2024
1 parent 50dd569 commit f5a775a
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,16 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { deepClone } from 'common/util/deepClone';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
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';

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) => {
startAppListening({
actionCreator: socketInvocationError,
effect: (action, { getState }) => {
effect: (action) => {
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 } =
action.payload.data;
const { source_node_id, error_type, error_message, error_traceback } = action.payload.data;
const nes = deepClone($nodeExecutionStates.get()[source_node_id]);
if (nes) {
nes.status = zNodeStatus.enum.FAILED;
Expand All @@ -53,19 +25,6 @@ export const addInvocationErrorEventListener = (startAppListening: AppStartListe
};
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,
});
},
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { deepClone } from 'common/util/deepClone';
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
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 { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue';
import { socketQueueItemStatusChanged } from 'services/events/actions';
Expand All @@ -12,7 +14,7 @@ const log = logger('socketio');
export const addSocketQueueItemStatusChangedEventListener = (startAppListening: AppStartListening) => {
startAppListening({
actionCreator: socketQueueItemStatusChanged,
effect: async (action, { dispatch }) => {
effect: async (action, { dispatch, getState }) => {
// we've got new status for the queue item, batch and queue
const { queue_item, batch_status, queue_status } = action.payload.data;

Expand Down Expand Up @@ -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) => {
if (!nes) {
return;
Expand All @@ -67,6 +69,26 @@ export const addSocketQueueItemStatusChangedEventListener = (startAppListening:
clone.outputs = [];
$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,
});
}
},
});
Expand Down
60 changes: 60 additions & 0 deletions invokeai/frontend/web/src/features/toast/ErrorToastDescription.tsx
Original file line number Diff line number Diff line change
@@ -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' } };

This file was deleted.

0 comments on commit f5a775a

Please sign in to comment.