feat(ui): rework image viewer

- Rework styling
- Replace "CurrentImageDisplay" entirely
- Add a super short fade to reduce jarring transition
- Make the viewer a singleton component, overlaid on everything else - reduces change when switching tabs
This commit is contained in:
psychedelicious 2024-05-03 09:37:52 +10:00 committed by Kent Keirsey
parent c05e52ebae
commit 33617fc06a
14 changed files with 294 additions and 253 deletions

View File

@ -1,24 +0,0 @@
import { Flex } from '@invoke-ai/ui-library';
import { memo } from 'react';
import CurrentImageButtons from './CurrentImageButtons';
import CurrentImagePreview from './CurrentImagePreview';
const CurrentImageDisplay = () => {
return (
<Flex
position="relative"
flexDirection="column"
height="100%"
width="100%"
rowGap={4}
alignItems="center"
justifyContent="center"
>
<CurrentImageButtons />
<CurrentImagePreview />
</Flex>
);
};
export default memo(CurrentImageDisplay);

View File

@ -1,79 +0,0 @@
import { Button, Flex } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import CurrentImageButtons from 'features/gallery/components/CurrentImage/CurrentImageButtons';
import CurrentImagePreview from 'features/gallery/components/CurrentImage/CurrentImagePreview';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiArrowLeftBold } from 'react-icons/pi';
const TAB_NAME_TO_TKEY: Record<InvokeTabName, string> = {
generation: 'ui.tabs.generation',
canvas: 'ui.tabs.canvas',
workflows: 'ui.tabs.workflows',
models: 'ui.tabs.models',
queue: 'ui.tabs.queue',
};
export const ImageViewer = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen);
const activeTabName = useAppSelector(activeTabNameSelector);
const activeTabLabel = useMemo(
() => t('gallery.backToEditor', { tab: t(TAB_NAME_TO_TKEY[activeTabName]) }),
[t, activeTabName]
);
const onClose = useCallback(() => {
dispatch(isImageViewerOpenChanged(false));
}, [dispatch]);
const onOpen = useCallback(() => {
dispatch(isImageViewerOpenChanged(true));
}, [dispatch]);
useHotkeys('esc', onClose, { enabled: isOpen }, [isOpen]);
useHotkeys('i', onOpen, { enabled: !isOpen }, [isOpen]);
if (!isOpen) {
return null;
}
return (
<Flex
layerStyle="first"
borderRadius="base"
position="absolute"
flexDirection="column"
top={0}
right={0}
bottom={0}
left={0}
p={2}
rowGap={4}
alignItems="center"
justifyContent="center"
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
>
<CurrentImageButtons />
<CurrentImagePreview />
<Button
aria-label={activeTabLabel}
tooltip={activeTabLabel}
onClick={onClose}
leftIcon={<PiArrowLeftBold />}
position="absolute"
top={2}
insetInlineEnd={2}
>
{t('common.back')}
</Button>
</Flex>
);
});
ImageViewer.displayName = 'ImageViewer';

View File

@ -0,0 +1,24 @@
import { Button } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowLeftBold } from 'react-icons/pi';
import { TAB_NAME_TO_TKEY, useImageViewer } from './useImageViewer';
export const BackToEditorButton = () => {
const { t } = useTranslation();
const { onClose } = useImageViewer();
const activeTabName = useAppSelector(activeTabNameSelector);
const tooltip = useMemo(
() => t('gallery.backToEditor', { tab: t(TAB_NAME_TO_TKEY[activeTabName]) }),
[t, activeTabName]
);
return (
<Button aria-label={tooltip} tooltip={tooltip} onClick={onClose} leftIcon={<PiArrowLeftBold />} variant="ghost">
{t('common.back')}
</Button>
);
};

View File

@ -1,7 +1,6 @@
import { ButtonGroup, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { ButtonGroup, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppToaster } from 'app/components/Toaster';
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
@ -17,7 +16,6 @@ import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUps
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { setShouldShowImageDetails, setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
@ -27,8 +25,6 @@ import {
PiAsteriskBold,
PiDotsThreeOutlineFill,
PiFlowArrowBold,
PiHourglassHighBold,
PiInfoBold,
PiPlantBold,
PiQuotesBold,
PiRulerBold,
@ -48,15 +44,12 @@ const selectShouldDisableToolbarButtons = createSelector(
const CurrentImageButtons = () => {
const dispatch = useAppDispatch();
const isConnected = useAppSelector((s) => s.system.isConnected);
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer);
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
const selection = useAppSelector((s) => s.gallery.selection);
const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons);
const isUpscalingEnabled = useFeatureStatus('upscaling');
const isQueueMutationInProgress = useIsQueueMutationInProgress();
const toaster = useAppToaster();
const { t } = useTranslation();
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
@ -120,28 +113,6 @@ const CurrentImageButtons = () => {
[isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected]
);
const handleClickShowImageDetails = useCallback(
() => dispatch(setShouldShowImageDetails(!shouldShowImageDetails)),
[dispatch, shouldShowImageDetails]
);
useHotkeys(
'i',
() => {
if (imageDTO) {
handleClickShowImageDetails();
} else {
toaster({
title: t('toast.metadataLoadFailed'),
status: 'error',
duration: 2500,
isClosable: true,
});
}
},
[imageDTO, shouldShowImageDetails, toaster]
);
useHotkeys(
'delete',
() => {
@ -150,106 +121,80 @@ const CurrentImageButtons = () => {
[dispatch, imageDTO]
);
const handleClickProgressImagesToggle = useCallback(() => {
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
}, [dispatch, shouldShowProgressInViewer]);
return (
<>
<Flex flexWrap="wrap" justifyContent="center" alignItems="center" gap={2}>
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
<Menu isLazy>
<MenuButton
as={IconButton}
aria-label={t('parameters.imageActions')}
tooltip={t('parameters.imageActions')}
isDisabled={!imageDTO}
icon={<PiDotsThreeOutlineFill />}
/>
<MenuList>{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}</MenuList>
</Menu>
</ButtonGroup>
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
<Menu isLazy>
<MenuButton
as={IconButton}
aria-label={t('parameters.imageActions')}
tooltip={t('parameters.imageActions')}
isDisabled={!imageDTO}
icon={<PiDotsThreeOutlineFill />}
/>
<MenuList>{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}</MenuList>
</Menu>
</ButtonGroup>
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
<IconButton
icon={<PiFlowArrowBold />}
tooltip={`${t('nodes.loadWorkflow')} (W)`}
aria-label={`${t('nodes.loadWorkflow')} (W)`}
isDisabled={!imageDTO?.has_workflow}
onClick={handleLoadWorkflow}
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiArrowsCounterClockwiseBold />}
tooltip={`${t('parameters.remixImage')} (R)`}
aria-label={`${t('parameters.remixImage')} (R)`}
isDisabled={!hasMetadata}
onClick={remix}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiQuotesBold />}
tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.usePrompt')} (P)`}
isDisabled={!hasPrompts}
onClick={recallPrompts}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiPlantBold />}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={!hasSeed}
onClick={recallSeed}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiRulerBold />}
tooltip={`${t('parameters.useSize')} (D)`}
aria-label={`${t('parameters.useSize')} (D)`}
onClick={handleUseSize}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiAsteriskBold />}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={!hasMetadata}
onClick={recallAll}
/>
</ButtonGroup>
<ButtonGroup isDisabled={shouldDisableToolbarButtons}>
<IconButton
icon={<PiFlowArrowBold />}
tooltip={`${t('nodes.loadWorkflow')} (W)`}
aria-label={`${t('nodes.loadWorkflow')} (W)`}
isDisabled={!imageDTO?.has_workflow}
onClick={handleLoadWorkflow}
isLoading={getAndLoadEmbeddedWorkflowResult.isLoading}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiArrowsCounterClockwiseBold />}
tooltip={`${t('parameters.remixImage')} (R)`}
aria-label={`${t('parameters.remixImage')} (R)`}
isDisabled={!hasMetadata}
onClick={remix}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiQuotesBold />}
tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.usePrompt')} (P)`}
isDisabled={!hasPrompts}
onClick={recallPrompts}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiPlantBold />}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={!hasSeed}
onClick={recallSeed}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiRulerBold />}
tooltip={`${t('parameters.useSize')} (D)`}
aria-label={`${t('parameters.useSize')} (D)`}
onClick={handleUseSize}
/>
<IconButton
isLoading={isLoadingMetadata}
icon={<PiAsteriskBold />}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={!hasMetadata}
onClick={recallAll}
/>
</ButtonGroup>
{isUpscalingEnabled && (
<ButtonGroup isDisabled={isQueueMutationInProgress}>
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
</ButtonGroup>
)}
<ButtonGroup>
<IconButton
icon={<PiInfoBold />}
tooltip={`${t('parameters.info')} (I)`}
aria-label={`${t('parameters.info')} (I)`}
isChecked={shouldShowImageDetails}
onClick={handleClickShowImageDetails}
/>
{isUpscalingEnabled && (
<ButtonGroup isDisabled={isQueueMutationInProgress}>
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
</ButtonGroup>
)}
<ButtonGroup>
<IconButton
aria-label={t('settings.displayInProgress')}
tooltip={t('settings.displayInProgress')}
icon={<PiHourglassHighBold />}
isChecked={shouldShowProgressInViewer}
onClick={handleClickProgressImagesToggle}
/>
</ButtonGroup>
<ButtonGroup>
<DeleteImageButton onClick={handleDelete} />
</ButtonGroup>
</Flex>
<ButtonGroup>
<DeleteImageButton onClick={handleDelete} />
</ButtonGroup>
</>
);
};

View File

@ -5,7 +5,6 @@ import { useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import ProgressImage from 'features/gallery/components/CurrentImage/ProgressImage';
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
@ -17,6 +16,8 @@ import { useTranslation } from 'react-i18next';
import { PiImageBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import ProgressImage from './ProgressImage';
const selectLastSelectedImageName = createSelector(
selectLastSelectedImage,
(lastSelectedImage) => lastSelectedImage?.image_name

View File

@ -0,0 +1,90 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { BackToEditorButton } from './BackToEditorButton';
import CurrentImageButtons from './CurrentImageButtons';
import CurrentImagePreview from './CurrentImagePreview';
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.07 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.07 },
};
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
export const ImageViewer = memo(() => {
const { isOpen, onToggle, onClose } = useImageViewer();
const activeTabName = useAppSelector(activeTabNameSelector);
const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]);
const shouldShowViewer = useMemo(() => {
if (!isViewerEnabled) {
return false;
}
return isOpen;
}, [isOpen, isViewerEnabled]);
useHotkeys('shift+s', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
return (
<AnimatePresence>
{shouldShowViewer && (
<Flex
as={motion.div}
initial={initial}
animate={animate}
exit={exit}
layerStyle="first"
borderRadius="base"
position="absolute"
flexDirection="column"
top={0}
right={0}
bottom={0}
left={0}
p={2}
rowGap={4}
alignItems="center"
justifyContent="center"
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
>
<Flex w="full" gap={2}>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineEnd="auto">
<ToggleProgressButton />
<ToggleMetadataViewerButton />
</Flex>
</Flex>
<Flex flex={1} gap={2} justifyContent="center">
<CurrentImageButtons />
</Flex>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto">
<BackToEditorButton />
</Flex>
</Flex>
</Flex>
<CurrentImagePreview />
</Flex>
)}
</AnimatePresence>
);
});
ImageViewer.displayName = 'ImageViewer';

View File

@ -0,0 +1,42 @@
import { IconButton } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppToaster } from 'app/components/Toaster';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import { setShouldShowImageDetails } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiInfoBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
export const ToggleMetadataViewerButton = memo(() => {
const dispatch = useAppDispatch();
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
const toaster = useAppToaster();
const { t } = useTranslation();
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
const toggleMetadataViewer = useCallback(
() => dispatch(setShouldShowImageDetails(!shouldShowImageDetails)),
[dispatch, shouldShowImageDetails]
);
useHotkeys('i', toggleMetadataViewer, { enabled: Boolean(imageDTO) }, [imageDTO, shouldShowImageDetails, toaster]);
return (
<IconButton
icon={<PiInfoBold />}
tooltip={`${t('parameters.info')} (I)`}
aria-label={`${t('parameters.info')} (I)`}
onClick={toggleMetadataViewer}
isDisabled={!imageDTO}
variant="outline"
colorScheme={shouldShowImageDetails ? 'invokeBlue' : 'base'}
/>
);
});
ToggleMetadataViewerButton.displayName = 'ToggleMetadataViewerButton';

View File

@ -0,0 +1,29 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiHourglassHighBold } from 'react-icons/pi';
export const ToggleProgressButton = memo(() => {
const dispatch = useAppDispatch();
const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer);
const { t } = useTranslation();
const onClick = useCallback(() => {
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
}, [dispatch, shouldShowProgressInViewer]);
return (
<IconButton
aria-label={t('settings.displayInProgress')}
tooltip={t('settings.displayInProgress')}
icon={<PiHourglassHighBold />}
onClick={onClick}
variant="outline"
colorScheme={shouldShowProgressInViewer ? 'invokeBlue' : 'base'}
/>
);
});
ToggleProgressButton.displayName = 'ToggleProgressButton';

View File

@ -0,0 +1,31 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { useCallback } from 'react';
export const TAB_NAME_TO_TKEY: Record<InvokeTabName, string> = {
generation: 'ui.tabs.generation',
canvas: 'ui.tabs.canvas',
workflows: 'ui.tabs.workflows',
models: 'ui.tabs.models',
queue: 'ui.tabs.queue',
};
export const useImageViewer = () => {
const dispatch = useAppDispatch();
const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen);
const onClose = useCallback(() => {
dispatch(isImageViewerOpenChanged(false));
}, [dispatch]);
const onOpen = useCallback(() => {
dispatch(isImageViewerOpenChanged(true));
}, [dispatch]);
const onToggle = useCallback(() => {
dispatch(isImageViewerOpenChanged(!isOpen));
}, [dispatch, isOpen]);
return { isOpen, onOpen, onClose, onToggle };
};

View File

@ -4,6 +4,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
@ -253,10 +254,11 @@ const InvokeTabs = () => {
/>
</>
)}
<Panel id="main-panel" order={1} minSize={20}>
<Panel style={{ position: 'relative' }} id="main-panel" order={1} minSize={20}>
<TabPanels w="full" h="full">
{tabPanels}
</TabPanels>
<ImageViewer />
</Panel>
{shouldShowGalleryPanel && (
<>

View File

@ -1,30 +1,14 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay';
import { ImageViewer } from 'features/gallery/components/ImageViewer';
import { Box } from '@invoke-ai/ui-library';
import NodeEditor from 'features/nodes/components/NodeEditor';
import { memo } from 'react';
import { ReactFlowProvider } from 'reactflow';
const NodesTab = () => {
const mode = useAppSelector((s) => s.workflow.mode);
if (mode === 'edit') {
return (
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
<ReactFlowProvider>
<NodeEditor />
</ReactFlowProvider>
<ImageViewer />
</Box>
);
}
return (
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
<Flex w="full" h="full">
<CurrentImageDisplay />
</Flex>
<ReactFlowProvider>
<NodeEditor />
</ReactFlowProvider>
</Box>
);
};

View File

@ -1,13 +1,11 @@
import { Box } from '@invoke-ai/ui-library';
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
import { ImageViewer } from 'features/gallery/components/ImageViewer';
import { memo } from 'react';
const TextToImageTab = () => {
return (
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
<ControlLayersEditor />
<ImageViewer />
</Box>
);
};

View File

@ -6,7 +6,6 @@ import { CANVAS_TAB_TESTID } from 'features/canvas/store/constants';
import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks';
import type { CanvasInitialImageDropData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop';
import { ImageViewer } from 'features/gallery/components/ImageViewer';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@ -42,7 +41,6 @@ const UnifiedCanvasTab = () => {
>
<IAICanvasToolbar />
<IAICanvas />
<ImageViewer />
{isValidDrop(droppableData, active) && (
<IAIDropOverlay isOver={isOver} label={t('toast.setCanvasInitialImage')} />
)}