ui: enhance intermediates clear, enhance board auto-add (#3851)

* feat(ui): enhance clear intermediates feature

- retrieve the # of intermediates using a new query (just uses list images endpoint w/ limit of 0)
- display the count in the UI
- add types for clearIntermediates mutation
- minor styling and verbiage changes

* feat(ui): remove unused settings option for guides

* feat(ui): use solid badge variant

consistent with the rest of the usage of badges

* feat(ui): update board ctx menu, add board auto-add

- add context menu to system boards - only open is select board. did this so that you dont think its broken when you click it
- add auto-add board. you can right click a user board to enable it for auto-add, or use the gallery settings popover to select it. the invoke button has a tooltip on a short delay to remind you that you have auto-add enabled
- made useBoardName hook, provide it a board id and it gets your the board name
- removed `boardIdToAdTo` state & logic, updated workflows to auto-switch and auto-add on image generation

* fix(ui): clear controlnet when clearing intermediates

* feat: Make Add Board icon a button

* feat(db, api): clear intermediates now clears all of them

* feat(ui): make reset webui text subtext style

* feat(ui): board name change submits on blur

---------

Co-authored-by: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com>
This commit is contained in:
psychedelicious 2023-07-20 15:44:22 +10:00 committed by GitHub
parent 82554b25fe
commit 187cf906fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 647 additions and 341 deletions

View File

@ -1,8 +1,7 @@
import io import io
from typing import Optional from typing import Optional
from fastapi import (Body, HTTPException, Path, Query, Request, Response, from fastapi import Body, HTTPException, Path, Query, Request, Response, UploadFile
UploadFile)
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from PIL import Image from PIL import Image
@ -11,9 +10,11 @@ from invokeai.app.invocations.metadata import ImageMetadata
from invokeai.app.models.image import ImageCategory, ResourceOrigin from invokeai.app.models.image import ImageCategory, ResourceOrigin
from invokeai.app.services.image_record_storage import OffsetPaginatedResults from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from invokeai.app.services.item_storage import PaginatedResults from invokeai.app.services.item_storage import PaginatedResults
from invokeai.app.services.models.image_record import (ImageDTO, from invokeai.app.services.models.image_record import (
ImageRecordChanges, ImageDTO,
ImageUrlsDTO) ImageRecordChanges,
ImageUrlsDTO,
)
from ..dependencies import ApiDependencies from ..dependencies import ApiDependencies
@ -84,15 +85,16 @@ async def delete_image(
# TODO: Does this need any exception handling at all? # TODO: Does this need any exception handling at all?
pass pass
@images_router.post("/clear-intermediates", operation_id="clear_intermediates") @images_router.post("/clear-intermediates", operation_id="clear_intermediates")
async def clear_intermediates() -> int: async def clear_intermediates() -> int:
"""Clears first 100 intermediates""" """Clears all intermediates"""
try: try:
count_deleted = ApiDependencies.invoker.services.images.delete_many(is_intermediate=True) count_deleted = ApiDependencies.invoker.services.images.delete_intermediates()
return count_deleted return count_deleted
except Exception as e: except Exception as e:
# TODO: Does this need any exception handling at all? raise HTTPException(status_code=500, detail="Failed to clear intermediates")
pass pass
@ -130,6 +132,7 @@ async def get_image_dto(
except Exception as e: except Exception as e:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
@images_router.get( @images_router.get(
"/{image_name}/metadata", "/{image_name}/metadata",
operation_id="get_image_metadata", operation_id="get_image_metadata",
@ -254,7 +257,8 @@ async def list_image_dtos(
default=None, description="Whether to list intermediate images." default=None, description="Whether to list intermediate images."
), ),
board_id: Optional[str] = Query( board_id: Optional[str] = Query(
default=None, description="The board id to filter by. Use 'none' to find images without a board." default=None,
description="The board id to filter by. Use 'none' to find images without a board.",
), ),
offset: int = Query(default=0, description="The page offset"), offset: int = Query(default=0, description="The page offset"),
limit: int = Query(default=10, description="The number of images per page"), limit: int = Query(default=10, description="The number of images per page"),

View File

@ -122,6 +122,11 @@ class ImageRecordStorageBase(ABC):
"""Deletes many image records.""" """Deletes many image records."""
pass pass
@abstractmethod
def delete_intermediates(self) -> list[str]:
"""Deletes all intermediate image records, returning a list of deleted image names."""
pass
@abstractmethod @abstractmethod
def save( def save(
self, self,
@ -461,6 +466,32 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
finally: finally:
self._lock.release() self._lock.release()
def delete_intermediates(self) -> list[str]:
try:
self._lock.acquire()
self._cursor.execute(
"""--sql
SELECT image_name FROM images
WHERE is_intermediate = TRUE;
"""
)
result = cast(list[sqlite3.Row], self._cursor.fetchall())
image_names = list(map(lambda r: r[0], result))
self._cursor.execute(
"""--sql
DELETE FROM images
WHERE is_intermediate = TRUE;
"""
)
self._conn.commit()
return image_names
except sqlite3.Error as e:
self._conn.rollback()
raise ImageRecordDeleteException from e
finally:
self._lock.release()
def save( def save(
self, self,
image_name: str, image_name: str,

View File

@ -6,21 +6,33 @@ from typing import TYPE_CHECKING, Optional
from PIL.Image import Image as PILImageType from PIL.Image import Image as PILImageType
from invokeai.app.invocations.metadata import ImageMetadata from invokeai.app.invocations.metadata import ImageMetadata
from invokeai.app.models.image import (ImageCategory, from invokeai.app.models.image import (
InvalidImageCategoryException, ImageCategory,
InvalidOriginException, ResourceOrigin) InvalidImageCategoryException,
from invokeai.app.services.board_image_record_storage import \ InvalidOriginException,
BoardImageRecordStorageBase ResourceOrigin,
)
from invokeai.app.services.board_image_record_storage import BoardImageRecordStorageBase
from invokeai.app.services.image_file_storage import ( from invokeai.app.services.image_file_storage import (
ImageFileDeleteException, ImageFileNotFoundException, ImageFileDeleteException,
ImageFileSaveException, ImageFileStorageBase) ImageFileNotFoundException,
ImageFileSaveException,
ImageFileStorageBase,
)
from invokeai.app.services.image_record_storage import ( from invokeai.app.services.image_record_storage import (
ImageRecordDeleteException, ImageRecordNotFoundException, ImageRecordDeleteException,
ImageRecordSaveException, ImageRecordStorageBase, OffsetPaginatedResults) ImageRecordNotFoundException,
ImageRecordSaveException,
ImageRecordStorageBase,
OffsetPaginatedResults,
)
from invokeai.app.services.item_storage import ItemStorageABC from invokeai.app.services.item_storage import ItemStorageABC
from invokeai.app.services.models.image_record import (ImageDTO, ImageRecord, from invokeai.app.services.models.image_record import (
ImageRecordChanges, ImageDTO,
image_record_to_dto) ImageRecord,
ImageRecordChanges,
image_record_to_dto,
)
from invokeai.app.services.resource_name import NameServiceBase from invokeai.app.services.resource_name import NameServiceBase
from invokeai.app.services.urls import UrlServiceBase from invokeai.app.services.urls import UrlServiceBase
from invokeai.app.util.metadata import get_metadata_graph_from_raw_session from invokeai.app.util.metadata import get_metadata_graph_from_raw_session
@ -109,12 +121,10 @@ class ImageServiceABC(ABC):
pass pass
@abstractmethod @abstractmethod
def delete_many(self, is_intermediate: bool) -> int: def delete_intermediates(self) -> int:
"""Deletes many images.""" """Deletes all intermediate images."""
pass pass
@abstractmethod @abstractmethod
def delete_images_on_board(self, board_id: str): def delete_images_on_board(self, board_id: str):
"""Deletes all images on a board.""" """Deletes all images on a board."""
@ -401,21 +411,13 @@ class ImageService(ImageServiceABC):
except Exception as e: except Exception as e:
self._services.logger.error("Problem deleting image records and files") self._services.logger.error("Problem deleting image records and files")
raise e raise e
def delete_many(self, is_intermediate: bool): def delete_intermediates(self) -> int:
try: try:
# only clears 100 at a time image_names = self._services.image_records.delete_intermediates()
images = self._services.image_records.get_many(offset=0, limit=100, is_intermediate=is_intermediate,) count = len(image_names)
count = len(images.items) for image_name in image_names:
image_name_list = list(
map(
lambda r: r.image_name,
images.items,
)
)
for image_name in image_name_list:
self._services.image_files.delete(image_name) self._services.image_files.delete(image_name)
self._services.image_records.delete_many(image_name_list)
return count return count
except ImageRecordDeleteException: except ImageRecordDeleteException:
self._services.logger.error(f"Failed to delete image records") self._services.logger.error(f"Failed to delete image records")

View File

@ -6,11 +6,7 @@ import {
imageSelected, imageSelected,
} from 'features/gallery/store/gallerySlice'; } from 'features/gallery/store/gallerySlice';
import { progressImageSet } from 'features/system/store/systemSlice'; import { progressImageSet } from 'features/system/store/systemSlice';
import { import { imagesAdapter, imagesApi } from 'services/api/endpoints/images';
SYSTEM_BOARDS,
imagesAdapter,
imagesApi,
} from 'services/api/endpoints/images';
import { isImageOutput } from 'services/api/guards'; import { isImageOutput } from 'services/api/guards';
import { sessionCanceled } from 'services/api/thunks/session'; import { sessionCanceled } from 'services/api/thunks/session';
import { import {
@ -32,8 +28,7 @@ export const addInvocationCompleteEventListener = () => {
); );
const session_id = action.payload.data.graph_execution_state_id; const session_id = action.payload.data.graph_execution_state_id;
const { cancelType, isCancelScheduled, boardIdToAddTo } = const { cancelType, isCancelScheduled } = getState().system;
getState().system;
// Handle scheduled cancelation // Handle scheduled cancelation
if (cancelType === 'scheduled' && isCancelScheduled) { if (cancelType === 'scheduled' && isCancelScheduled) {
@ -88,26 +83,28 @@ export const addInvocationCompleteEventListener = () => {
) )
); );
// add image to the board if we had one selected const { autoAddBoardId } = gallery;
if (boardIdToAddTo && !SYSTEM_BOARDS.includes(boardIdToAddTo)) {
// add image to the board if auto-add is enabled
if (autoAddBoardId) {
dispatch( dispatch(
imagesApi.endpoints.addImageToBoard.initiate({ imagesApi.endpoints.addImageToBoard.initiate({
board_id: boardIdToAddTo, board_id: autoAddBoardId,
imageDTO, imageDTO,
}) })
); );
} }
const { selectedBoardId } = gallery; const { selectedBoardId, shouldAutoSwitch } = gallery;
if (boardIdToAddTo && boardIdToAddTo !== selectedBoardId) {
dispatch(boardIdSelected(boardIdToAddTo));
} else if (!boardIdToAddTo) {
dispatch(boardIdSelected('all'));
}
// If auto-switch is enabled, select the new image // If auto-switch is enabled, select the new image
if (getState().gallery.shouldAutoSwitch) { if (shouldAutoSwitch) {
// if auto-add is enabled, switch the board as the image comes in
if (autoAddBoardId && autoAddBoardId !== selectedBoardId) {
dispatch(boardIdSelected(autoAddBoardId));
} else if (!autoAddBoardId) {
dispatch(boardIdSelected('images'));
}
dispatch(imageSelected(imageDTO.image_name)); dispatch(imageSelected(imageDTO.image_name));
} }
} }

View File

@ -0,0 +1,80 @@
import { SelectItem } from '@mantine/core';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
import IAIMantineSelectItemWithTooltip from 'common/components/IAIMantineSelectItemWithTooltip';
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
import { useCallback, useRef } from 'react';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
const selector = createSelector(
[stateSelector],
({ gallery }) => {
const { autoAddBoardId } = gallery;
return {
autoAddBoardId,
};
},
defaultSelectorOptions
);
const BoardAutoAddSelect = () => {
const dispatch = useAppDispatch();
const { autoAddBoardId } = useAppSelector(selector);
const inputRef = useRef<HTMLInputElement>(null);
const { boards, hasBoards } = useListAllBoardsQuery(undefined, {
selectFromResult: ({ data }) => {
const boards: SelectItem[] = [
{
label: 'None',
value: 'none',
},
];
data?.forEach(({ board_id, board_name }) => {
boards.push({
label: board_name,
value: board_id,
});
});
return {
boards,
hasBoards: boards.length > 1,
};
},
});
const handleChange = useCallback(
(v: string | null) => {
if (!v) {
return;
}
dispatch(autoAddBoardIdChanged(v === 'none' ? null : v));
},
[dispatch]
);
return (
<IAIMantineSearchableSelect
label="Auto-Add Board"
inputRef={inputRef}
autoFocus
placeholder={'Select a Board'}
value={autoAddBoardId}
data={boards}
nothingFound="No matching Boards"
itemComponent={IAIMantineSelectItemWithTooltip}
disabled={!hasBoards}
filter={(value, item: SelectItem) =>
item.label?.toLowerCase().includes(value.toLowerCase().trim()) ||
item.value.toLowerCase().includes(value.toLowerCase().trim())
}
onChange={handleChange}
/>
);
};
export default BoardAutoAddSelect;

View File

@ -0,0 +1,60 @@
import { Box, MenuItem, MenuList } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback } from 'react';
import { FaFolder } from 'react-icons/fa';
import { BoardDTO } from 'services/api/types';
import { menuListMotionProps } from 'theme/components/menu';
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
import SystemBoardContextMenuItems from './SystemBoardContextMenuItems';
type Props = {
board?: BoardDTO;
board_id: string;
children: ContextMenuProps<HTMLDivElement>['children'];
setBoardToDelete?: (board?: BoardDTO) => void;
};
const BoardContextMenu = memo(
({ board, board_id, setBoardToDelete, children }: Props) => {
const dispatch = useAppDispatch();
const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected(board?.board_id ?? board_id));
}, [board?.board_id, board_id, dispatch]);
return (
<Box sx={{ touchAction: 'none', height: 'full' }}>
<ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }}
menuButtonProps={{
bg: 'transparent',
_hover: { bg: 'transparent' },
}}
renderMenu={() => (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
>
<MenuItem icon={<FaFolder />} onClickCapture={handleSelectBoard}>
Select Board
</MenuItem>
{!board && <SystemBoardContextMenuItems board_id={board_id} />}
{board && (
<GalleryBoardContextMenuItems
board={board}
setBoardToDelete={setBoardToDelete}
/>
)}
</MenuList>
)}
>
{children}
</ContextMenu>
</Box>
);
}
);
BoardContextMenu.displayName = 'HoverableBoard';
export default BoardContextMenu;

View File

@ -1,5 +1,6 @@
import IAIButton from 'common/components/IAIButton'; import IAIIconButton from 'common/components/IAIIconButton';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { FaPlus } from 'react-icons/fa';
import { useCreateBoardMutation } from 'services/api/endpoints/boards'; import { useCreateBoardMutation } from 'services/api/endpoints/boards';
const DEFAULT_BOARD_NAME = 'My Board'; const DEFAULT_BOARD_NAME = 'My Board';
@ -12,15 +13,14 @@ const AddBoardButton = () => {
}, [createBoard]); }, [createBoard]);
return ( return (
<IAIButton <IAIIconButton
icon={<FaPlus />}
isLoading={isLoading} isLoading={isLoading}
tooltip="Add Board"
aria-label="Add Board" aria-label="Add Board"
onClick={handleCreateBoard} onClick={handleCreateBoard}
size="sm" size="sm"
sx={{ px: 4 }} />
>
Add Board
</IAIButton>
); );
}; };

View File

@ -38,6 +38,7 @@ const AllAssetsBoard = ({ isSelected }: { isSelected: boolean }) => {
return ( return (
<GenericBoard <GenericBoard
board_id="assets"
onClick={handleClick} onClick={handleClick}
isSelected={isSelected} isSelected={isSelected}
icon={FaFileImage} icon={FaFileImage}

View File

@ -38,6 +38,7 @@ const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
return ( return (
<GenericBoard <GenericBoard
board_id="images"
onClick={handleClick} onClick={handleClick}
isSelected={isSelected} isSelected={isSelected}
icon={FaImages} icon={FaImages}

View File

@ -29,6 +29,7 @@ const BatchBoard = ({ isSelected }: { isSelected: boolean }) => {
return ( return (
<GenericBoard <GenericBoard
board_id="batch"
droppableData={droppableData} droppableData={droppableData}
onClick={handleBatchBoardClick} onClick={handleBatchBoardClick}
isSelected={isSelected} isSelected={isSelected}

View File

@ -1,31 +1,41 @@
import { import {
Badge, Badge,
Box, Box,
ChakraProps,
Editable, Editable,
EditableInput, EditableInput,
EditablePreview, EditablePreview,
Flex, Flex,
Image, Image,
MenuItem,
MenuList,
Text, Text,
useColorMode, useColorMode,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { skipToken } from '@reduxjs/toolkit/dist/query'; import { skipToken } from '@reduxjs/toolkit/dist/query';
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd'; import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
import { useAppDispatch } from 'app/store/storeHooks'; import { stateSelector } from 'app/store/store';
import { ContextMenu } from 'chakra-ui-contextmenu'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDroppable from 'common/components/IAIDroppable'; import IAIDroppable from 'common/components/IAIDroppable';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { boardIdSelected } from 'features/gallery/store/gallerySlice'; import { boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { FaTrash, FaUser } from 'react-icons/fa'; import { FaUser } from 'react-icons/fa';
import { useUpdateBoardMutation } from 'services/api/endpoints/boards'; import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { BoardDTO } from 'services/api/types'; import { BoardDTO } from 'services/api/types';
import { menuListMotionProps } from 'theme/components/menu';
import { mode } from 'theme/util/mode'; import { mode } from 'theme/util/mode';
import BoardContextMenu from '../BoardContextMenu';
const AUTO_ADD_BADGE_STYLES: ChakraProps['sx'] = {
bg: 'accent.200',
color: 'blackAlpha.900',
};
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
bg: 'base.500',
color: 'whiteAlpha.900',
};
interface GalleryBoardProps { interface GalleryBoardProps {
board: BoardDTO; board: BoardDTO;
isSelected: boolean; isSelected: boolean;
@ -35,6 +45,22 @@ interface GalleryBoardProps {
const GalleryBoard = memo( const GalleryBoard = memo(
({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => { ({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ gallery }) => {
const isSelectedForAutoAdd =
board.board_id === gallery.autoAddBoardId;
return { isSelectedForAutoAdd };
},
defaultSelectorOptions
),
[board.board_id]
);
const { isSelectedForAutoAdd } = useAppSelector(selector);
const { currentData: coverImage } = useGetImageDTOQuery( const { currentData: coverImage } = useGetImageDTOQuery(
board.cover_image_name ?? skipToken board.cover_image_name ?? skipToken
@ -53,10 +79,6 @@ const GalleryBoard = memo(
updateBoard({ board_id, changes: { board_name: newBoardName } }); updateBoard({ board_id, changes: { board_name: newBoardName } });
}; };
const handleDeleteBoard = useCallback(() => {
setBoardToDelete(board);
}, [board, setBoardToDelete]);
const droppableData: MoveBoardDropData = useMemo( const droppableData: MoveBoardDropData = useMemo(
() => ({ () => ({
id: board_id, id: board_id,
@ -68,37 +90,10 @@ const GalleryBoard = memo(
return ( return (
<Box sx={{ touchAction: 'none', height: 'full' }}> <Box sx={{ touchAction: 'none', height: 'full' }}>
<ContextMenu<HTMLDivElement> <BoardContextMenu
menuProps={{ size: 'sm', isLazy: true }} board={board}
menuButtonProps={{ board_id={board_id}
bg: 'transparent', setBoardToDelete={setBoardToDelete}
_hover: { bg: 'transparent' },
}}
renderMenu={() => (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
>
{board.image_count > 0 && (
<>
{/* <MenuItem
isDisabled={!board.image_count}
icon={<FaImages />}
onClickCapture={handleAddBoardToBatch}
>
Add Board to Batch
</MenuItem> */}
</>
)}
<MenuItem
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
icon={<FaTrash />}
onClickCapture={handleDeleteBoard}
>
Delete Board
</MenuItem>
</MenuList>
)}
> >
{(ref) => ( {(ref) => (
<Flex <Flex
@ -154,7 +149,16 @@ const GalleryBoard = memo(
p: 1, p: 1,
}} }}
> >
<Badge variant="solid">{board.image_count}</Badge> <Badge
variant="solid"
sx={
isSelectedForAutoAdd
? AUTO_ADD_BADGE_STYLES
: BASE_BADGE_STYLES
}
>
{board.image_count}
</Badge>
</Flex> </Flex>
<IAIDroppable <IAIDroppable
data={droppableData} data={droppableData}
@ -172,7 +176,7 @@ const GalleryBoard = memo(
> >
<Editable <Editable
defaultValue={board_name} defaultValue={board_name}
submitOnBlur={false} submitOnBlur={true}
onSubmit={(nextValue) => { onSubmit={(nextValue) => {
handleUpdateBoardName(nextValue); handleUpdateBoardName(nextValue);
}} }}
@ -205,7 +209,7 @@ const GalleryBoard = memo(
</Flex> </Flex>
</Flex> </Flex>
)} )}
</ContextMenu> </BoardContextMenu>
</Box> </Box>
); );
} }

View File

@ -2,9 +2,12 @@ import { As, Badge, Flex } from '@chakra-ui/react';
import { TypesafeDroppableData } from 'app/components/ImageDnd/typesafeDnd'; import { TypesafeDroppableData } from 'app/components/ImageDnd/typesafeDnd';
import IAIDroppable from 'common/components/IAIDroppable'; import IAIDroppable from 'common/components/IAIDroppable';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { BoardId } from 'features/gallery/store/gallerySlice';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import BoardContextMenu from '../BoardContextMenu';
type GenericBoardProps = { type GenericBoardProps = {
board_id: BoardId;
droppableData?: TypesafeDroppableData; droppableData?: TypesafeDroppableData;
onClick: () => void; onClick: () => void;
isSelected: boolean; isSelected: boolean;
@ -22,6 +25,7 @@ const formatBadgeCount = (count: number) =>
const GenericBoard = (props: GenericBoardProps) => { const GenericBoard = (props: GenericBoardProps) => {
const { const {
board_id,
droppableData, droppableData,
onClick, onClick,
isSelected, isSelected,
@ -32,67 +36,72 @@ const GenericBoard = (props: GenericBoardProps) => {
} = props; } = props;
return ( return (
<Flex <BoardContextMenu board_id={board_id}>
sx={{ {(ref) => (
flexDir: 'column',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
w: 'full',
h: 'full',
borderRadius: 'base',
}}
>
<Flex
onClick={onClick}
sx={{
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 'base',
w: 'full',
aspectRatio: '1/1',
overflow: 'hidden',
shadow: isSelected ? 'selected.light' : undefined,
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
flexShrink: 0,
}}
>
<IAINoContentFallback
boxSize={8}
icon={icon}
sx={{
border: '2px solid var(--invokeai-colors-base-200)',
_dark: { border: '2px solid var(--invokeai-colors-base-800)' },
}}
/>
<Flex <Flex
ref={ref}
sx={{ sx={{
position: 'absolute', flexDir: 'column',
insetInlineEnd: 0, justifyContent: 'space-between',
top: 0, alignItems: 'center',
p: 1, cursor: 'pointer',
w: 'full',
h: 'full',
borderRadius: 'base',
}} }}
> >
{badgeCount !== undefined && ( <Flex
<Badge variant="solid">{formatBadgeCount(badgeCount)}</Badge> onClick={onClick}
)} sx={{
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 'base',
w: 'full',
aspectRatio: '1/1',
overflow: 'hidden',
shadow: isSelected ? 'selected.light' : undefined,
_dark: { shadow: isSelected ? 'selected.dark' : undefined },
flexShrink: 0,
}}
>
<IAINoContentFallback
boxSize={8}
icon={icon}
sx={{
border: '2px solid var(--invokeai-colors-base-200)',
_dark: { border: '2px solid var(--invokeai-colors-base-800)' },
}}
/>
<Flex
sx={{
position: 'absolute',
insetInlineEnd: 0,
top: 0,
p: 1,
}}
>
{badgeCount !== undefined && (
<Badge variant="solid">{formatBadgeCount(badgeCount)}</Badge>
)}
</Flex>
<IAIDroppable data={droppableData} dropLabel={dropLabel} />
</Flex>
<Flex
sx={{
h: 'full',
alignItems: 'center',
fontWeight: isSelected ? 600 : undefined,
fontSize: 'xs',
color: isSelected ? 'base.900' : 'base.700',
_dark: { color: isSelected ? 'base.50' : 'base.200' },
}}
>
{label}
</Flex>
</Flex> </Flex>
<IAIDroppable data={droppableData} dropLabel={dropLabel} /> )}
</Flex> </BoardContextMenu>
<Flex
sx={{
h: 'full',
alignItems: 'center',
fontWeight: isSelected ? 600 : undefined,
fontSize: 'xs',
color: isSelected ? 'base.900' : 'base.700',
_dark: { color: isSelected ? 'base.50' : 'base.200' },
}}
>
{label}
</Flex>
</Flex>
); );
}; };

View File

@ -39,6 +39,7 @@ const NoBoardBoard = ({ isSelected }: { isSelected: boolean }) => {
return ( return (
<GenericBoard <GenericBoard
board_id="no_board"
droppableData={droppableData} droppableData={droppableData}
dropLabel={<Text fontSize="md">Move</Text>} dropLabel={<Text fontSize="md">Move</Text>}
onClick={handleClick} onClick={handleClick}

View File

@ -0,0 +1,79 @@
import { MenuItem } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react';
import { FaMinus, FaPlus, FaTrash } from 'react-icons/fa';
import { BoardDTO } from 'services/api/types';
type Props = {
board: BoardDTO;
setBoardToDelete?: (board?: BoardDTO) => void;
};
const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ gallery }) => {
const isSelectedForAutoAdd =
board.board_id === gallery.autoAddBoardId;
return { isSelectedForAutoAdd };
},
defaultSelectorOptions
),
[board.board_id]
);
const { isSelectedForAutoAdd } = useAppSelector(selector);
const handleDelete = useCallback(() => {
if (!setBoardToDelete) {
return;
}
setBoardToDelete(board);
}, [board, setBoardToDelete]);
const handleToggleAutoAdd = useCallback(() => {
dispatch(
autoAddBoardIdChanged(isSelectedForAutoAdd ? null : board.board_id)
);
}, [board.board_id, dispatch, isSelectedForAutoAdd]);
return (
<>
{board.image_count > 0 && (
<>
{/* <MenuItem
isDisabled={!board.image_count}
icon={<FaImages />}
onClickCapture={handleAddBoardToBatch}
>
Add Board to Batch
</MenuItem> */}
</>
)}
<MenuItem
icon={isSelectedForAutoAdd ? <FaMinus /> : <FaPlus />}
onClickCapture={handleToggleAutoAdd}
>
{isSelectedForAutoAdd ? 'Disable Auto-Add' : 'Auto-Add to this Board'}
</MenuItem>
<MenuItem
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
icon={<FaTrash />}
onClickCapture={handleDelete}
>
Delete Board
</MenuItem>
</>
);
};
export default memo(GalleryBoardContextMenuItems);

View File

@ -0,0 +1,12 @@
import { BoardId } from 'features/gallery/store/gallerySlice';
import { memo } from 'react';
type Props = {
board_id: BoardId;
};
const SystemBoardContextMenuItems = ({ board_id }: Props) => {
return <></>;
};
export default memo(SystemBoardContextMenuItems);

View File

@ -1,20 +1,18 @@
import { ChevronUpIcon } from '@chakra-ui/icons'; import { ChevronUpIcon } from '@chakra-ui/icons';
import { Button, Flex, Text } from '@chakra-ui/react'; import { Box, Button, Flex, Spacer, Text } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { memo } from 'react'; import { memo } from 'react';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; import { useBoardName } from 'services/api/hooks/useBoardName';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
(state) => { (state) => {
const { selectedBoardId } = state.gallery; const { selectedBoardId } = state.gallery;
return { return { selectedBoardId };
selectedBoardId,
};
}, },
defaultSelectorOptions defaultSelectorOptions
); );
@ -27,25 +25,7 @@ type Props = {
const GalleryBoardName = (props: Props) => { const GalleryBoardName = (props: Props) => {
const { isOpen, onToggle } = props; const { isOpen, onToggle } = props;
const { selectedBoardId } = useAppSelector(selector); const { selectedBoardId } = useAppSelector(selector);
const { selectedBoardName } = useListAllBoardsQuery(undefined, { const boardName = useBoardName(selectedBoardId);
selectFromResult: ({ data }) => {
let selectedBoardName = '';
if (selectedBoardId === 'images') {
selectedBoardName = 'All Images';
} else if (selectedBoardId === 'assets') {
selectedBoardName = 'All Assets';
} else if (selectedBoardId === 'no_board') {
selectedBoardName = 'No Board';
} else if (selectedBoardId === 'batch') {
selectedBoardName = 'Batch';
} else {
const selectedBoard = data?.find((b) => b.board_id === selectedBoardId);
selectedBoardName = selectedBoard?.board_name || 'Unknown Board';
}
return { selectedBoardName };
},
});
return ( return (
<Flex <Flex
@ -54,6 +34,8 @@ const GalleryBoardName = (props: Props) => {
size="sm" size="sm"
variant="ghost" variant="ghost"
sx={{ sx={{
position: 'relative',
gap: 2,
w: 'full', w: 'full',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
@ -64,19 +46,22 @@ const GalleryBoardName = (props: Props) => {
}, },
}} }}
> >
<Text <Spacer />
noOfLines={1} <Box position="relative">
sx={{ <Text
w: 'full', noOfLines={1}
fontWeight: 600, sx={{
color: 'base.800', fontWeight: 600,
_dark: { color: 'base.800',
color: 'base.200', _dark: {
}, color: 'base.200',
}} },
> }}
{selectedBoardName} >
</Text> {boardName}
</Text>
</Box>
<Spacer />
<ChevronUpIcon <ChevronUpIcon
sx={{ sx={{
transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)', transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)',

View File

@ -1,19 +1,20 @@
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover'; import IAIPopover from 'common/components/IAIPopover';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { setGalleryImageMinimumWidth } from 'features/gallery/store/gallerySlice'; import {
setGalleryImageMinimumWidth,
shouldAutoSwitchChanged,
} from 'features/gallery/store/gallerySlice';
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaWrench } from 'react-icons/fa'; import { FaWrench } from 'react-icons/fa';
import BoardAutoAddSelect from './Boards/BoardAutoAddSelect';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { shouldAutoSwitchChanged } from 'features/gallery/store/gallerySlice';
const selector = createSelector( const selector = createSelector(
[stateSelector], [stateSelector],
@ -50,7 +51,7 @@ const GallerySettingsPopover = () => {
/> />
} }
> >
<Flex direction="column" gap={2}> <Flex direction="column" gap={4}>
<IAISlider <IAISlider
value={galleryImageMinimumWidth} value={galleryImageMinimumWidth}
onChange={handleChangeGalleryImageMinimumWidth} onChange={handleChangeGalleryImageMinimumWidth}
@ -68,6 +69,7 @@ const GallerySettingsPopover = () => {
dispatch(shouldAutoSwitchChanged(e.target.checked)) dispatch(shouldAutoSwitchChanged(e.target.checked))
} }
/> />
<BoardAutoAddSelect />
</Flex> </Flex>
</IAIPopover> </IAIPopover>
); );

View File

@ -25,6 +25,7 @@ export type BoardId =
type GalleryState = { type GalleryState = {
selection: string[]; selection: string[];
shouldAutoSwitch: boolean; shouldAutoSwitch: boolean;
autoAddBoardId: string | null;
galleryImageMinimumWidth: number; galleryImageMinimumWidth: number;
selectedBoardId: BoardId; selectedBoardId: BoardId;
batchImageNames: string[]; batchImageNames: string[];
@ -34,6 +35,7 @@ type GalleryState = {
export const initialGalleryState: GalleryState = { export const initialGalleryState: GalleryState = {
selection: [], selection: [],
shouldAutoSwitch: true, shouldAutoSwitch: true,
autoAddBoardId: null,
galleryImageMinimumWidth: 96, galleryImageMinimumWidth: 96,
selectedBoardId: 'images', selectedBoardId: 'images',
batchImageNames: [], batchImageNames: [],
@ -123,14 +125,34 @@ export const gallerySlice = createSlice({
state.batchImageNames = []; state.batchImageNames = [];
state.selection = []; state.selection = [];
}, },
autoAddBoardIdChanged: (state, action: PayloadAction<string | null>) => {
state.autoAddBoardId = action.payload;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addMatcher( builder.addMatcher(
boardsApi.endpoints.deleteBoard.matchFulfilled, boardsApi.endpoints.deleteBoard.matchFulfilled,
(state, action) => { (state, action) => {
if (action.meta.arg.originalArgs === state.selectedBoardId) { const deletedBoardId = action.meta.arg.originalArgs;
if (deletedBoardId === state.selectedBoardId) {
state.selectedBoardId = 'images'; state.selectedBoardId = 'images';
} }
if (deletedBoardId === state.autoAddBoardId) {
state.autoAddBoardId = null;
}
}
);
builder.addMatcher(
boardsApi.endpoints.listAllBoards.matchFulfilled,
(state, action) => {
const boards = action.payload;
if (!state.autoAddBoardId) {
return;
}
if (!boards.map((b) => b.board_id).includes(state.autoAddBoardId)) {
state.autoAddBoardId = null;
}
} }
); );
}, },
@ -147,6 +169,7 @@ export const {
isBatchEnabledChanged, isBatchEnabledChanged,
imagesAddedToBatch, imagesAddedToBatch,
imagesRemovedFromBatch, imagesRemovedFromBatch,
autoAddBoardIdChanged,
} = gallerySlice.actions; } = gallerySlice.actions;
export default gallerySlice.reducer; export default gallerySlice.reducer;

View File

@ -1,6 +1,9 @@
import { Box, ChakraProps } from '@chakra-ui/react'; import { Box, ChakraProps, Tooltip } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { userInvoked } from 'app/store/actions'; import { userInvoked } from 'app/store/actions';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIButton, { IAIButtonProps } from 'common/components/IAIButton'; import IAIButton, { IAIButtonProps } from 'common/components/IAIButton';
import IAIIconButton, { import IAIIconButton, {
IAIIconButtonProps, IAIIconButtonProps,
@ -8,11 +11,13 @@ import IAIIconButton, {
import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke';
import { clampSymmetrySteps } from 'features/parameters/store/generationSlice'; import { clampSymmetrySteps } from 'features/parameters/store/generationSlice';
import ProgressBar from 'features/system/components/ProgressBar'; import ProgressBar from 'features/system/components/ProgressBar';
import { selectIsBusy } from 'features/system/store/systemSelectors';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaPlay } from 'react-icons/fa'; import { FaPlay } from 'react-icons/fa';
import { useBoardName } from 'services/api/hooks/useBoardName';
const IN_PROGRESS_STYLES: ChakraProps['sx'] = { const IN_PROGRESS_STYLES: ChakraProps['sx'] = {
_disabled: { _disabled: {
@ -26,6 +31,20 @@ const IN_PROGRESS_STYLES: ChakraProps['sx'] = {
}, },
}; };
const selector = createSelector(
[stateSelector, activeTabNameSelector, selectIsBusy],
({ gallery }, activeTabName, isBusy) => {
const { autoAddBoardId } = gallery;
return {
isBusy,
autoAddBoardId,
activeTabName,
};
},
defaultSelectorOptions
);
interface InvokeButton interface InvokeButton
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> { extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
iconButton?: boolean; iconButton?: boolean;
@ -35,8 +54,8 @@ export default function InvokeButton(props: InvokeButton) {
const { iconButton = false, ...rest } = props; const { iconButton = false, ...rest } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isReady = useIsReadyToInvoke(); const isReady = useIsReadyToInvoke();
const activeTabName = useAppSelector(activeTabNameSelector); const { isBusy, autoAddBoardId, activeTabName } = useAppSelector(selector);
const isProcessing = useAppSelector((state) => state.system.isProcessing); const autoAddBoardName = useBoardName(autoAddBoardId);
const handleInvoke = useCallback(() => { const handleInvoke = useCallback(() => {
dispatch(clampSymmetrySteps()); dispatch(clampSymmetrySteps());
@ -75,43 +94,52 @@ export default function InvokeButton(props: InvokeButton) {
<ProgressBar /> <ProgressBar />
</Box> </Box>
)} )}
{iconButton ? ( <Tooltip
<IAIIconButton placement="top"
aria-label={t('parameters.invoke')} hasArrow
type="submit" openDelay={500}
icon={<FaPlay />} label={
isDisabled={!isReady || isProcessing} autoAddBoardId ? `Auto-Adding to ${autoAddBoardName}` : undefined
onClick={handleInvoke} }
tooltip={t('parameters.invoke')} >
tooltipProps={{ placement: 'top' }} {iconButton ? (
colorScheme="accent" <IAIIconButton
id="invoke-button" aria-label={t('parameters.invoke')}
{...rest} type="submit"
sx={{ icon={<FaPlay />}
w: 'full', isDisabled={!isReady || isBusy}
flexGrow: 1, onClick={handleInvoke}
...(isProcessing ? IN_PROGRESS_STYLES : {}), tooltip={t('parameters.invoke')}
}} tooltipProps={{ placement: 'top' }}
/> colorScheme="accent"
) : ( id="invoke-button"
<IAIButton {...rest}
aria-label={t('parameters.invoke')} sx={{
type="submit" w: 'full',
isDisabled={!isReady || isProcessing} flexGrow: 1,
onClick={handleInvoke} ...(isBusy ? IN_PROGRESS_STYLES : {}),
colorScheme="accent" }}
id="invoke-button" />
{...rest} ) : (
sx={{ <IAIButton
w: 'full', aria-label={t('parameters.invoke')}
flexGrow: 1, type="submit"
fontWeight: 700, isDisabled={!isReady || isBusy}
...(isProcessing ? IN_PROGRESS_STYLES : {}), onClick={handleInvoke}
}} colorScheme="accent"
> id="invoke-button"
Invoke {...rest}
</IAIButton> sx={{
)} w: 'full',
flexGrow: 1,
fontWeight: 700,
...(isBusy ? IN_PROGRESS_STYLES : {}),
}}
>
Invoke
</IAIButton>
)}
</Tooltip>
</Box> </Box>
</Box> </Box>
); );

View File

@ -1,33 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors';
import { setShouldLoopback } from 'features/parameters/store/postprocessingSlice';
import { useTranslation } from 'react-i18next';
import { FaRecycle } from 'react-icons/fa';
const loopbackSelector = createSelector(
postprocessingSelector,
({ shouldLoopback }) => shouldLoopback
);
const LoopbackButton = () => {
const dispatch = useAppDispatch();
const shouldLoopback = useAppSelector(loopbackSelector);
const { t } = useTranslation();
return (
<IAIIconButton
aria-label={t('parameters.toggleLoopback')}
tooltip={t('parameters.toggleLoopback')}
isChecked={shouldLoopback}
icon={<FaRecycle />}
onClick={() => {
dispatch(setShouldLoopback(!shouldLoopback));
}}
/>
);
};
export default LoopbackButton;

View File

@ -9,7 +9,6 @@ const ProcessButtons = () => {
return ( return (
<Flex gap={2}> <Flex gap={2}>
<InvokeButton /> <InvokeButton />
{/* {activeTabName === 'img2img' && <LoopbackButton />} */}
<CancelButton /> <CancelButton />
</Flex> </Flex>
); );

View File

@ -1,60 +1,71 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCallback, useEffect, useState } from 'react';
import { StyledFlex } from './SettingsModal';
import { Heading, Text } from '@chakra-ui/react'; import { Heading, Text } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCallback, useEffect } from 'react';
import IAIButton from '../../../../common/components/IAIButton'; import IAIButton from '../../../../common/components/IAIButton';
import { useClearIntermediatesMutation } from '../../../../services/api/endpoints/images'; import {
import { addToast } from '../../store/systemSlice'; useClearIntermediatesMutation,
useGetIntermediatesCountQuery,
} from '../../../../services/api/endpoints/images';
import { resetCanvas } from '../../../canvas/store/canvasSlice'; import { resetCanvas } from '../../../canvas/store/canvasSlice';
import { addToast } from '../../store/systemSlice';
import { StyledFlex } from './SettingsModal';
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
export default function SettingsClearIntermediates() { export default function SettingsClearIntermediates() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [isDisabled, setIsDisabled] = useState(false);
const { data: intermediatesCount, refetch: updateIntermediatesCount } =
useGetIntermediatesCountQuery();
const [clearIntermediates, { isLoading: isLoadingClearIntermediates }] = const [clearIntermediates, { isLoading: isLoadingClearIntermediates }] =
useClearIntermediatesMutation(); useClearIntermediatesMutation();
const handleClickClearIntermediates = useCallback(() => { const handleClickClearIntermediates = useCallback(() => {
clearIntermediates({}) clearIntermediates()
.unwrap() .unwrap()
.then((response) => { .then((response) => {
dispatch(controlNetReset());
dispatch(resetCanvas()); dispatch(resetCanvas());
dispatch( dispatch(
addToast({ addToast({
title: title: `Cleared ${response} intermediates`,
response === 0
? `No intermediates to clear`
: `Successfully cleared ${response} intermediates`,
status: 'info', status: 'info',
}) })
); );
if (response < 100) {
setIsDisabled(true);
}
}); });
}, [clearIntermediates, dispatch]); }, [clearIntermediates, dispatch]);
useEffect(() => {
// update the count on mount
updateIntermediatesCount();
}, [updateIntermediatesCount]);
const buttonText = intermediatesCount
? `Clear ${intermediatesCount} Intermediate${
intermediatesCount > 1 ? 's' : ''
}`
: 'No Intermediates to Clear';
return ( return (
<StyledFlex> <StyledFlex>
<Heading size="sm">Clear Intermediates</Heading> <Heading size="sm">Clear Intermediates</Heading>
<IAIButton <IAIButton
colorScheme="error" colorScheme="warning"
onClick={handleClickClearIntermediates} onClick={handleClickClearIntermediates}
isLoading={isLoadingClearIntermediates} isLoading={isLoadingClearIntermediates}
isDisabled={isDisabled} isDisabled={!intermediatesCount}
> >
{isDisabled ? 'Intermediates Cleared' : 'Clear 100 Intermediates'} {buttonText}
</IAIButton> </IAIButton>
<Text> <Text fontWeight="bold">
Will permanently delete first 100 intermediates found on disk and in Clearing intermediates will reset your Canvas and ControlNet state.
database
</Text> </Text>
<Text fontWeight="bold">This will also clear your canvas state.</Text> <Text variant="subtext">
<Text>
Intermediate images are byproducts of generation, different from the Intermediate images are byproducts of generation, different from the
result images in the gallery. Purging intermediates will free disk result images in the gallery. Clearing intermediates will free disk
space. Your gallery images will not be deleted. space.
</Text> </Text>
<Text variant="subtext">Your gallery images will not be deleted.</Text>
</StyledFlex> </StyledFlex>
); );
} }

View File

@ -24,7 +24,6 @@ import {
setEnableImageDebugging, setEnableImageDebugging,
setIsNodesEnabled, setIsNodesEnabled,
setShouldConfirmOnDelete, setShouldConfirmOnDelete,
setShouldDisplayGuides,
shouldAntialiasProgressImageChanged, shouldAntialiasProgressImageChanged,
shouldLogToConsoleChanged, shouldLogToConsoleChanged,
} from 'features/system/store/systemSlice'; } from 'features/system/store/systemSlice';
@ -56,7 +55,6 @@ const selector = createSelector(
(system: SystemState, ui: UIState) => { (system: SystemState, ui: UIState) => {
const { const {
shouldConfirmOnDelete, shouldConfirmOnDelete,
shouldDisplayGuides,
enableImageDebugging, enableImageDebugging,
consoleLogLevel, consoleLogLevel,
shouldLogToConsole, shouldLogToConsole,
@ -73,7 +71,6 @@ const selector = createSelector(
return { return {
shouldConfirmOnDelete, shouldConfirmOnDelete,
shouldDisplayGuides,
enableImageDebugging, enableImageDebugging,
shouldUseCanvasBetaLayout, shouldUseCanvasBetaLayout,
shouldUseSliders, shouldUseSliders,
@ -139,7 +136,6 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
const { const {
shouldConfirmOnDelete, shouldConfirmOnDelete,
shouldDisplayGuides,
enableImageDebugging, enableImageDebugging,
shouldUseCanvasBetaLayout, shouldUseCanvasBetaLayout,
shouldUseSliders, shouldUseSliders,
@ -195,7 +191,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
<Modal <Modal
isOpen={isSettingsModalOpen} isOpen={isSettingsModalOpen}
onClose={onSettingsModalClose} onClose={onSettingsModalClose}
size="xl" size="2xl"
isCentered isCentered
> >
<ModalOverlay /> <ModalOverlay />
@ -231,14 +227,6 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
<StyledFlex> <StyledFlex>
<Heading size="sm">{t('settings.ui')}</Heading> <Heading size="sm">{t('settings.ui')}</Heading>
<SettingSwitch
label={t('settings.displayHelpIcons')}
isChecked={shouldDisplayGuides}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldDisplayGuides(e.target.checked))
}
/>
<SettingSwitch <SettingSwitch
label={t('settings.useSlidersForAll')} label={t('settings.useSlidersForAll')}
isChecked={shouldUseSliders} isChecked={shouldUseSliders}
@ -317,8 +305,12 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
</IAIButton> </IAIButton>
{shouldShowResetWebUiText && ( {shouldShowResetWebUiText && (
<> <>
<Text>{t('settings.resetWebUIDesc1')}</Text> <Text variant="subtext">
<Text>{t('settings.resetWebUIDesc2')}</Text> {t('settings.resetWebUIDesc1')}
</Text>
<Text variant="subtext">
{t('settings.resetWebUIDesc2')}
</Text>
</> </>
)} )}
</StyledFlex> </StyledFlex>

View File

@ -38,7 +38,6 @@ export interface SystemState {
currentIteration: number; currentIteration: number;
totalIterations: number; totalIterations: number;
currentStatusHasSteps: boolean; currentStatusHasSteps: boolean;
shouldDisplayGuides: boolean;
isCancelable: boolean; isCancelable: boolean;
enableImageDebugging: boolean; enableImageDebugging: boolean;
toastQueue: UseToastOptions[]; toastQueue: UseToastOptions[];
@ -84,14 +83,12 @@ export interface SystemState {
shouldAntialiasProgressImage: boolean; shouldAntialiasProgressImage: boolean;
language: keyof typeof LANGUAGES; language: keyof typeof LANGUAGES;
isUploading: boolean; isUploading: boolean;
boardIdToAddTo?: string;
isNodesEnabled: boolean; isNodesEnabled: boolean;
} }
export const initialSystemState: SystemState = { export const initialSystemState: SystemState = {
isConnected: false, isConnected: false,
isProcessing: false, isProcessing: false,
shouldDisplayGuides: true,
isGFPGANAvailable: true, isGFPGANAvailable: true,
isESRGANAvailable: true, isESRGANAvailable: true,
shouldConfirmOnDelete: true, shouldConfirmOnDelete: true,
@ -134,9 +131,6 @@ export const systemSlice = createSlice({
setShouldConfirmOnDelete: (state, action: PayloadAction<boolean>) => { setShouldConfirmOnDelete: (state, action: PayloadAction<boolean>) => {
state.shouldConfirmOnDelete = action.payload; state.shouldConfirmOnDelete = action.payload;
}, },
setShouldDisplayGuides: (state, action: PayloadAction<boolean>) => {
state.shouldDisplayGuides = action.payload;
},
setIsCancelable: (state, action: PayloadAction<boolean>) => { setIsCancelable: (state, action: PayloadAction<boolean>) => {
state.isCancelable = action.payload; state.isCancelable = action.payload;
}, },
@ -204,7 +198,6 @@ export const systemSlice = createSlice({
*/ */
builder.addCase(appSocketSubscribed, (state, action) => { builder.addCase(appSocketSubscribed, (state, action) => {
state.sessionId = action.payload.sessionId; state.sessionId = action.payload.sessionId;
state.boardIdToAddTo = action.payload.boardId;
state.canceledSession = ''; state.canceledSession = '';
}); });
@ -213,7 +206,6 @@ export const systemSlice = createSlice({
*/ */
builder.addCase(appSocketUnsubscribed, (state) => { builder.addCase(appSocketUnsubscribed, (state) => {
state.sessionId = null; state.sessionId = null;
state.boardIdToAddTo = undefined;
}); });
/** /**
@ -390,7 +382,6 @@ export const {
setIsProcessing, setIsProcessing,
setShouldConfirmOnDelete, setShouldConfirmOnDelete,
setCurrentStatus, setCurrentStatus,
setShouldDisplayGuides,
setIsCancelable, setIsCancelable,
setEnableImageDebugging, setEnableImageDebugging,
addToast, addToast,

View File

@ -98,16 +98,7 @@ export default function ModelListItem(props: ModelListItemProps) {
onClick={handleSelectModel} onClick={handleSelectModel}
> >
<Flex gap={4} alignItems="center"> <Flex gap={4} alignItems="center">
<Badge <Badge minWidth={14} p={0.5} fontSize="sm" variant="solid">
minWidth={14}
p={1}
fontSize="sm"
sx={{
bg: 'base.350',
color: 'base.900',
_dark: { bg: 'base.500' },
}}
>
{ {
modelBaseTypeMap[ modelBaseTypeMap[
model.base_model as keyof typeof modelBaseTypeMap model.base_model as keyof typeof modelBaseTypeMap

View File

@ -127,6 +127,13 @@ export const imagesApi = api.injectEndpoints({
// 24 hours - reducing this to a few minutes would reduce memory usage. // 24 hours - reducing this to a few minutes would reduce memory usage.
keepUnusedDataFor: 86400, keepUnusedDataFor: 86400,
}), }),
getIntermediatesCount: build.query<number, void>({
query: () => ({ url: getListImagesUrl({ is_intermediate: true }) }),
providesTags: ['IntermediatesCount'],
transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
return response.total;
},
}),
getImageDTO: build.query<ImageDTO, string>({ getImageDTO: build.query<ImageDTO, string>({
query: (image_name) => ({ url: `images/${image_name}` }), query: (image_name) => ({ url: `images/${image_name}` }),
providesTags: (result, error, arg) => { providesTags: (result, error, arg) => {
@ -148,8 +155,9 @@ export const imagesApi = api.injectEndpoints({
}, },
keepUnusedDataFor: 86400, // 24 hours keepUnusedDataFor: 86400, // 24 hours
}), }),
clearIntermediates: build.mutation({ clearIntermediates: build.mutation<number, void>({
query: () => ({ url: `images/clear-intermediates`, method: 'POST' }), query: () => ({ url: `images/clear-intermediates`, method: 'POST' }),
invalidatesTags: ['IntermediatesCount'],
}), }),
deleteImage: build.mutation<void, ImageDTO>({ deleteImage: build.mutation<void, ImageDTO>({
query: ({ image_name }) => ({ query: ({ image_name }) => ({
@ -617,6 +625,7 @@ export const imagesApi = api.injectEndpoints({
}); });
export const { export const {
useGetIntermediatesCountQuery,
useListImagesQuery, useListImagesQuery,
useLazyListImagesQuery, useLazyListImagesQuery,
useGetImageDTOQuery, useGetImageDTOQuery,

View File

@ -0,0 +1,26 @@
import { BoardId } from 'features/gallery/store/gallerySlice';
import { useListAllBoardsQuery } from '../endpoints/boards';
export const useBoardName = (board_id: BoardId | null | undefined) => {
const { boardName } = useListAllBoardsQuery(undefined, {
selectFromResult: ({ data }) => {
let boardName = '';
if (board_id === 'images') {
boardName = 'All Images';
} else if (board_id === 'assets') {
boardName = 'All Assets';
} else if (board_id === 'no_board') {
boardName = 'No Board';
} else if (board_id === 'batch') {
boardName = 'Batch';
} else {
const selectedBoard = data?.find((b) => b.board_id === board_id);
boardName = selectedBoard?.board_name || 'Unknown Board';
}
return { boardName };
},
});
return boardName;
};