From abaa33e22c3787476142ad78933857461b357b05 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 11 Jun 2025 23:21:56 +1000
Subject: [PATCH] wip
---
invokeai/frontend/web/package.json | 1 +
invokeai/frontend/web/pnpm-lock.yaml | 16 +
.../src/common/components/Loading/Loading.tsx | 1 +
.../AdvancedSession/AdvancedSession.tsx | 194 ++++++----
.../components/SimpleSession/InitialState.tsx | 8 +-
.../SimpleSession/SimpleSession.tsx | 49 ++-
.../SimpleSession/SimpleSessionNoId.tsx | 15 +
.../Boards/BoardsList/BoardsSearch.tsx | 3 +
.../components/BoardsListPanelContent.tsx | 35 +-
.../features/gallery/components/Gallery.tsx | 2 +-
.../gallery/components/GalleryTopBar.tsx | 106 +++---
.../ImageGrid/useGallerySearchTerm.ts | 3 +-
.../ImageViewer/CurrentImagePreview2.tsx | 110 ++++++
.../components/ImageViewer/ImageViewer2.tsx | 125 +++++++
.../components/ImageViewer/ProgressImage2.tsx | 56 +++
.../components/ImageViewer/ViewerToolbar2.tsx | 18 +
.../gallery/hooks/useGalleryHotkeys.ts | 3 +-
.../features/gallery/hooks/useImageActions.ts | 12 +-
.../AdvancedSettingsAccordion.tsx | 62 ++--
.../UpscaleTabAdvancedSettingsAccordion.tsx | 90 +++++
.../GenerationSettingsAccordion.tsx | 17 +-
.../UpscaleTabGenerationSettingsAccordion.tsx | 92 +++++
.../src/features/ui/components/AppContent.tsx | 62 ++--
.../ui/components/LeftPanelContent.tsx | 2 +-
.../ui/components/MainPanelContent.tsx | 9 +-
.../src/features/ui/components/TabButton.tsx | 5 +-
.../features/ui/components/VerticalNavBar.tsx | 2 +-
.../src/features/ui/layouts/AutoLayout.tsx | 45 +++
.../ui/layouts/TabWithoutCloseButton.tsx | 35 ++
.../ui/layouts/auto-layout-context.tsx | 14 +
.../ui/layouts/canvas-tab-auto-layout.tsx | 339 ++++++++++++++++++
.../ui/layouts/generate-tab-auto-layout.tsx | 175 +++++++++
.../layouts/use-collapsible-gridview-panel.ts | 98 +++++
.../web/src/features/ui/store/uiSelectors.ts | 2 +
.../web/src/features/ui/store/uiSlice.ts | 4 +-
.../ui/styles/dockview-theme-invoke.css | 65 ++++
.../web/src/features/ui/styles/theme.ts | 6 +
37 files changed, 1602 insertions(+), 279 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSessionNoId.tsx
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview2.tsx
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer2.tsx
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage2.tsx
create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar2.tsx
create mode 100644 invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx
create mode 100644 invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/UpscaleTabGenerationSettingsAccordion.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx
create mode 100644 invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts
create mode 100644 invokeai/frontend/web/src/features/ui/styles/dockview-theme-invoke.css
create mode 100644 invokeai/frontend/web/src/features/ui/styles/theme.ts
diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json
index f7c2513edb..c5e580c049 100644
--- a/invokeai/frontend/web/package.json
+++ b/invokeai/frontend/web/package.json
@@ -67,6 +67,7 @@
"chakra-react-select": "^4.9.2",
"cmdk": "^1.1.1",
"compare-versions": "^6.1.1",
+ "dockview": "^4.3.1",
"filesize": "^10.1.6",
"fracturedjsonjs": "^4.1.0",
"framer-motion": "^11.10.0",
diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml
index 79de0c48c1..61fba0669c 100644
--- a/invokeai/frontend/web/pnpm-lock.yaml
+++ b/invokeai/frontend/web/pnpm-lock.yaml
@@ -50,6 +50,9 @@ dependencies:
compare-versions:
specifier: ^6.1.1
version: 6.1.1
+ dockview:
+ specifier: ^4.3.1
+ version: 4.3.1(react@18.3.1)
filesize:
specifier: ^10.1.6
version: 10.1.6
@@ -4492,6 +4495,19 @@ packages:
resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==}
dev: false
+ /dockview-core@4.3.1:
+ resolution: {integrity: sha512-cjGIXKc1wtHHkeKisuDLNt3HSHCVzvabxm1K9Auna27A9T3QR7ISOiTJyEUKUPllkcztFYBut0vwnnvwLnPAuQ==}
+ dev: false
+
+ /dockview@4.3.1(react@18.3.1):
+ resolution: {integrity: sha512-D4SvZPs1GJxGUBPkrehlKNGsWlSDaBiPuSYI+IEXnZ7b2bCUs1/h954sVs7xyykqEW3r6TkPKLWdTR/47Q7/QQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ dependencies:
+ dockview-core: 4.3.1
+ react: 18.3.1
+ dev: false
+
/doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
diff --git a/invokeai/frontend/web/src/common/components/Loading/Loading.tsx b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx
index 7caf12952a..b8bbcf668a 100644
--- a/invokeai/frontend/web/src/common/components/Loading/Loading.tsx
+++ b/invokeai/frontend/web/src/common/components/Loading/Loading.tsx
@@ -17,6 +17,7 @@ const Loading = () => {
right={0}
bottom={0}
left={0}
+ zIndex={99999}
>
{
}, []);
return (
-
-
-
-
-
-
- renderMenu={renderMenu} withLongPress={false}>
- {(ref) => (
-
-
+
+
+ Welcome
+ Workspace
+ Viewer
+
+
+
+
+
+
+
+
-
- {showHUD && }
-
-
-
-
-
-
-
+
+
+
+ renderMenu={renderMenu} withLongPress={false}>
+ {(ref) => (
+
+
+
+
+ {showHUD && }
+
+
+
+
+
+
+
+
+
+ )}
+
+ {id !== null && (
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
- )}
-
- {id !== null && (
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
});
AdvancedSession.displayName = 'AdvancedSession';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
index dc968e5a51..73d867cff2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/InitialState.tsx
@@ -1,4 +1,4 @@
-import { Alert, Button, Divider, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
+import { Alert, Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { InitialStateAddAStyleReference } from 'features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference';
import { InitialStateGenerateFromText } from 'features/controlLayers/components/SimpleSession/InitialStateGenerateFromText';
@@ -14,11 +14,7 @@ export const InitialState = memo(() => {
return (
-
- Get Started
-
-
-
+
Get started with Invoke.
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
index 750066526e..54b0be8e1a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSession.tsx
@@ -1,16 +1,45 @@
-import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
+import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
-import { StagingArea } from 'features/controlLayers/components/SimpleSession/StagingArea';
-import { memo } from 'react';
+import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
+import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
+import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
+import { selectShowGenerateTabSplashScreen } from 'features/ui/store/uiSelectors';
+import { showGenerateTabSplashScreenChanged } from 'features/ui/store/uiSlice';
+import { memo, useCallback } from 'react';
+
+export const SimpleSession = memo(() => {
+ const showGenerateTabSplashScreen = useAppSelector(selectShowGenerateTabSplashScreen);
+ const dispatch = useAppDispatch();
+
+ const showSplashScreen = useCallback(() => {
+ dispatch(showGenerateTabSplashScreenChanged(true));
+ }, [dispatch]);
-export const SimpleSession = memo(({ id }: { id: string | null }) => {
- if (id === null) {
- return ;
- }
return (
-
-
-
+
+
+ Launchpad
+ Viewer
+ Generation Progress
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
});
SimpleSession.displayName = 'SimpleSession';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSessionNoId.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSessionNoId.tsx
new file mode 100644
index 0000000000..e7b3657a95
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/SimpleSessionNoId.tsx
@@ -0,0 +1,15 @@
+import { Divider, Flex } from '@invoke-ai/ui-library';
+import { StagingAreaHeader } from 'features/controlLayers/components/SimpleSession/StagingAreaHeader';
+import { StagingAreaNoItems } from 'features/controlLayers/components/SimpleSession/StagingAreaNoItems';
+import { memo } from 'react';
+
+export const SimpleSessionNoId = memo(() => {
+ return (
+
+
+
+
+
+ );
+});
+SimpleSessionNoId.displayName = 'StSimpleSessionNoIdagingArea';
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx
index 5c578a5539..3b66a41c63 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsSearch.tsx
@@ -1,5 +1,6 @@
import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { buildUseDisclosure } from 'common/hooks/useBoolean';
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
import type { ChangeEvent, KeyboardEvent } from 'react';
@@ -7,6 +8,8 @@ import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
+export const [useBoardSearchDisclosure, $boardSearchIsOpen] = buildUseDisclosure(false);
+
export const BoardsSearch = memo(() => {
const dispatch = useAppDispatch();
const boardSearchText = useAppSelector(selectBoardSearchText);
diff --git a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx
index b456cbaaa7..bb80d8e160 100644
--- a/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/BoardsListPanelContent.tsx
@@ -1,25 +1,26 @@
-import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
import { Box, Collapse, Divider, Flex } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
import { BoardsListWrapper } from 'features/gallery/components/Boards/BoardsList/BoardsListWrapper';
-import { BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
+import { $boardSearchIsOpen, BoardsSearch } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
+import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar';
import type { CSSProperties } from 'react';
import { memo } from 'react';
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
-export const BoardsListPanelContent = memo(
- ({ boardSearchDisclosure }: { boardSearchDisclosure: UseDisclosureReturn }) => {
- return (
-
-
-
-
-
-
-
-
-
- );
- }
-);
+export const BoardsListPanelContent = memo(() => {
+ const boardSearchDisclosure = useStore($boardSearchIsOpen);
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+});
BoardsListPanelContent.displayName = 'BoardsListPanelContent';
diff --git a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
index 785bc53af8..0fcfa7dd82 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx
@@ -70,7 +70,7 @@ export const Gallery = memo(() => {
const boardName = useBoardName(selectedBoardId);
return (
-
+
diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
index e84e733c58..a5ff519384 100644
--- a/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/GalleryTopBar.tsx
@@ -1,67 +1,67 @@
-import type { UseDisclosureReturn } from '@invoke-ai/ui-library';
import { Button, Flex, IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useBoardSearchDisclosure } from 'features/gallery/components/Boards/BoardsList/BoardsSearch';
import { BoardsSettingsPopover } from 'features/gallery/components/Boards/BoardsSettingsPopover';
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
-import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
+import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
+import { useCollapsibleGridviewPanel } from 'features/ui/layouts/use-collapsible-gridview-panel';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold, PiCaretUpBold, PiMagnifyingGlassBold } from 'react-icons/pi';
-export const GalleryTopBar = memo(
- ({
- boardsListPanel,
- boardSearchDisclosure,
- }: {
- boardsListPanel: UsePanelReturn;
- boardSearchDisclosure: UseDisclosureReturn;
- }) => {
- const { t } = useTranslation();
- const dispatch = useAppDispatch();
- const boardSearchText = useAppSelector(selectBoardSearchText);
+export const GalleryTopBar = memo(() => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const boardSearchText = useAppSelector(selectBoardSearchText);
+ const boardSearchDisclosure = useBoardSearchDisclosure();
+ const api = useAutoLayoutContext();
+ const boardsPanel = useCollapsibleGridviewPanel(api, 'boards', 'vertical', 256);
+ const isBoardsPanelCollapsed = useStore(boardsPanel.$isCollapsed);
- const onClickBoardSearch = useCallback(() => {
- if (boardSearchText.length) {
- dispatch(boardSearchTextChanged(''));
- }
- boardSearchDisclosure.onToggle();
- boardsListPanel.expand();
- }, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
+ const onClickBoardSearch = useCallback(() => {
+ if (boardSearchText.length) {
+ dispatch(boardSearchTextChanged(''));
+ }
+ if (!boardSearchDisclosure.isOpen && boardsPanel.$isCollapsed.get()) {
+ boardsPanel.expand();
+ }
+ boardSearchDisclosure.toggle();
+ }, [boardSearchText.length, boardSearchDisclosure, dispatch, boardsPanel]);
- return (
-
-
- : }
- >
- {boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
-
-
-
-
-
-
-
- }
- colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
- />
-
+ return (
+
+
+ : }
+ >
+ {isBoardsPanelCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
+
- );
- }
-);
+
+
+
+
+
+ }
+ colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
+ />
+
+
+ );
+});
GalleryTopBar.displayName = 'GalleryTopBar';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/useGallerySearchTerm.ts b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/useGallerySearchTerm.ts
index b9651e3d8e..6071d8a8f8 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/useGallerySearchTerm.ts
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/useGallerySearchTerm.ts
@@ -1,5 +1,4 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { selectSearchTerm } from 'features/gallery/store/gallerySelectors';
import { searchTermChanged } from 'features/gallery/store/gallerySlice';
import { debounce } from 'lodash-es';
@@ -7,7 +6,7 @@ import { useCallback, useMemo, useState } from 'react';
export const useGallerySearchTerm = () => {
// Highlander!
- useAssertSingleton('gallery-search-state');
+ // useAssertSingleton('gallery-search-state');
const dispatch = useAppDispatch();
const searchTerm = useAppSelector(selectSearchTerm);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview2.tsx
new file mode 100644
index 0000000000..9e323bd920
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview2.tsx
@@ -0,0 +1,110 @@
+import { Box, Flex } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useAppSelector } from 'app/store/storeHooks';
+import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
+import { DndImage } from 'features/dnd/DndImage';
+import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
+import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
+import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
+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 { $hasLastProgressImage } from 'services/events/stores';
+
+import { NoContentForViewer } from './NoContentForViewer';
+
+export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
+ const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
+
+ // Show and hide the next/prev buttons on mouse move
+ const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState(false);
+ const timeoutId = useRef(0);
+ const onMouseOver = useCallback(() => {
+ setShouldShowNextPrevButtons(true);
+ window.clearTimeout(timeoutId.current);
+ }, []);
+ const onMouseOut = useCallback(() => {
+ timeoutId.current = window.setTimeout(() => {
+ setShouldShowNextPrevButtons(false);
+ }, 500);
+ }, []);
+
+ return (
+
+
+
+
+
+ {shouldShowImageDetails && imageDTO && (
+
+
+
+ )}
+
+ {shouldShowNextPrevButtons && imageDTO && (
+
+
+
+ )}
+
+
+ );
+});
+CurrentImagePreview.displayName = 'CurrentImagePreview';
+
+const ImageContent = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
+ const hasProgressImage = useStore($hasLastProgressImage);
+ const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
+
+ if (!imageDTO) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+});
+ImageContent.displayName = 'ImageContent';
+
+const initial: AnimationProps['initial'] = {
+ opacity: 0,
+};
+const animateArrows: AnimationProps['animate'] = {
+ opacity: 1,
+ transition: { duration: 0.07 },
+};
+const exit: AnimationProps['exit'] = {
+ opacity: 0,
+ transition: { duration: 0.07 },
+};
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer2.tsx
new file mode 100644
index 0000000000..07cab441af
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer2.tsx
@@ -0,0 +1,125 @@
+import { Box, Flex, IconButton, type SystemStyleObject, useOutsideClick } from '@invoke-ai/ui-library';
+import { skipToken } from '@reduxjs/toolkit/query';
+import { useAppSelector } from 'app/store/storeHooks';
+import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
+import { selectImageToCompare } from 'features/gallery/components/ImageViewer/common';
+import { CurrentImagePreview } from 'features/gallery/components/ImageViewer/CurrentImagePreview2';
+import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
+import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
+import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors';
+import { memo, useRef } from 'react';
+import { useHotkeys } from 'react-hotkeys-hook';
+import { useTranslation } from 'react-i18next';
+import { PiXBold } from 'react-icons/pi';
+import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+
+import { useImageViewer } from './useImageViewer';
+
+// type Props = {
+// closeButton?: ReactNode;
+// };
+
+// const useFocusRegionOptions = {
+// focusOnMount: true,
+// };
+
+// const FOCUS_REGION_STYLES: SystemStyleObject = {
+// display: 'flex',
+// width: 'full',
+// height: 'full',
+// position: 'absolute',
+// flexDirection: 'column',
+// inset: 0,
+// alignItems: 'center',
+// justifyContent: 'center',
+// overflow: 'hidden',
+// };
+
+export const ImageViewer = memo(() => {
+ const lastSelectedImageName = useAppSelector(selectLastSelectedImageName);
+ const { data: lastSelectedImageDTO } = useGetImageDTOQuery(lastSelectedImageName ?? skipToken);
+ const comparisonImageDTO = useAppSelector(selectImageToCompare);
+
+ if (lastSelectedImageDTO && comparisonImageDTO) {
+ return ;
+ }
+
+ return ;
+});
+
+ImageViewer.displayName = 'ImageViewer';
+
+const imageViewerContainerSx: SystemStyleObject = {
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ transition: 'opacity 0.15s ease',
+ opacity: 1,
+ pointerEvents: 'auto',
+ '&[data-hidden="true"]': {
+ opacity: 0,
+ pointerEvents: 'none',
+ },
+ backdropFilter: 'blur(10px) brightness(70%)',
+};
+
+export const ImageViewerModal = memo(() => {
+ const ref = useRef(null);
+ const imageViewer = useImageViewer();
+ useOutsideClick({
+ ref,
+ handler: imageViewer.close,
+ });
+
+ useHotkeys(
+ 'esc',
+ imageViewer.close,
+ {
+ preventDefault: true,
+ enabled: imageViewer.isOpen,
+ },
+ [imageViewer.isOpen]
+ );
+
+ return (
+
+
+
+
+
+
+ );
+});
+
+ImageViewerModal.displayName = 'GatedImageViewer';
+
+const ImageViewerCloseButton = memo(() => {
+ const { t } = useTranslation();
+ const imageViewer = useImageViewer();
+ useAssertSingleton('ImageViewerCloseButton');
+ useHotkeys('esc', imageViewer.close);
+ return (
+ }
+ variant="link"
+ alignSelf="stretch"
+ onClick={imageViewer.close}
+ />
+ );
+});
+
+ImageViewerCloseButton.displayName = 'ImageViewerCloseButton';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage2.tsx
new file mode 100644
index 0000000000..850ebc63e1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage2.tsx
@@ -0,0 +1,56 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex, Image } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { IAINoContentFallback } from 'common/components/IAIImageFallback';
+import { selectSystemSlice } from 'features/system/store/systemSlice';
+import { memo, useMemo } from 'react';
+import { PiPulseBold } from 'react-icons/pi';
+import { $lastProgressImage } from 'services/events/stores';
+
+const selectShouldAntialiasProgressImage = createSelector(
+ selectSystemSlice,
+ (system) => system.shouldAntialiasProgressImage
+);
+
+export const ProgressImage = memo(() => {
+ const progressImage = useStore($lastProgressImage);
+ const shouldAntialiasProgressImage = useAppSelector(selectShouldAntialiasProgressImage);
+
+ const sx = useMemo(
+ () => ({
+ imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated',
+ }),
+ [shouldAntialiasProgressImage]
+ );
+
+ if (!progressImage) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+});
+
+ProgressImage.displayName = 'ProgressImage';
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar2.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar2.tsx
new file mode 100644
index 0000000000..f8dc34d654
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar2.tsx
@@ -0,0 +1,18 @@
+import { ButtonGroup, Flex } from '@invoke-ai/ui-library';
+import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
+import { memo } from 'react';
+
+import CurrentImageButtons from './CurrentImageButtons';
+
+export const ViewerToolbar = memo(() => {
+ return (
+
+
+
+
+
+
+ );
+});
+
+ViewerToolbar.displayName = 'ViewerToolbar';
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
index ab6da024fd..0f2a175c1c 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts
@@ -1,6 +1,5 @@
import { useAppSelector } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
-import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
@@ -14,7 +13,7 @@ import { useListImagesQuery } from 'services/api/endpoints/images';
* Registers gallery hotkeys. This hook is a singleton.
*/
export const useGalleryHotkeys = () => {
- useAssertSingleton('useGalleryHotkeys');
+ // useAssertSingleton('useGalleryHotkeys');
const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination();
const selection = useAppSelector((s) => s.gallery.selection);
const queryArgs = useAppSelector(selectListImagesQueryArgs);
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
index 87f0b6aba2..cb37c2c11c 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts
@@ -1,6 +1,7 @@
import { useStore } from '@nanostores/react';
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useAppStore } from 'app/store/nanostores/store';
+import { useAppSelector } from 'app/store/storeHooks';
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
import {
@@ -24,11 +25,10 @@ import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
import type { ImageDTO } from 'services/api/types';
export const useImageActions = (imageDTO: ImageDTO) => {
- const dispatch = useAppDispatch();
+ const { dispatch, getState } = useAppStore();
const { t } = useTranslation();
const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId);
const isStaging = useAppSelector(selectIsStaging);
- const activeTabName = useAppSelector(selectActiveTab);
const { metadata } = useDebouncedMetadata(imageDTO.image_name);
const [hasMetadata, setHasMetadata] = useState(false);
const [hasSeed, setHasSeed] = useState(false);
@@ -82,18 +82,20 @@ export const useImageActions = (imageDTO: ImageDTO) => {
if (!metadata) {
return;
}
+ const activeTabName = selectActiveTab(getState());
parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', isStaging ? ['width', 'height'] : []);
clearStylePreset();
- }, [metadata, activeTabName, isStaging, clearStylePreset]);
+ }, [metadata, getState, isStaging, clearStylePreset]);
const remix = useCallback(() => {
if (!metadata) {
return;
}
+ const activeTabName = selectActiveTab(getState());
// Recalls all metadata parameters except seed
parseAndRecallAllMetadata(metadata, activeTabName === 'canvas', ['seed']);
clearStylePreset();
- }, [activeTabName, metadata, clearStylePreset]);
+ }, [metadata, getState, clearStylePreset]);
const recallSeed = useCallback(() => {
if (!metadata) {
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx
index 00dc4754d6..37475ad6aa 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx
@@ -12,12 +12,10 @@ import ParamClipSkip from 'features/parameters/components/Advanced/ParamClipSkip
import ParamT5EncoderModelSelect from 'features/parameters/components/Advanced/ParamT5EncoderModelSelect';
import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSeamlessXAxis';
import ParamSeamlessYAxis from 'features/parameters/components/Seamless/ParamSeamlessYAxis';
-import { ParamSeed } from 'features/parameters/components/Seed/ParamSeed';
import ParamFLUXVAEModelSelect from 'features/parameters/components/VAEModel/ParamFLUXVAEModelSelect';
import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect';
import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision';
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
-import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetModelConfigQuery } from 'services/api/endpoints/models';
@@ -33,7 +31,6 @@ const formLabelProps2: FormLabelProps = {
export const AdvancedSettingsAccordion = memo(() => {
const vaeKey = useAppSelector(selectVAEKey);
const { currentData: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken);
- const activeTabName = useAppSelector(selectActiveTab);
const isFLUX = useAppSelector(selectIsFLUX);
const isSD3 = useAppSelector(selectIsSD3);
@@ -68,19 +65,16 @@ export const AdvancedSettingsAccordion = memo(() => {
if (params.seamlessXAxis || params.seamlessYAxis) {
badges.push('seamless');
}
- if (activeTabName === 'upscaling' && !params.shouldRandomizeSeed) {
- badges.push('Manual Seed');
- }
}
return badges;
}),
- [vaeConfig, activeTabName]
+ [vaeConfig]
);
const badges = useAppSelector(selectBadges);
const { t } = useTranslation();
const { isOpen, onToggle } = useStandaloneAccordionToggle({
- id: `'advanced-settings-${activeTabName}`,
+ id: `'advanced-settings-generate`,
defaultIsOpen: false,
});
@@ -91,39 +85,33 @@ export const AdvancedSettingsAccordion = memo(() => {
{isFLUX ? : }
{!isFLUX && !isSD3 && }
- {activeTabName === 'upscaling' ? (
-
- ) : (
+ {!isFLUX && !isSD3 && (
<>
- {!isFLUX && !isSD3 && (
- <>
-
-
-
-
-
-
-
-
-
-
- >
- )}
- {isFLUX && (
-
-
-
+
+
+
+
+
+
+
+
- )}
- {isSD3 && (
-
-
-
-
-
- )}
+
>
)}
+ {isFLUX && (
+
+
+
+
+ )}
+ {isSD3 && (
+
+
+
+
+
+ )}
);
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx
new file mode 100644
index 0000000000..04b55e80c6
--- /dev/null
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/UpscaleTabAdvancedSettingsAccordion.tsx
@@ -0,0 +1,90 @@
+import type { FormLabelProps } from '@invoke-ai/ui-library';
+import { Flex, StandaloneAccordion } from '@invoke-ai/ui-library';
+import { skipToken } from '@reduxjs/toolkit/query';
+import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectIsFLUX, selectIsSD3, selectParamsSlice, selectVAEKey } from 'features/controlLayers/store/paramsSlice';
+import { ParamSeed } from 'features/parameters/components/Seed/ParamSeed';
+import ParamFLUXVAEModelSelect from 'features/parameters/components/VAEModel/ParamFLUXVAEModelSelect';
+import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVAEModelSelect';
+import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision';
+import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
+import { memo, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useGetModelConfigQuery } from 'services/api/endpoints/models';
+
+const formLabelProps: FormLabelProps = {
+ minW: '9.2rem',
+};
+
+const formLabelProps2: FormLabelProps = {
+ flexGrow: 1,
+};
+
+export const AdvancedSettingsAccordion = memo(() => {
+ const vaeKey = useAppSelector(selectVAEKey);
+ const { currentData: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken);
+ const isFLUX = useAppSelector(selectIsFLUX);
+ const isSD3 = useAppSelector(selectIsSD3);
+
+ const selectBadges = useMemo(
+ () =>
+ createMemoizedSelector([selectParamsSlice, selectIsFLUX], (params, isFLUX) => {
+ const badges: (string | number)[] = [];
+ if (isFLUX) {
+ if (vaeConfig) {
+ let vaeBadge = vaeConfig.name;
+ if (params.vaePrecision === 'fp16') {
+ vaeBadge += ` ${params.vaePrecision}`;
+ }
+ badges.push(vaeBadge);
+ }
+ } else {
+ if (vaeConfig) {
+ let vaeBadge = vaeConfig.name;
+ if (params.vaePrecision === 'fp16') {
+ vaeBadge += ` ${params.vaePrecision}`;
+ }
+ badges.push(vaeBadge);
+ } else if (params.vaePrecision === 'fp16') {
+ badges.push(`VAE ${params.vaePrecision}`);
+ }
+ if (params.clipSkip) {
+ badges.push(`Skip ${params.clipSkip}`);
+ }
+ if (params.cfgRescaleMultiplier) {
+ badges.push(`Rescale ${params.cfgRescaleMultiplier}`);
+ }
+ if (params.seamlessXAxis || params.seamlessYAxis) {
+ badges.push('seamless');
+ }
+ if (!params.shouldRandomizeSeed) {
+ badges.push('Manual Seed');
+ }
+ }
+
+ return badges;
+ }),
+ [vaeConfig]
+ );
+ const badges = useAppSelector(selectBadges);
+ const { t } = useTranslation();
+ const { isOpen, onToggle } = useStandaloneAccordionToggle({
+ id: `'advanced-settings-upscaling`,
+ defaultIsOpen: false,
+ });
+
+ return (
+
+
+
+ {isFLUX ? : }
+ {!isFLUX && !isSD3 && }
+
+
+
+
+ );
+});
+
+AdvancedSettingsAccordion.displayName = 'AdvancedSettingsAccordion';
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
index 0be0a0c602..1e5e29884b 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
@@ -12,14 +12,11 @@ import ParamGuidance from 'features/parameters/components/Core/ParamGuidance';
import ParamScheduler from 'features/parameters/components/Core/ParamScheduler';
import ParamSteps from 'features/parameters/components/Core/ParamSteps';
import { DisabledModelWarning } from 'features/parameters/components/MainModel/DisabledModelWarning';
-import ParamUpscaleCFGScale from 'features/parameters/components/Upscale/ParamUpscaleCFGScale';
-import ParamUpscaleScheduler from 'features/parameters/components/Upscale/ParamUpscaleScheduler';
import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
import { API_BASE_MODELS } from 'features/parameters/types/constants';
import { MainModelPicker } from 'features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker';
import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
-import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig';
@@ -32,16 +29,12 @@ const formLabelProps: FormLabelProps = {
export const GenerationSettingsAccordion = memo(() => {
const { t } = useTranslation();
const modelConfig = useSelectedModelConfig();
- const activeTabName = useAppSelector(selectActiveTab);
const isFLUX = useAppSelector(selectIsFLUX);
const isSD3 = useAppSelector(selectIsSD3);
const isCogView4 = useAppSelector(selectIsCogView4);
const isApiModel = useIsApiModel();
- const isUpscaling = useMemo(() => {
- return activeTabName === 'upscaling';
- }, [activeTabName]);
const selectBadges = useMemo(
() =>
createMemoizedSelector(selectLoRAsSlice, (loras) => {
@@ -63,8 +56,8 @@ export const GenerationSettingsAccordion = memo(() => {
defaultIsOpen: false,
});
const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({
- id: `generation-settings-${activeTabName}`,
- defaultIsOpen: activeTabName !== 'upscaling',
+ id: `generation-settings-generate`,
+ defaultIsOpen: true,
});
return (
@@ -85,12 +78,10 @@ export const GenerationSettingsAccordion = memo(() => {
- {!isFLUX && !isSD3 && !isCogView4 && !isUpscaling && }
- {isUpscaling && }
+ {!isFLUX && !isSD3 && !isCogView4 && }
{isFLUX && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && }
- {isUpscaling && }
- {!isFLUX && !isUpscaling && }
+ {!isFLUX && }
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/UpscaleTabGenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/UpscaleTabGenerationSettingsAccordion.tsx
new file mode 100644
index 0000000000..31221105e1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/UpscaleTabGenerationSettingsAccordion.tsx
@@ -0,0 +1,92 @@
+import type { FormLabelProps } from '@invoke-ai/ui-library';
+import { Box, Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library';
+import { EMPTY_ARRAY } from 'app/store/constants';
+import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice';
+import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
+import { LoRAList } from 'features/lora/components/LoRAList';
+import LoRASelect from 'features/lora/components/LoRASelect';
+import ParamGuidance from 'features/parameters/components/Core/ParamGuidance';
+import ParamSteps from 'features/parameters/components/Core/ParamSteps';
+import { DisabledModelWarning } from 'features/parameters/components/MainModel/DisabledModelWarning';
+import ParamUpscaleCFGScale from 'features/parameters/components/Upscale/ParamUpscaleCFGScale';
+import ParamUpscaleScheduler from 'features/parameters/components/Upscale/ParamUpscaleScheduler';
+import { useIsApiModel } from 'features/parameters/hooks/useIsApiModel';
+import { API_BASE_MODELS } from 'features/parameters/types/constants';
+import { MainModelPicker } from 'features/settingsAccordions/components/GenerationSettingsAccordion/MainModelPicker';
+import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
+import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
+import { memo, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig';
+import { isFluxFillMainModelModelConfig } from 'services/api/types';
+
+const formLabelProps: FormLabelProps = {
+ minW: '4rem',
+};
+
+export const UpscaleTabGenerationSettingsAccordion = memo(() => {
+ const { t } = useTranslation();
+ const modelConfig = useSelectedModelConfig();
+ const isFLUX = useAppSelector(selectIsFLUX);
+
+ const isApiModel = useIsApiModel();
+
+ const selectBadges = useMemo(
+ () =>
+ createMemoizedSelector(selectLoRAsSlice, (loras) => {
+ const enabledLoRAsCount = loras.loras.filter((l) => l.isEnabled).length;
+ const loraTabBadges = enabledLoRAsCount ? [`${enabledLoRAsCount} ${t('models.concepts')}`] : EMPTY_ARRAY;
+ const accordionBadges =
+ modelConfig && API_BASE_MODELS.includes(modelConfig.base)
+ ? [modelConfig.name]
+ : modelConfig
+ ? [modelConfig.name, modelConfig.base]
+ : EMPTY_ARRAY;
+ return { loraTabBadges, accordionBadges };
+ }),
+ [modelConfig, t]
+ );
+ const { loraTabBadges, accordionBadges } = useAppSelector(selectBadges);
+ const { isOpen: isOpenExpander, onToggle: onToggleExpander } = useExpanderToggle({
+ id: 'generation-settings-advanced',
+ defaultIsOpen: false,
+ });
+ const { isOpen: isOpenAccordion, onToggle: onToggleAccordion } = useStandaloneAccordionToggle({
+ id: `generation-settings-upscaling`,
+ defaultIsOpen: false,
+ });
+
+ return (
+
+
+
+
+
+ {!isApiModel && }
+ {!isApiModel && }
+
+ {!isApiModel && (
+
+
+
+
+
+ {isFLUX && modelConfig && !isFluxFillMainModelModelConfig(modelConfig) && }
+
+
+
+
+ )}
+
+
+ );
+});
+
+UpscaleTabGenerationSettingsAccordion.displayName = 'UpscaleTabGenerationSettingsAccordion';
diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
index 199e5d8c43..74bcdc6119 100644
--- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
@@ -1,15 +1,16 @@
-import { Flex } from '@invoke-ai/ui-library';
+import 'dockview/dist/styles/dockview.css';
+import 'features/ui/styles/dockview-theme-invoke.css';
+
+import { TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
-import { FloatingLeftPanelButtons } from 'features/ui/components/FloatingLeftPanelButtons';
-import { FloatingRightPanelButtons } from 'features/ui/components/FloatingRightPanelButtons';
-import { LeftPanelContent } from 'features/ui/components/LeftPanelContent';
-import { MainPanelContent } from 'features/ui/components/MainPanelContent';
-import { RightPanelContent } from 'features/ui/components/RightPanelContent';
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel';
+import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout';
+import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
import {
$isLeftPanelOpen,
$isRightPanelOpen,
@@ -21,9 +22,6 @@ import {
import type { CSSProperties } from 'react';
import { memo, useMemo, useRef } from 'react';
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
-import { Panel, PanelGroup } from 'react-resizable-panels';
-
-import { VerticalResizeHandle } from './tabs/ResizeHandle';
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%', minWidth: 0 };
@@ -31,6 +29,7 @@ const onLeftPanelCollapse = (isCollapsed: boolean) => $isLeftPanelOpen.set(!isCo
const onRightPanelCollapse = (isCollapsed: boolean) => $isRightPanelOpen.set(!isCollapsed);
export const AppContent = memo(() => {
+ const tab = useAppSelector(selectActiveTab);
const imperativePanelGroupRef = useRef(null);
useDndMonitor();
@@ -108,38 +107,19 @@ export const AppContent = memo(() => {
});
return (
-
-
-
- {withLeftPanel && (
- <>
-
-
-
-
- >
- )}
-
-
- {withLeftPanel && }
- {withRightPanel && }
-
- {withRightPanel && (
- <>
-
-
-
-
- >
- )}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
);
});
AppContent.displayName = 'AppContent';
diff --git a/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
index a603d540e7..e30de8bf17 100644
--- a/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/LeftPanelContent.tsx
@@ -12,7 +12,7 @@ export const LeftPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
return (
-
+
{tab === 'generate' && }
diff --git a/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx b/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
index 58010ba503..ec5e41deff 100644
--- a/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/MainPanelContent.tsx
@@ -1,27 +1,22 @@
import { useAppSelector } from 'app/store/storeHooks';
import { AdvancedSession } from 'features/controlLayers/components/AdvancedSession/AdvancedSession';
import { SimpleSession } from 'features/controlLayers/components/SimpleSession/SimpleSession';
-import { selectCanvasSessionId, selectGenerateSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
import QueueTab from 'features/ui/components/tabs/QueueTab';
import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
-import { atom } from 'nanostores';
import { memo } from 'react';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
-export const $simpleId = atom(null);
-export const $advancedId = atom(null);
-
export const MainPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
- const generateId = useAppSelector(selectGenerateSessionId);
const canvasId = useAppSelector(selectCanvasSessionId);
if (tab === 'generate') {
- return ;
+ return ;
}
if (tab === 'canvas') {
return ;
diff --git a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
index 8974a9c326..1a8850f309 100644
--- a/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/TabButton.tsx
@@ -1,5 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
-import { IconButton, Tooltip } from '@invoke-ai/ui-library';
+import { IconButton, Tab, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
@@ -25,7 +25,8 @@ export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: React
return (
- {
const customNavComponent = useStore($customNavComponent);
return (
-
+
diff --git a/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx b/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx
new file mode 100644
index 0000000000..a1feebb13f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/AutoLayout.tsx
@@ -0,0 +1,45 @@
+import { useAppSelector } from 'app/store/storeHooks';
+import type { GridviewApi, IGridviewReactProps } from 'dockview';
+import { GridviewReact, Orientation } from 'dockview';
+import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
+import { canvasTabComponents, initializeCanvasTabLayout } from 'features/ui/layouts/canvas-tab-auto-layout';
+import { generateTabComponents, initializeGenerateTabLayout } from 'features/ui/layouts/generate-tab-auto-layout';
+import { selectActiveTab } from 'features/ui/store/uiSelectors';
+import type { TabName } from 'features/ui/store/uiTypes';
+import { memo, useCallback, useEffect, useState } from 'react';
+
+const components: IGridviewReactProps['components'] = {
+ ...generateTabComponents,
+ ...canvasTabComponents,
+};
+
+export const AutoLayout = memo(() => {
+ const tab = useAppSelector(selectActiveTab);
+ const [api, setApi] = useState(null);
+ const syncLayout = useCallback((tab: TabName, api: GridviewApi) => {
+ if (tab === 'generate') {
+ initializeGenerateTabLayout(api);
+ } else if (tab === 'canvas') {
+ initializeCanvasTabLayout(api);
+ }
+ }, []);
+ const onReady = useCallback((event) => {
+ setApi(event.api);
+ }, []);
+ useEffect(() => {
+ if (api) {
+ syncLayout(tab, api);
+ }
+ }, [api, syncLayout, tab]);
+ return (
+
+
+
+ );
+});
+AutoLayout.displayName = 'AutoLayout';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx b/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx
new file mode 100644
index 0000000000..77e469a462
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/TabWithoutCloseButton.tsx
@@ -0,0 +1,35 @@
+import { Flex, Text } from '@invoke-ai/ui-library';
+import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
+import type { IDockviewPanelHeaderProps } from 'dockview';
+import { useCallback, useEffect, useId, useRef } from 'react';
+
+export const TabWithoutCloseButton = (props: IDockviewPanelHeaderProps) => {
+ const id = useId();
+ const ref = useRef(null);
+ const setActive = useCallback(() => {
+ if (!props.api.isActive) {
+ props.api.setActive();
+ }
+ }, [props.api]);
+
+ useCallbackOnDragEnter(setActive, ref, 300);
+
+ useEffect(() => {
+ const el = document.querySelector(`[data-id="${id}"]`);
+ if (!el) {
+ return;
+ }
+ const parentTab = el.closest('.dv-tab');
+ if (!parentTab) {
+ return;
+ }
+ parentTab.setAttribute('draggable', 'false');
+ }, [id]);
+
+ return (
+
+ {props.api.title ?? props.api.id}
+
+ );
+};
+TabWithoutCloseButton.displayName = 'TabWithoutCloseButton';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
new file mode 100644
index 0000000000..d86ae4bee1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/auto-layout-context.tsx
@@ -0,0 +1,14 @@
+import type { GridviewApi } from 'dockview';
+import type { PropsWithChildren } from 'react';
+import { createContext, useContext } from 'react';
+
+const AutoLayoutContext = createContext(null);
+
+export const AutoLayoutProvider = (props: PropsWithChildren<{ api: GridviewApi | null }>) => {
+ return {props.children};
+};
+
+export const useAutoLayoutContext = () => {
+ const api = useContext(AutoLayoutContext);
+ return api;
+};
diff --git a/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx
new file mode 100644
index 0000000000..43ea4dc4e1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/canvas-tab-auto-layout.tsx
@@ -0,0 +1,339 @@
+import { Box, ContextMenu, Divider, Flex, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
+import { DockviewReact, GridviewReact, Orientation } from 'dockview';
+import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
+import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
+import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus';
+import { CanvasContextMenuGlobalMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuGlobalMenuItems';
+import { CanvasContextMenuSelectedEntityMenuItems } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems';
+import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
+import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
+import { Filter } from 'features/controlLayers/components/Filters/Filter';
+import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD';
+import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent';
+import { SelectObject } from 'features/controlLayers/components/SelectObject/SelectObject';
+import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
+import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { StagingAreaItemsList } from 'features/controlLayers/components/SimpleSession/StagingAreaItemsList';
+import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
+import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
+import { Transform } from 'features/controlLayers/components/Transform/Transform';
+import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
+import { selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
+import { Gallery } from 'features/gallery/components/Gallery';
+import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
+import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
+import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
+import QueueControls from 'features/queue/components/QueueControls';
+import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
+import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
+import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
+import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
+import { dockviewTheme } from 'features/ui/styles/theme';
+import { memo, useCallback, useState } from 'react';
+import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
+
+const MenuContent = memo(() => {
+ return (
+
+
+
+
+
+
+ );
+});
+MenuContent.displayName = 'MenuContent';
+
+const canvasBgSx = {
+ position: 'relative',
+ w: 'full',
+ h: 'full',
+ borderRadius: 'base',
+ overflow: 'hidden',
+ bg: 'base.900',
+ '&[data-dynamic-grid="true"]': {
+ bg: 'base.850',
+ },
+};
+
+export const CanvasPanel = memo(() => {
+ const dynamicGrid = useAppSelector(selectDynamicGrid);
+ const showHUD = useAppSelector(selectShowHUD);
+ const canvasId = useAppSelector(selectCanvasSessionId);
+
+ const renderMenu = useCallback(() => {
+ return ;
+ }, []);
+
+ return (
+
+
+
+
+
+ renderMenu={renderMenu} withLongPress={false}>
+ {(ref) => (
+
+
+
+
+ {showHUD && }
+
+
+
+
+
+
+
+
+
+ )}
+
+ {canvasId !== null && (
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+CanvasPanel.displayName = 'CanvasPanel';
+
+const LayersPanelContent = memo(() => (
+
+
+
+));
+LayersPanelContent.displayName = 'LayersPanelContent';
+
+const ViewerPanelContent = memo(() => (
+
+
+
+
+
+));
+ViewerPanelContent.displayName = 'ViewerPanelContent';
+
+const ProgressPanelContent = memo(() => (
+
+
+
+));
+ProgressPanelContent.displayName = 'ProgressPanelContent';
+
+const mainPanelComponents: IDockviewReactProps['components'] = {
+ welcome: InitialState,
+ canvas: CanvasPanel,
+ viewer: ViewerPanelContent,
+ progress: ProgressPanelContent,
+};
+
+const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
+ const { api } = event;
+ api.addPanel({
+ id: 'welcome',
+ component: 'welcome',
+ title: 'Launchpad',
+ });
+ api.addPanel({
+ id: 'canvas',
+ component: 'canvas',
+ title: 'Canvas',
+ position: {
+ direction: 'within',
+ referencePanel: 'welcome',
+ },
+ });
+ api.addPanel({
+ id: 'viewer',
+ component: 'viewer',
+ title: 'Image Viewer',
+ position: {
+ direction: 'within',
+ referencePanel: 'welcome',
+ },
+ });
+ api.addPanel({
+ id: 'progress',
+ component: 'progress',
+ title: 'Generation Progress',
+ position: {
+ direction: 'within',
+ referencePanel: 'welcome',
+ },
+ });
+
+ const disposables = [
+ api.onWillShowOverlay((e) => {
+ if (e.kind === 'header_space' || e.kind === 'tab') {
+ return;
+ }
+ e.preventDefault();
+ }),
+ ];
+
+ return () => {
+ disposables.forEach((disposable) => {
+ disposable.dispose();
+ });
+ };
+};
+
+const MainPanel = memo(() => {
+ return (
+
+
+
+ );
+});
+MainPanel.displayName = 'MainPanel';
+
+const Left = memo(() => {
+ return (
+
+
+
+
+
+
+ );
+});
+Left.displayName = 'Left';
+
+export const canvasTabComponents: IGridviewReactProps['components'] = {
+ left: Left,
+ main: MainPanel,
+ boards: BoardsListPanelContent,
+ gallery: Gallery,
+ layers: LayersPanelContent,
+};
+
+export const initializeCanvasTabLayout = (api: GridviewApi) => {
+ const main = api.addPanel({
+ id: 'main',
+ component: 'main',
+ minimumWidth: 256,
+ });
+ const left = api.addPanel({
+ id: 'left',
+ component: 'left',
+ minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'left',
+ referencePanel: 'main',
+ },
+ });
+ api.addPanel({
+ id: 'gallery',
+ component: 'gallery',
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ minimumHeight: 232,
+ position: {
+ direction: 'right',
+ referencePanel: 'main',
+ },
+ });
+ api.addPanel({
+ id: 'layers',
+ component: 'layers',
+ minimumHeight: 256,
+ position: {
+ direction: 'below',
+ referencePanel: 'gallery',
+ },
+ });
+ const boards = api.addPanel({
+ id: 'boards',
+ component: 'boards',
+ minimumHeight: 36,
+ position: {
+ direction: 'above',
+ referencePanel: 'gallery',
+ },
+ });
+ left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+};
+
+export const CanvasTabAutoLayout = memo(() => {
+ const [api, setApi] = useState(null);
+ const onReady = useCallback((event) => {
+ setApi(event.api);
+ initializeCanvasTabLayout(event.api);
+ }, []);
+ return (
+
+
+
+ );
+});
+CanvasTabAutoLayout.displayName = 'CanvasTabAutoLayout';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx
new file mode 100644
index 0000000000..f97a404652
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/generate-tab-auto-layout.tsx
@@ -0,0 +1,175 @@
+import { Box, Divider, Flex } from '@invoke-ai/ui-library';
+import type { GridviewApi, IDockviewReactProps, IGridviewReactProps } from 'dockview';
+import { DockviewReact, GridviewReact, Orientation } from 'dockview';
+import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
+import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
+import { Gallery } from 'features/gallery/components/Gallery';
+import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer2';
+import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
+import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar2';
+import QueueControls from 'features/queue/components/QueueControls';
+import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
+import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
+import { TabWithoutCloseButton } from 'features/ui/layouts/TabWithoutCloseButton';
+import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/store/uiSlice';
+import { dockviewTheme } from 'features/ui/styles/theme';
+import { memo, useCallback, useState } from 'react';
+
+const ViewerPanelContent = memo(() => (
+
+
+
+
+
+));
+ViewerPanelContent.displayName = 'ViewerPanelContent';
+
+const ProgressPanelContent = memo(() => (
+
+
+
+));
+ProgressPanelContent.displayName = 'ProgressPanelContent';
+
+const mainPanelComponents: IDockviewReactProps['components'] = {
+ welcome: InitialState,
+ viewer: ViewerPanelContent,
+ progress: ProgressPanelContent,
+};
+
+const onReadyMainPanel: IDockviewReactProps['onReady'] = (event) => {
+ const { api } = event;
+ api.addPanel({
+ id: 'welcome',
+ component: 'welcome',
+ title: 'Launchpad',
+ });
+ api.addPanel({
+ id: 'viewer',
+ component: 'viewer',
+ title: 'Image Viewer',
+ position: {
+ direction: 'within',
+ referencePanel: 'welcome',
+ },
+ });
+ api.addPanel({
+ id: 'progress',
+ component: 'progress',
+ title: 'Generation Progress',
+ position: {
+ direction: 'within',
+ referencePanel: 'welcome',
+ },
+ });
+
+ const disposables = [
+ api.onWillShowOverlay((e) => {
+ if (e.kind === 'header_space' || e.kind === 'tab') {
+ return;
+ }
+ e.preventDefault();
+ }),
+ ];
+
+ return () => {
+ disposables.forEach((disposable) => {
+ disposable.dispose();
+ });
+ };
+};
+
+const MainPanel = memo(() => {
+ return (
+
+
+
+ );
+});
+MainPanel.displayName = 'MainPanel';
+
+const Left = memo(() => {
+ return (
+
+
+
+
+
+
+ );
+});
+Left.displayName = 'Left';
+
+export const generateTabComponents: IGridviewReactProps['components'] = {
+ left: Left,
+ main: MainPanel,
+ boards: BoardsListPanelContent,
+ gallery: Gallery,
+};
+
+export const initializeGenerateTabLayout = (api: GridviewApi) => {
+ const main = api.addPanel({
+ id: 'main',
+ component: 'main',
+ minimumWidth: 256,
+ });
+ const left = api.addPanel({
+ id: 'left',
+ component: 'left',
+ minimumWidth: LEFT_PANEL_MIN_SIZE_PX,
+ position: {
+ direction: 'left',
+ referencePanel: 'main',
+ },
+ });
+ api.addPanel({
+ id: 'gallery',
+ component: 'gallery',
+ minimumWidth: RIGHT_PANEL_MIN_SIZE_PX,
+ minimumHeight: 232,
+ position: {
+ direction: 'right',
+ referencePanel: 'main',
+ },
+ });
+ const boards = api.addPanel({
+ id: 'boards',
+ component: 'boards',
+ minimumHeight: 36,
+ position: {
+ direction: 'above',
+ referencePanel: 'gallery',
+ },
+ });
+ left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
+ boards.api.setSize({ height: 256, width: RIGHT_PANEL_MIN_SIZE_PX });
+};
+
+export const GenerateTabAutoLayout = memo(() => {
+ const [api, setApi] = useState(null);
+ const onReady = useCallback((event) => {
+ console.log('GenerateTabAutoLayout onReady');
+ setApi(event.api);
+ initializeGenerateTabLayout(event.api);
+ }, []);
+ return (
+
+
+
+ );
+});
+GenerateTabAutoLayout.displayName = 'GenerateTabAutoLayout';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts b/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts
new file mode 100644
index 0000000000..7d6bd82989
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/use-collapsible-gridview-panel.ts
@@ -0,0 +1,98 @@
+import type { GridviewApi, GridviewPanelApi, IGridviewPanel } from 'dockview';
+import { atom } from 'nanostores';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+
+const getIsCollapsed = (
+ panel: IGridviewPanel,
+ orientation: 'vertical' | 'horizontal',
+ collapsedSize?: number
+) => {
+ if (orientation === 'vertical') {
+ return panel.height <= (collapsedSize ?? panel.minimumHeight);
+ }
+ return panel.width <= (collapsedSize ?? panel.minimumWidth);
+};
+
+export const useCollapsibleGridviewPanel = (
+ api: GridviewApi | null,
+ panelId: string,
+ orientation: 'horizontal' | 'vertical',
+ defaultSize: number,
+ collapsedSize?: number
+) => {
+ const $isCollapsed = useState(() => atom(false))[0];
+ const collapse = useCallback(() => {
+ if (!api) {
+ return;
+ }
+ const panel = api.getPanel(panelId);
+ if (!panel) {
+ return;
+ }
+ if (orientation === 'vertical') {
+ panel.api.setSize({ height: collapsedSize ?? panel.minimumHeight });
+ } else {
+ panel.api.setSize({ width: collapsedSize ?? panel.minimumWidth });
+ }
+ }, [api, collapsedSize, orientation, panelId]);
+
+ const expand = useCallback(() => {
+ if (!api) {
+ return;
+ }
+ const panel = api.getPanel(panelId);
+ if (!panel) {
+ return;
+ }
+ if (orientation === 'vertical') {
+ panel.api.setSize({ height: defaultSize });
+ } else {
+ panel.api.setSize({ width: defaultSize });
+ }
+ }, [api, defaultSize, orientation, panelId]);
+
+ const toggle = useCallback(() => {
+ if (!api) {
+ return;
+ }
+ const panel = api.getPanel(panelId);
+ if (!panel) {
+ return;
+ }
+ const isCollapsed = getIsCollapsed(panel, orientation, collapsedSize);
+ if (isCollapsed) {
+ expand();
+ } else {
+ collapse();
+ }
+ }, [api, panelId, orientation, collapsedSize, expand, collapse]);
+
+ useEffect(() => {
+ if (!api) {
+ return;
+ }
+ const panel = api.getPanel(panelId);
+ if (!panel) {
+ return;
+ }
+
+ const disposable = panel.api.onDidDimensionsChange(() => {
+ const isCollapsed = getIsCollapsed(panel, orientation, collapsedSize);
+ $isCollapsed.set(isCollapsed);
+ });
+
+ return () => {
+ disposable.dispose();
+ };
+ }, [$isCollapsed, api, collapsedSize, orientation, panelId]);
+
+ return useMemo(
+ () => ({
+ $isCollapsed,
+ expand,
+ collapse,
+ toggle,
+ }),
+ [$isCollapsed, collapse, expand, toggle]
+ );
+};
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
index 3990c28aa2..4b11ca8d6a 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSelectors.ts
@@ -5,3 +5,5 @@ export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTa
export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails);
export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer);
export const selectActiveTabCanvasRightPanel = createSelector(selectUiSlice, (ui) => ui.activeTabCanvasRightPanel);
+export const selectShowGenerateTabSplashScreen = createSelector(selectUiSlice, (ui) => ui.showGenerateTabSplashScreen);
+export const selectShowCanvasTabSplashScreen = createSelector(selectUiSlice, (ui) => ui.showCanvasTabSplashScreen);
diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
index c96be420bb..2637b58d89 100644
--- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
+++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts
@@ -123,11 +123,11 @@ export const uiPersistConfig: PersistConfig = {
};
const TABS_WITH_LEFT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows', 'generate'] as const;
-export const LEFT_PANEL_MIN_SIZE_PX = 400;
+export const LEFT_PANEL_MIN_SIZE_PX = 420;
export const $isLeftPanelOpen = atom(true);
export const selectWithLeftPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_LEFT_PANEL.includes(ui.activeTab));
const TABS_WITH_RIGHT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows', 'generate'] as const;
-export const RIGHT_PANEL_MIN_SIZE_PX = 390;
+export const RIGHT_PANEL_MIN_SIZE_PX = 420;
export const $isRightPanelOpen = atom(true);
export const selectWithRightPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_RIGHT_PANEL.includes(ui.activeTab));
diff --git a/invokeai/frontend/web/src/features/ui/styles/dockview-theme-invoke.css b/invokeai/frontend/web/src/features/ui/styles/dockview-theme-invoke.css
new file mode 100644
index 0000000000..06a1e99c58
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/styles/dockview-theme-invoke.css
@@ -0,0 +1,65 @@
+.dockview-theme-invoke {
+ --dv-paneview-active-outline-color: var(--invoke-colors-invokeBlue-300);
+ --dv-tabs-and-actions-container-font-size: var(--invoke-fontSizes-sm);
+ --dv-tabs-and-actions-container-height: var(--invoke-sizes-8);
+ --dv-drag-over-background-color: var(--invoke-colors-baseAlpha-400);
+ --dv-drag-over-border-color: var(--invoke-colors-base-300);
+ --dv-tabs-container-scrollbar-color: #888;
+ --dv-icon-hover-background-color: rgba(90, 93, 94, 0.31);
+ --dv-floating-box-shadow: none;
+ --dv-overlay-z-index: 999;
+
+ --dv-tab-font-size: inherit;
+ --dv-border-radius: 0;
+ --dv-tab-margin: 0;
+ --dv-sash-color: transparent;
+ --dv-active-sash-color: var(--invoke-colors-base-700);
+ --dv-active-sash-transition-duration: 0.15s;
+ --dv-active-sash-transition-delay: 0.1s;
+
+ --dv-group-view-background-color: var(--invoke-colors-base-900);
+
+ --dv-tabs-and-actions-container-background-color: var(--invoke-colors-base-850);
+
+ --dv-activegroup-visiblepanel-tab-color: var(--invoke-colors-base-50);
+ --dv-activegroup-visiblepanel-tab-background-color: var(--invoke-colors-base-700);
+
+ --dv-activegroup-hiddenpanel-tab-color: var(--invoke-colors-base-300);
+ --dv-activegroup-hiddenpanel-tab-background-color: var(--invoke-colors-base-850);
+
+ --dv-inactivegroup-visiblepanel-tab-color: var(--invoke-colors-base-500);
+ --dv-inactivegroup-visiblepanel-tab-background-color: var(--invoke-colors-base-800);
+
+ --dv-inactivegroup-hiddenpanel-tab-color: var(--invoke-colors-base-600);
+ --dv-inactivegroup-hiddenpanel-tab-background-color: var(--invoke-colors-base-850);
+
+ --dv-tab-divider-color: var(--invoke-colors-base-700);
+ --dv-inactivegroup-tab-divider-color: var(--invoke-colors-base-800);
+
+ --dv-separator-border: var(--invoke-colors-base-750);
+ --dv-paneview-header-border-color: rgba(204, 204, 204, 0.2);
+}
+
+.dv-default-tab-content {
+ margin-right: 0px !important;
+}
+
+.dv-groupview-floating {
+ border-radius: var(--invoke-space-2);
+ border-width: 1px;
+ border-color: var(--invoke-colors-base-800);
+ filter: drop-shadow(0px 0px 3px rgba(0, 0, 0, 0.4)) drop-shadow(5px 5px 10px rgba(0, 0, 0, 0.6));
+}
+
+.dv-resize-container {
+ border: none;
+}
+
+.dv-tab {
+ /* margin-right: 2px; */
+}
+
+.dv-inactive-group .dv-tabs-container.dv-horizontal .dv-tab:not(:first-child)::before {
+ /* this is the tab divider */
+ background-color: var(--dv-inactivegroup-tab-divider-color);
+}
diff --git a/invokeai/frontend/web/src/features/ui/styles/theme.ts b/invokeai/frontend/web/src/features/ui/styles/theme.ts
new file mode 100644
index 0000000000..032bc48e34
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/styles/theme.ts
@@ -0,0 +1,6 @@
+import type { DockviewTheme } from 'dockview';
+
+export const dockviewTheme: DockviewTheme = {
+ name: 'invoke',
+ className: 'dockview-theme-invoke',
+};