Skip to content

Adds recall metadata & warning modal #8117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@
"orderBy": "Order By",
"outpaint": "outpaint",
"outputs": "Outputs",
"postprocessing": "Post Processing",
"random": "Random",
"postprocessing": "Post Processing", "random": "Random",
"recall": "Recall",
"reportBugLabel": "Report Bug",
"safetensors": "Safetensors",
"save": "Save",
Expand Down Expand Up @@ -393,10 +393,11 @@
"compareHelp1": "Hold <Kbd>Alt</Kbd> while clicking a gallery image or using the arrow keys to change the compare image.",
"compareHelp2": "Press <Kbd>M</Kbd> to cycle through comparison modes.",
"compareHelp3": "Press <Kbd>C</Kbd> to swap the compared images.",
"compareHelp4": "Press <Kbd>Z</Kbd> or <Kbd>Esc</Kbd> to exit.",
"openViewer": "Open Viewer",
"compareHelp4": "Press <Kbd>Z</Kbd> or <Kbd>Esc</Kbd> to exit.", "openViewer": "Open Viewer",
"closeViewer": "Close Viewer",
"move": "Move"
"move": "Move",
"recallParametersCanvasWarning": "Recalling parameters will potentially overwrite your current canvas settings and may affect active layers. Some canvas parameters may be overwritten.",
"activeCanvasData": "Active Canvas Data: {{data}}"
},
"hotkeys": {
"hotkeys": "Hotkeys",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import { RecallMetadataConfirmationAlertDialog } from 'features/gallery/components/ImageGrid/RecallMetadataConfirmationAlertDialog';
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal';
import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal';
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
Expand Down Expand Up @@ -47,9 +48,9 @@ export const GlobalModalIsolator = memo(() => {
<ShareWorkflowModal />
<RefreshAfterResetModal />
<DeleteBoardModal />
<GlobalImageHotkeys />
<NewGallerySessionDialog />
<GlobalImageHotkeys /> <NewGallerySessionDialog />
<NewCanvasSessionDialog />
<RecallMetadataConfirmationAlertDialog />
<ImageContextMenu />
<FullscreenDropzone />
<VideosModal />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useRecallMetadataWithConfirmation } from 'features/gallery/components/ImageGrid/RecallMetadataConfirmationAlertDialog';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { memo } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowBendUpLeftBold,
Expand All @@ -17,10 +18,19 @@ export const ImageMenuItemMetadataRecallActions = memo(() => {
const { t } = useTranslation();
const imageDTO = useImageDTOContext();
const subMenu = useSubMenu();
const { recallWithConfirmation } = useRecallMetadataWithConfirmation();

const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, createAsPreset } =
useImageActions(imageDTO);

const handleRecallAll = useCallback(() => {
if (hasMetadata) {
recallWithConfirmation(() => {
recallAll();
});
}
}, [hasMetadata, recallAll, recallWithConfirmation]);

return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiArrowBendUpLeftBold />}>
<Menu {...subMenu.menuProps}>
Expand All @@ -36,8 +46,7 @@ export const ImageMenuItemMetadataRecallActions = memo(() => {
</MenuItem>
<MenuItem icon={<PiPlantBold />} onClick={recallSeed} isDisabled={!hasSeed}>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem icon={<PiAsteriskBold />} onClick={recallAll} isDisabled={!hasMetadata}>
</MenuItem> <MenuItem icon={<PiAsteriskBold />} onClick={handleRecallAll} isDisabled={!hasMetadata}>
{t('parameters.useAll')}
</MenuItem>
<MenuItem icon={<PiPaintBrushBold />} onClick={createAsPreset} isDisabled={!hasPrompts}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useAppSelector } from 'app/store/storeHooks';
import { GalleryImageDeleteIconButton } from 'features/gallery/components/ImageGrid/GalleryImageDeleteIconButton';
import { GalleryImageOpenInViewerIconButton } from 'features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton';
import { GalleryImageRecallAllIconButton } from 'features/gallery/components/ImageGrid/GalleryImageRecallAllIconButton';
import { GalleryImageSizeBadge } from 'features/gallery/components/ImageGrid/GalleryImageSizeBadge';
import { GalleryImageStarIconButton } from 'features/gallery/components/ImageGrid/GalleryImageStarIconButton';
import { selectAlwaysShouldImageSizeBadge } from 'features/gallery/store/gallerySelectors';
Expand All @@ -14,11 +15,11 @@ type Props = {

export const GalleryImageHoverIcons = memo(({ imageDTO, isHovered }: Props) => {
const alwaysShowImageSizeBadge = useAppSelector(selectAlwaysShouldImageSizeBadge);

return (
<>
{(isHovered || alwaysShowImageSizeBadge) && <GalleryImageSizeBadge imageDTO={imageDTO} />}
{(isHovered || imageDTO.starred) && <GalleryImageStarIconButton imageDTO={imageDTO} />}
{isHovered && <GalleryImageRecallAllIconButton imageDTO={imageDTO} />}
{isHovered && <GalleryImageDeleteIconButton imageDTO={imageDTO} />}
{isHovered && <GalleryImageOpenInViewerIconButton imageDTO={imageDTO} />}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { DndImageIcon } from 'features/dnd/DndImageIcon';
import { useRecallMetadataWithConfirmation } from 'features/gallery/components/ImageGrid/RecallMetadataConfirmationAlertDialog';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { memo, useCallback } from 'react';
import { PiAsteriskBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';

type Props = {
imageDTO: ImageDTO;
};

export const GalleryImageRecallAllIconButton = memo(({ imageDTO }: Props) => {
const imageActions = useImageActions(imageDTO);
const { recallWithConfirmation } = useRecallMetadataWithConfirmation();

const onClick = useCallback(() => {
if (imageActions.hasMetadata) {
recallWithConfirmation(() => {
imageActions.recallAll();
});
}
}, [imageActions, recallWithConfirmation]);

return (
<DndImageIcon
onClick={onClick}
icon={<PiAsteriskBold />}
tooltip="Recall"
position="absolute"
insetBlockStart={2}
insetInlineStart="50%"
transform="translateX(-50%)"
isDisabled={!imageActions.hasMetadata}
/>
);
});

GalleryImageRecallAllIconButton.displayName = 'GalleryImageRecallAllIconButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import {
selectActiveControlLayerEntities,
selectActiveInpaintMaskEntities,
selectActiveRasterLayerEntities,
selectActiveReferenceImageEntities,
selectActiveRegionalGuidanceEntities,
} from 'features/controlLayers/store/selectors';
import {
selectSystemShouldConfirmOnNewSession,
shouldConfirmOnNewSessionToggled,
} from 'features/system/store/systemSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';

type RecallMetadataCallback = () => void;

const [useRecallMetadataConfirmationDialog] = buildUseBoolean(false);

let pendingRecallCallback: RecallMetadataCallback | null = null;

export const useRecallMetadataWithConfirmation = () => {
const dialog = useRecallMetadataConfirmationDialog();
const shouldConfirm = useAppSelector(selectSystemShouldConfirmOnNewSession);

// Check if there are any active canvas layers that would be affected
const activeRasterLayers = useAppSelector(selectActiveRasterLayerEntities);
const activeControlLayers = useAppSelector(selectActiveControlLayerEntities);
const activeInpaintMasks = useAppSelector(selectActiveInpaintMaskEntities);
const activeRegionalGuidance = useAppSelector(selectActiveRegionalGuidanceEntities);
const activeReferenceImages = useAppSelector(selectActiveReferenceImageEntities);

const hasActiveCanvasData = useMemo(() => {
return (
activeRasterLayers.length > 0 ||
activeControlLayers.length > 0 ||
activeInpaintMasks.length > 0 ||
activeRegionalGuidance.length > 0 ||
activeReferenceImages.length > 0
);
}, [
activeRasterLayers.length,
activeControlLayers.length,
activeInpaintMasks.length,
activeRegionalGuidance.length,
activeReferenceImages.length,
]);

const recallWithConfirmation = useCallback(
(recallCallback: RecallMetadataCallback) => {
// If there's no active canvas data or user has disabled confirmations, recall immediately
if (!hasActiveCanvasData || !shouldConfirm) {
recallCallback();
return;
}

// Store the callback and show the confirmation dialog
pendingRecallCallback = recallCallback;
dialog.setTrue();
},
[dialog, hasActiveCanvasData, shouldConfirm]
);

return {
recallWithConfirmation,
hasActiveCanvasData,
};
};

export const RecallMetadataConfirmationAlertDialog = memo(() => {
useAssertSingleton('RecallMetadataConfirmationAlertDialog');
const { t } = useTranslation();
const dispatch = useAppDispatch();
const dialog = useRecallMetadataConfirmationDialog();
const shouldConfirm = useAppSelector(selectSystemShouldConfirmOnNewSession);

const activeRasterLayers = useAppSelector(selectActiveRasterLayerEntities);
const activeControlLayers = useAppSelector(selectActiveControlLayerEntities);
const activeInpaintMasks = useAppSelector(selectActiveInpaintMaskEntities);
const activeRegionalGuidance = useAppSelector(selectActiveRegionalGuidanceEntities);
const activeReferenceImages = useAppSelector(selectActiveReferenceImageEntities);

const onConfirm = useCallback(() => {
if (pendingRecallCallback) {
pendingRecallCallback();
pendingRecallCallback = null;
}
dialog.setFalse();
}, [dialog]);

const onCancel = useCallback(() => {
pendingRecallCallback = null;
dialog.setFalse();
}, [dialog]);

const onToggleConfirm = useCallback(() => {
dispatch(shouldConfirmOnNewSessionToggled());
}, [dispatch]);
const getCanvasDataSummary = useCallback(() => {
const items = [];
if (activeRasterLayers.length > 0) {
items.push(t('controlLayers.rasterLayer_withCount_other', { count: activeRasterLayers.length }));
}
if (activeControlLayers.length > 0) {
items.push(t('controlLayers.controlLayer_withCount_other', { count: activeControlLayers.length }));
}
if (activeInpaintMasks.length > 0) {
items.push(t('controlLayers.inpaintMask_withCount_other', { count: activeInpaintMasks.length }));
}
if (activeRegionalGuidance.length > 0) {
items.push(t('controlLayers.regionalGuidance_withCount_other', { count: activeRegionalGuidance.length }));
}
if (activeReferenceImages.length > 0) {
items.push(t('controlLayers.globalReferenceImage_withCount_other', { count: activeReferenceImages.length }));
}
return items.join(', ');
}, [activeRasterLayers.length, activeControlLayers.length, activeInpaintMasks.length, activeRegionalGuidance.length, activeReferenceImages.length, t]);

return (
<ConfirmationAlertDialog
isOpen={dialog.isTrue}
onClose={onCancel}
title={t('metadata.recallParameters')}
acceptCallback={onConfirm}
cancelCallback={onCancel}
acceptButtonText={t('common.recall')}
cancelButtonText={t('common.cancel')}
useInert={false}
>
<Flex direction="column" gap={3}>
<Text>{t('gallery.recallParametersCanvasWarning')}</Text>
<Text fontWeight="semibold">
{t('gallery.activeCanvasData', { data: getCanvasDataSummary() })}
</Text>
<Text>{t('common.areYouSure')}</Text>
<FormControl>
<FormLabel>{t('common.dontAskMeAgain')}</FormLabel>
<Checkbox isChecked={!shouldConfirm} onChange={onToggleConfirm} />
</FormControl>
</Flex>
</ConfirmationAlertDialog>
);
});

RecallMetadataConfirmationAlertDialog.displayName = 'RecallMetadataConfirmationAlertDialog';
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { useAppSelector } from 'app/store/storeHooks';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
import { useRecallMetadataWithConfirmation } from 'features/gallery/components/ImageGrid/RecallMetadataConfirmationAlertDialog';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowsCounterClockwiseBold,
Expand Down Expand Up @@ -43,6 +44,15 @@ const CurrentImageButtonsContent = memo(({ imageDTO }: { imageDTO: ImageDTO }) =
const imageActions = useImageActions(imageDTO);
const isStaging = useAppSelector(selectIsStaging);
const isUpscalingEnabled = useFeatureStatus('upscaling');
const { recallWithConfirmation } = useRecallMetadataWithConfirmation();

const handleRecallAll = useCallback(() => {
if (imageActions.hasMetadata) {
recallWithConfirmation(() => {
imageActions.recallAll();
});
}
}, [imageActions, recallWithConfirmation]);

return (
<>
Expand Down Expand Up @@ -105,15 +115,14 @@ const CurrentImageButtonsContent = memo(({ imageDTO }: { imageDTO: ImageDTO }) =
alignSelf="stretch"
onClick={imageActions.recallSize}
isDisabled={isStaging}
/>
<IconButton
/> <IconButton
icon={<PiAsteriskBold />}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={!imageActions.hasMetadata}
variant="link"
alignSelf="stretch"
onClick={imageActions.recallAll}
onClick={handleRecallAll}
/>

{isUpscalingEnabled && <PostProcessingPopover imageDTO={imageDTO} />}
Expand Down
Loading