mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 10:15:05 -05:00
refactor(ui): canvas flow (wip)
This commit is contained in:
@@ -6,11 +6,11 @@ import { selectIsLocal } from 'features/system/store/configSlice';
|
||||
import { selectSystemShouldShowInvocationProgressDetail } from 'features/system/store/systemSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { $invocationProgressMessage } from 'services/events/stores';
|
||||
import { $lastProgressMessage } from 'services/events/stores';
|
||||
|
||||
const CanvasAlertsInvocationProgressContentLocal = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const invocationProgressMessage = useStore($invocationProgressMessage);
|
||||
const invocationProgressMessage = useStore($lastProgressMessage);
|
||||
|
||||
if (!invocationProgressMessage) {
|
||||
return null;
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Button, ContextMenu, Flex, IconButton, Image, Menu, MenuButton, MenuList, Text } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
Button,
|
||||
ContextMenu,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Image,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
Text,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
|
||||
import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
|
||||
import { CanvasAlertsSendingToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo';
|
||||
@@ -32,13 +45,17 @@ import {
|
||||
stagingAreaNextStagedImageSelected,
|
||||
stagingAreaPrevStagedImageSelected,
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { newCanvasFromImage } from 'features/imageActions/actions';
|
||||
import type { ProgressImage } from 'features/nodes/types/common';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi';
|
||||
import type { ImageDTO, S } from 'services/api/types';
|
||||
import { $lastCanvasProgressImage, $socket } from 'services/events/stores';
|
||||
import type { Equals } from 'tsafe';
|
||||
import type { Equals, Param0 } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
|
||||
@@ -80,45 +97,190 @@ export const CanvasMainPanelContent = memo(() => {
|
||||
|
||||
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
|
||||
|
||||
const generateWithStartingImageDndTargetData = newCanvasFromImageDndTarget.getData({
|
||||
type: 'raster_layer',
|
||||
withResize: true,
|
||||
});
|
||||
const generateWithStartingImageAndInpaintMaskDndTargetData = newCanvasFromImageDndTarget.getData({
|
||||
type: 'raster_layer',
|
||||
withInpaintMask: true,
|
||||
});
|
||||
const generateWithControlImageDndTargetData = newCanvasFromImageDndTarget.getData({
|
||||
type: 'control_layer',
|
||||
withResize: true,
|
||||
});
|
||||
|
||||
const NoActiveSession = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const newSesh = useCallback(() => {
|
||||
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
No Active Session
|
||||
</Text>
|
||||
<Button display="flex" flexDir="column" gap={2} p={8} minH={0} minW={0} onClick={newSesh}>
|
||||
<Text>New Canvas Session</Text>
|
||||
<Text>- New Canvas Session</Text>
|
||||
<Text>- 1 Inpaint mask layer added</Text>
|
||||
<Heading>Get Started with Invoke</Heading>
|
||||
<Button variant="ghost" onClick={newSesh}>
|
||||
Start a new Canvas Session
|
||||
</Button>
|
||||
<Flex flexDir="column" gap={2} p={8} border="dashed yellow 2px">
|
||||
<Text>Generate with Starting Image</Text>
|
||||
<Text>- New Canvas Session</Text>
|
||||
<Text>- Dropped image as raster layer</Text>
|
||||
<Text>- Bbox resized</Text>
|
||||
</Flex>
|
||||
<Flex flexDir="column" gap={2} p={8} border="dashed yellow 2px">
|
||||
<Text>Generate with Control Image</Text>
|
||||
<Text>- New Canvas Session</Text>
|
||||
<Text>- Dropped image as control layer</Text>
|
||||
<Text>- Bbox resized</Text>
|
||||
</Flex>
|
||||
<Flex flexDir="column" gap={2} p={8} border="dashed yellow 2px">
|
||||
<Text>Edit Image</Text>
|
||||
<Text>- New Canvas Session</Text>
|
||||
<Text>- Dropped image as raster layer</Text>
|
||||
<Text>- Bbox resized</Text>
|
||||
<Text>- 1 Inpaint mask layer added</Text>
|
||||
<Text>or</Text>
|
||||
<Flex flexDir="column" maxW={512}>
|
||||
<GenerateWithStartingImage />
|
||||
<GenerateWithControlImage />
|
||||
<GenerateWithStartingImageAndInpaintMask />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
NoActiveSession.displayName = 'NoActiveSession';
|
||||
|
||||
const GenerateWithStartingImage = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const useImageUploadButtonOptions = useMemo<Param0<typeof useImageUploadButton>>(
|
||||
() => ({
|
||||
onUpload: (imageDTO: ImageDTO) => {
|
||||
newCanvasFromImage({ imageDTO, type: 'raster_layer', withResize: true, getState, dispatch });
|
||||
},
|
||||
allowMultiple: false,
|
||||
}),
|
||||
[dispatch, getState]
|
||||
);
|
||||
const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
UploadButton: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
color="base.300"
|
||||
{...uploadApi.getUploadButtonProps()}
|
||||
rightIcon={<PiUploadBold />}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[uploadApi]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex position="relative" flexDir="column">
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
Generate with a Starting Image
|
||||
</Text>
|
||||
<Text color="base.300">Regenerate the starting image using the model (Image to Image).</Text>
|
||||
<Text color="base.300">
|
||||
<Trans i18nKey="controlLayers.uploadOrDragAnImage" components={components} />
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Text>
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasFromImageDndTarget}
|
||||
dndTargetData={generateWithStartingImageDndTargetData}
|
||||
label="Drop"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
GenerateWithStartingImage.displayName = 'GenerateWithStartingImage';
|
||||
|
||||
const GenerateWithControlImage = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const useImageUploadButtonOptions = useMemo<Param0<typeof useImageUploadButton>>(
|
||||
() => ({
|
||||
onUpload: (imageDTO: ImageDTO) => {
|
||||
newCanvasFromImage({ imageDTO, type: 'control_layer', withResize: true, getState, dispatch });
|
||||
},
|
||||
allowMultiple: false,
|
||||
}),
|
||||
[dispatch, getState]
|
||||
);
|
||||
const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
UploadButton: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
color="base.300"
|
||||
{...uploadApi.getUploadButtonProps()}
|
||||
rightIcon={<PiUploadBold />}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[uploadApi]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex position="relative" flexDir="column">
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
Generate with a Control Image
|
||||
</Text>
|
||||
<Text color="base.300">
|
||||
Generate a new image using the control image to guide the structure and composition (Text to Image with
|
||||
Control).
|
||||
</Text>
|
||||
<Text color="base.300">
|
||||
<Trans i18nKey="controlLayers.uploadOrDragAnImage" components={components} />
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Text>
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasFromImageDndTarget}
|
||||
dndTargetData={generateWithControlImageDndTargetData}
|
||||
label="Drop"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
GenerateWithControlImage.displayName = 'GenerateWithControlImage';
|
||||
|
||||
const GenerateWithStartingImageAndInpaintMask = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { getState, dispatch } = useAppStore();
|
||||
const useImageUploadButtonOptions = useMemo<Param0<typeof useImageUploadButton>>(
|
||||
() => ({
|
||||
onUpload: (imageDTO: ImageDTO) => {
|
||||
newCanvasFromImage({ imageDTO, type: 'raster_layer', withInpaintMask: true, getState, dispatch });
|
||||
},
|
||||
allowMultiple: false,
|
||||
}),
|
||||
[dispatch, getState]
|
||||
);
|
||||
const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
UploadButton: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
color="base.300"
|
||||
{...uploadApi.getUploadButtonProps()}
|
||||
rightIcon={<PiUploadBold />}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[uploadApi]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex position="relative" flexDir="column">
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
Edit Image
|
||||
</Text>
|
||||
<Text color="base.300">Edit the image by regenerating parts of it (Inpaint).</Text>
|
||||
<Text color="base.300">
|
||||
<Trans i18nKey="controlLayers.uploadOrDragAnImage" components={components} />
|
||||
<input {...uploadApi.getUploadInputProps()} />
|
||||
</Text>
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasFromImageDndTarget}
|
||||
dndTargetData={generateWithStartingImageAndInpaintMaskDndTargetData}
|
||||
label="Drop"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask';
|
||||
|
||||
type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
|
||||
|
||||
const SimpleActiveSession = memo(() => {
|
||||
@@ -213,6 +375,9 @@ const SelectedImage = memo(() => {
|
||||
src={selectedImage.imageDTO.image_url}
|
||||
width={selectedImage.imageDTO.width}
|
||||
height={selectedImage.imageDTO.height}
|
||||
onLoad={() => {
|
||||
console.log('onload');
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { dropTargetForExternal, monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
|
||||
import { Box, Tab } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectEntityCountActive } from 'features/controlLayers/store/selectors';
|
||||
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd/dnd';
|
||||
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
||||
import type { DndTargetState } from 'features/dnd/types';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
|
||||
export const CanvasRightPanelStacked = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const activeTab = useAppSelector(selectActiveTabCanvasRightPanel);
|
||||
const imageViewer = useImageViewer();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const tabIndex = useMemo(() => {
|
||||
if (activeTab === 'gallery') {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
const onClickViewerToggleButton = useCallback(() => {
|
||||
imageViewer.open();
|
||||
}, [imageViewer]);
|
||||
|
||||
const onChangeTab = useCallback(
|
||||
(index: number) => {
|
||||
if (index === 0) {
|
||||
dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||
} else {
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelGroup direction="vertical">
|
||||
<Panel>
|
||||
<GalleryPanelContent />
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel>
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasLayersPanelContent />
|
||||
</CanvasManagerProviderGate>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasRightPanelStacked.displayName = 'CanvasRightPanelStacked';
|
||||
|
||||
const PanelTabs = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const store = useAppStore();
|
||||
const activeEntityCount = useAppSelector(selectEntityCountActive);
|
||||
const [layersTabDndState, setLayersTabDndState] = useState<DndTargetState>('idle');
|
||||
const [galleryTabDndState, setGalleryTabDndState] = useState<DndTargetState>('idle');
|
||||
const layersTabRef = useRef<HTMLDivElement>(null);
|
||||
const galleryTabRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
const layersTabLabel = useMemo(() => {
|
||||
if (activeEntityCount === 0) {
|
||||
return t('controlLayers.layer_other');
|
||||
}
|
||||
return `${t('controlLayers.layer_other')} (${activeEntityCount})`;
|
||||
}, [activeEntityCount, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!layersTabRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getIsOnLayersTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'layers';
|
||||
|
||||
const onDragEnter = () => {
|
||||
// If we are already on the layers tab, do nothing
|
||||
if (getIsOnLayersTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Else set the state to active and switch to the layers tab after a timeout
|
||||
setLayersTabDndState('over');
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
timeoutRef.current = null;
|
||||
store.dispatch(activeTabCanvasRightPanelChanged('layers'));
|
||||
// When we switch tabs, the other tab should be pending
|
||||
setLayersTabDndState('idle');
|
||||
setGalleryTabDndState('potential');
|
||||
}, 300);
|
||||
};
|
||||
const onDragLeave = () => {
|
||||
// Set the state to idle or pending depending on the current tab
|
||||
if (getIsOnLayersTab()) {
|
||||
setLayersTabDndState('idle');
|
||||
} else {
|
||||
setLayersTabDndState('potential');
|
||||
}
|
||||
// Abort the tab switch if it hasn't happened yet
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
const onDragStart = () => {
|
||||
// Set the state to pending when a drag starts
|
||||
setLayersTabDndState('potential');
|
||||
};
|
||||
return combine(
|
||||
dropTargetForElements({
|
||||
element: layersTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForElements({
|
||||
canMonitor: ({ source }) => {
|
||||
if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
// Only monitor if we are not already on the gallery tab
|
||||
return !getIsOnLayersTab();
|
||||
},
|
||||
onDragStart,
|
||||
}),
|
||||
dropTargetForExternal({
|
||||
element: layersTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForExternal({
|
||||
canMonitor: () => !getIsOnLayersTab(),
|
||||
onDragStart,
|
||||
})
|
||||
);
|
||||
}, [store]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!galleryTabRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getIsOnGalleryTab = () => selectActiveTabCanvasRightPanel(store.getState()) === 'gallery';
|
||||
|
||||
const onDragEnter = () => {
|
||||
// If we are already on the gallery tab, do nothing
|
||||
if (getIsOnGalleryTab()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Else set the state to active and switch to the gallery tab after a timeout
|
||||
setGalleryTabDndState('over');
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
timeoutRef.current = null;
|
||||
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
// When we switch tabs, the other tab should be pending
|
||||
setGalleryTabDndState('idle');
|
||||
setLayersTabDndState('potential');
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const onDragLeave = () => {
|
||||
// Set the state to idle or pending depending on the current tab
|
||||
if (getIsOnGalleryTab()) {
|
||||
setGalleryTabDndState('idle');
|
||||
} else {
|
||||
setGalleryTabDndState('potential');
|
||||
}
|
||||
// Abort the tab switch if it hasn't happened yet
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragStart = () => {
|
||||
// Set the state to pending when a drag starts
|
||||
setGalleryTabDndState('potential');
|
||||
};
|
||||
|
||||
return combine(
|
||||
dropTargetForElements({
|
||||
element: galleryTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForElements({
|
||||
canMonitor: ({ source }) => {
|
||||
if (!singleImageDndSource.typeGuard(source.data) && !multipleImageDndSource.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
// Only monitor if we are not already on the gallery tab
|
||||
return !getIsOnGalleryTab();
|
||||
},
|
||||
onDragStart,
|
||||
}),
|
||||
dropTargetForExternal({
|
||||
element: galleryTabRef.current,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
}),
|
||||
monitorForExternal({
|
||||
canMonitor: () => !getIsOnGalleryTab(),
|
||||
onDragStart,
|
||||
})
|
||||
);
|
||||
}, [store]);
|
||||
|
||||
useEffect(() => {
|
||||
const onDrop = () => {
|
||||
// Reset the dnd state when a drop happens
|
||||
setGalleryTabDndState('idle');
|
||||
setLayersTabDndState('idle');
|
||||
};
|
||||
const cleanup = combine(monitorForElements({ onDrop }), monitorForExternal({ onDrop }));
|
||||
|
||||
return () => {
|
||||
cleanup();
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tab ref={layersTabRef} position="relative" w={32}>
|
||||
<Box as="span" w="full">
|
||||
{layersTabLabel}
|
||||
</Box>
|
||||
<DndDropOverlay dndState={layersTabDndState} withBackdrop={false} />
|
||||
</Tab>
|
||||
<Tab ref={galleryTabRef} position="relative" w={32}>
|
||||
<Box as="span" w="full">
|
||||
{t('gallery.gallery')}
|
||||
</Box>
|
||||
<DndDropOverlay dndState={galleryTabDndState} withBackdrop={false} />
|
||||
</Tab>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
PanelTabs.displayName = 'PanelTabs';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { UploadImageButton } from 'common/hooks/useImageUploadButton';
|
||||
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
|
||||
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
||||
import type { setGlobalReferenceImageDndTarget, setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
@@ -51,7 +51,7 @@ export const IPAdapterImagePreview = memo(
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full" alignItems="center" data-error={!imageDTO && !image?.image_name}>
|
||||
{!imageDTO && (
|
||||
<UploadImageButton
|
||||
<UploadImageIconButton
|
||||
w="full"
|
||||
h="full"
|
||||
isError={!imageDTO && !image?.image_name}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const useNewGallerySession = () => {
|
||||
const newSessionDialog = useNewGallerySessionDialog();
|
||||
|
||||
const newGallerySessionImmediate = useCallback(() => {
|
||||
dispatch(canvasSessionStarted({ sessionType: 'simple' }));
|
||||
dispatch(canvasSessionStarted({ sessionType: null }));
|
||||
dispatch(activeTabCanvasRightPanelChanged('gallery'));
|
||||
}, [dispatch]);
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { $invocationProgressMessage } from 'services/events/stores';
|
||||
import { $lastProgressMessage } from 'services/events/stores';
|
||||
|
||||
export const useDeferredModelLoadingInvocationProgressMessage = () => {
|
||||
const { t } = useTranslation();
|
||||
const invocationProgressMessage = useStore($invocationProgressMessage);
|
||||
const invocationProgressMessage = useStore($lastProgressMessage);
|
||||
const [delayedMessage, setDelayedMessage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
|
||||
import { fetchModelConfigByIdentifier } from 'features/metadata/util/modelFetchingHelpers';
|
||||
import type { ProgressImage} from 'features/nodes/types/common';
|
||||
import { zMainModelBase, zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas';
|
||||
import {
|
||||
@@ -437,10 +438,12 @@ export type LoRA = {
|
||||
};
|
||||
|
||||
export type StagingAreaImage = {
|
||||
sessionId: string;
|
||||
imageDTO: ImageDTO;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
};
|
||||
export type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
|
||||
|
||||
export const zAspectRatioID = z.enum(['Free', '21:9', '9:21', '16:9', '3:2', '4:3', '1:1', '3:4', '2:3', '9:16']);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { BoardId } from 'features/gallery/store/types';
|
||||
import {
|
||||
addImagesToBoard,
|
||||
createNewCanvasEntityFromImage,
|
||||
newCanvasFromImage,
|
||||
removeImagesFromBoard,
|
||||
replaceCanvasEntityObjectsWithImage,
|
||||
setComparisonImage,
|
||||
@@ -343,7 +344,35 @@ export const newCanvasEntityFromImageDndTarget: DndTarget<
|
||||
createNewCanvasEntityFromImage({ type, imageDTO, dispatch, getState });
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region New Canvas from Image
|
||||
const _newCanvas = buildTypeAndKey('new-canvas-entity-from-image');
|
||||
type NewCanvasFromImageDndTargetData = DndData<
|
||||
typeof _newCanvas.type,
|
||||
typeof _newCanvas.key,
|
||||
{
|
||||
type: CanvasEntityType | 'regional_guidance_with_reference_image';
|
||||
withResize?: boolean;
|
||||
withInpaintMask?: boolean;
|
||||
}
|
||||
>;
|
||||
export const newCanvasFromImageDndTarget: DndTarget<NewCanvasFromImageDndTargetData, SingleImageDndSourceData> = {
|
||||
..._newCanvas,
|
||||
typeGuard: buildTypeGuard(_newCanvas.key),
|
||||
getData: buildGetData(_newCanvas.key, _newCanvas.type),
|
||||
isValid: ({ sourceData }) => {
|
||||
if (!singleImageDndSource.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handler: ({ sourceData, targetData, dispatch, getState }) => {
|
||||
const { type, withResize } = targetData.payload;
|
||||
const { imageDTO } = sourceData.payload;
|
||||
newCanvasFromImage({ type, imageDTO, dispatch, getState, withResize });
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Replace Canvas Entity Objects With Image
|
||||
@@ -471,6 +500,7 @@ export const dndTargets = [
|
||||
replaceCanvasEntityObjectsWithImageDndTarget,
|
||||
addImageToBoardDndTarget,
|
||||
removeImageFromBoardDndTarget,
|
||||
newCanvasFromImageDndTarget,
|
||||
// Single or Multiple Image
|
||||
addImageToBoardDndTarget,
|
||||
removeImageFromBoardDndTarget,
|
||||
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
|
||||
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
||||
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
|
||||
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||
@@ -43,6 +46,7 @@ const GalleryPanelContent = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
|
||||
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
const sessionType = useAppSelector(selectCanvasSessionType);
|
||||
|
||||
const boardsListPanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
@@ -56,6 +60,30 @@ const GalleryPanelContent = () => {
|
||||
);
|
||||
const boardsListPanel = usePanel(boardsListPanelOptions);
|
||||
|
||||
const galleryPanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
id: 'gallery-panel',
|
||||
minSizePx: 128,
|
||||
defaultSizePx: 256,
|
||||
imperativePanelGroupRef,
|
||||
panelGroupDirection: 'vertical',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const galleryPanel = usePanel(galleryPanelOptions);
|
||||
|
||||
const canvasLayersPanelOptions = useMemo<UsePanelOptions>(
|
||||
() => ({
|
||||
id: 'canvas-layers-panel',
|
||||
minSizePx: 128,
|
||||
defaultSizePx: 256,
|
||||
imperativePanelGroupRef,
|
||||
panelGroupDirection: 'vertical',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const canvasLayersPanel = usePanel(canvasLayersPanelOptions);
|
||||
|
||||
const handleClickBoardSearch = useCallback(() => {
|
||||
if (boardSearchText.length) {
|
||||
dispatch(boardSearchTextChanged(''));
|
||||
@@ -98,7 +126,7 @@ const GalleryPanelContent = () => {
|
||||
</Flex>
|
||||
|
||||
<PanelGroup ref={imperativePanelGroupRef} direction="vertical" autoSaveId="boards-list-panel">
|
||||
<Panel collapsible {...boardsListPanel.panelProps}>
|
||||
<Panel order={0} id="boards-panel" collapsible {...boardsListPanel.panelProps}>
|
||||
<Flex flexDir="column" w="full" h="full">
|
||||
<Collapse in={boardSearchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||
<Box w="full" pt={2}>
|
||||
@@ -109,10 +137,20 @@ const GalleryPanelContent = () => {
|
||||
<BoardsListWrapper />
|
||||
</Flex>
|
||||
</Panel>
|
||||
<HorizontalResizeHandle id="gallery-panel-handle" {...boardsListPanel.resizeHandleProps} />
|
||||
<Panel id="gallery-wrapper-panel" minSize={20}>
|
||||
<HorizontalResizeHandle id="boards-list-to-gallery-panel-handle" {...boardsListPanel.resizeHandleProps} />
|
||||
<Panel order={1} id="gallery-wrapper-panel" collapsible {...galleryPanel.panelProps}>
|
||||
<Gallery />
|
||||
</Panel>
|
||||
{sessionType === 'advanced' && (
|
||||
<>
|
||||
<HorizontalResizeHandle id="gallery-panel-to-layers-handle" {...galleryPanel.resizeHandleProps} />
|
||||
<Panel order={2} id="canvas-layers-panel" collapsible {...canvasLayersPanel.panelProps}>
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasLayersPanelContent />
|
||||
</CanvasManagerProviderGate>
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</PanelGroup>
|
||||
</FocusRegionWrapper>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { AnimationProps } from 'framer-motion';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { $hasProgressImage } from 'services/events/stores';
|
||||
import { $hasLastProgressImage } from 'services/events/stores';
|
||||
|
||||
import { NoContentForViewer } from './NoContentForViewer';
|
||||
import ProgressImage from './ProgressImage';
|
||||
@@ -86,7 +86,7 @@ const CurrentImagePreview = ({ imageDTO }: { imageDTO?: ImageDTO }) => {
|
||||
export default memo(CurrentImagePreview);
|
||||
|
||||
const ImageContent = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
|
||||
const hasProgressImage = useStore($hasProgressImage);
|
||||
const hasProgressImage = useStore($hasLastProgressImage);
|
||||
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
|
||||
|
||||
if (hasProgressImage && shouldShowProgressInViewer) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { $progressImage } from 'services/events/stores';
|
||||
import { $lastProgressImage } from 'services/events/stores';
|
||||
|
||||
const selectShouldAntialiasProgressImage = createSelector(
|
||||
selectSystemSlice,
|
||||
@@ -13,7 +13,7 @@ const selectShouldAntialiasProgressImage = createSelector(
|
||||
);
|
||||
|
||||
const CurrentImagePreview = () => {
|
||||
const progressImage = useStore($progressImage);
|
||||
const progressImage = useStore($lastProgressImage);
|
||||
const shouldAntialiasProgressImage = useAppSelector(selectShouldAntialiasProgressImage);
|
||||
|
||||
const sx = useMemo<SystemStyleObject>(
|
||||
|
||||
@@ -3,9 +3,9 @@ import { deepClone } from 'common/util/deepClone';
|
||||
import { selectDefaultIPAdapter, selectDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { canvasReset } from 'features/controlLayers/store/actions';
|
||||
import {
|
||||
bboxChangedFromCanvas,
|
||||
canvasClearHistory,
|
||||
controlLayerAdded,
|
||||
entityRasterized,
|
||||
inpaintMaskAdded,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
rgAdded,
|
||||
rgIPAdapterImageChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { canvasSessionStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectBboxModelBase, selectBboxRect } from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
@@ -147,10 +148,11 @@ export const newCanvasFromImage = async (arg: {
|
||||
imageDTO: ImageDTO;
|
||||
type: CanvasEntityType | 'regional_guidance_with_reference_image';
|
||||
withResize?: boolean;
|
||||
withInpaintMask?: boolean;
|
||||
dispatch: AppDispatch;
|
||||
getState: () => RootState;
|
||||
}) => {
|
||||
const { type, imageDTO, withResize = false, dispatch, getState } = arg;
|
||||
const { type, imageDTO, withResize = false, withInpaintMask = false, dispatch, getState } = arg;
|
||||
const state = getState();
|
||||
|
||||
const base = selectBboxModelBase(state);
|
||||
@@ -192,10 +194,14 @@ export const newCanvasFromImage = async (arg: {
|
||||
objects: [imageObject],
|
||||
} satisfies Partial<CanvasRasterLayerState>;
|
||||
addFitOnLayerInitCallback(overrides.id);
|
||||
dispatch(canvasReset());
|
||||
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
||||
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
if (withInpaintMask) {
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
}
|
||||
dispatch(canvasClearHistory());
|
||||
break;
|
||||
}
|
||||
case 'control_layer': {
|
||||
@@ -205,10 +211,14 @@ export const newCanvasFromImage = async (arg: {
|
||||
controlAdapter: deepClone(initialControlNet),
|
||||
} satisfies Partial<CanvasControlLayerState>;
|
||||
addFitOnLayerInitCallback(overrides.id);
|
||||
dispatch(canvasReset());
|
||||
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
||||
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
||||
dispatch(controlLayerAdded({ overrides, isSelected: true }));
|
||||
if (withInpaintMask) {
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
}
|
||||
dispatch(canvasClearHistory());
|
||||
break;
|
||||
}
|
||||
case 'inpaint_mask': {
|
||||
@@ -217,10 +227,14 @@ export const newCanvasFromImage = async (arg: {
|
||||
objects: [imageObject],
|
||||
} satisfies Partial<CanvasInpaintMaskState>;
|
||||
addFitOnLayerInitCallback(overrides.id);
|
||||
dispatch(canvasReset());
|
||||
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
||||
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
||||
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
|
||||
if (withInpaintMask) {
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
}
|
||||
dispatch(canvasClearHistory());
|
||||
break;
|
||||
}
|
||||
case 'regional_guidance': {
|
||||
@@ -229,25 +243,37 @@ export const newCanvasFromImage = async (arg: {
|
||||
objects: [imageObject],
|
||||
} satisfies Partial<CanvasRegionalGuidanceState>;
|
||||
addFitOnLayerInitCallback(overrides.id);
|
||||
dispatch(canvasReset());
|
||||
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
|
||||
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
|
||||
dispatch(rgAdded({ overrides, isSelected: true }));
|
||||
if (withInpaintMask) {
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
}
|
||||
dispatch(canvasClearHistory());
|
||||
break;
|
||||
}
|
||||
case 'reference_image': {
|
||||
const ipAdapter = deepClone(selectDefaultRefImageConfig(getState()));
|
||||
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
|
||||
dispatch(canvasReset());
|
||||
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||
dispatch(referenceImageAdded({ overrides: { ipAdapter }, isSelected: true }));
|
||||
if (withInpaintMask) {
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
}
|
||||
dispatch(canvasClearHistory());
|
||||
break;
|
||||
}
|
||||
case 'regional_guidance_with_reference_image': {
|
||||
const ipAdapter = deepClone(selectDefaultIPAdapter(getState()));
|
||||
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
|
||||
const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }];
|
||||
dispatch(canvasReset());
|
||||
dispatch(canvasSessionStarted({ sessionType: 'advanced' }));
|
||||
dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true }));
|
||||
if (withInpaintMask) {
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
}
|
||||
dispatch(canvasClearHistory());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { UploadImageButton } from 'common/hooks/useImageUploadButton';
|
||||
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
|
||||
import type { SetNodeImageFieldImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setNodeImageFieldImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
@@ -66,7 +66,7 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
|
||||
return (
|
||||
<Flex position="relative" className={NO_DRAG_CLASS} w="full" h={32} alignItems="stretch">
|
||||
{!imageDTO && (
|
||||
<UploadImageButton
|
||||
<UploadImageIconButton
|
||||
w="full"
|
||||
h="auto"
|
||||
isError={fieldTemplate.required && !field.value}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { UploadImageButton } from 'common/hooks/useImageUploadButton';
|
||||
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
|
||||
import type { SetUpscaleInitialImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setUpscaleInitialImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
@@ -34,7 +34,7 @@ export const UpscaleInitialImage = () => {
|
||||
return (
|
||||
<Flex justifyContent="flex-start">
|
||||
<Flex position="relative" w={36} h={36} alignItems="center" justifyContent="center">
|
||||
{!imageDTO && <UploadImageButton w="full" h="full" isError={!imageDTO} onUpload={onUpload} fontSize={36} />}
|
||||
{!imageDTO && <UploadImageIconButton w="full" h="full" isError={!imageDTO} onUpload={onUpload} fontSize={36} />}
|
||||
{imageDTO && (
|
||||
<>
|
||||
<DndImage imageDTO={imageDTO} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
|
||||
import { CanvasRightPanel } from 'features/controlLayers/components/CanvasRightPanel';
|
||||
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useDndMonitor } from 'features/dnd/useDndMonitor';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||
@@ -161,11 +161,9 @@ AppContent.displayName = 'AppContent';
|
||||
|
||||
const RightPanelContent = memo(() => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const sessionType = useAppSelector(selectCanvasSessionType);
|
||||
|
||||
if (tab === 'canvas') {
|
||||
return <CanvasRightPanel />;
|
||||
}
|
||||
if (tab === 'upscaling' || tab === 'workflows') {
|
||||
if (tab === 'upscaling' || tab === 'workflows' || tab === 'canvas') {
|
||||
return <GalleryPanelContent />;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftMo
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
|
||||
import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
|
||||
@@ -30,10 +31,11 @@ const FloatingSidePanelButtons = ({ togglePanel }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isCancelAndClearAllEnabled = useFeatureStatus('cancelAndClearAll');
|
||||
const sessionType = useAppSelector(selectCanvasSessionType);
|
||||
|
||||
return (
|
||||
<Flex pos="absolute" transform="translate(0, -50%)" top="50%" insetInlineStart={2} direction="column" gap={2}>
|
||||
{tab === 'canvas' && (
|
||||
{tab === 'canvas' && sessionType === 'advanced' && (
|
||||
<CanvasManagerProviderGate>
|
||||
<ToolChooser />
|
||||
</CanvasManagerProviderGate>
|
||||
|
||||
Reference in New Issue
Block a user