Boards UI update and add support for private boards (#6588)

## Summary
Update Boards UI in the gallery and adds support for creating and
displaying private boards
<!--A description of the changes in this PR. Include the kind of change
(fix, feature, docs, etc), the "why" and the "how". Screenshots or
videos are useful for frontend changes.-->

## Related Issues / Discussions

<!--WHEN APPLICABLE: List any related issues or discussions on github or
discord. If this PR closes an issue, please use the "Closes #1234"
format, so that the issue will be automatically closed when the PR
merges.-->

## QA Instructions
Can view private boards by setting config.allowPrivateBoards to true
<!--WHEN APPLICABLE: Describe how you have tested the changes in this
PR. Provide enough detail that a reviewer can reproduce your tests.-->

## Merge Plan

<!--WHEN APPLICABLE: Large PRs, or PRs that touch sensitive things like
DB schemas, may need some care when merging. For example, a careful
rebase by the change author, timing to not interfere with a pending
release, or a message to contributors on discord after merging.-->

## Checklist

- [ ] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Documentation added / updated (if applicable)_
This commit is contained in:
chainchompa 2024-07-09 10:52:01 -04:00 committed by GitHub
commit c90b5541e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 508 additions and 476 deletions

View File

@ -31,6 +31,7 @@ class DeleteBoardResult(BaseModel):
) )
async def create_board( async def create_board(
board_name: str = Query(description="The name of the board to create"), board_name: str = Query(description="The name of the board to create"),
is_private: bool = Query(default=False, description="Whether the board is private"),
) -> BoardDTO: ) -> BoardDTO:
"""Creates a board""" """Creates a board"""
try: try:

View File

@ -24,6 +24,8 @@ class BoardRecord(BaseModelExcludeNull):
"""The name of the cover image of the board.""" """The name of the cover image of the board."""
archived: bool = Field(description="Whether or not the board is archived.") archived: bool = Field(description="Whether or not the board is archived.")
"""Whether or not the board is archived.""" """Whether or not the board is archived."""
is_private: Optional[bool] = Field(default=None, description="Whether the board is private.")
"""Whether the board is private."""
def deserialize_board_record(board_dict: dict) -> BoardRecord: def deserialize_board_record(board_dict: dict) -> BoardRecord:
@ -38,6 +40,7 @@ def deserialize_board_record(board_dict: dict) -> BoardRecord:
updated_at = board_dict.get("updated_at", get_iso_timestamp()) updated_at = board_dict.get("updated_at", get_iso_timestamp())
deleted_at = board_dict.get("deleted_at", get_iso_timestamp()) deleted_at = board_dict.get("deleted_at", get_iso_timestamp())
archived = board_dict.get("archived", False) archived = board_dict.get("archived", False)
is_private = board_dict.get("is_private", False)
return BoardRecord( return BoardRecord(
board_id=board_id, board_id=board_id,
@ -47,6 +50,7 @@ def deserialize_board_record(board_dict: dict) -> BoardRecord:
updated_at=updated_at, updated_at=updated_at,
deleted_at=deleted_at, deleted_at=deleted_at,
archived=archived, archived=archived,
is_private=is_private,
) )

View File

@ -17,9 +17,12 @@
}, },
"boards": { "boards": {
"addBoard": "Add Board", "addBoard": "Add Board",
"addPrivateBoard": "Add Private Board",
"addSharedBoard": "Add Shared Board",
"archiveBoard": "Archive Board", "archiveBoard": "Archive Board",
"archived": "Archived", "archived": "Archived",
"autoAddBoard": "Auto-Add Board", "autoAddBoard": "Auto-Add Board",
"boards": "Boards",
"selectedForAutoAdd": "Selected for Auto-Add", "selectedForAutoAdd": "Selected for Auto-Add",
"bottomMessage": "Deleting this board and its images will reset any features currently using them.", "bottomMessage": "Deleting this board and its images will reset any features currently using them.",
"cancel": "Cancel", "cancel": "Cancel",
@ -36,8 +39,10 @@
"movingImagesToBoard_other": "Moving {{count}} images to board:", "movingImagesToBoard_other": "Moving {{count}} images to board:",
"myBoard": "My Board", "myBoard": "My Board",
"noMatching": "No matching Boards", "noMatching": "No matching Boards",
"private": "Private",
"searchBoard": "Search Boards...", "searchBoard": "Search Boards...",
"selectBoard": "Select a Board", "selectBoard": "Select a Board",
"shared": "Shared",
"topMessage": "This board contains images used in the following features:", "topMessage": "This board contains images used in the following features:",
"unarchiveBoard": "Unarchive Board", "unarchiveBoard": "Unarchive Board",
"uncategorized": "Uncategorized", "uncategorized": "Uncategorized",

View File

@ -15,8 +15,6 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
matcher: isAnyOf( matcher: isAnyOf(
// Updating a board may change its archived status // Updating a board may change its archived status
boardsApi.endpoints.updateBoard.matchFulfilled, boardsApi.endpoints.updateBoard.matchFulfilled,
// If the selected/auto-add board was deleted from a different session, we'll only know during the list request,
boardsApi.endpoints.listAllBoards.matchFulfilled,
// If a board is deleted, we'll need to reset the auto-add board // If a board is deleted, we'll need to reset the auto-add board
imagesApi.endpoints.deleteBoard.matchFulfilled, imagesApi.endpoints.deleteBoard.matchFulfilled,
imagesApi.endpoints.deleteBoardAndImages.matchFulfilled, imagesApi.endpoints.deleteBoardAndImages.matchFulfilled,

View File

@ -65,6 +65,7 @@ export type AppConfig = {
*/ */
shouldUpdateImagesOnConnect: boolean; shouldUpdateImagesOnConnect: boolean;
shouldFetchMetadataFromApi: boolean; shouldFetchMetadataFromApi: boolean;
allowPrivateBoards: boolean;
disabledTabs: InvokeTabName[]; disabledTabs: InvokeTabName[];
disabledFeatures: AppFeature[]; disabledFeatures: AppFeature[];
disabledSDFeatures: SDFeature[]; disabledSDFeatures: SDFeature[];

View File

@ -52,8 +52,8 @@ const IAIDropOverlay = (props: Props) => {
bottom={0.5} bottom={0.5}
opacity={1} opacity={1}
borderWidth={2} borderWidth={2}
borderColor={isOver ? 'base.50' : 'base.300'} borderColor={isOver ? 'base.300' : 'base.500'}
borderRadius="lg" borderRadius="base"
borderStyle="dashed" borderStyle="dashed"
transitionProperty="common" transitionProperty="common"
transitionDuration="0.1s" transitionDuration="0.1s"

View File

@ -0,0 +1,14 @@
import { Badge } from '@invoke-ai/ui-library';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
export const AutoAddBadge = memo(() => {
const { t } = useTranslation();
return (
<Badge color="invokeBlue.400" borderColor="invokeBlue.700" borderWidth={1} bg="transparent" flexShrink={0}>
{t('common.auto')}
</Badge>
);
});
AutoAddBadge.displayName = 'AutoAddBadge';

View File

@ -1,16 +0,0 @@
import { Badge, Flex } from '@invoke-ai/ui-library';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const AutoAddIcon = () => {
const { t } = useTranslation();
return (
<Flex position="absolute" insetInlineEnd={0} top={0} p={1}>
<Badge variant="solid" bg="invokeBlue.400">
{t('common.auto')}
</Badge>
</Flex>
);
};
export default memo(AutoAddIcon);

View File

@ -1,26 +1,48 @@
import { IconButton } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library';
import { memo, useCallback } from 'react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi'; import { PiPlusBold } from 'react-icons/pi';
import { useCreateBoardMutation } from 'services/api/endpoints/boards'; import { useCreateBoardMutation } from 'services/api/endpoints/boards';
const AddBoardButton = () => { type Props = {
isPrivateBoard: boolean;
};
const AddBoardButton = ({ isPrivateBoard }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
const [createBoard, { isLoading }] = useCreateBoardMutation(); const [createBoard, { isLoading }] = useCreateBoardMutation();
const DEFAULT_BOARD_NAME = t('boards.myBoard'); const label = useMemo(() => {
const handleCreateBoard = useCallback(() => { if (!allowPrivateBoards) {
createBoard(DEFAULT_BOARD_NAME); return t('boards.addBoard');
}, [createBoard, DEFAULT_BOARD_NAME]); }
if (isPrivateBoard) {
return t('boards.addPrivateBoard');
}
return t('boards.addSharedBoard');
}, [allowPrivateBoards, isPrivateBoard, t]);
const handleCreateBoard = useCallback(async () => {
try {
const board = await createBoard({ board_name: t('boards.myBoard'), is_private: isPrivateBoard }).unwrap();
dispatch(boardIdSelected({ boardId: board.board_id }));
} catch {
//no-op
}
}, [t, createBoard, isPrivateBoard, dispatch]);
return ( return (
<IconButton <IconButton
icon={<PiPlusBold />} icon={<PiPlusBold />}
isLoading={isLoading} isLoading={isLoading}
tooltip={t('boards.addBoard')} tooltip={label}
aria-label={t('boards.addBoard')} aria-label={label}
onClick={handleCreateBoard} onClick={handleCreateBoard}
size="sm" size="md"
data-testid="add-board-button" data-testid="add-board-button"
variant="ghost"
/> />
); );
}; };

View File

@ -1,11 +1,15 @@
import { Collapse, Flex, Grid, GridItem } from '@invoke-ai/ui-library'; import { Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal'; import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
import GallerySettingsPopover from 'features/gallery/components/GallerySettingsPopover/GallerySettingsPopover';
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors'; import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { memo, useState } from 'react'; import { memo, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretUpBold } from 'react-icons/pi';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
import type { BoardDTO } from 'services/api/types'; import type { BoardDTO } from 'services/api/types';
@ -19,56 +23,112 @@ const overlayScrollbarsStyles: CSSProperties = {
width: '100%', width: '100%',
}; };
type Props = { const BoardsList = () => {
isOpen: boolean;
};
const BoardsList = (props: Props) => {
const { isOpen } = props;
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId); const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText); const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
const queryArgs = useAppSelector(selectListBoardsQueryArgs); const queryArgs = useAppSelector(selectListBoardsQueryArgs);
const { data: boards } = useListAllBoardsQuery(queryArgs); const { data: boards } = useListAllBoardsQuery(queryArgs);
const filteredBoards = boardSearchText
? boards?.filter((board) => board.board_name.toLowerCase().includes(boardSearchText.toLowerCase()))
: boards;
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>(); const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
const privateBoardsDisclosure = useDisclosure({ defaultIsOpen: false });
const sharedBoardsDisclosure = useDisclosure({ defaultIsOpen: false });
const { t } = useTranslation();
const { filteredPrivateBoards, filteredSharedBoards } = useMemo(() => {
const filteredBoards = boardSearchText
? boards?.filter((board) => board.board_name.toLowerCase().includes(boardSearchText.toLowerCase()))
: boards;
const filteredPrivateBoards = filteredBoards?.filter((board) => board.is_private) ?? EMPTY_ARRAY;
const filteredSharedBoards = filteredBoards?.filter((board) => !board.is_private) ?? EMPTY_ARRAY;
return { filteredPrivateBoards, filteredSharedBoards };
}, [boardSearchText, boards]);
return ( return (
<> <>
<Collapse in={isOpen} animateOpacity> <Flex layerStyle="first" flexDir="column" borderRadius="base">
<Flex layerStyle="first" flexDir="column" gap={2} p={2} mt={2} borderRadius="base"> <Flex gap={2} alignItems="center" pb={2}>
<Flex gap={2} alignItems="center"> <BoardsSearch />
<BoardsSearch /> <GallerySettingsPopover />
<AddBoardButton /> </Flex>
</Flex> {allowPrivateBoards && (
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}> <>
<Grid <Flex w="full" gap={2}>
className="list-container" <Flex
data-testid="boards-list" flexGrow={1}
gridTemplateColumns="repeat(auto-fill, minmax(90px, 1fr))" onClick={privateBoardsDisclosure.onToggle}
maxH={346} gap={2}
> alignItems="center"
<GridItem p={1.5} data-testid="no-board"> cursor="pointer"
<NoBoardBoard isSelected={selectedBoardId === 'none'} /> >
</GridItem> <Icon
{filteredBoards && as={PiCaretUpBold}
filteredBoards.map((board, index) => ( boxSize={4}
<GridItem key={board.board_id} p={1.5} data-testid={`board-${index}`}> transform={privateBoardsDisclosure.isOpen ? 'rotate(0deg)' : 'rotate(180deg)'}
transitionProperty="common"
transitionDuration="normal"
color="base.400"
/>
<Text fontSize="md" fontWeight="medium" userSelect="none">
{t('boards.private')}
</Text>
</Flex>
<AddBoardButton isPrivateBoard={true} />
</Flex>
<Collapse in={privateBoardsDisclosure.isOpen} animateOpacity>
<OverlayScrollbarsComponent
defer
style={overlayScrollbarsStyles}
options={overlayScrollbarsParams.options}
>
<Flex direction="column" maxH={346} gap={1}>
{allowPrivateBoards && <NoBoardBoard isSelected={selectedBoardId === 'none'} />}
{filteredPrivateBoards.map((board) => (
<GalleryBoard <GalleryBoard
board={board} board={board}
isSelected={selectedBoardId === board.board_id} isSelected={selectedBoardId === board.board_id}
setBoardToDelete={setBoardToDelete} setBoardToDelete={setBoardToDelete}
key={board.board_id}
/> />
</GridItem> ))}
))} </Flex>
</Grid> </OverlayScrollbarsComponent>
</OverlayScrollbarsComponent> </Collapse>
</>
)}
<Flex w="full" gap={2}>
<Flex onClick={sharedBoardsDisclosure.onToggle} gap={2} alignItems="center" cursor="pointer" flexGrow={1}>
<Icon
as={PiCaretUpBold}
boxSize={4}
transform={sharedBoardsDisclosure.isOpen ? 'rotate(0deg)' : 'rotate(180deg)'}
transitionProperty="common"
transitionDuration="normal"
color="base.400"
/>
<Text fontSize="md" fontWeight="medium" userSelect="none">
{allowPrivateBoards ? t('boards.shared') : t('boards.boards')}
</Text>
</Flex>
<AddBoardButton isPrivateBoard={false} />
</Flex> </Flex>
</Collapse> <Collapse in={sharedBoardsDisclosure.isOpen} animateOpacity>
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex direction="column" maxH={346} gap={1}>
{!allowPrivateBoards && <NoBoardBoard isSelected={selectedBoardId === 'none'} />}
{filteredSharedBoards.map((board) => (
<GalleryBoard
board={board}
isSelected={selectedBoardId === board.board_id}
setBoardToDelete={setBoardToDelete}
key={board.board_id}
/>
))}
</Flex>
</OverlayScrollbarsComponent>
</Collapse>
</Flex>
<DeleteBoardModal boardToDelete={boardToDelete} setBoardToDelete={setBoardToDelete} /> <DeleteBoardModal boardToDelete={boardToDelete} setBoardToDelete={setBoardToDelete} />
</> </>
); );
}; };
export default memo(BoardsList); export default memo(BoardsList);

View File

@ -1,36 +1,41 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library'; import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Editable, EditableInput, EditablePreview, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library'; import {
import { createSelector } from '@reduxjs/toolkit'; Editable,
EditableInput,
EditablePreview,
Flex,
Icon,
Image,
Text,
Tooltip,
useDisclosure,
} from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query'; import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDroppable from 'common/components/IAIDroppable'; import IAIDroppable from 'common/components/IAIDroppable';
import SelectionOverlay from 'common/components/SelectionOverlay';
import type { AddToBoardDropData } from 'features/dnd/types'; import type { AddToBoardDropData } from 'features/dnd/types';
import AutoAddIcon from 'features/gallery/components/Boards/AutoAddIcon'; import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
import BoardContextMenu from 'features/gallery/components/Boards/BoardContextMenu'; import BoardContextMenu from 'features/gallery/components/Boards/BoardContextMenu';
import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip'; import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip';
import { autoAddBoardIdChanged, boardIdSelected, selectGallerySlice } from 'features/gallery/store/gallerySlice'; import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiArchiveBold, PiImagesSquare } from 'react-icons/pi'; import { PiArchiveBold, PiImageSquare } from 'react-icons/pi';
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 type { BoardDTO } from 'services/api/types'; import type { BoardDTO } from 'services/api/types';
const editableInputStyles: SystemStyleObject = { const editableInputStyles: SystemStyleObject = {
p: 0, p: 0,
fontSize: 'md',
w: '100%',
_focusVisible: { _focusVisible: {
p: 0, p: 0,
textAlign: 'center',
}, },
}; };
const ArchivedIcon = () => { const _hover: SystemStyleObject = {
return ( bg: 'base.800',
<Box position="absolute" top={1} insetInlineEnd={2} p={0} minW={0}>
<Icon as={PiArchiveBold} fill="base.300" filter="drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))" />
</Box>
);
}; };
interface GalleryBoardProps { interface GalleryBoardProps {
@ -42,71 +47,53 @@ interface GalleryBoardProps {
const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => { const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick); const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
const selectIsSelectedForAutoAdd = useMemo( const editingDisclosure = useDisclosure();
() => createSelector(selectGallerySlice, (gallery) => board.board_id === gallery.autoAddBoardId), const [localBoardName, setLocalBoardName] = useState(board.board_name);
[board.board_id]
);
const isSelectedForAutoAdd = useAppSelector(selectIsSelectedForAutoAdd);
const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(() => {
setIsHovered(true);
}, []);
const handleMouseOut = useCallback(() => {
setIsHovered(false);
}, []);
const { currentData: coverImage } = useGetImageDTOQuery(board.cover_image_name ?? skipToken);
const { board_name, board_id } = board;
const [localBoardName, setLocalBoardName] = useState(board_name);
const handleSelectBoard = useCallback(() => { const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected({ boardId: board_id })); dispatch(boardIdSelected({ boardId: board.board_id }));
if (autoAssignBoardOnClick) { if (autoAssignBoardOnClick) {
dispatch(autoAddBoardIdChanged(board_id)); dispatch(autoAddBoardIdChanged(board.board_id));
} }
}, [board_id, autoAssignBoardOnClick, dispatch]); }, [dispatch, board.board_id, autoAssignBoardOnClick]);
const [updateBoard, { isLoading: isUpdateBoardLoading }] = useUpdateBoardMutation(); const [updateBoard, { isLoading: isUpdateBoardLoading }] = useUpdateBoardMutation();
const droppableData: AddToBoardDropData = useMemo( const droppableData: AddToBoardDropData = useMemo(
() => ({ () => ({
id: board_id, id: board.board_id,
actionType: 'ADD_TO_BOARD', actionType: 'ADD_TO_BOARD',
context: { boardId: board_id }, context: { boardId: board.board_id },
}), }),
[board_id] [board.board_id]
); );
const handleSubmit = useCallback( const handleSubmit = useCallback(
async (newBoardName: string) => { async (newBoardName: string) => {
// empty strings are not allowed
if (!newBoardName.trim()) { if (!newBoardName.trim()) {
setLocalBoardName(board_name); // empty strings are not allowed
return; setLocalBoardName(board.board_name);
} } else if (newBoardName === board.board_name) {
// don't updated the board name if it hasn't changed
} else {
try {
const { board_name } = await updateBoard({
board_id: board.board_id,
changes: { board_name: newBoardName },
}).unwrap();
// don't updated the board name if it hasn't changed // update local state
if (newBoardName === board_name) { setLocalBoardName(board_name);
return; } catch {
} // revert on error
setLocalBoardName(board.board_name);
try { }
const { board_name } = await updateBoard({
board_id,
changes: { board_name: newBoardName },
}).unwrap();
// update local state
setLocalBoardName(board_name);
} catch {
// revert on error
setLocalBoardName(board_name);
} }
editingDisclosure.onClose();
}, },
[board_id, board_name, updateBoard] [board.board_id, board.board_name, editingDisclosure, updateBoard]
); );
const handleChange = useCallback((newBoardName: string) => { const handleChange = useCallback((newBoardName: string) => {
@ -114,98 +101,91 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
}, []); }, []);
return ( return (
<Box w="full" h="full" userSelect="none"> <BoardContextMenu board={board} setBoardToDelete={setBoardToDelete}>
<Flex {(ref) => (
onMouseOver={handleMouseOver} <Tooltip
onMouseOut={handleMouseOut} label={<BoardTotalsTooltip board_id={board.board_id} isArchived={Boolean(board.archived)} />}
position="relative" openDelay={1000}
justifyContent="center" >
alignItems="center" <Flex
aspectRatio="1/1" position="relative"
w="full" ref={ref}
h="full" onClick={handleSelectBoard}
> w="full"
<BoardContextMenu board={board} setBoardToDelete={setBoardToDelete}> alignItems="center"
{(ref) => ( borderRadius="base"
<Tooltip cursor="pointer"
label={<BoardTotalsTooltip board_id={board.board_id} isArchived={Boolean(board.archived)} />} py={1}
openDelay={1000} px={2}
gap={2}
bg={isSelected ? 'base.800' : undefined}
_hover={_hover}
>
<CoverImage board={board} />
<Editable
as={Flex}
alignItems="center"
gap={4}
flexGrow={1}
onEdit={editingDisclosure.onOpen}
value={localBoardName}
isDisabled={isUpdateBoardLoading}
submitOnBlur={true}
onChange={handleChange}
onSubmit={handleSubmit}
> >
<Flex <EditablePreview
ref={ref} p={0}
onClick={handleSelectBoard} fontSize="md"
w="full" textOverflow="ellipsis"
h="full" noOfLines={1}
position="relative" w="fit-content"
justifyContent="center" wordBreak="break-all"
alignItems="center" color={isSelected ? 'base.100' : 'base.400'}
borderRadius="base" fontWeight={isSelected ? 'semibold' : 'normal'}
cursor="pointer" />
bg="base.800" <EditableInput sx={editableInputStyles} />
> </Editable>
{board.archived && <ArchivedIcon />} {autoAddBoardId === board.board_id && !editingDisclosure.isOpen && <AutoAddBadge />}
{coverImage?.thumbnail_url ? ( {board.archived && !editingDisclosure.isOpen && (
<Image <Icon
src={coverImage?.thumbnail_url} as={PiArchiveBold}
draggable={false} fill="base.300"
objectFit="cover" filter="drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))"
w="full" />
h="full" )}
maxH="full" {!editingDisclosure.isOpen && <Text variant="subtext">{board.image_count}</Text>}
borderRadius="base"
borderBottomRadius="lg"
/>
) : (
<Flex w="full" h="full" justifyContent="center" alignItems="center">
<Icon boxSize={14} as={PiImagesSquare} mt={-6} opacity={0.7} color="base.500" />
</Flex>
)}
{isSelectedForAutoAdd && <AutoAddIcon />}
<SelectionOverlay isSelected={isSelected} isSelectedForCompare={false} isHovered={isHovered} />
<Flex
position="absolute"
bottom={0}
left={0}
p={1}
justifyContent="center"
alignItems="center"
w="full"
maxW="full"
borderBottomRadius="base"
bg={isSelected ? 'invokeBlue.400' : 'base.600'}
color={isSelected ? 'base.800' : 'base.100'}
lineHeight="short"
fontSize="xs"
>
<Editable
value={localBoardName}
isDisabled={isUpdateBoardLoading}
submitOnBlur={true}
onChange={handleChange}
onSubmit={handleSubmit}
w="full"
>
<EditablePreview
p={0}
fontWeight={isSelected ? 'bold' : 'normal'}
textAlign="center"
overflow="hidden"
textOverflow="ellipsis"
noOfLines={1}
color="inherit"
/>
<EditableInput sx={editableInputStyles} />
</Editable>
</Flex>
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} /> <IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
</Flex> </Flex>
</Tooltip> </Tooltip>
)} )}
</BoardContextMenu> </BoardContextMenu>
</Flex>
</Box>
); );
}; };
export default memo(GalleryBoard); export default memo(GalleryBoard);
const CoverImage = ({ board }: { board: BoardDTO }) => {
const { currentData: coverImage } = useGetImageDTOQuery(board.cover_image_name ?? skipToken);
if (coverImage) {
return (
<Image
src={coverImage.thumbnail_url}
draggable={false}
objectFit="cover"
w={8}
h={8}
borderRadius="base"
borderBottomRadius="lg"
/>
);
}
return (
<Flex w={8} h={8} justifyContent="center" alignItems="center">
<Icon boxSize={8} as={PiImageSquare} opacity={0.7} color="base.500" />
</Flex>
);
};

View File

@ -1,23 +1,32 @@
import { Box, Flex, Image, Text, Tooltip } from '@invoke-ai/ui-library'; import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDroppable from 'common/components/IAIDroppable'; import IAIDroppable from 'common/components/IAIDroppable';
import SelectionOverlay from 'common/components/SelectionOverlay';
import type { RemoveFromBoardDropData } from 'features/dnd/types'; import type { RemoveFromBoardDropData } from 'features/dnd/types';
import AutoAddIcon from 'features/gallery/components/Boards/AutoAddIcon'; import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip'; import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip';
import NoBoardBoardContextMenu from 'features/gallery/components/Boards/NoBoardBoardContextMenu'; import NoBoardBoardContextMenu from 'features/gallery/components/Boards/NoBoardBoardContextMenu';
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice'; import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
import InvokeLogoSVG from 'public/assets/images/invoke-symbol-wht-lrg.svg'; import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
import { useBoardName } from 'services/api/hooks/useBoardName'; import { useBoardName } from 'services/api/hooks/useBoardName';
interface Props { interface Props {
isSelected: boolean; isSelected: boolean;
} }
const _hover: SystemStyleObject = {
bg: 'base.800',
};
const NoBoardBoard = memo(({ isSelected }: Props) => { const NoBoardBoard = memo(({ isSelected }: Props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { imagesTotal } = useGetBoardImagesTotalQuery('none', {
selectFromResult: ({ data }) => {
return { imagesTotal: data?.total ?? 0 };
},
});
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick); const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
const boardName = useBoardName('none'); const boardName = useBoardName('none');
@ -27,15 +36,6 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
dispatch(autoAddBoardIdChanged('none')); dispatch(autoAddBoardIdChanged('none'));
} }
}, [dispatch, autoAssignBoardOnClick]); }, [dispatch, autoAssignBoardOnClick]);
const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(() => {
setIsHovered(true);
}, []);
const handleMouseOut = useCallback(() => {
setIsHovered(false);
}, []);
const droppableData: RemoveFromBoardDropData = useMemo( const droppableData: RemoveFromBoardDropData = useMemo(
() => ({ () => ({
@ -46,74 +46,49 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
); );
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Box w="full" h="full" userSelect="none"> <NoBoardBoardContextMenu>
<Flex {(ref) => (
onMouseOver={handleMouseOver} <Tooltip label={<BoardTotalsTooltip board_id="none" isArchived={false} />} openDelay={1000}>
onMouseOut={handleMouseOut} <Flex
position="relative" position="relative"
justifyContent="center" ref={ref}
alignItems="center" onClick={handleSelectBoard}
aspectRatio="1/1" w="full"
borderRadius="base" alignItems="center"
w="full" borderRadius="base"
h="full" cursor="pointer"
> px={2}
<NoBoardBoardContextMenu> py={1}
{(ref) => ( gap={2}
<Tooltip label={<BoardTotalsTooltip board_id="none" isArchived={false} />} openDelay={1000}> bg={isSelected ? 'base.800' : undefined}
<Flex _hover={_hover}
ref={ref} >
onClick={handleSelectBoard} <Flex w={8} h={8} justifyContent="center" alignItems="center">
w="full" {/* iconified from public/assets/images/invoke-symbol-wht-lrg.svg */}
h="full" <Icon boxSize={6} opacity={1} stroke="base.500" viewBox="0 0 66 66" fill="none">
position="relative" <path
justifyContent="center" d="M43.9137 16H63.1211V3H3.12109V16H22.3285L43.9137 50H63.1211V63H3.12109V50H22.3285"
alignItems="center" strokeWidth="5"
borderRadius="base" />
cursor="pointer" </Icon>
bg="base.800" </Flex>
>
<Flex w="full" h="full" justifyContent="center" alignItems="center"> <Text
<Image fontSize="md"
src={InvokeLogoSVG} color={isSelected ? 'base.100' : 'base.400'}
alt="invoke-ai-logo" fontWeight={isSelected ? 'semibold' : 'normal'}
opacity={0.7} noOfLines={1}
mixBlendMode="overlay" flexGrow={1}
mt={-6} >
w={16} {boardName}
h={16} </Text>
minW={16} {autoAddBoardId === 'none' && <AutoAddBadge />}
minH={16} <Text variant="subtext">{imagesTotal}</Text>
userSelect="none" <IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
/> </Flex>
</Flex> </Tooltip>
{autoAddBoardId === 'none' && <AutoAddIcon />} )}
<Flex </NoBoardBoardContextMenu>
position="absolute"
bottom={0}
left={0}
p={1}
justifyContent="center"
alignItems="center"
w="full"
maxW="full"
borderBottomRadius="base"
bg={isSelected ? 'invokeBlue.400' : 'base.600'}
color={isSelected ? 'base.800' : 'base.100'}
lineHeight="short"
fontSize="xs"
fontWeight={isSelected ? 'bold' : 'normal'}
>
{boardName}
</Flex>
<SelectionOverlay isSelected={isSelected} isSelectedForCompare={false} isHovered={isHovered} />
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
</Flex>
</Tooltip>
)}
</NoBoardBoardContextMenu>
</Flex>
</Box>
); );
}); });

View File

@ -1,48 +1,17 @@
import { Button, Flex, Icon, Spacer } from '@invoke-ai/ui-library'; import { Flex, Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { memo, useMemo } from 'react'; import { memo } from 'react';
import { PiCaretUpBold } from 'react-icons/pi';
import { useBoardName } from 'services/api/hooks/useBoardName'; import { useBoardName } from 'services/api/hooks/useBoardName';
type Props = { const GalleryBoardName = () => {
isOpen: boolean;
onToggle: () => void;
};
const GalleryBoardName = (props: Props) => {
const { isOpen, onToggle } = props;
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId); const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
const boardName = useBoardName(selectedBoardId); const boardName = useBoardName(selectedBoardId);
const formattedBoardName = useMemo(() => {
if (boardName.length > 20) {
return `${boardName.substring(0, 20)}...`;
}
return boardName;
}, [boardName]);
return ( return (
<Flex <Flex w="full" borderWidth={1} borderRadius="base" alignItems="center" justifyContent="center" px={2}>
as={Button} <Text fontWeight="semibold" fontSize="md" noOfLines={1} wordBreak="break-all" color="base.200">
onClick={onToggle} {boardName}
size="sm" </Text>
position="relative"
gap={2}
w="full"
justifyContent="center"
alignItems="center"
px={2}
>
<Spacer />
{formattedBoardName}
<Spacer />
<Icon
as={PiCaretUpBold}
boxSize={4}
transform={isOpen ? 'rotate(0deg)' : 'rotate(180deg)'}
transitionProperty="common"
transitionDuration="normal"
/>
</Flex> </Flex>
); );
}; };

View File

@ -1,4 +1,4 @@
import { Box, Button, ButtonGroup, Flex, Tab, TabList, Tabs, useDisclosure } from '@invoke-ai/ui-library'; import { Button, ButtonGroup, Flex, Tab, TabList, Tabs } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { $galleryHeader } from 'app/store/nanostores/galleryHeader'; import { $galleryHeader } from 'app/store/nanostores/galleryHeader';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
@ -10,7 +10,6 @@ import { RiServerLine } from 'react-icons/ri';
import BoardsList from './Boards/BoardsList/BoardsList'; import BoardsList from './Boards/BoardsList/BoardsList';
import GalleryBoardName from './GalleryBoardName'; import GalleryBoardName from './GalleryBoardName';
import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopover';
import GalleryImageGrid from './ImageGrid/GalleryImageGrid'; import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
import { GalleryPagination } from './ImageGrid/GalleryPagination'; import { GalleryPagination } from './ImageGrid/GalleryPagination';
import { GallerySearch } from './ImageGrid/GallerySearch'; import { GallerySearch } from './ImageGrid/GallerySearch';
@ -20,7 +19,6 @@ const ImageGalleryContent = () => {
const galleryView = useAppSelector((s) => s.gallery.galleryView); const galleryView = useAppSelector((s) => s.gallery.galleryView);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const galleryHeader = useStore($galleryHeader); const galleryHeader = useStore($galleryHeader);
const { isOpen: isBoardListOpen, onToggle: onToggleBoardList } = useDisclosure({ defaultIsOpen: true });
const handleClickImages = useCallback(() => { const handleClickImages = useCallback(() => {
dispatch(galleryViewChanged('images')); dispatch(galleryViewChanged('images'));
@ -42,15 +40,8 @@ const ImageGalleryContent = () => {
gap={2} gap={2}
> >
{galleryHeader} {galleryHeader}
<Box> <BoardsList />
<Flex alignItems="center" justifyContent="space-between" gap={2}> <GalleryBoardName />
<GalleryBoardName isOpen={isBoardListOpen} onToggle={onToggleBoardList} />
<GallerySettingsPopover />
</Flex>
<Box>
<BoardsList isOpen={isBoardListOpen} />
</Box>
</Box>
<Flex alignItems="center" justifyContent="space-between" gap={2}> <Flex alignItems="center" justifyContent="space-between" gap={2}>
<Tabs index={galleryView === 'images' ? 0 : 1} variant="unstyled" size="sm" w="full"> <Tabs index={galleryView === 'images' ? 0 : 1} variant="unstyled" size="sm" w="full">
<TabList> <TabList>

View File

@ -18,6 +18,7 @@ const initialConfigState: AppConfig = {
isLocal: true, isLocal: true,
shouldUpdateImagesOnConnect: false, shouldUpdateImagesOnConnect: false,
shouldFetchMetadataFromApi: false, shouldFetchMetadataFromApi: false,
allowPrivateBoards: false,
disabledTabs: [], disabledTabs: [],
disabledFeatures: ['lightbox', 'faceRestore', 'batches'], disabledFeatures: ['lightbox', 'faceRestore', 'batches'],
disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'], disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'],

View File

@ -1,5 +1,11 @@
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types'; import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
import type { BoardDTO, ListBoardsArgs, OffsetPaginatedResults_ImageDTO_, UpdateBoardArg } from 'services/api/types'; import type {
BoardDTO,
CreateBoardArg,
ListBoardsArgs,
OffsetPaginatedResults_ImageDTO_,
UpdateBoardArg,
} from 'services/api/types';
import { getListImagesUrl } from 'services/api/util'; import { getListImagesUrl } from 'services/api/util';
import type { ApiTagDescription } from '..'; import type { ApiTagDescription } from '..';
@ -87,11 +93,11 @@ export const boardsApi = api.injectEndpoints({
* Boards Mutations * Boards Mutations
*/ */
createBoard: build.mutation<BoardDTO, string>({ createBoard: build.mutation<BoardDTO, CreateBoardArg>({
query: (board_name) => ({ query: ({ board_name, is_private }) => ({
url: buildBoardsUrl(), url: buildBoardsUrl(),
method: 'POST', method: 'POST',
params: { board_name }, params: { board_name, is_private },
}), }),
invalidatesTags: [{ type: 'Board', id: LIST_TAG }], invalidatesTags: [{ type: 'Board', id: LIST_TAG }],
}), }),

View File

@ -1058,6 +1058,11 @@ export type components = {
* @description Whether or not the board is archived. * @description Whether or not the board is archived.
*/ */
archived: boolean; archived: boolean;
/**
* Is Private
* @description Whether the board is private.
*/
is_private?: boolean | null;
/** /**
* Image Count * Image Count
* @description The number of images in the board. * @description The number of images in the board.
@ -6561,6 +6566,12 @@ export type components = {
* @default false * @default false
*/ */
tiled?: boolean; tiled?: boolean;
/**
* Tile Size
* @description The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the
* @default 0
*/
tile_size?: number;
/** /**
* Fp32 * Fp32
* @description Whether or not to use full float32 precision * @description Whether or not to use full float32 precision
@ -7293,145 +7304,145 @@ export type components = {
project_id: string | null; project_id: string | null;
}; };
InvocationOutputMap: { InvocationOutputMap: {
save_image: components["schemas"]["ImageOutput"];
integer_math: components["schemas"]["IntegerOutput"];
segment_anything_processor: components["schemas"]["ImageOutput"];
sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"];
zoe_depth_image_processor: components["schemas"]["ImageOutput"];
collect: components["schemas"]["CollectInvocationOutput"];
range: components["schemas"]["IntegerCollectionOutput"];
unsharp_mask: components["schemas"]["ImageOutput"];
string_replace: components["schemas"]["StringOutput"];
face_identifier: components["schemas"]["ImageOutput"];
heuristic_resize: components["schemas"]["ImageOutput"];
range_of_size: components["schemas"]["IntegerCollectionOutput"];
latents_collection: components["schemas"]["LatentsCollectionOutput"];
color_map_image_processor: components["schemas"]["ImageOutput"];
img_ilerp: components["schemas"]["ImageOutput"];
infill_patchmatch: components["schemas"]["ImageOutput"];
face_off: components["schemas"]["FaceOffOutput"];
string_collection: components["schemas"]["StringCollectionOutput"];
sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"];
sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"];
string_join: components["schemas"]["StringOutput"];
lblend: components["schemas"]["LatentsOutput"];
conditioning_collection: components["schemas"]["ConditioningCollectionOutput"];
string_split_neg: components["schemas"]["StringPosNegOutput"];
img_watermark: components["schemas"]["ImageOutput"];
infill_lama: components["schemas"]["ImageOutput"];
div: components["schemas"]["IntegerOutput"];
show_image: components["schemas"]["ImageOutput"];
tile_to_properties: components["schemas"]["TileToPropertiesOutput"];
sub: components["schemas"]["IntegerOutput"];
normalbae_image_processor: components["schemas"]["ImageOutput"];
invert_tensor_mask: components["schemas"]["MaskOutput"];
create_gradient_mask: components["schemas"]["GradientMaskOutput"];
string_split: components["schemas"]["String2Output"];
step_param_easing: components["schemas"]["FloatCollectionOutput"];
metadata: components["schemas"]["MetadataOutput"];
img_pad_crop: components["schemas"]["ImageOutput"];
integer: components["schemas"]["IntegerOutput"];
img_mul: components["schemas"]["ImageOutput"];
calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"];
color: components["schemas"]["ColorOutput"];
infill_rgba: components["schemas"]["ImageOutput"];
t2i_adapter: components["schemas"]["T2IAdapterOutput"];
denoise_latents: components["schemas"]["LatentsOutput"];
img_lerp: components["schemas"]["ImageOutput"];
img_channel_offset: components["schemas"]["ImageOutput"];
img_crop: components["schemas"]["ImageOutput"];
alpha_mask_to_tensor: components["schemas"]["MaskOutput"];
color_correct: components["schemas"]["ImageOutput"];
calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"];
img_hue_adjust: components["schemas"]["ImageOutput"];
lresize: components["schemas"]["LatentsOutput"];
img_blur: components["schemas"]["ImageOutput"];
compel: components["schemas"]["ConditioningOutput"];
sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"];
float_to_int: components["schemas"]["IntegerOutput"];
boolean: components["schemas"]["BooleanOutput"];
string_join_three: components["schemas"]["StringOutput"];
add: components["schemas"]["IntegerOutput"];
merge_tiles_to_image: components["schemas"]["ImageOutput"];
core_metadata: components["schemas"]["MetadataOutput"];
lscale: components["schemas"]["LatentsOutput"];
mlsd_image_processor: components["schemas"]["ImageOutput"];
image_collection: components["schemas"]["ImageCollectionOutput"];
crop_latents: components["schemas"]["LatentsOutput"];
image_mask_to_tensor: components["schemas"]["MaskOutput"];
lora_collection_loader: components["schemas"]["LoRALoaderOutput"];
ip_adapter: components["schemas"]["IPAdapterOutput"];
pidi_image_processor: components["schemas"]["ImageOutput"];
rand_int: components["schemas"]["IntegerOutput"];
img_conv: components["schemas"]["ImageOutput"];
scheduler: components["schemas"]["SchedulerOutput"];
img_paste: components["schemas"]["ImageOutput"];
noise: components["schemas"]["NoiseOutput"];
img_scale: components["schemas"]["ImageOutput"];
i2l: components["schemas"]["LatentsOutput"];
main_model_loader: components["schemas"]["ModelLoaderOutput"];
blank_image: components["schemas"]["ImageOutput"];
mask_edge: components["schemas"]["ImageOutput"];
seamless: components["schemas"]["SeamlessModeOutput"];
esrgan: components["schemas"]["ImageOutput"];
canvas_paste_back: components["schemas"]["ImageOutput"];
mul: components["schemas"]["IntegerOutput"];
dynamic_prompt: components["schemas"]["StringCollectionOutput"];
controlnet: components["schemas"]["ControlOutput"];
l2i: components["schemas"]["ImageOutput"];
ideal_size: components["schemas"]["IdealSizeOutput"];
latents: components["schemas"]["LatentsOutput"];
midas_depth_image_processor: components["schemas"]["ImageOutput"];
tomask: components["schemas"]["ImageOutput"];
float_math: components["schemas"]["FloatOutput"];
round_float: components["schemas"]["FloatOutput"];
cv_inpaint: components["schemas"]["ImageOutput"];
create_denoise_mask: components["schemas"]["DenoiseMaskOutput"];
model_identifier: components["schemas"]["ModelIdentifierOutput"];
pair_tile_image: components["schemas"]["PairTileImageOutput"];
lineart_image_processor: components["schemas"]["ImageOutput"];
img_nsfw: components["schemas"]["ImageOutput"];
infill_cv2: components["schemas"]["ImageOutput"];
clip_skip: components["schemas"]["CLIPSkipInvocationOutput"];
dw_openpose_image_processor: components["schemas"]["ImageOutput"];
img_resize: components["schemas"]["ImageOutput"];
iterate: components["schemas"]["IterateInvocationOutput"];
rectangle_mask: components["schemas"]["MaskOutput"]; rectangle_mask: components["schemas"]["MaskOutput"];
canny_image_processor: components["schemas"]["ImageOutput"]; hed_image_processor: components["schemas"]["ImageOutput"];
calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; compel: components["schemas"]["ConditioningOutput"];
img_resize: components["schemas"]["ImageOutput"];
ideal_size: components["schemas"]["IdealSizeOutput"];
rand_int: components["schemas"]["IntegerOutput"];
clip_skip: components["schemas"]["CLIPSkipInvocationOutput"];
string_collection: components["schemas"]["StringCollectionOutput"];
create_gradient_mask: components["schemas"]["GradientMaskOutput"];
round_float: components["schemas"]["FloatOutput"];
scheduler: components["schemas"]["SchedulerOutput"];
main_model_loader: components["schemas"]["ModelLoaderOutput"];
string_split: components["schemas"]["String2Output"];
mask_from_id: components["schemas"]["ImageOutput"]; mask_from_id: components["schemas"]["ImageOutput"];
metadata_item: components["schemas"]["MetadataItemOutput"]; collect: components["schemas"]["CollectInvocationOutput"];
infill_tile: components["schemas"]["ImageOutput"]; heuristic_resize: components["schemas"]["ImageOutput"];
tiled_multi_diffusion_denoise_latents: components["schemas"]["LatentsOutput"]; tomask: components["schemas"]["ImageOutput"];
img_channel_multiply: components["schemas"]["ImageOutput"];
boolean_collection: components["schemas"]["BooleanCollectionOutput"]; boolean_collection: components["schemas"]["BooleanCollectionOutput"];
lora_loader: components["schemas"]["LoRALoaderOutput"]; core_metadata: components["schemas"]["MetadataOutput"];
float_collection: components["schemas"]["FloatCollectionOutput"]; canny_image_processor: components["schemas"]["ImageOutput"];
string: components["schemas"]["StringOutput"]; string_replace: components["schemas"]["StringOutput"];
freeu: components["schemas"]["UNetOutput"];
lineart_anime_image_processor: components["schemas"]["ImageOutput"];
depth_anything_image_processor: components["schemas"]["ImageOutput"];
image: components["schemas"]["ImageOutput"];
face_mask_detection: components["schemas"]["FaceMaskOutput"]; face_mask_detection: components["schemas"]["FaceMaskOutput"];
integer: components["schemas"]["IntegerOutput"];
img_watermark: components["schemas"]["ImageOutput"];
img_crop: components["schemas"]["ImageOutput"];
t2i_adapter: components["schemas"]["T2IAdapterOutput"];
create_denoise_mask: components["schemas"]["DenoiseMaskOutput"];
rand_float: components["schemas"]["FloatOutput"]; rand_float: components["schemas"]["FloatOutput"];
float: components["schemas"]["FloatOutput"]; zoe_depth_image_processor: components["schemas"]["ImageOutput"];
random_range: components["schemas"]["IntegerCollectionOutput"]; face_off: components["schemas"]["FaceOffOutput"];
integer_collection: components["schemas"]["IntegerCollectionOutput"]; tile_to_properties: components["schemas"]["TileToPropertiesOutput"];
sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; color_map_image_processor: components["schemas"]["ImageOutput"];
mask_combine: components["schemas"]["ImageOutput"]; lineart_anime_image_processor: components["schemas"]["ImageOutput"];
tile_image_processor: components["schemas"]["ImageOutput"]; face_identifier: components["schemas"]["ImageOutput"];
float_math: components["schemas"]["FloatOutput"];
mediapipe_face_processor: components["schemas"]["ImageOutput"];
img_channel_multiply: components["schemas"]["ImageOutput"];
metadata_item: components["schemas"]["MetadataItemOutput"];
img_ilerp: components["schemas"]["ImageOutput"];
conditioning: components["schemas"]["ConditioningOutput"];
pidi_image_processor: components["schemas"]["ImageOutput"];
seamless: components["schemas"]["SeamlessModeOutput"];
latents: components["schemas"]["LatentsOutput"];
img_chan: components["schemas"]["ImageOutput"]; img_chan: components["schemas"]["ImageOutput"];
vae_loader: components["schemas"]["VAEOutput"]; model_identifier: components["schemas"]["ModelIdentifierOutput"];
noise: components["schemas"]["NoiseOutput"];
string_join: components["schemas"]["StringOutput"];
blank_image: components["schemas"]["ImageOutput"];
calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"];
invert_tensor_mask: components["schemas"]["MaskOutput"];
save_image: components["schemas"]["ImageOutput"];
unsharp_mask: components["schemas"]["ImageOutput"];
image_mask_to_tensor: components["schemas"]["MaskOutput"];
step_param_easing: components["schemas"]["FloatCollectionOutput"];
merge_tiles_to_image: components["schemas"]["ImageOutput"];
integer_collection: components["schemas"]["IntegerCollectionOutput"];
calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"];
integer_math: components["schemas"]["IntegerOutput"];
range: components["schemas"]["IntegerCollectionOutput"];
prompt_from_file: components["schemas"]["StringCollectionOutput"]; prompt_from_file: components["schemas"]["StringCollectionOutput"];
segment_anything_processor: components["schemas"]["ImageOutput"];
freeu: components["schemas"]["UNetOutput"];
sub: components["schemas"]["IntegerOutput"];
lresize: components["schemas"]["LatentsOutput"];
float: components["schemas"]["FloatOutput"];
float_collection: components["schemas"]["FloatCollectionOutput"];
dynamic_prompt: components["schemas"]["StringCollectionOutput"];
infill_lama: components["schemas"]["ImageOutput"];
l2i: components["schemas"]["ImageOutput"];
img_lerp: components["schemas"]["ImageOutput"];
ip_adapter: components["schemas"]["IPAdapterOutput"];
lora_collection_loader: components["schemas"]["LoRALoaderOutput"];
color: components["schemas"]["ColorOutput"];
tiled_multi_diffusion_denoise_latents: components["schemas"]["LatentsOutput"];
cv_inpaint: components["schemas"]["ImageOutput"];
lscale: components["schemas"]["LatentsOutput"];
string: components["schemas"]["StringOutput"];
sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"];
string_join_three: components["schemas"]["StringOutput"];
midas_depth_image_processor: components["schemas"]["ImageOutput"];
esrgan: components["schemas"]["ImageOutput"];
sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"];
mul: components["schemas"]["IntegerOutput"];
normalbae_image_processor: components["schemas"]["ImageOutput"];
infill_rgba: components["schemas"]["ImageOutput"];
sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"];
vae_loader: components["schemas"]["VAEOutput"];
float_to_int: components["schemas"]["IntegerOutput"];
lora_selector: components["schemas"]["LoRASelectorOutput"];
crop_latents: components["schemas"]["LatentsOutput"];
img_mul: components["schemas"]["ImageOutput"];
float_range: components["schemas"]["FloatCollectionOutput"]; float_range: components["schemas"]["FloatCollectionOutput"];
merge_metadata: components["schemas"]["MetadataOutput"]; merge_metadata: components["schemas"]["MetadataOutput"];
sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; img_blur: components["schemas"]["ImageOutput"];
hed_image_processor: components["schemas"]["ImageOutput"]; boolean: components["schemas"]["BooleanOutput"];
lora_selector: components["schemas"]["LoRASelectorOutput"]; tile_image_processor: components["schemas"]["ImageOutput"];
conditioning: components["schemas"]["ConditioningOutput"]; mlsd_image_processor: components["schemas"]["ImageOutput"];
infill_patchmatch: components["schemas"]["ImageOutput"];
img_pad_crop: components["schemas"]["ImageOutput"];
leres_image_processor: components["schemas"]["ImageOutput"]; leres_image_processor: components["schemas"]["ImageOutput"];
mediapipe_face_processor: components["schemas"]["ImageOutput"]; sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"];
dw_openpose_image_processor: components["schemas"]["ImageOutput"];
img_scale: components["schemas"]["ImageOutput"];
pair_tile_image: components["schemas"]["PairTileImageOutput"];
lblend: components["schemas"]["LatentsOutput"];
range_of_size: components["schemas"]["IntegerCollectionOutput"];
image_collection: components["schemas"]["ImageCollectionOutput"];
calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"];
img_channel_offset: components["schemas"]["ImageOutput"];
alpha_mask_to_tensor: components["schemas"]["MaskOutput"];
infill_cv2: components["schemas"]["ImageOutput"];
mask_combine: components["schemas"]["ImageOutput"];
string_split_neg: components["schemas"]["StringPosNegOutput"];
sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"];
lineart_image_processor: components["schemas"]["ImageOutput"];
img_nsfw: components["schemas"]["ImageOutput"];
image: components["schemas"]["ImageOutput"];
content_shuffle_image_processor: components["schemas"]["ImageOutput"]; content_shuffle_image_processor: components["schemas"]["ImageOutput"];
canvas_paste_back: components["schemas"]["ImageOutput"];
iterate: components["schemas"]["IterateInvocationOutput"];
div: components["schemas"]["IntegerOutput"];
latents_collection: components["schemas"]["LatentsCollectionOutput"];
img_conv: components["schemas"]["ImageOutput"];
mask_edge: components["schemas"]["ImageOutput"];
conditioning_collection: components["schemas"]["ConditioningCollectionOutput"];
img_hue_adjust: components["schemas"]["ImageOutput"];
depth_anything_image_processor: components["schemas"]["ImageOutput"];
lora_loader: components["schemas"]["LoRALoaderOutput"];
sdxl_compel_prompt: components["schemas"]["ConditioningOutput"];
add: components["schemas"]["IntegerOutput"];
controlnet: components["schemas"]["ControlOutput"];
color_correct: components["schemas"]["ImageOutput"];
random_range: components["schemas"]["IntegerCollectionOutput"];
denoise_latents: components["schemas"]["LatentsOutput"];
metadata: components["schemas"]["MetadataOutput"];
i2l: components["schemas"]["LatentsOutput"];
show_image: components["schemas"]["ImageOutput"];
img_paste: components["schemas"]["ImageOutput"];
infill_tile: components["schemas"]["ImageOutput"];
}; };
/** /**
* InvocationStartedEvent * InvocationStartedEvent
@ -7769,6 +7780,12 @@ export type components = {
* @default false * @default false
*/ */
tiled?: boolean; tiled?: boolean;
/**
* Tile Size
* @description The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the
* @default 0
*/
tile_size?: number;
/** /**
* Fp32 * Fp32
* @description Whether or not to use full float32 precision * @description Whether or not to use full float32 precision
@ -15014,6 +15031,8 @@ export type operations = {
query: { query: {
/** @description The name of the board to create */ /** @description The name of the board to create */
board_name: string; board_name: string;
/** @description Whether the board is private */
is_private?: boolean;
}; };
}; };
responses: { responses: {

View File

@ -11,6 +11,8 @@ export type ListBoardsArgs = NonNullable<paths['/api/v1/boards/']['get']['parame
export type DeleteBoardResult = export type DeleteBoardResult =
paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json']; paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json'];
export type CreateBoardArg = paths['/api/v1/boards/']['post']['parameters']['query'];
export type UpdateBoardArg = paths['/api/v1/boards/{board_id}']['patch']['parameters']['path'] & { export type UpdateBoardArg = paths['/api/v1/boards/{board_id}']['patch']['parameters']['path'] & {
changes: paths['/api/v1/boards/{board_id}']['patch']['requestBody']['content']['application/json']; changes: paths['/api/v1/boards/{board_id}']['patch']['requestBody']['content']['application/json'];
}; };