Merge branch 'main' into lstein/installer-for-new-model-layout

This commit is contained in:
Lincoln Stein
2023-06-23 01:45:05 +01:00
committed by GitHub
136 changed files with 4085 additions and 1013 deletions

View File

@ -23,6 +23,8 @@ import GlobalHotkeys from './GlobalHotkeys';
import Toaster from './Toaster';
import DeleteImageModal from 'features/gallery/components/DeleteImageModal';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
import { useListModelsQuery } from 'services/apiSlice';
const DEFAULT_CONFIG = {};
@ -45,6 +47,18 @@ const App = ({
const isApplicationReady = useIsApplicationReady();
const { data: pipelineModels } = useListModelsQuery({
model_type: 'pipeline',
});
const { data: controlnetModels } = useListModelsQuery({
model_type: 'controlnet',
});
const { data: vaeModels } = useListModelsQuery({ model_type: 'vae' });
const { data: loraModels } = useListModelsQuery({ model_type: 'lora' });
const { data: embeddingModels } = useListModelsQuery({
model_type: 'embedding',
});
const [loadingOverridden, setLoadingOverridden] = useState(false);
const dispatch = useAppDispatch();
@ -143,6 +157,7 @@ const App = ({
</Portal>
</Grid>
<DeleteImageModal />
<UpdateImageBoardModal />
<Toaster />
<GlobalHotkeys />
</>

View File

@ -21,6 +21,8 @@ import {
DeleteImageContext,
DeleteImageContextProvider,
} from 'app/contexts/DeleteImageContext';
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
import { AddImageToBoardContextProvider } from '../contexts/AddImageToBoardContext';
const App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@ -76,11 +78,13 @@ const InvokeAIUI = ({
<ThemeLocaleProvider>
<ImageDndContext>
<DeleteImageContextProvider>
<App
config={config}
headerComponent={headerComponent}
setIsReady={setIsReady}
/>
<AddImageToBoardContextProvider>
<App
config={config}
headerComponent={headerComponent}
setIsReady={setIsReady}
/>
</AddImageToBoardContextProvider>
</DeleteImageContextProvider>
</ImageDndContext>
</ThemeLocaleProvider>

View File

@ -0,0 +1,89 @@
import { useDisclosure } from '@chakra-ui/react';
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
import { ImageDTO } from 'services/api';
import { useAddImageToBoardMutation } from 'services/apiSlice';
export type ImageUsage = {
isInitialImage: boolean;
isCanvasImage: boolean;
isNodesImage: boolean;
isControlNetImage: boolean;
};
type AddImageToBoardContextValue = {
/**
* Whether the move image dialog is open.
*/
isOpen: boolean;
/**
* Closes the move image dialog.
*/
onClose: () => void;
/**
* The image pending movement
*/
image?: ImageDTO;
onClickAddToBoard: (image: ImageDTO) => void;
handleAddToBoard: (boardId: string) => void;
};
export const AddImageToBoardContext =
createContext<AddImageToBoardContextValue>({
isOpen: false,
onClose: () => undefined,
onClickAddToBoard: () => undefined,
handleAddToBoard: () => undefined,
});
type Props = PropsWithChildren;
export const AddImageToBoardContextProvider = (props: Props) => {
const [imageToMove, setImageToMove] = useState<ImageDTO>();
const { isOpen, onOpen, onClose } = useDisclosure();
const [addImageToBoard, result] = useAddImageToBoardMutation();
// Clean up after deleting or dismissing the modal
const closeAndClearImageToDelete = useCallback(() => {
setImageToMove(undefined);
onClose();
}, [onClose]);
const onClickAddToBoard = useCallback(
(image?: ImageDTO) => {
if (!image) {
return;
}
setImageToMove(image);
onOpen();
},
[setImageToMove, onOpen]
);
const handleAddToBoard = useCallback(
(boardId: string) => {
if (imageToMove) {
addImageToBoard({
board_id: boardId,
image_name: imageToMove.image_name,
});
closeAndClearImageToDelete();
}
},
[addImageToBoard, closeAndClearImageToDelete, imageToMove]
);
return (
<AddImageToBoardContext.Provider
value={{
isOpen,
image: imageToMove,
onClose: closeAndClearImageToDelete,
onClickAddToBoard,
handleAddToBoard,
}}
>
{props.children}
</AddImageToBoardContext.Provider>
);
};

View File

@ -35,25 +35,23 @@ export const selectImageUsage = createSelector(
(state: RootState, image_name?: string) => image_name,
],
(generation, canvas, nodes, controlNet, image_name) => {
const isInitialImage = generation.initialImage?.image_name === image_name;
const isInitialImage = generation.initialImage?.imageName === image_name;
const isCanvasImage = canvas.layerState.objects.some(
(obj) => obj.kind === 'image' && obj.image.image_name === image_name
(obj) => obj.kind === 'image' && obj.imageName === image_name
);
const isNodesImage = nodes.nodes.some((node) => {
return some(
node.data.inputs,
(input) =>
input.type === 'image' && input.value?.image_name === image_name
(input) => input.type === 'image' && input.value === image_name
);
});
const isControlNetImage = some(
controlNet.controlNets,
(c) =>
c.controlImage?.image_name === image_name ||
c.processedControlImage?.image_name === image_name
c.controlImage === image_name || c.processedControlImage === image_name
);
const imageUsage: ImageUsage = {

View File

@ -5,7 +5,6 @@ import { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersist
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
import { postprocessingPersistDenylist } from 'features/parameters/store/postprocessingPersistDenylist';
import { modelsPersistDenylist } from 'features/system/store/modelsPersistDenylist';
import { systemPersistDenylist } from 'features/system/store/systemPersistDenylist';
import { uiPersistDenylist } from 'features/ui/store/uiPersistDenylist';
import { omit } from 'lodash-es';
@ -18,7 +17,6 @@ const serializationDenylist: {
gallery: galleryPersistDenylist,
generation: generationPersistDenylist,
lightbox: lightboxPersistDenylist,
models: modelsPersistDenylist,
nodes: nodesPersistDenylist,
postprocessing: postprocessingPersistDenylist,
system: systemPersistDenylist,

View File

@ -7,7 +7,6 @@ import { initialNodesState } from 'features/nodes/store/nodesSlice';
import { initialGenerationState } from 'features/parameters/store/generationSlice';
import { initialPostprocessingState } from 'features/parameters/store/postprocessingSlice';
import { initialConfigState } from 'features/system/store/configSlice';
import { initialModelsState } from 'features/system/store/modelSlice';
import { initialSystemState } from 'features/system/store/systemSlice';
import { initialHotkeysState } from 'features/ui/store/hotkeysSlice';
import { initialUIState } from 'features/ui/store/uiSlice';
@ -21,7 +20,6 @@ const initialStates: {
gallery: initialGalleryState,
generation: initialGenerationState,
lightbox: initialLightboxState,
models: initialModelsState,
nodes: initialNodesState,
postprocessing: initialPostprocessingState,
system: initialSystemState,

View File

@ -73,6 +73,15 @@ import { addImageCategoriesChangedListener } from './listeners/imageCategoriesCh
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
import { addUpdateImageUrlsOnConnectListener } from './listeners/updateImageUrlsOnConnect';
import {
addImageAddedToBoardFulfilledListener,
addImageAddedToBoardRejectedListener,
} from './listeners/imageAddedToBoard';
import { addBoardIdSelectedListener } from './listeners/boardIdSelected';
import {
addImageRemovedFromBoardFulfilledListener,
addImageRemovedFromBoardRejectedListener,
} from './listeners/imageRemovedFromBoard';
export const listenerMiddleware = createListenerMiddleware();
@ -92,6 +101,12 @@ export type AppListenerEffect = ListenerEffect<
AppDispatch
>;
/**
* The RTK listener middleware is a lightweight alternative sagas/observables.
*
* Most side effect logic should live in a listener.
*/
// Image uploaded
addImageUploadedFulfilledListener();
addImageUploadedRejectedListener();
@ -183,3 +198,10 @@ addControlNetAutoProcessListener();
// Update image URLs on connect
addUpdateImageUrlsOnConnectListener();
// Boards
addImageAddedToBoardFulfilledListener();
addImageAddedToBoardRejectedListener();
addImageRemovedFromBoardFulfilledListener();
addImageRemovedFromBoardRejectedListener();
addBoardIdSelectedListener();

View File

@ -0,0 +1,99 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { boardIdSelected } from 'features/gallery/store/boardSlice';
import { selectImagesAll } from 'features/gallery/store/imagesSlice';
import { IMAGES_PER_PAGE, receivedPageOfImages } from 'services/thunks/image';
import { api } from 'services/apiSlice';
import { imageSelected } from 'features/gallery/store/gallerySlice';
const moduleLog = log.child({ namespace: 'boards' });
export const addBoardIdSelectedListener = () => {
startAppListening({
actionCreator: boardIdSelected,
effect: (action, { getState, dispatch }) => {
const boardId = action.payload;
// we need to check if we need to fetch more images
const state = getState();
const allImages = selectImagesAll(state);
if (!boardId) {
// a board was unselected
dispatch(imageSelected(allImages[0]?.image_name));
return;
}
const { categories } = state.images;
const filteredImages = allImages.filter((i) => {
const isInCategory = categories.includes(i.image_category);
const isInSelectedBoard = boardId ? i.board_id === boardId : true;
return isInCategory && isInSelectedBoard;
});
// get the board from the cache
const { data: boards } = api.endpoints.listAllBoards.select()(state);
const board = boards?.find((b) => b.board_id === boardId);
if (!board) {
// can't find the board in cache...
dispatch(imageSelected(allImages[0]?.image_name));
return;
}
dispatch(imageSelected(board.cover_image_name));
// if we haven't loaded one full page of images from this board, load more
if (
filteredImages.length < board.image_count &&
filteredImages.length < IMAGES_PER_PAGE
) {
dispatch(receivedPageOfImages({ categories, boardId }));
}
},
});
};
export const addBoardIdSelected_changeSelectedImage_listener = () => {
startAppListening({
actionCreator: boardIdSelected,
effect: (action, { getState, dispatch }) => {
const boardId = action.payload;
const state = getState();
// we need to check if we need to fetch more images
if (!boardId) {
// a board was unselected - we don't need to do anything
return;
}
const { categories } = state.images;
const filteredImages = selectImagesAll(state).filter((i) => {
const isInCategory = categories.includes(i.image_category);
const isInSelectedBoard = boardId ? i.board_id === boardId : true;
return isInCategory && isInSelectedBoard;
});
// get the board from the cache
const { data: boards } = api.endpoints.listAllBoards.select()(state);
const board = boards?.find((b) => b.board_id === boardId);
if (!board) {
// can't find the board in cache...
return;
}
// if we haven't loaded one full page of images from this board, load more
if (
filteredImages.length < board.image_count &&
filteredImages.length < IMAGES_PER_PAGE
) {
dispatch(receivedPageOfImages({ categories, boardId }));
}
},
});
};

View File

@ -34,7 +34,7 @@ export const addControlNetImageProcessedListener = () => {
[controlNet.processorNode.id]: {
...controlNet.processorNode,
is_intermediate: true,
image: pick(controlNet.controlImage, ['image_name']),
image: { image_name: controlNet.controlImage },
},
},
};
@ -81,7 +81,7 @@ export const addControlNetImageProcessedListener = () => {
dispatch(
controlNetProcessedImageChanged({
controlNetId,
processedControlImage,
processedControlImage: processedControlImage.image_name,
})
);
}

View File

@ -0,0 +1,40 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageMetadataReceived } from 'services/thunks/image';
import { api } from 'services/apiSlice';
const moduleLog = log.child({ namespace: 'boards' });
export const addImageAddedToBoardFulfilledListener = () => {
startAppListening({
matcher: api.endpoints.addImageToBoard.matchFulfilled,
effect: (action, { getState, dispatch }) => {
const { board_id, image_name } = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { board_id, image_name } },
'Image added to board'
);
dispatch(
imageMetadataReceived({
imageName: image_name,
})
);
},
});
};
export const addImageAddedToBoardRejectedListener = () => {
startAppListening({
matcher: api.endpoints.addImageToBoard.matchRejected,
effect: (action, { getState, dispatch }) => {
const { board_id, image_name } = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { board_id, image_name } },
'Problem adding image to board'
);
},
});
};

View File

@ -12,12 +12,16 @@ export const addImageCategoriesChangedListener = () => {
startAppListening({
actionCreator: imageCategoriesChanged,
effect: (action, { getState, dispatch }) => {
const filteredImagesCount = selectFilteredImagesAsArray(
getState()
).length;
const state = getState();
const filteredImagesCount = selectFilteredImagesAsArray(state).length;
if (!filteredImagesCount) {
dispatch(receivedPageOfImages());
dispatch(
receivedPageOfImages({
categories: action.payload,
boardId: state.boards.selectedBoardId,
})
);
}
},
});

View File

@ -6,15 +6,15 @@ import { clamp } from 'lodash-es';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import {
imageRemoved,
selectImagesEntities,
selectImagesIds,
} from 'features/gallery/store/imagesSlice';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { api } from 'services/apiSlice';
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
const moduleLog = log.child({ namespace: 'image' });
/**
* Called when the user requests an image deletion
@ -22,7 +22,7 @@ const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
export const addRequestedImageDeletionListener = () => {
startAppListening({
actionCreator: requestedImageDeletion,
effect: (action, { dispatch, getState }) => {
effect: async (action, { dispatch, getState, condition }) => {
const { image, imageUsage } = action.payload;
const { image_name } = image;
@ -30,9 +30,8 @@ export const addRequestedImageDeletionListener = () => {
const state = getState();
const selectedImage = state.gallery.selectedImage;
if (selectedImage && selectedImage.image_name === image_name) {
if (selectedImage === image_name) {
const ids = selectImagesIds(state);
const entities = selectImagesEntities(state);
const deletedImageIndex = ids.findIndex(
(result) => result.toString() === image_name
@ -48,10 +47,8 @@ export const addRequestedImageDeletionListener = () => {
const newSelectedImageId = filteredIds[newSelectedImageIndex];
const newSelectedImage = entities[newSelectedImageId];
if (newSelectedImageId) {
dispatch(imageSelected(newSelectedImage));
dispatch(imageSelected(newSelectedImageId as string));
} else {
dispatch(imageSelected());
}
@ -79,7 +76,21 @@ export const addRequestedImageDeletionListener = () => {
dispatch(imageRemoved(image_name));
// Delete from server
dispatch(imageDeleted({ imageName: image_name }));
const { requestId } = dispatch(imageDeleted({ imageName: image_name }));
// Wait for successful deletion, then trigger boards to re-fetch
const wasImageDeleted = await condition(
(action): action is ReturnType<typeof imageDeleted.fulfilled> =>
imageDeleted.fulfilled.match(action) &&
action.meta.requestId === requestId,
30000
);
if (wasImageDeleted) {
dispatch(
api.util.invalidateTags([{ type: 'Board', id: image.board_id }])
);
}
},
});
};

View File

@ -0,0 +1,40 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageMetadataReceived } from 'services/thunks/image';
import { api } from 'services/apiSlice';
const moduleLog = log.child({ namespace: 'boards' });
export const addImageRemovedFromBoardFulfilledListener = () => {
startAppListening({
matcher: api.endpoints.removeImageFromBoard.matchFulfilled,
effect: (action, { getState, dispatch }) => {
const { board_id, image_name } = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { board_id, image_name } },
'Image added to board'
);
dispatch(
imageMetadataReceived({
imageName: image_name,
})
);
},
});
};
export const addImageRemovedFromBoardRejectedListener = () => {
startAppListening({
matcher: api.endpoints.removeImageFromBoard.matchRejected,
effect: (action, { getState, dispatch }) => {
const { board_id, image_name } = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { board_id, image_name } },
'Problem adding image to board'
);
},
});
};

View File

@ -46,7 +46,12 @@ export const addImageUploadedFulfilledListener = () => {
if (postUploadAction?.type === 'SET_CONTROLNET_IMAGE') {
const { controlNetId } = postUploadAction;
dispatch(controlNetImageChanged({ controlNetId, controlImage: image }));
dispatch(
controlNetImageChanged({
controlNetId,
controlImage: image.image_name,
})
);
return;
}

View File

@ -1,9 +1,8 @@
import { startAppListening } from '../..';
import { log } from 'app/logging/useLogger';
import { appSocketConnected, socketConnected } from 'services/events/actions';
import { receivedPageOfImages } from 'services/thunks/image';
import { receivedModels } from 'services/thunks/model';
import { receivedOpenAPISchema } from 'services/thunks/schema';
import { startAppListening } from '../..';
const moduleLog = log.child({ namespace: 'socketio' });
@ -15,16 +14,17 @@ export const addSocketConnectedEventListener = () => {
moduleLog.debug({ timestamp }, 'Connected');
const { models, nodes, config, images } = getState();
const { nodes, config, images } = getState();
const { disabledTabs } = config;
if (!images.ids.length) {
dispatch(receivedPageOfImages());
}
if (!models.ids.length) {
dispatch(receivedModels());
dispatch(
receivedPageOfImages({
categories: ['general'],
isIntermediate: false,
})
);
}
if (!nodes.schema && !disabledTabs.includes('nodes')) {

View File

@ -9,6 +9,7 @@ import { imageMetadataReceived } from 'services/thunks/image';
import { sessionCanceled } from 'services/thunks/session';
import { isImageOutput } from 'services/types/guards';
import { progressImageSet } from 'features/system/store/systemSlice';
import { api } from 'services/apiSlice';
const moduleLog = log.child({ namespace: 'socketio' });
const nodeDenylist = ['dataURL_image'];
@ -24,7 +25,8 @@ export const addInvocationCompleteEventListener = () => {
const sessionId = action.payload.data.graph_execution_state_id;
const { cancelType, isCancelScheduled } = getState().system;
const { cancelType, isCancelScheduled, boardIdToAddTo } =
getState().system;
// Handle scheduled cancelation
if (cancelType === 'scheduled' && isCancelScheduled) {
@ -57,6 +59,15 @@ export const addInvocationCompleteEventListener = () => {
dispatch(addImageToStagingArea(imageDTO));
}
if (boardIdToAddTo && !imageDTO.is_intermediate) {
dispatch(
api.endpoints.addImageToBoard.initiate({
board_id: boardIdToAddTo,
image_name,
})
);
}
dispatch(progressImageSet(null));
}
// pass along the socket event as an application action

View File

@ -22,15 +22,15 @@ const selectAllUsedImages = createSelector(
selectImagesEntities,
],
(generation, canvas, nodes, controlNet, imageEntities) => {
const allUsedImages: ImageDTO[] = [];
const allUsedImages: string[] = [];
if (generation.initialImage) {
allUsedImages.push(generation.initialImage);
allUsedImages.push(generation.initialImage.imageName);
}
canvas.layerState.objects.forEach((obj) => {
if (obj.kind === 'image') {
allUsedImages.push(obj.image);
allUsedImages.push(obj.imageName);
}
});
@ -53,7 +53,7 @@ const selectAllUsedImages = createSelector(
forEach(imageEntities, (image) => {
if (image) {
allUsedImages.push(image);
allUsedImages.push(image.image_name);
}
});
@ -80,7 +80,7 @@ export const addUpdateImageUrlsOnConnectListener = () => {
`Fetching new image URLs for ${allUsedImages.length} images`
);
allUsedImages.forEach(({ image_name }) => {
allUsedImages.forEach((image_name) => {
dispatch(
imageUrlsReceived({
imageName: image_name,

View File

@ -5,40 +5,39 @@ import {
configureStore,
} from '@reduxjs/toolkit';
import { rememberReducer, rememberEnhancer } from 'redux-remember';
import dynamicMiddlewares from 'redux-dynamic-middlewares';
import { rememberEnhancer, rememberReducer } from 'redux-remember';
import canvasReducer from 'features/canvas/store/canvasSlice';
import controlNetReducer from 'features/controlNet/store/controlNetSlice';
import galleryReducer from 'features/gallery/store/gallerySlice';
import imagesReducer from 'features/gallery/store/imagesSlice';
import lightboxReducer from 'features/lightbox/store/lightboxSlice';
import generationReducer from 'features/parameters/store/generationSlice';
import controlNetReducer from 'features/controlNet/store/controlNetSlice';
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
import systemReducer from 'features/system/store/systemSlice';
// import sessionReducer from 'features/system/store/sessionSlice';
import configReducer from 'features/system/store/configSlice';
import uiReducer from 'features/ui/store/uiSlice';
import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import modelsReducer from 'features/system/store/modelSlice';
import nodesReducer from 'features/nodes/store/nodesSlice';
import boardsReducer from 'features/gallery/store/boardSlice';
import configReducer from 'features/system/store/configSlice';
import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import uiReducer from 'features/ui/store/uiSlice';
import { listenerMiddleware } from './middleware/listenerMiddleware';
import { actionSanitizer } from './middleware/devtools/actionSanitizer';
import { stateSanitizer } from './middleware/devtools/stateSanitizer';
import { actionsDenylist } from './middleware/devtools/actionsDenylist';
import { stateSanitizer } from './middleware/devtools/stateSanitizer';
import { LOCALSTORAGE_PREFIX } from './constants';
import { serialize } from './enhancers/reduxRemember/serialize';
import { unserialize } from './enhancers/reduxRemember/unserialize';
import { LOCALSTORAGE_PREFIX } from './constants';
import { api } from 'services/apiSlice';
const allReducers = {
canvas: canvasReducer,
gallery: galleryReducer,
generation: generationReducer,
lightbox: lightboxReducer,
models: modelsReducer,
nodes: nodesReducer,
postprocessing: postprocessingReducer,
system: systemReducer,
@ -47,7 +46,9 @@ const allReducers = {
hotkeys: hotkeysReducer,
images: imagesReducer,
controlNet: controlNetReducer,
boards: boardsReducer,
// session: sessionReducer,
[api.reducerPath]: api.reducer,
};
const rootReducer = combineReducers(allReducers);
@ -59,12 +60,12 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
'gallery',
'generation',
'lightbox',
// 'models',
'nodes',
'postprocessing',
'system',
'ui',
'controlNet',
// 'boards',
// 'hotkeys',
// 'config',
];
@ -84,6 +85,7 @@ export const store = configureStore({
immutableCheck: false,
serializableCheck: false,
})
.concat(api.middleware)
.concat(dynamicMiddlewares)
.prepend(listenerMiddleware.middleware),
devTools: {

View File

@ -9,7 +9,7 @@ import {
import { useDraggable, useDroppable } from '@dnd-kit/core';
import { useCombinedRefs } from '@dnd-kit/utilities';
import IAIIconButton from 'common/components/IAIIconButton';
import { IAIImageFallback } from 'common/components/IAIImageFallback';
import { IAIImageLoadingFallback } from 'common/components/IAIImageFallback';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { AnimatePresence } from 'framer-motion';
import { ReactElement, SyntheticEvent, useCallback } from 'react';
@ -53,7 +53,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
isDropDisabled = false,
isDragDisabled = false,
isUploadDisabled = false,
fallback = <IAIImageFallback />,
fallback = <IAIImageLoadingFallback />,
payloadImage,
minSize = 24,
postUploadAction,

View File

@ -1,10 +1,20 @@
import { Flex, FlexProps, Spinner, SpinnerProps } from '@chakra-ui/react';
import {
As,
Flex,
FlexProps,
Icon,
IconProps,
Spinner,
SpinnerProps,
} from '@chakra-ui/react';
import { ReactElement } from 'react';
import { FaImage } from 'react-icons/fa';
type Props = FlexProps & {
spinnerProps?: SpinnerProps;
};
export const IAIImageFallback = (props: Props) => {
export const IAIImageLoadingFallback = (props: Props) => {
const { spinnerProps, ...rest } = props;
const { sx, ...restFlexProps } = rest;
return (
@ -25,3 +35,35 @@ export const IAIImageFallback = (props: Props) => {
</Flex>
);
};
type IAINoImageFallbackProps = {
flexProps?: FlexProps;
iconProps?: IconProps;
as?: As;
};
export const IAINoImageFallback = (props: IAINoImageFallbackProps) => {
const { sx: flexSx, ...restFlexProps } = props.flexProps ?? { sx: {} };
const { sx: iconSx, ...restIconProps } = props.iconProps ?? { sx: {} };
return (
<Flex
sx={{
bg: 'base.900',
opacity: 0.7,
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
...flexSx,
}}
{...restFlexProps}
>
<Icon
as={props.as ?? FaImage}
sx={{ color: 'base.700', ...iconSx }}
{...restIconProps}
/>
</Flex>
);
};

View File

@ -1,14 +1,21 @@
import { Image } from 'react-konva';
import { skipToken } from '@reduxjs/toolkit/dist/query';
import { Image, Rect } from 'react-konva';
import { useGetImageDTOQuery } from 'services/apiSlice';
import useImage from 'use-image';
import { CanvasImage } from '../store/canvasTypes';
type IAICanvasImageProps = {
url: string;
x: number;
y: number;
canvasImage: CanvasImage;
};
const IAICanvasImage = (props: IAICanvasImageProps) => {
const { url, x, y } = props;
const [image] = useImage(url, 'anonymous');
const { width, height, x, y, imageName } = props.canvasImage;
const { data: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken);
const [image] = useImage(imageDTO?.image_url ?? '', 'anonymous');
if (!imageDTO) {
return <Rect x={x} y={y} width={width} height={height} fill="red" />;
}
return <Image x={x} y={y} image={image} listening={false} />;
};

View File

@ -39,14 +39,7 @@ const IAICanvasObjectRenderer = () => {
<Group name="outpainting-objects" listening={false}>
{objects.map((obj, i) => {
if (isCanvasBaseImage(obj)) {
return (
<IAICanvasImage
key={i}
x={obj.x}
y={obj.y}
url={obj.image.image_url}
/>
);
return <IAICanvasImage key={i} canvasImage={obj} />;
} else if (isCanvasBaseLine(obj)) {
const line = (
<Line

View File

@ -59,11 +59,7 @@ const IAICanvasStagingArea = (props: Props) => {
return (
<Group {...rest}>
{shouldShowStagingImage && currentStagingAreaImage && (
<IAICanvasImage
url={currentStagingAreaImage.image.image_url}
x={x}
y={y}
/>
<IAICanvasImage canvasImage={currentStagingAreaImage} />
)}
{shouldShowStagingOutline && (
<Group>

View File

@ -203,7 +203,7 @@ export const canvasSlice = createSlice({
y: 0,
width: width,
height: height,
image: image,
imageName: image.image_name,
},
],
};
@ -325,7 +325,7 @@ export const canvasSlice = createSlice({
kind: 'image',
layer: 'base',
...state.layerState.stagingArea.boundingBox,
image,
imageName: image.image_name,
});
state.layerState.stagingArea.selectedImageIndex =
@ -865,25 +865,25 @@ export const canvasSlice = createSlice({
state.doesCanvasNeedScaling = true;
});
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
const { image_name, image_url, thumbnail_url } = action.payload;
// builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
// const { image_name, image_url, thumbnail_url } = action.payload;
state.layerState.objects.forEach((object) => {
if (object.kind === 'image') {
if (object.image.image_name === image_name) {
object.image.image_url = image_url;
object.image.thumbnail_url = thumbnail_url;
}
}
});
// state.layerState.objects.forEach((object) => {
// if (object.kind === 'image') {
// if (object.image.image_name === image_name) {
// object.image.image_url = image_url;
// object.image.thumbnail_url = thumbnail_url;
// }
// }
// });
state.layerState.stagingArea.images.forEach((stagedImage) => {
if (stagedImage.image.image_name === image_name) {
stagedImage.image.image_url = image_url;
stagedImage.image.thumbnail_url = thumbnail_url;
}
});
});
// state.layerState.stagingArea.images.forEach((stagedImage) => {
// if (stagedImage.image.image_name === image_name) {
// stagedImage.image.image_url = image_url;
// stagedImage.image.thumbnail_url = thumbnail_url;
// }
// });
// });
},
});

View File

@ -38,7 +38,7 @@ export type CanvasImage = {
y: number;
width: number;
height: number;
image: ImageDTO;
imageName: string;
};
export type CanvasMaskLine = {

View File

@ -11,9 +11,11 @@ import IAIDndImage from 'common/components/IAIDndImage';
import { createSelector } from '@reduxjs/toolkit';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { AnimatePresence, motion } from 'framer-motion';
import { IAIImageFallback } from 'common/components/IAIImageFallback';
import { IAIImageLoadingFallback } from 'common/components/IAIImageFallback';
import IAIIconButton from 'common/components/IAIIconButton';
import { FaUndo } from 'react-icons/fa';
import { useGetImageDTOQuery } from 'services/apiSlice';
import { skipToken } from '@reduxjs/toolkit/dist/query';
const selector = createSelector(
controlNetSelector,
@ -31,24 +33,45 @@ type Props = {
const ControlNetImagePreview = (props: Props) => {
const { imageSx } = props;
const { controlNetId, controlImage, processedControlImage, processorType } =
props.controlNet;
const {
controlNetId,
controlImage: controlImageName,
processedControlImage: processedControlImageName,
processorType,
} = props.controlNet;
const dispatch = useAppDispatch();
const { pendingControlImages } = useAppSelector(selector);
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
const {
data: controlImage,
isLoading: isLoadingControlImage,
isError: isErrorControlImage,
isSuccess: isSuccessControlImage,
} = useGetImageDTOQuery(controlImageName ?? skipToken);
const {
data: processedControlImage,
isLoading: isLoadingProcessedControlImage,
isError: isErrorProcessedControlImage,
isSuccess: isSuccessProcessedControlImage,
} = useGetImageDTOQuery(processedControlImageName ?? skipToken);
const handleDrop = useCallback(
(droppedImage: ImageDTO) => {
if (controlImage?.image_name === droppedImage.image_name) {
if (controlImageName === droppedImage.image_name) {
return;
}
setIsMouseOverImage(false);
dispatch(
controlNetImageChanged({ controlNetId, controlImage: droppedImage })
controlNetImageChanged({
controlNetId,
controlImage: droppedImage.image_name,
})
);
},
[controlImage, controlNetId, dispatch]
[controlImageName, controlNetId, dispatch]
);
const handleResetControlImage = useCallback(() => {
@ -150,7 +173,7 @@ const ControlNetImagePreview = (props: Props) => {
h: 'full',
}}
>
<IAIImageFallback />
<IAIImageLoadingFallback />
</Box>
)}
{controlImage && (

View File

@ -39,8 +39,8 @@ export type ControlNetConfig = {
weight: number;
beginStepPct: number;
endStepPct: number;
controlImage: ImageDTO | null;
processedControlImage: ImageDTO | null;
controlImage: string | null;
processedControlImage: string | null;
processorType: ControlNetProcessorType;
processorNode: RequiredControlNetProcessorNode;
shouldAutoConfig: boolean;
@ -80,7 +80,7 @@ export const controlNetSlice = createSlice({
},
controlNetAddedFromImage: (
state,
action: PayloadAction<{ controlNetId: string; controlImage: ImageDTO }>
action: PayloadAction<{ controlNetId: string; controlImage: string }>
) => {
const { controlNetId, controlImage } = action.payload;
state.controlNets[controlNetId] = {
@ -108,7 +108,7 @@ export const controlNetSlice = createSlice({
state,
action: PayloadAction<{
controlNetId: string;
controlImage: ImageDTO | null;
controlImage: string | null;
}>
) => {
const { controlNetId, controlImage } = action.payload;
@ -125,7 +125,7 @@ export const controlNetSlice = createSlice({
state,
action: PayloadAction<{
controlNetId: string;
processedControlImage: ImageDTO | null;
processedControlImage: string | null;
}>
) => {
const { controlNetId, processedControlImage } = action.payload;
@ -260,30 +260,30 @@ export const controlNetSlice = createSlice({
// Preemptively remove the image from the gallery
const { imageName } = action.meta.arg;
forEach(state.controlNets, (c) => {
if (c.controlImage?.image_name === imageName) {
if (c.controlImage === imageName) {
c.controlImage = null;
c.processedControlImage = null;
}
if (c.processedControlImage?.image_name === imageName) {
if (c.processedControlImage === imageName) {
c.processedControlImage = null;
}
});
});
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
const { image_name, image_url, thumbnail_url } = action.payload;
// builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
// const { image_name, image_url, thumbnail_url } = action.payload;
forEach(state.controlNets, (c) => {
if (c.controlImage?.image_name === image_name) {
c.controlImage.image_url = image_url;
c.controlImage.thumbnail_url = thumbnail_url;
}
if (c.processedControlImage?.image_name === image_name) {
c.processedControlImage.image_url = image_url;
c.processedControlImage.thumbnail_url = thumbnail_url;
}
});
});
// forEach(state.controlNets, (c) => {
// if (c.controlImage?.image_name === image_name) {
// c.controlImage.image_url = image_url;
// c.controlImage.thumbnail_url = thumbnail_url;
// }
// if (c.processedControlImage?.image_name === image_name) {
// c.processedControlImage.image_url = image_url;
// c.processedControlImage.thumbnail_url = thumbnail_url;
// }
// });
// });
builder.addCase(appSocketInvocationError, (state, action) => {
state.pendingControlImages = [];

View File

@ -0,0 +1,27 @@
import IAIButton from 'common/components/IAIButton';
import { useCallback } from 'react';
import { useCreateBoardMutation } from 'services/apiSlice';
const DEFAULT_BOARD_NAME = 'My Board';
const AddBoardButton = () => {
const [createBoard, { isLoading }] = useCreateBoardMutation();
const handleCreateBoard = useCallback(() => {
createBoard(DEFAULT_BOARD_NAME);
}, [createBoard]);
return (
<IAIButton
isLoading={isLoading}
aria-label="Add Board"
onClick={handleCreateBoard}
size="sm"
sx={{ px: 4 }}
>
Add Board
</IAIButton>
);
};
export default AddBoardButton;

View File

@ -0,0 +1,93 @@
import { Flex, Text } from '@chakra-ui/react';
import { FaImages } from 'react-icons/fa';
import { boardIdSelected } from '../../store/boardSlice';
import { useDispatch } from 'react-redux';
import { IAINoImageFallback } from 'common/components/IAIImageFallback';
import { AnimatePresence } from 'framer-motion';
import { SelectedItemOverlay } from '../SelectedItemOverlay';
import { useCallback } from 'react';
import { ImageDTO } from 'services/api';
import { useRemoveImageFromBoardMutation } from 'services/apiSlice';
import { useDroppable } from '@dnd-kit/core';
import IAIDropOverlay from 'common/components/IAIDropOverlay';
const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
const dispatch = useDispatch();
const handleAllImagesBoardClick = () => {
dispatch(boardIdSelected());
};
const [removeImageFromBoard, { isLoading }] =
useRemoveImageFromBoardMutation();
const handleDrop = useCallback(
(droppedImage: ImageDTO) => {
if (!droppedImage.board_id) {
return;
}
removeImageFromBoard({
board_id: droppedImage.board_id,
image_name: droppedImage.image_name,
});
},
[removeImageFromBoard]
);
const {
isOver,
setNodeRef,
active: isDropActive,
} = useDroppable({
id: `board_droppable_all_images`,
data: {
handleDrop,
},
});
return (
<Flex
sx={{
flexDir: 'column',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
w: 'full',
h: 'full',
borderRadius: 'base',
}}
onClick={handleAllImagesBoardClick}
>
<Flex
ref={setNodeRef}
sx={{
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 'base',
w: 'full',
aspectRatio: '1/1',
}}
>
<IAINoImageFallback iconProps={{ boxSize: 8 }} as={FaImages} />
<AnimatePresence>
{isSelected && <SelectedItemOverlay />}
</AnimatePresence>
<AnimatePresence>
{isDropActive && <IAIDropOverlay isOver={isOver} />}
</AnimatePresence>
</Flex>
<Text
sx={{
color: isSelected ? 'base.50' : 'base.200',
fontWeight: isSelected ? 600 : undefined,
fontSize: 'xs',
}}
>
All Images
</Text>
</Flex>
);
};
export default AllImagesBoard;

View File

@ -0,0 +1,134 @@
import {
Collapse,
Flex,
Grid,
IconButton,
Input,
InputGroup,
InputRightElement,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import {
boardsSelector,
setBoardSearchText,
} from 'features/gallery/store/boardSlice';
import { memo, useState } from 'react';
import HoverableBoard from './HoverableBoard';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import AddBoardButton from './AddBoardButton';
import AllImagesBoard from './AllImagesBoard';
import { CloseIcon } from '@chakra-ui/icons';
import { useListAllBoardsQuery } from 'services/apiSlice';
const selector = createSelector(
[boardsSelector],
(boardsState) => {
const { selectedBoardId, searchText } = boardsState;
return { selectedBoardId, searchText };
},
defaultSelectorOptions
);
type Props = {
isOpen: boolean;
};
const BoardsList = (props: Props) => {
const { isOpen } = props;
const dispatch = useAppDispatch();
const { selectedBoardId, searchText } = useAppSelector(selector);
const { data: boards } = useListAllBoardsQuery();
const filteredBoards = searchText
? boards?.filter((board) =>
board.board_name.toLowerCase().includes(searchText.toLowerCase())
)
: boards;
const [searchMode, setSearchMode] = useState(false);
const handleBoardSearch = (searchTerm: string) => {
setSearchMode(searchTerm.length > 0);
dispatch(setBoardSearchText(searchTerm));
};
const clearBoardSearch = () => {
setSearchMode(false);
dispatch(setBoardSearchText(''));
};
return (
<Collapse in={isOpen} animateOpacity>
<Flex
sx={{
flexDir: 'column',
gap: 2,
bg: 'base.800',
borderRadius: 'base',
p: 2,
mt: 2,
}}
>
<Flex sx={{ gap: 2, alignItems: 'center' }}>
<InputGroup>
<Input
placeholder="Search Boards..."
value={searchText}
onChange={(e) => {
handleBoardSearch(e.target.value);
}}
/>
{searchText && searchText.length && (
<InputRightElement>
<IconButton
onClick={clearBoardSearch}
size="xs"
variant="ghost"
aria-label="Clear Search"
icon={<CloseIcon boxSize={3} />}
/>
</InputRightElement>
)}
</InputGroup>
<AddBoardButton />
</Flex>
<OverlayScrollbarsComponent
defer
style={{ height: '100%', width: '100%' }}
options={{
scrollbars: {
visibility: 'auto',
autoHide: 'move',
autoHideDelay: 1300,
theme: 'os-theme-dark',
},
}}
>
<Grid
className="list-container"
sx={{
gap: 2,
gridTemplateRows: '5.5rem 5.5rem',
gridAutoFlow: 'column dense',
gridAutoColumns: '4rem',
}}
>
{!searchMode && <AllImagesBoard isSelected={!selectedBoardId} />}
{filteredBoards &&
filteredBoards.map((board) => (
<HoverableBoard
key={board.board_id}
board={board}
isSelected={selectedBoardId === board.board_id}
/>
))}
</Grid>
</OverlayScrollbarsComponent>
</Flex>
</Collapse>
);
};
export default memo(BoardsList);

View File

@ -0,0 +1,193 @@
import {
Badge,
Box,
Editable,
EditableInput,
EditablePreview,
Flex,
Image,
MenuItem,
MenuList,
} from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { memo, useCallback } from 'react';
import { FaFolder, FaTrash } from 'react-icons/fa';
import { ContextMenu } from 'chakra-ui-contextmenu';
import { BoardDTO, ImageDTO } from 'services/api';
import { IAINoImageFallback } from 'common/components/IAIImageFallback';
import { boardIdSelected } from 'features/gallery/store/boardSlice';
import {
useAddImageToBoardMutation,
useDeleteBoardMutation,
useGetImageDTOQuery,
useUpdateBoardMutation,
} from 'services/apiSlice';
import { skipToken } from '@reduxjs/toolkit/dist/query';
import { useDroppable } from '@dnd-kit/core';
import { AnimatePresence } from 'framer-motion';
import IAIDropOverlay from 'common/components/IAIDropOverlay';
import { SelectedItemOverlay } from '../SelectedItemOverlay';
interface HoverableBoardProps {
board: BoardDTO;
isSelected: boolean;
}
const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
const dispatch = useAppDispatch();
const { data: coverImage } = useGetImageDTOQuery(
board.cover_image_name ?? skipToken
);
const { board_name, board_id } = board;
const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected(board_id));
}, [board_id, dispatch]);
const [updateBoard, { isLoading: isUpdateBoardLoading }] =
useUpdateBoardMutation();
const [deleteBoard, { isLoading: isDeleteBoardLoading }] =
useDeleteBoardMutation();
const [addImageToBoard, { isLoading: isAddImageToBoardLoading }] =
useAddImageToBoardMutation();
const handleUpdateBoardName = (newBoardName: string) => {
updateBoard({ board_id, changes: { board_name: newBoardName } });
};
const handleDeleteBoard = useCallback(() => {
deleteBoard(board_id);
}, [board_id, deleteBoard]);
const handleDrop = useCallback(
(droppedImage: ImageDTO) => {
if (droppedImage.board_id === board_id) {
return;
}
addImageToBoard({ board_id, image_name: droppedImage.image_name });
},
[addImageToBoard, board_id]
);
const {
isOver,
setNodeRef,
active: isDropActive,
} = useDroppable({
id: `board_droppable_${board_id}`,
data: {
handleDrop,
},
});
return (
<Box sx={{ touchAction: 'none' }}>
<ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => (
<MenuList sx={{ visibility: 'visible !important' }}>
<MenuItem
sx={{ color: 'error.300' }}
icon={<FaTrash />}
onClickCapture={handleDeleteBoard}
>
Delete Board
</MenuItem>
</MenuList>
)}
>
{(ref) => (
<Flex
key={board_id}
userSelect="none"
ref={ref}
sx={{
flexDir: 'column',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
w: 'full',
h: 'full',
}}
>
<Flex
ref={setNodeRef}
onClick={handleSelectBoard}
sx={{
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 'base',
w: 'full',
aspectRatio: '1/1',
overflow: 'hidden',
}}
>
{board.cover_image_name && coverImage?.image_url && (
<Image src={coverImage?.image_url} draggable={false} />
)}
{!(board.cover_image_name && coverImage?.image_url) && (
<IAINoImageFallback iconProps={{ boxSize: 8 }} as={FaFolder} />
)}
<Flex
sx={{
position: 'absolute',
insetInlineEnd: 0,
top: 0,
p: 1,
}}
>
<Badge variant="solid">{board.image_count}</Badge>
</Flex>
<AnimatePresence>
{isSelected && <SelectedItemOverlay />}
</AnimatePresence>
<AnimatePresence>
{isDropActive && <IAIDropOverlay isOver={isOver} />}
</AnimatePresence>
</Flex>
<Box sx={{ width: 'full' }}>
<Editable
defaultValue={board_name}
submitOnBlur={false}
onSubmit={(nextValue) => {
handleUpdateBoardName(nextValue);
}}
>
<EditablePreview
sx={{
color: isSelected ? 'base.50' : 'base.200',
fontWeight: isSelected ? 600 : undefined,
fontSize: 'xs',
textAlign: 'center',
p: 0,
}}
noOfLines={1}
/>
<EditableInput
sx={{
color: 'base.50',
fontSize: 'xs',
borderColor: 'base.500',
p: 0,
outline: 0,
}}
/>
</Editable>
</Box>
</Flex>
)}
</ContextMenu>
</Box>
);
});
HoverableBoard.displayName = 'HoverableBoard';
export default HoverableBoard;

View File

@ -0,0 +1,93 @@
import {
AlertDialog,
AlertDialogBody,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
Box,
Flex,
Spinner,
Text,
} from '@chakra-ui/react';
import IAIButton from 'common/components/IAIButton';
import { memo, useContext, useRef, useState } from 'react';
import { AddImageToBoardContext } from '../../../../app/contexts/AddImageToBoardContext';
import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { useListAllBoardsQuery } from 'services/apiSlice';
const UpdateImageBoardModal = () => {
// const boards = useSelector(selectBoardsAll);
const { data: boards, isFetching } = useListAllBoardsQuery();
const { isOpen, onClose, handleAddToBoard, image } = useContext(
AddImageToBoardContext
);
const [selectedBoard, setSelectedBoard] = useState<string | null>();
const cancelRef = useRef<HTMLButtonElement>(null);
const currentBoard = boards?.find(
(board) => board.board_id === image?.board_id
);
return (
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
isCentered
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{currentBoard ? 'Move Image to Board' : 'Add Image to Board'}
</AlertDialogHeader>
<AlertDialogBody>
<Box>
<Flex direction="column" gap={3}>
{currentBoard && (
<Text>
Moving this image from{' '}
<strong>{currentBoard.board_name}</strong> to
</Text>
)}
{isFetching ? (
<Spinner />
) : (
<IAIMantineSelect
placeholder="Select Board"
onChange={(v) => setSelectedBoard(v)}
value={selectedBoard}
data={(boards ?? []).map((board) => ({
label: board.board_name,
value: board.board_id,
}))}
/>
)}
</Flex>
</Box>
</AlertDialogBody>
<AlertDialogFooter>
<IAIButton onClick={onClose}>Cancel</IAIButton>
<IAIButton
isDisabled={!selectedBoard}
colorScheme="accent"
onClick={() => {
if (selectedBoard) {
handleAddToBoard(selectedBoard);
}
}}
ml={3}
>
{currentBoard ? 'Move' : 'Add'}
</IAIButton>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
);
};
export default memo(UpdateImageBoardModal);

View File

@ -51,9 +51,12 @@ import { useAppToaster } from 'app/components/Toaster';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
import { DeleteImageButton } from './DeleteImageModal';
import { selectImagesById } from '../store/imagesSlice';
import { RootState } from 'app/store/store';
const currentImageButtonsSelector = createSelector(
[
(state: RootState) => state,
systemSelector,
gallerySelector,
postprocessingSelector,
@ -61,7 +64,7 @@ const currentImageButtonsSelector = createSelector(
lightboxSelector,
activeTabNameSelector,
],
(system, gallery, postprocessing, ui, lightbox, activeTabName) => {
(state, system, gallery, postprocessing, ui, lightbox, activeTabName) => {
const {
isProcessing,
isConnected,
@ -81,6 +84,8 @@ const currentImageButtonsSelector = createSelector(
shouldShowProgressInViewer,
} = ui;
const imageDTO = selectImagesById(state, gallery.selectedImage ?? '');
const { selectedImage } = gallery;
return {
@ -97,10 +102,10 @@ const currentImageButtonsSelector = createSelector(
activeTabName,
isLightboxOpen,
shouldHidePreview,
image: selectedImage,
seed: selectedImage?.metadata?.seed,
prompt: selectedImage?.metadata?.positive_conditioning,
negativePrompt: selectedImage?.metadata?.negative_conditioning,
image: imageDTO,
seed: imageDTO?.metadata?.seed,
prompt: imageDTO?.metadata?.positive_conditioning,
negativePrompt: imageDTO?.metadata?.negative_conditioning,
shouldShowProgressInViewer,
};
},

View File

@ -9,12 +9,12 @@ import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from './NextPrevImageButtons';
import { memo, useCallback } from 'react';
import { systemSelector } from 'features/system/store/systemSelectors';
import { configSelector } from '../../system/store/configSelectors';
import { useAppToaster } from 'app/components/Toaster';
import { imageSelected } from '../store/gallerySlice';
import IAIDndImage from 'common/components/IAIDndImage';
import { ImageDTO } from 'services/api';
import { IAIImageFallback } from 'common/components/IAIImageFallback';
import { IAIImageLoadingFallback } from 'common/components/IAIImageFallback';
import { useGetImageDTOQuery } from 'services/apiSlice';
import { skipToken } from '@reduxjs/toolkit/dist/query';
export const imagesSelector = createSelector(
[uiSelector, gallerySelector, systemSelector],
@ -29,7 +29,7 @@ export const imagesSelector = createSelector(
return {
shouldShowImageDetails,
shouldHidePreview,
image: selectedImage,
selectedImage,
progressImage,
shouldShowProgressInViewer,
shouldAntialiasProgressImage,
@ -45,11 +45,23 @@ export const imagesSelector = createSelector(
const CurrentImagePreview = () => {
const {
shouldShowImageDetails,
image,
selectedImage,
progressImage,
shouldShowProgressInViewer,
shouldAntialiasProgressImage,
} = useAppSelector(imagesSelector);
// const image = useAppSelector((state: RootState) =>
// selectImagesById(state, selectedImage ?? '')
// );
const {
data: image,
isLoading,
isError,
isSuccess,
} = useGetImageDTOQuery(selectedImage ?? skipToken);
const dispatch = useAppDispatch();
const handleDrop = useCallback(
@ -57,7 +69,7 @@ const CurrentImagePreview = () => {
if (droppedImage.image_name === image?.image_name) {
return;
}
dispatch(imageSelected(droppedImage));
dispatch(imageSelected(droppedImage.image_name));
},
[dispatch, image?.image_name]
);
@ -98,14 +110,14 @@ const CurrentImagePreview = () => {
}}
>
<IAIDndImage
image={image}
image={selectedImage && image ? image : undefined}
onDrop={handleDrop}
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
fallback={<IAIImageLoadingFallback sx={{ bg: 'none' }} />}
isUploadDisabled={true}
/>
</Flex>
)}
{shouldShowImageDetails && image && (
{shouldShowImageDetails && image && selectedImage && (
<Box
sx={{
position: 'absolute',
@ -119,7 +131,7 @@ const CurrentImagePreview = () => {
<ImageMetadataViewer image={image} />
</Box>
)}
{!shouldShowImageDetails && image && (
{!shouldShowImageDetails && image && selectedImage && (
<Box
sx={{
position: 'absolute',

View File

@ -2,7 +2,14 @@ import { Box, Flex, Icon, Image, MenuItem, MenuList } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useContext, useState } from 'react';
import { FaCheck, FaExpand, FaImage, FaShare, FaTrash } from 'react-icons/fa';
import {
FaCheck,
FaExpand,
FaFolder,
FaImage,
FaShare,
FaTrash,
} from 'react-icons/fa';
import { ContextMenu } from 'chakra-ui-contextmenu';
import {
resizeAndScaleCanvas,
@ -27,6 +34,8 @@ import { useAppToaster } from 'app/components/Toaster';
import { ImageDTO } from 'services/api';
import { useDraggable } from '@dnd-kit/core';
import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
import { AddImageToBoardContext } from '../../../app/contexts/AddImageToBoardContext';
import { useRemoveImageFromBoardMutation } from 'services/apiSlice';
export const selector = createSelector(
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
@ -62,17 +71,10 @@ interface HoverableImageProps {
isSelected: boolean;
}
const memoEqualityCheck = (
prev: HoverableImageProps,
next: HoverableImageProps
) =>
prev.image.image_name === next.image.image_name &&
prev.isSelected === next.isSelected;
/**
* Gallery image component with delete/use all/use seed buttons on hover.
*/
const HoverableImage = memo((props: HoverableImageProps) => {
const HoverableImage = (props: HoverableImageProps) => {
const dispatch = useAppDispatch();
const {
activeTabName,
@ -93,6 +95,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
const { onDelete } = useContext(DeleteImageContext);
const { onClickAddToBoard } = useContext(AddImageToBoardContext);
const handleDelete = useCallback(() => {
onDelete(image);
}, [image, onDelete]);
@ -106,11 +109,13 @@ const HoverableImage = memo((props: HoverableImageProps) => {
},
});
const [removeFromBoard] = useRemoveImageFromBoardMutation();
const handleMouseOver = () => setIsHovered(true);
const handleMouseOut = () => setIsHovered(false);
const handleSelectImage = useCallback(() => {
dispatch(imageSelected(image));
dispatch(imageSelected(image.image_name));
}, [image, dispatch]);
// Recall parameters handlers
@ -168,6 +173,17 @@ const HoverableImage = memo((props: HoverableImageProps) => {
// dispatch(setIsLightboxOpen(true));
};
const handleAddToBoard = useCallback(() => {
onClickAddToBoard(image);
}, [image, onClickAddToBoard]);
const handleRemoveFromBoard = useCallback(() => {
if (!image.board_id) {
return;
}
removeFromBoard({ board_id: image.board_id, image_name: image.image_name });
}, [image.board_id, image.image_name, removeFromBoard]);
const handleOpenInNewTab = () => {
window.open(image.image_url, '_blank');
};
@ -244,6 +260,17 @@ const HoverableImage = memo((props: HoverableImageProps) => {
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
)}
<MenuItem icon={<FaFolder />} onClickCapture={handleAddToBoard}>
{image.board_id ? 'Change Board' : 'Add to Board'}
</MenuItem>
{image.board_id && (
<MenuItem
icon={<FaFolder />}
onClickCapture={handleRemoveFromBoard}
>
Remove from Board
</MenuItem>
)}
<MenuItem
sx={{ color: 'error.300' }}
icon={<FaTrash />}
@ -339,8 +366,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
</ContextMenu>
</Box>
);
}, memoEqualityCheck);
};
HoverableImage.displayName = 'HoverableImage';
export default HoverableImage;
export default memo(HoverableImage);

View File

@ -1,12 +1,15 @@
import {
Box,
Button,
ButtonGroup,
Flex,
FlexProps,
Grid,
Icon,
Text,
VStack,
forwardRef,
useDisclosure,
} from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
@ -20,6 +23,7 @@ import {
setGalleryImageObjectFit,
setShouldAutoSwitchToNewImages,
setShouldUseSingleGalleryColumn,
setGalleryView,
} from 'features/gallery/store/gallerySlice';
import { togglePinGalleryPanel } from 'features/ui/store/uiSlice';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
@ -53,41 +57,51 @@ import {
selectImagesAll,
} from '../store/imagesSlice';
import { receivedPageOfImages } from 'services/thunks/image';
import BoardsList from './Boards/BoardsList';
import { boardsSelector } from '../store/boardSlice';
import { ChevronUpIcon } from '@chakra-ui/icons';
import { useListAllBoardsQuery } from 'services/apiSlice';
const categorySelector = createSelector(
const itemSelector = createSelector(
[(state: RootState) => state],
(state) => {
const { images } = state;
const { categories } = images;
const { categories, total: allImagesTotal, isLoading } = state.images;
const { selectedBoardId } = state.boards;
const allImages = selectImagesAll(state);
const filteredImages = allImages.filter((i) =>
categories.includes(i.image_category)
);
const images = allImages.filter((i) => {
const isInCategory = categories.includes(i.image_category);
const isInSelectedBoard = selectedBoardId
? i.board_id === selectedBoardId
: true;
return isInCategory && isInSelectedBoard;
});
return {
images: filteredImages,
isLoading: images.isLoading,
areMoreImagesAvailable: filteredImages.length < images.total,
categories: images.categories,
images,
allImagesTotal,
isLoading,
categories,
selectedBoardId,
};
},
defaultSelectorOptions
);
const mainSelector = createSelector(
[gallerySelector, uiSelector],
(gallery, ui) => {
[gallerySelector, uiSelector, boardsSelector],
(gallery, ui, boards) => {
const {
galleryImageMinimumWidth,
galleryImageObjectFit,
shouldAutoSwitchToNewImages,
shouldUseSingleGalleryColumn,
selectedImage,
galleryView,
} = gallery;
const { shouldPinGallery } = ui;
return {
shouldPinGallery,
galleryImageMinimumWidth,
@ -95,6 +109,8 @@ const mainSelector = createSelector(
shouldAutoSwitchToNewImages,
shouldUseSingleGalleryColumn,
selectedImage,
galleryView,
selectedBoardId: boards.selectedBoardId,
};
},
defaultSelectorOptions
@ -126,21 +142,44 @@ const ImageGalleryContent = () => {
shouldAutoSwitchToNewImages,
shouldUseSingleGalleryColumn,
selectedImage,
galleryView,
} = useAppSelector(mainSelector);
const { images, areMoreImagesAvailable, isLoading, categories } =
useAppSelector(categorySelector);
const { images, isLoading, allImagesTotal, categories, selectedBoardId } =
useAppSelector(itemSelector);
const { selectedBoard } = useListAllBoardsQuery(undefined, {
selectFromResult: ({ data }) => ({
selectedBoard: data?.find((b) => b.board_id === selectedBoardId),
}),
});
const filteredImagesTotal = useMemo(
() => selectedBoard?.image_count ?? allImagesTotal,
[allImagesTotal, selectedBoard?.image_count]
);
const areMoreAvailable = useMemo(() => {
return images.length < filteredImagesTotal;
}, [images.length, filteredImagesTotal]);
const handleLoadMoreImages = useCallback(() => {
dispatch(receivedPageOfImages());
}, [dispatch]);
dispatch(
receivedPageOfImages({
categories,
boardId: selectedBoardId,
})
);
}, [categories, dispatch, selectedBoardId]);
const handleEndReached = useMemo(() => {
if (areMoreImagesAvailable && !isLoading) {
if (areMoreAvailable && !isLoading) {
return handleLoadMoreImages;
}
return undefined;
}, [areMoreImagesAvailable, handleLoadMoreImages, isLoading]);
}, [areMoreAvailable, handleLoadMoreImages, isLoading]);
const { isOpen: isBoardListOpen, onToggle } = useDisclosure();
const handleChangeGalleryImageMinimumWidth = (v: number) => {
dispatch(setGalleryImageMinimumWidth(v));
@ -172,46 +211,79 @@ const ImageGalleryContent = () => {
const handleClickImagesCategory = useCallback(() => {
dispatch(imageCategoriesChanged(IMAGE_CATEGORIES));
dispatch(setGalleryView('images'));
}, [dispatch]);
const handleClickAssetsCategory = useCallback(() => {
dispatch(imageCategoriesChanged(ASSETS_CATEGORIES));
dispatch(setGalleryView('assets'));
}, [dispatch]);
return (
<Flex
<VStack
sx={{
gap: 2,
flexDirection: 'column',
h: 'full',
w: 'full',
borderRadius: 'base',
}}
>
<Flex
ref={resizeObserverRef}
alignItems="center"
justifyContent="space-between"
>
<ButtonGroup isAttached>
<IAIIconButton
tooltip={t('gallery.images')}
aria-label={t('gallery.images')}
onClick={handleClickImagesCategory}
isChecked={categories === IMAGE_CATEGORIES}
<Box sx={{ w: 'full' }}>
<Flex
ref={resizeObserverRef}
sx={{
alignItems: 'center',
justifyContent: 'space-between',
gap: 2,
}}
>
<ButtonGroup isAttached>
<IAIIconButton
tooltip={t('gallery.images')}
aria-label={t('gallery.images')}
onClick={handleClickImagesCategory}
isChecked={galleryView === 'images'}
size="sm"
icon={<FaImage />}
/>
<IAIIconButton
tooltip={t('gallery.assets')}
aria-label={t('gallery.assets')}
onClick={handleClickAssetsCategory}
isChecked={galleryView === 'assets'}
size="sm"
icon={<FaServer />}
/>
</ButtonGroup>
<Flex
as={Button}
onClick={onToggle}
size="sm"
icon={<FaImage />}
/>
<IAIIconButton
tooltip={t('gallery.assets')}
aria-label={t('gallery.assets')}
onClick={handleClickAssetsCategory}
isChecked={categories === ASSETS_CATEGORIES}
size="sm"
icon={<FaServer />}
/>
</ButtonGroup>
<Flex gap={2}>
variant="ghost"
sx={{
w: 'full',
justifyContent: 'center',
alignItems: 'center',
px: 2,
_hover: {
bg: 'base.800',
},
}}
>
<Text
noOfLines={1}
sx={{ w: 'full', color: 'base.200', fontWeight: 600 }}
>
{selectedBoard ? selectedBoard.board_name : 'All Images'}
</Text>
<ChevronUpIcon
sx={{
transform: isBoardListOpen ? 'rotate(0deg)' : 'rotate(180deg)',
transitionProperty: 'common',
transitionDuration: 'normal',
}}
/>
</Flex>
<IAIPopover
triggerComponent={
<IAIIconButton
@ -269,9 +341,12 @@ const ImageGalleryContent = () => {
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
/>
</Flex>
</Flex>
<Flex direction="column" gap={2} h="full">
{images.length || areMoreImagesAvailable ? (
<Box>
<BoardsList isOpen={isBoardListOpen} />
</Box>
</Box>
<Flex direction="column" gap={2} h="full" w="full">
{images.length || areMoreAvailable ? (
<>
<Box ref={rootRef} data-overlayscrollbars="" h="100%">
{shouldUseSingleGalleryColumn ? (
@ -280,14 +355,12 @@ const ImageGalleryContent = () => {
data={images}
endReached={handleEndReached}
scrollerRef={(ref) => setScrollerRef(ref)}
itemContent={(index, image) => (
itemContent={(index, item) => (
<Flex sx={{ pb: 2 }}>
<HoverableImage
key={`${image.image_name}-${image.thumbnail_url}`}
image={image}
isSelected={
selectedImage?.image_name === image?.image_name
}
key={`${item.image_name}-${item.thumbnail_url}`}
image={item}
isSelected={selectedImage === item?.image_name}
/>
</Flex>
)}
@ -302,13 +375,11 @@ const ImageGalleryContent = () => {
List: ListContainer,
}}
scrollerRef={setScroller}
itemContent={(index, image) => (
itemContent={(index, item) => (
<HoverableImage
key={`${image.image_name}-${image.thumbnail_url}`}
image={image}
isSelected={
selectedImage?.image_name === image?.image_name
}
key={`${item.image_name}-${item.thumbnail_url}`}
image={item}
isSelected={selectedImage === item?.image_name}
/>
)}
/>
@ -316,12 +387,12 @@ const ImageGalleryContent = () => {
</Box>
<IAIButton
onClick={handleLoadMoreImages}
isDisabled={!areMoreImagesAvailable}
isDisabled={!areMoreAvailable}
isLoading={isLoading}
loadingText="Loading"
flexShrink={0}
>
{areMoreImagesAvailable
{areMoreAvailable
? t('gallery.loadMore')
: t('gallery.allImagesLoaded')}
</IAIButton>
@ -350,7 +421,7 @@ const ImageGalleryContent = () => {
</Flex>
)}
</Flex>
</Flex>
</VStack>
);
};

View File

@ -93,19 +93,11 @@ type ImageMetadataViewerProps = {
image: ImageDTO;
};
// TODO: I don't know if this is needed.
const memoEqualityCheck = (
prev: ImageMetadataViewerProps,
next: ImageMetadataViewerProps
) => prev.image.image_name === next.image.image_name;
// TODO: Show more interesting information in this component.
/**
* Image metadata viewer overlays currently selected image and provides
* access to any of its metadata for use in processing.
*/
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
const dispatch = useAppDispatch();
const {
recallBothPrompts,
@ -333,8 +325,6 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
</Flex>
</Flex>
);
}, memoEqualityCheck);
};
ImageMetadataViewer.displayName = 'ImageMetadataViewer';
export default ImageMetadataViewer;
export default memo(ImageMetadataViewer);

View File

@ -42,7 +42,7 @@ export const nextPrevImageButtonsSelector = createSelector(
}
const currentImageIndex = filteredImageIds.findIndex(
(i) => i === selectedImage.image_name
(i) => i === selectedImage
);
const nextImageIndex = clamp(
@ -71,6 +71,8 @@ export const nextPrevImageButtonsSelector = createSelector(
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
nextImage,
prevImage,
nextImageId,
prevImageId,
};
},
{
@ -84,7 +86,7 @@ const NextPrevImageButtons = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { isOnFirstImage, isOnLastImage, nextImage, prevImage } =
const { isOnFirstImage, isOnLastImage, nextImageId, prevImageId } =
useAppSelector(nextPrevImageButtonsSelector);
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
@ -99,19 +101,19 @@ const NextPrevImageButtons = () => {
}, []);
const handlePrevImage = useCallback(() => {
dispatch(imageSelected(prevImage));
}, [dispatch, prevImage]);
dispatch(imageSelected(prevImageId));
}, [dispatch, prevImageId]);
const handleNextImage = useCallback(() => {
dispatch(imageSelected(nextImage));
}, [dispatch, nextImage]);
dispatch(imageSelected(nextImageId));
}, [dispatch, nextImageId]);
useHotkeys(
'left',
() => {
handlePrevImage();
},
[prevImage]
[prevImageId]
);
useHotkeys(
@ -119,7 +121,7 @@ const NextPrevImageButtons = () => {
() => {
handleNextImage();
},
[nextImage]
[nextImageId]
);
return (

View File

@ -0,0 +1,26 @@
import { motion } from 'framer-motion';
export const SelectedItemOverlay = () => (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.1 },
}}
exit={{
opacity: 0,
transition: { duration: 0.1 },
}}
style={{
position: 'absolute',
top: 0,
insetInlineStart: 0,
width: '100%',
height: '100%',
boxShadow: 'inset 0px 0px 0px 2px var(--invokeai-colors-accent-300)',
borderRadius: 'var(--invokeai-radii-base)',
}}
/>
);

View File

@ -0,0 +1,23 @@
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { selectBoardsAll } from './boardSlice';
export const boardSelector = (state: RootState) => state.boards.entities;
export const searchBoardsSelector = createSelector(
(state: RootState) => state,
(state) => {
const {
boards: { searchText },
} = state;
if (!searchText) {
// If no search text provided, return all entities
return selectBoardsAll(state);
}
return selectBoardsAll(state).filter((i) =>
i.board_name.toLowerCase().includes(searchText.toLowerCase())
);
}
);

View File

@ -0,0 +1,47 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { api } from 'services/apiSlice';
type BoardsState = {
searchText: string;
selectedBoardId?: string;
updateBoardModalOpen: boolean;
};
export const initialBoardsState: BoardsState = {
updateBoardModalOpen: false,
searchText: '',
};
const boardsSlice = createSlice({
name: 'boards',
initialState: initialBoardsState,
reducers: {
boardIdSelected: (state, action: PayloadAction<string | undefined>) => {
state.selectedBoardId = action.payload;
},
setBoardSearchText: (state, action: PayloadAction<string>) => {
state.searchText = action.payload;
},
setUpdateBoardModalOpen: (state, action: PayloadAction<boolean>) => {
state.updateBoardModalOpen = action.payload;
},
},
extraReducers: (builder) => {
builder.addMatcher(
api.endpoints.deleteBoard.matchFulfilled,
(state, action) => {
if (action.meta.arg.originalArgs === state.selectedBoardId) {
state.selectedBoardId = undefined;
}
}
);
},
});
export const { boardIdSelected, setBoardSearchText, setUpdateBoardModalOpen } =
boardsSlice.actions;
export const boardsSelector = (state: RootState) => state.boards;
export default boardsSlice.reducer;

View File

@ -1,17 +1,16 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { ImageDTO } from 'services/api';
import { imageUpserted } from './imagesSlice';
import { imageUrlsReceived } from 'services/thunks/image';
type GalleryImageObjectFitType = 'contain' | 'cover';
export interface GalleryState {
selectedImage?: ImageDTO;
selectedImage?: string;
galleryImageMinimumWidth: number;
galleryImageObjectFit: GalleryImageObjectFitType;
shouldAutoSwitchToNewImages: boolean;
shouldUseSingleGalleryColumn: boolean;
galleryView: 'images' | 'assets' | 'boards';
}
export const initialGalleryState: GalleryState = {
@ -19,13 +18,14 @@ export const initialGalleryState: GalleryState = {
galleryImageObjectFit: 'cover',
shouldAutoSwitchToNewImages: true,
shouldUseSingleGalleryColumn: false,
galleryView: 'images',
};
export const gallerySlice = createSlice({
name: 'gallery',
initialState: initialGalleryState,
reducers: {
imageSelected: (state, action: PayloadAction<ImageDTO | undefined>) => {
imageSelected: (state, action: PayloadAction<string | undefined>) => {
state.selectedImage = action.payload;
// TODO: if the user selects an image, disable the auto switch?
// state.shouldAutoSwitchToNewImages = false;
@ -48,6 +48,12 @@ export const gallerySlice = createSlice({
) => {
state.shouldUseSingleGalleryColumn = action.payload;
},
setGalleryView: (
state,
action: PayloadAction<'images' | 'assets' | 'boards'>
) => {
state.galleryView = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(imageUpserted, (state, action) => {
@ -55,17 +61,17 @@ export const gallerySlice = createSlice({
state.shouldAutoSwitchToNewImages &&
action.payload.image_category === 'general'
) {
state.selectedImage = action.payload;
state.selectedImage = action.payload.image_name;
}
});
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
const { image_name, image_url, thumbnail_url } = action.payload;
// builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
// const { image_name, image_url, thumbnail_url } = action.payload;
if (state.selectedImage?.image_name === image_name) {
state.selectedImage.image_url = image_url;
state.selectedImage.thumbnail_url = thumbnail_url;
}
});
// if (state.selectedImage?.image_name === image_name) {
// state.selectedImage.image_url = image_url;
// state.selectedImage.thumbnail_url = thumbnail_url;
// }
// });
},
});
@ -75,6 +81,7 @@ export const {
setGalleryImageObjectFit,
setShouldAutoSwitchToNewImages,
setShouldUseSingleGalleryColumn,
setGalleryView,
} = gallerySlice.actions;
export default gallerySlice.reducer;

View File

@ -11,7 +11,6 @@ import { dateComparator } from 'common/util/dateComparator';
import { keyBy } from 'lodash-es';
import {
imageDeleted,
imageMetadataReceived,
imageUrlsReceived,
receivedPageOfImages,
} from 'services/thunks/image';
@ -74,11 +73,21 @@ const imagesSlice = createSlice({
});
builder.addCase(receivedPageOfImages.fulfilled, (state, action) => {
state.isLoading = false;
const { boardId, categories, imageOrigin, isIntermediate } =
action.meta.arg;
const { items, offset, limit, total } = action.payload;
imagesAdapter.upsertMany(state, items);
if (!categories?.includes('general') || boardId) {
// need to skip updating the total images count if the images recieved were for a specific board
// TODO: this doesn't work when on the Asset tab/category...
return;
}
state.offset = offset;
state.limit = limit;
state.total = total;
imagesAdapter.upsertMany(state, items);
});
builder.addCase(imageDeleted.pending, (state, action) => {
// Image deleted
@ -154,3 +163,16 @@ export const selectFilteredImagesIds = createSelector(
.map((i) => i.image_name);
}
);
// export const selectImageById = createSelector(
// (state: RootState, imageId) => state,
// (state) => {
// const {
// images: { categories },
// } = state;
// return selectImagesAll(state)
// .filter((i) => categories.includes(i.image_category))
// .map((i) => i.image_name);
// }
// );

View File

@ -11,6 +11,8 @@ import { FieldComponentProps } from './types';
import IAIDndImage from 'common/components/IAIDndImage';
import { ImageDTO } from 'services/api';
import { Flex } from '@chakra-ui/react';
import { useGetImageDTOQuery } from 'services/apiSlice';
import { skipToken } from '@reduxjs/toolkit/dist/query';
const ImageInputFieldComponent = (
props: FieldComponentProps<ImageInputFieldValue, ImageInputFieldTemplate>
@ -19,9 +21,16 @@ const ImageInputFieldComponent = (
const dispatch = useAppDispatch();
const {
data: image,
isLoading,
isError,
isSuccess,
} = useGetImageDTOQuery(field.value ?? skipToken);
const handleDrop = useCallback(
(droppedImage: ImageDTO) => {
if (field.value?.image_name === droppedImage.image_name) {
if (field.value === droppedImage.image_name) {
return;
}
@ -29,11 +38,11 @@ const ImageInputFieldComponent = (
fieldValueChanged({
nodeId,
fieldName: field.name,
value: droppedImage,
value: droppedImage.image_name,
})
);
},
[dispatch, field.name, field.value?.image_name, nodeId]
[dispatch, field.name, field.value, nodeId]
);
const handleReset = useCallback(() => {
@ -56,7 +65,7 @@ const ImageInputFieldComponent = (
}}
>
<IAIDndImage
image={field.value}
image={image}
onDrop={handleDrop}
onReset={handleReset}
resetIconSize="sm"

View File

@ -1,28 +1,18 @@
import { Select } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { SelectItem } from '@mantine/core';
import { useAppDispatch } from 'app/store/storeHooks';
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
import {
ModelInputFieldTemplate,
ModelInputFieldValue,
} from 'features/nodes/types/types';
import { selectModelsIds } from 'features/system/store/modelSlice';
import { isEqual } from 'lodash-es';
import { ChangeEvent, memo } from 'react';
import { FieldComponentProps } from './types';
const availableModelsSelector = createSelector(
[selectModelsIds],
(allModelNames) => {
return { allModelNames };
// return map(modelList, (_, name) => name);
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
import { memo, useCallback, useEffect, useMemo } from 'react';
import { FieldComponentProps } from './types';
import { forEach, isString } from 'lodash-es';
import { MODEL_TYPE_MAP as BASE_MODEL_NAME_MAP } from 'features/system/components/ModelSelect';
import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { useTranslation } from 'react-i18next';
import { useListModelsQuery } from 'services/apiSlice';
const ModelInputFieldComponent = (
props: FieldComponentProps<ModelInputFieldValue, ModelInputFieldTemplate>
@ -30,28 +20,82 @@ const ModelInputFieldComponent = (
const { nodeId, field } = props;
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { allModelNames } = useAppSelector(availableModelsSelector);
const { data: pipelineModels } = useListModelsQuery({
model_type: 'pipeline',
});
const handleValueChanged = (e: ChangeEvent<HTMLSelectElement>) => {
dispatch(
fieldValueChanged({
nodeId,
fieldName: field.name,
value: e.target.value,
})
);
};
const data = useMemo(() => {
if (!pipelineModels) {
return [];
}
const data: SelectItem[] = [];
forEach(pipelineModels.entities, (model, id) => {
if (!model) {
return;
}
data.push({
value: id,
label: model.name,
group: BASE_MODEL_NAME_MAP[model.base_model],
});
});
return data;
}, [pipelineModels]);
const selectedModel = useMemo(
() => pipelineModels?.entities[field.value ?? pipelineModels.ids[0]],
[pipelineModels?.entities, pipelineModels?.ids, field.value]
);
const handleValueChanged = useCallback(
(v: string | null) => {
if (!v) {
return;
}
dispatch(
fieldValueChanged({
nodeId,
fieldName: field.name,
value: v,
})
);
},
[dispatch, field.name, nodeId]
);
useEffect(() => {
if (field.value && pipelineModels?.ids.includes(field.value)) {
return;
}
const firstModel = pipelineModels?.ids[0];
if (!isString(firstModel)) {
return;
}
handleValueChanged(firstModel);
}, [field.value, handleValueChanged, pipelineModels?.ids]);
return (
<Select
<IAIMantineSelect
tooltip={selectedModel?.description}
label={
selectedModel?.base_model &&
BASE_MODEL_NAME_MAP[selectedModel?.base_model]
}
value={field.value}
placeholder="Pick one"
data={data}
onChange={handleValueChanged}
value={field.value || allModelNames[0]}
>
{allModelNames.map((option) => (
<option key={option}>{option}</option>
))}
</Select>
/>
);
};

View File

@ -101,21 +101,6 @@ const nodesSlice = createSlice({
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
state.schema = action.payload;
});
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
const { image_name, image_url, thumbnail_url } = action.payload;
state.nodes.forEach((node) => {
forEach(node.data.inputs, (input) => {
if (input.type === 'image') {
if (input.value?.image_name === image_name) {
input.value.image_url = image_url;
input.value.thumbnail_url = thumbnail_url;
}
}
});
});
});
},
});

View File

@ -214,7 +214,7 @@ export type VaeInputFieldValue = FieldValueBase & {
export type ImageInputFieldValue = FieldValueBase & {
type: 'image';
value?: ImageDTO;
value?: string;
};
export type ModelInputFieldValue = FieldValueBase & {

View File

@ -65,15 +65,13 @@ export const addControlNetToLinearGraph = (
if (processedControlImage && processorType !== 'none') {
// We've already processed the image in the app, so we can just use the processed image
const { image_name } = processedControlImage;
controlNetNode.image = {
image_name,
image_name: processedControlImage,
};
} else if (controlImage) {
// The control image is preprocessed
const { image_name } = controlImage;
controlNetNode.image = {
image_name,
image_name: controlImage,
};
} else {
// Skip ControlNets without an unprocessed image - should never happen if everything is working correctly

View File

@ -23,6 +23,7 @@ import {
} from './constants';
import { set } from 'lodash-es';
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph';
import { modelIdToPipelineModelField } from '../modelIdToPipelineModelField';
const moduleLog = log.child({ namespace: 'nodes' });
@ -36,7 +37,7 @@ export const buildCanvasImageToImageGraph = (
const {
positivePrompt,
negativePrompt,
model: model_name,
model: modelId,
cfgScale: cfg_scale,
scheduler,
steps,
@ -49,6 +50,8 @@ export const buildCanvasImageToImageGraph = (
// The bounding box determines width and height, not the width and height params
const { width, height } = state.canvas.boundingBoxDimensions;
const model = modelIdToPipelineModelField(modelId);
/**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
* full graph here as a template. Then use the parameters from app state and set friendlier node
@ -85,9 +88,9 @@ export const buildCanvasImageToImageGraph = (
id: NOISE,
},
[MODEL_LOADER]: {
type: 'sd1_model_loader',
type: 'pipeline_model_loader',
id: MODEL_LOADER,
model_name,
model,
},
[LATENTS_TO_IMAGE]: {
type: 'l2i',

View File

@ -17,6 +17,7 @@ import {
INPAINT_GRAPH,
INPAINT,
} from './constants';
import { modelIdToPipelineModelField } from '../modelIdToPipelineModelField';
const moduleLog = log.child({ namespace: 'nodes' });
@ -31,7 +32,7 @@ export const buildCanvasInpaintGraph = (
const {
positivePrompt,
negativePrompt,
model: model_name,
model: modelId,
cfgScale: cfg_scale,
scheduler,
steps,
@ -54,6 +55,8 @@ export const buildCanvasInpaintGraph = (
// We may need to set the inpaint width and height to scale the image
const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } = state.canvas;
const model = modelIdToPipelineModelField(modelId);
const graph: NonNullableGraph = {
id: INPAINT_GRAPH,
nodes: {
@ -99,9 +102,9 @@ export const buildCanvasInpaintGraph = (
prompt: negativePrompt,
},
[MODEL_LOADER]: {
type: 'sd1_model_loader',
type: 'pipeline_model_loader',
id: MODEL_LOADER,
model_name,
model,
},
[RANGE_OF_SIZE]: {
type: 'range_of_size',

View File

@ -14,6 +14,7 @@ import {
TEXT_TO_LATENTS,
} from './constants';
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph';
import { modelIdToPipelineModelField } from '../modelIdToPipelineModelField';
/**
* Builds the Canvas tab's Text to Image graph.
@ -24,7 +25,7 @@ export const buildCanvasTextToImageGraph = (
const {
positivePrompt,
negativePrompt,
model: model_name,
model: modelId,
cfgScale: cfg_scale,
scheduler,
steps,
@ -36,6 +37,8 @@ export const buildCanvasTextToImageGraph = (
// The bounding box determines width and height, not the width and height params
const { width, height } = state.canvas.boundingBoxDimensions;
const model = modelIdToPipelineModelField(modelId);
/**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
* full graph here as a template. Then use the parameters from app state and set friendlier node
@ -80,9 +83,9 @@ export const buildCanvasTextToImageGraph = (
steps,
},
[MODEL_LOADER]: {
type: 'sd1_model_loader',
type: 'pipeline_model_loader',
id: MODEL_LOADER,
model_name,
model,
},
[LATENTS_TO_IMAGE]: {
type: 'l2i',

View File

@ -22,6 +22,7 @@ import {
} from './constants';
import { set } from 'lodash-es';
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph';
import { modelIdToPipelineModelField } from '../modelIdToPipelineModelField';
const moduleLog = log.child({ namespace: 'nodes' });
@ -34,7 +35,7 @@ export const buildLinearImageToImageGraph = (
const {
positivePrompt,
negativePrompt,
model: model_name,
model: modelId,
cfgScale: cfg_scale,
scheduler,
steps,
@ -62,6 +63,8 @@ export const buildLinearImageToImageGraph = (
throw new Error('No initial image found in state');
}
const model = modelIdToPipelineModelField(modelId);
// copy-pasted graph from node editor, filled in with state values & friendly node ids
const graph: NonNullableGraph = {
id: IMAGE_TO_IMAGE_GRAPH,
@ -89,9 +92,9 @@ export const buildLinearImageToImageGraph = (
id: NOISE,
},
[MODEL_LOADER]: {
type: 'sd1_model_loader',
type: 'pipeline_model_loader',
id: MODEL_LOADER,
model_name,
model,
},
[LATENTS_TO_IMAGE]: {
type: 'l2i',
@ -274,7 +277,7 @@ export const buildLinearImageToImageGraph = (
id: RESIZE,
type: 'img_resize',
image: {
image_name: initialImage.image_name,
image_name: initialImage.imageName,
},
is_intermediate: true,
width,
@ -311,7 +314,7 @@ export const buildLinearImageToImageGraph = (
} else {
// We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly
set(graph.nodes[IMAGE_TO_LATENTS], 'image', {
image_name: initialImage.image_name,
image_name: initialImage.imageName,
});
// Pass the image's dimensions to the `NOISE` node

View File

@ -1,6 +1,10 @@
import { RootState } from 'app/store/store';
import { NonNullableGraph } from 'features/nodes/types/types';
import { RandomIntInvocation, RangeOfSizeInvocation } from 'services/api';
import {
BaseModelType,
RandomIntInvocation,
RangeOfSizeInvocation,
} from 'services/api';
import {
ITERATE,
LATENTS_TO_IMAGE,
@ -14,6 +18,7 @@ import {
TEXT_TO_LATENTS,
} from './constants';
import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph';
import { modelIdToPipelineModelField } from '../modelIdToPipelineModelField';
type TextToImageGraphOverrides = {
width: number;
@ -27,7 +32,7 @@ export const buildLinearTextToImageGraph = (
const {
positivePrompt,
negativePrompt,
model: model_name,
model: modelId,
cfgScale: cfg_scale,
scheduler,
steps,
@ -38,6 +43,8 @@ export const buildLinearTextToImageGraph = (
shouldRandomizeSeed,
} = state.generation;
const model = modelIdToPipelineModelField(modelId);
/**
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
* full graph here as a template. Then use the parameters from app state and set friendlier node
@ -82,9 +89,9 @@ export const buildLinearTextToImageGraph = (
steps,
},
[MODEL_LOADER]: {
type: 'sd1_model_loader',
type: 'pipeline_model_loader',
id: MODEL_LOADER,
model_name,
model,
},
[LATENTS_TO_IMAGE]: {
type: 'l2i',

View File

@ -1,9 +1,10 @@
import { Graph } from 'services/api';
import { v4 as uuidv4 } from 'uuid';
import { cloneDeep, forEach, omit, reduce, values } from 'lodash-es';
import { cloneDeep, omit, reduce } from 'lodash-es';
import { RootState } from 'app/store/store';
import { InputFieldValue } from 'features/nodes/types/types';
import { AnyInvocation } from 'services/events/types';
import { modelIdToPipelineModelField } from '../modelIdToPipelineModelField';
/**
* We need to do special handling for some fields
@ -24,6 +25,12 @@ export const parseFieldValue = (field: InputFieldValue) => {
}
}
if (field.type === 'model') {
if (field.value) {
return modelIdToPipelineModelField(field.value);
}
}
return field.value;
};

View File

@ -7,7 +7,7 @@ export const NOISE = 'noise';
export const RANDOM_INT = 'rand_int';
export const RANGE_OF_SIZE = 'range_of_size';
export const ITERATE = 'iterate';
export const MODEL_LOADER = 'model_loader';
export const MODEL_LOADER = 'pipeline_model_loader';
export const IMAGE_TO_LATENTS = 'image_to_latents';
export const LATENTS_TO_LATENTS = 'latents_to_latents';
export const RESIZE = 'resize_image';

View File

@ -0,0 +1,18 @@
import { BaseModelType, PipelineModelField } from 'services/api';
/**
* Crudely converts a model id to a pipeline model field
* TODO: Make better
*/
export const modelIdToPipelineModelField = (
modelId: string
): PipelineModelField => {
const [base_model, model_type, model_name] = modelId.split('/');
const field: PipelineModelField = {
base_model: base_model as BaseModelType,
model_name,
};
return field;
};

View File

@ -57,7 +57,7 @@ export const buildImg2ImgNode = (
}
imageToImageNode.image = {
image_name: initialImage.image_name,
image_name: initialImage.imageName,
};
}

View File

@ -6,7 +6,7 @@ import ParamScheduler from './ParamScheduler';
const ParamSchedulerAndModel = () => {
return (
<Flex gap={3} w="full">
<Box w="20rem">
<Box w="25rem">
<ParamScheduler />
</Box>
<Box w="full">

View File

@ -10,7 +10,9 @@ import { generationSelector } from 'features/parameters/store/generationSelector
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDndImage from 'common/components/IAIDndImage';
import { ImageDTO } from 'services/api';
import { IAIImageFallback } from 'common/components/IAIImageFallback';
import { IAIImageLoadingFallback } from 'common/components/IAIImageFallback';
import { useGetImageDTOQuery } from 'services/apiSlice';
import { skipToken } from '@reduxjs/toolkit/dist/query';
const selector = createSelector(
[generationSelector],
@ -27,14 +29,21 @@ const InitialImagePreview = () => {
const { initialImage } = useAppSelector(selector);
const dispatch = useAppDispatch();
const {
data: image,
isLoading,
isError,
isSuccess,
} = useGetImageDTOQuery(initialImage?.imageName ?? skipToken);
const handleDrop = useCallback(
(droppedImage: ImageDTO) => {
if (droppedImage.image_name === initialImage?.image_name) {
if (droppedImage.image_name === initialImage?.imageName) {
return;
}
dispatch(initialImageChanged(droppedImage));
},
[dispatch, initialImage?.image_name]
[dispatch, initialImage]
);
const handleReset = useCallback(() => {
@ -53,10 +62,10 @@ const InitialImagePreview = () => {
}}
>
<IAIDndImage
image={initialImage}
image={image}
onDrop={handleDrop}
onReset={handleReset}
fallback={<IAIImageFallback sx={{ bg: 'none' }} />}
fallback={<IAIImageLoadingFallback sx={{ bg: 'none' }} />}
postUploadAction={{ type: 'SET_INITIAL_IMAGE' }}
withResetIcon
/>

View File

@ -1,10 +1,9 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { DEFAULT_SCHEDULER_NAME } from 'app/constants';
import { configChanged } from 'features/system/store/configSlice';
import { clamp, sortBy } from 'lodash-es';
import { clamp } from 'lodash-es';
import { ImageDTO } from 'services/api';
import { imageUrlsReceived } from 'services/thunks/image';
import { receivedModels } from 'services/thunks/model';
import {
CfgScaleParam,
HeightParam,
@ -17,14 +16,13 @@ import {
StrengthParam,
WidthParam,
} from './parameterZodSchemas';
import { DEFAULT_SCHEDULER_NAME } from 'app/constants';
export interface GenerationState {
cfgScale: CfgScaleParam;
height: HeightParam;
img2imgStrength: StrengthParam;
infillMethod: string;
initialImage?: ImageDTO;
initialImage?: { imageName: string; width: number; height: number };
iterations: number;
perlin: number;
positivePrompt: PositivePromptParam;
@ -212,35 +210,20 @@ export const generationSlice = createSlice({
state.shouldUseNoiseSettings = action.payload;
},
initialImageChanged: (state, action: PayloadAction<ImageDTO>) => {
state.initialImage = action.payload;
const { image_name, width, height } = action.payload;
state.initialImage = { imageName: image_name, width, height };
},
modelSelected: (state, action: PayloadAction<string>) => {
state.model = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(receivedModels.fulfilled, (state, action) => {
if (!state.model) {
const firstModel = sortBy(action.payload, 'name')[0];
state.model = firstModel.name;
}
});
builder.addCase(configChanged, (state, action) => {
const defaultModel = action.payload.sd?.defaultModel;
if (defaultModel && !state.model) {
state.model = defaultModel;
}
});
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
const { image_name, image_url, thumbnail_url } = action.payload;
if (state.initialImage?.image_name === image_name) {
state.initialImage.image_url = image_url;
state.initialImage.thumbnail_url = thumbnail_url;
}
});
},
});

View File

@ -154,3 +154,17 @@ export type StrengthParam = z.infer<typeof zStrength>;
*/
export const isValidStrength = (val: unknown): val is StrengthParam =>
zStrength.safeParse(val).success;
// /**
// * Zod schema for BaseModelType
// */
// export const zBaseModelType = z.enum(['sd-1', 'sd-2']);
// /**
// * Type alias for base model type, inferred from its zod schema. Should be identical to the type alias from OpenAPI.
// */
// export type BaseModelType = z.infer<typeof zBaseModelType>;
// /**
// * Validates/type-guards a value as a base model type
// */
// export const isValidBaseModelType = (val: unknown): val is BaseModelType =>
// zBaseModelType.safeParse(val).success;

View File

@ -1,44 +1,59 @@
import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash-es';
import { memo, useCallback } from 'react';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIMantineSelect, {
IAISelectDataType,
} from 'common/components/IAIMantineSelect';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import IAIMantineSelect from 'common/components/IAIMantineSelect';
import { modelSelected } from 'features/parameters/store/generationSlice';
import { selectModelsAll, selectModelsById } from '../store/modelSlice';
const selector = createSelector(
[(state: RootState) => state, generationSelector],
(state, generation) => {
const selectedModel = selectModelsById(state, generation.model);
import { forEach, isString } from 'lodash-es';
import { SelectItem } from '@mantine/core';
import { RootState } from 'app/store/store';
import { useListModelsQuery } from 'services/apiSlice';
const modelData = selectModelsAll(state)
.map<IAISelectDataType>((m) => ({
value: m.name,
label: m.name,
}))
.sort((a, b) => a.label.localeCompare(b.label));
return {
selectedModel,
modelData,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
export const MODEL_TYPE_MAP = {
'sd-1': 'Stable Diffusion 1.x',
'sd-2': 'Stable Diffusion 2.x',
};
const ModelSelect = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { selectedModel, modelData } = useAppSelector(selector);
const selectedModelId = useAppSelector(
(state: RootState) => state.generation.model
);
const { data: pipelineModels } = useListModelsQuery({
model_type: 'pipeline',
});
const data = useMemo(() => {
if (!pipelineModels) {
return [];
}
const data: SelectItem[] = [];
forEach(pipelineModels.entities, (model, id) => {
if (!model) {
return;
}
data.push({
value: id,
label: model.name,
group: MODEL_TYPE_MAP[model.base_model],
});
});
return data;
}, [pipelineModels]);
const selectedModel = useMemo(
() => pipelineModels?.entities[selectedModelId],
[pipelineModels?.entities, selectedModelId]
);
const handleChangeModel = useCallback(
(v: string | null) => {
if (!v) {
@ -49,13 +64,27 @@ const ModelSelect = () => {
[dispatch]
);
useEffect(() => {
if (selectedModelId && pipelineModels?.ids.includes(selectedModelId)) {
return;
}
const firstModel = pipelineModels?.ids[0];
if (!isString(firstModel)) {
return;
}
handleChangeModel(firstModel);
}, [handleChangeModel, pipelineModels?.ids, selectedModelId]);
return (
<IAIMantineSelect
tooltip={selectedModel?.description}
label={t('modelManager.model')}
value={selectedModel?.name ?? ''}
value={selectedModelId}
placeholder="Pick one"
data={modelData}
data={data}
onChange={handleChangeModel}
/>
);

View File

@ -1,6 +1,5 @@
import { SCHEDULER_LABEL_MAP, SCHEDULER_NAMES } from 'app/constants';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIMantineMultiSelect from 'common/components/IAIMantineMultiSelect';
import { SchedulerParam } from 'features/parameters/store/parameterZodSchemas';
@ -16,6 +15,7 @@ const data = map(SCHEDULER_NAMES, (s) => ({
export default function SettingsSchedulers() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const enabledSchedulers = useAppSelector(

View File

@ -7,13 +7,12 @@ import { systemSelector } from '../store/systemSelectors';
const isApplicationReadySelector = createSelector(
[systemSelector, configSelector],
(system, config) => {
const { wereModelsReceived, wasSchemaParsed } = system;
const { wasSchemaParsed } = system;
const { disabledTabs } = config;
return {
disabledTabs,
wereModelsReceived,
wasSchemaParsed,
};
}
@ -23,21 +22,17 @@ const isApplicationReadySelector = createSelector(
* Checks if the application is ready to be used, i.e. if the initial startup process is finished.
*/
export const useIsApplicationReady = () => {
const { disabledTabs, wereModelsReceived, wasSchemaParsed } = useAppSelector(
const { disabledTabs, wasSchemaParsed } = useAppSelector(
isApplicationReadySelector
);
const isApplicationReady = useMemo(() => {
if (!wereModelsReceived) {
return false;
}
if (!disabledTabs.includes('nodes') && !wasSchemaParsed) {
return false;
}
return true;
}, [disabledTabs, wereModelsReceived, wasSchemaParsed]);
}, [disabledTabs, wasSchemaParsed]);
return isApplicationReady;
};

View File

@ -1,3 +0,0 @@
import { RootState } from 'app/store/store';
export const modelSelector = (state: RootState) => state.models;

View File

@ -1,47 +0,0 @@
import { createEntityAdapter } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { CkptModelInfo, DiffusersModelInfo } from 'services/api';
import { receivedModels } from 'services/thunks/model';
export type Model = (CkptModelInfo | DiffusersModelInfo) & {
name: string;
};
export const modelsAdapter = createEntityAdapter<Model>({
selectId: (model) => model.name,
sortComparer: (a, b) => a.name.localeCompare(b.name),
});
export const initialModelsState = modelsAdapter.getInitialState();
export type ModelsState = typeof initialModelsState;
export const modelsSlice = createSlice({
name: 'models',
initialState: initialModelsState,
reducers: {
modelAdded: modelsAdapter.upsertOne,
},
extraReducers(builder) {
/**
* Received Models - FULFILLED
*/
builder.addCase(receivedModels.fulfilled, (state, action) => {
const models = action.payload;
modelsAdapter.setAll(state, models);
});
},
});
export const {
selectAll: selectModelsAll,
selectById: selectModelsById,
selectEntities: selectModelsEntities,
selectIds: selectModelsIds,
selectTotal: selectModelsTotal,
} = modelsAdapter.getSelectors<RootState>((state) => state.models);
export const { modelAdded } = modelsSlice.actions;
export default modelsSlice.reducer;

View File

@ -1,6 +0,0 @@
import { ModelsState } from './modelSlice';
/**
* Models slice persist denylist
*/
export const modelsPersistDenylist: (keyof ModelsState)[] = ['entities', 'ids'];

View File

@ -1,20 +1,12 @@
import { UseToastOptions } from '@chakra-ui/react';
import { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/types/invokeai';
import { ProgressImage } from 'services/events/types';
import { makeToast } from '../../../app/components/Toaster';
import { isAnySessionRejected, sessionCanceled } from 'services/thunks/session';
import { receivedModels } from 'services/thunks/model';
import { parsedOpenAPISchema } from 'features/nodes/store/nodesSlice';
import { LogLevelName } from 'roarr';
import { InvokeLogLevel } from 'app/logging/useLogger';
import { TFuncKey } from 'i18next';
import { t } from 'i18next';
import { userInvoked } from 'app/store/actions';
import { LANGUAGES } from '../components/LanguagePicker';
import { imageUploaded } from 'services/thunks/image';
import { parsedOpenAPISchema } from 'features/nodes/store/nodesSlice';
import { TFuncKey, t } from 'i18next';
import { LogLevelName } from 'roarr';
import {
appSocketConnected,
appSocketDisconnected,
@ -26,6 +18,11 @@ import {
appSocketSubscribed,
appSocketUnsubscribed,
} from 'services/events/actions';
import { ProgressImage } from 'services/events/types';
import { imageUploaded } from 'services/thunks/image';
import { isAnySessionRejected, sessionCanceled } from 'services/thunks/session';
import { makeToast } from '../../../app/components/Toaster';
import { LANGUAGES } from '../components/LanguagePicker';
export type CancelStrategy = 'immediate' | 'scheduled';
@ -95,6 +92,7 @@ export interface SystemState {
shouldAntialiasProgressImage: boolean;
language: keyof typeof LANGUAGES;
isUploading: boolean;
boardIdToAddTo?: string;
}
export const initialSystemState: SystemState = {
@ -225,6 +223,7 @@ export const systemSlice = createSlice({
*/
builder.addCase(appSocketSubscribed, (state, action) => {
state.sessionId = action.payload.sessionId;
state.boardIdToAddTo = action.payload.boardId;
state.canceledSession = '';
});
@ -233,6 +232,7 @@ export const systemSlice = createSlice({
*/
builder.addCase(appSocketUnsubscribed, (state) => {
state.sessionId = null;
state.boardIdToAddTo = undefined;
});
/**
@ -376,13 +376,6 @@ export const systemSlice = createSlice({
);
});
/**
* Received available models from the backend
*/
builder.addCase(receivedModels.fulfilled, (state) => {
state.wereModelsReceived = true;
});
/**
* OpenAPI schema was parsed
*/

View File

@ -8,6 +8,10 @@ export type { OpenAPIConfig } from './core/OpenAPI';
export type { AddInvocation } from './models/AddInvocation';
export type { BaseModelType } from './models/BaseModelType';
export type { BoardChanges } from './models/BoardChanges';
export type { BoardDTO } from './models/BoardDTO';
export type { Body_create_board_image } from './models/Body_create_board_image';
export type { Body_remove_board_image } from './models/Body_remove_board_image';
export type { Body_upload_image } from './models/Body_upload_image';
export type { CannyImageProcessorInvocation } from './models/CannyImageProcessorInvocation';
export type { CkptModelInfo } from './models/CkptModelInfo';
@ -21,6 +25,8 @@ export type { ConditioningField } from './models/ConditioningField';
export type { ContentShuffleImageProcessorInvocation } from './models/ContentShuffleImageProcessorInvocation';
export type { ControlField } from './models/ControlField';
export type { ControlNetInvocation } from './models/ControlNetInvocation';
export type { ControlNetModelConfig } from './models/ControlNetModelConfig';
export type { ControlNetModelFormat } from './models/ControlNetModelFormat';
export type { ControlOutput } from './models/ControlOutput';
export type { CreateModelRequest } from './models/CreateModelRequest';
export type { CvInpaintInvocation } from './models/CvInpaintInvocation';
@ -63,14 +69,6 @@ export type { InfillTileInvocation } from './models/InfillTileInvocation';
export type { InpaintInvocation } from './models/InpaintInvocation';
export type { IntCollectionOutput } from './models/IntCollectionOutput';
export type { IntOutput } from './models/IntOutput';
export type { invokeai__backend__model_management__models__controlnet__ControlNetModel__Config } from './models/invokeai__backend__model_management__models__controlnet__ControlNetModel__Config';
export type { invokeai__backend__model_management__models__lora__LoRAModel__Config } from './models/invokeai__backend__model_management__models__lora__LoRAModel__Config';
export type { invokeai__backend__model_management__models__stable_diffusion__StableDiffusion1Model__CheckpointConfig } from './models/invokeai__backend__model_management__models__stable_diffusion__StableDiffusion1Model__CheckpointConfig';
export type { invokeai__backend__model_management__models__stable_diffusion__StableDiffusion1Model__DiffusersConfig } from './models/invokeai__backend__model_management__models__stable_diffusion__StableDiffusion1Model__DiffusersConfig';
export type { invokeai__backend__model_management__models__stable_diffusion__StableDiffusion2Model__CheckpointConfig } from './models/invokeai__backend__model_management__models__stable_diffusion__StableDiffusion2Model__CheckpointConfig';
export type { invokeai__backend__model_management__models__stable_diffusion__StableDiffusion2Model__DiffusersConfig } from './models/invokeai__backend__model_management__models__stable_diffusion__StableDiffusion2Model__DiffusersConfig';
export type { invokeai__backend__model_management__models__textual_inversion__TextualInversionModel__Config } from './models/invokeai__backend__model_management__models__textual_inversion__TextualInversionModel__Config';
export type { invokeai__backend__model_management__models__vae__VaeModel__Config } from './models/invokeai__backend__model_management__models__vae__VaeModel__Config';
export type { IterateInvocation } from './models/IterateInvocation';
export type { IterateInvocationOutput } from './models/IterateInvocationOutput';
export type { LatentsField } from './models/LatentsField';
@ -83,6 +81,8 @@ export type { LoadImageInvocation } from './models/LoadImageInvocation';
export type { LoraInfo } from './models/LoraInfo';
export type { LoraLoaderInvocation } from './models/LoraLoaderInvocation';
export type { LoraLoaderOutput } from './models/LoraLoaderOutput';
export type { LoRAModelConfig } from './models/LoRAModelConfig';
export type { LoRAModelFormat } from './models/LoRAModelFormat';
export type { MaskFromAlphaInvocation } from './models/MaskFromAlphaInvocation';
export type { MaskOutput } from './models/MaskOutput';
export type { MediapipeFaceProcessorInvocation } from './models/MediapipeFaceProcessorInvocation';
@ -98,12 +98,15 @@ export type { MultiplyInvocation } from './models/MultiplyInvocation';
export type { NoiseInvocation } from './models/NoiseInvocation';
export type { NoiseOutput } from './models/NoiseOutput';
export type { NormalbaeImageProcessorInvocation } from './models/NormalbaeImageProcessorInvocation';
export type { OffsetPaginatedResults_BoardDTO_ } from './models/OffsetPaginatedResults_BoardDTO_';
export type { OffsetPaginatedResults_ImageDTO_ } from './models/OffsetPaginatedResults_ImageDTO_';
export type { OpenposeImageProcessorInvocation } from './models/OpenposeImageProcessorInvocation';
export type { PaginatedResults_GraphExecutionState_ } from './models/PaginatedResults_GraphExecutionState_';
export type { ParamFloatInvocation } from './models/ParamFloatInvocation';
export type { ParamIntInvocation } from './models/ParamIntInvocation';
export type { PidiImageProcessorInvocation } from './models/PidiImageProcessorInvocation';
export type { PipelineModelField } from './models/PipelineModelField';
export type { PipelineModelLoaderInvocation } from './models/PipelineModelLoaderInvocation';
export type { PromptCollectionOutput } from './models/PromptCollectionOutput';
export type { PromptOutput } from './models/PromptOutput';
export type { RandomIntInvocation } from './models/RandomIntInvocation';
@ -115,20 +118,28 @@ export type { ResourceOrigin } from './models/ResourceOrigin';
export type { RestoreFaceInvocation } from './models/RestoreFaceInvocation';
export type { ScaleLatentsInvocation } from './models/ScaleLatentsInvocation';
export type { SchedulerPredictionType } from './models/SchedulerPredictionType';
export type { SD1ModelLoaderInvocation } from './models/SD1ModelLoaderInvocation';
export type { SD2ModelLoaderInvocation } from './models/SD2ModelLoaderInvocation';
export type { ShowImageInvocation } from './models/ShowImageInvocation';
export type { StableDiffusion1ModelCheckpointConfig } from './models/StableDiffusion1ModelCheckpointConfig';
export type { StableDiffusion1ModelDiffusersConfig } from './models/StableDiffusion1ModelDiffusersConfig';
export type { StableDiffusion1ModelFormat } from './models/StableDiffusion1ModelFormat';
export type { StableDiffusion2ModelCheckpointConfig } from './models/StableDiffusion2ModelCheckpointConfig';
export type { StableDiffusion2ModelDiffusersConfig } from './models/StableDiffusion2ModelDiffusersConfig';
export type { StableDiffusion2ModelFormat } from './models/StableDiffusion2ModelFormat';
export type { StepParamEasingInvocation } from './models/StepParamEasingInvocation';
export type { SubModelType } from './models/SubModelType';
export type { SubtractInvocation } from './models/SubtractInvocation';
export type { TextToLatentsInvocation } from './models/TextToLatentsInvocation';
export type { TextualInversionModelConfig } from './models/TextualInversionModelConfig';
export type { UNetField } from './models/UNetField';
export type { UpscaleInvocation } from './models/UpscaleInvocation';
export type { VaeField } from './models/VaeField';
export type { VaeModelConfig } from './models/VaeModelConfig';
export type { VaeModelFormat } from './models/VaeModelFormat';
export type { VaeRepo } from './models/VaeRepo';
export type { ValidationError } from './models/ValidationError';
export type { ZoeDepthImageProcessorInvocation } from './models/ZoeDepthImageProcessorInvocation';
export { BoardsService } from './services/BoardsService';
export { ImagesService } from './services/ImagesService';
export { ModelsService } from './services/ModelsService';
export { SessionsService } from './services/SessionsService';

View File

@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type BoardChanges = {
/**
* The board's new name.
*/
board_name?: string;
/**
* The name of the board's new cover image.
*/
cover_image_name?: string;
};

View File

@ -0,0 +1,38 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* Deserialized board record with cover image URL and image count.
*/
export type BoardDTO = {
/**
* The unique ID of the board.
*/
board_id: string;
/**
* The name of the board.
*/
board_name: string;
/**
* The created timestamp of the board.
*/
created_at: string;
/**
* The updated timestamp of the board.
*/
updated_at: string;
/**
* The deleted timestamp of the board.
*/
deleted_at?: string;
/**
* The name of the board's cover image.
*/
cover_image_name?: string;
/**
* The number of images in the board.
*/
image_count: number;
};

View File

@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Body_create_board_image = {
/**
* The id of the board to add to
*/
board_id: string;
/**
* The name of the image to add
*/
image_name: string;
};

View File

@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Body_remove_board_image = {
/**
* The id of the board
*/
board_id: string;
/**
* The name of the image to remove
*/
image_name: string;
};

View File

@ -0,0 +1,18 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BaseModelType } from './BaseModelType';
import type { ControlNetModelFormat } from './ControlNetModelFormat';
import type { ModelError } from './ModelError';
export type ControlNetModelConfig = {
name: string;
base_model: BaseModelType;
type: 'controlnet';
path: string;
description?: string;
model_format: ControlNetModelFormat;
error?: ModelError;
};

View File

@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* An enumeration.
*/
export type ControlNetModelFormat = 'checkpoint' | 'diffusers';

View File

@ -49,6 +49,7 @@ import type { OpenposeImageProcessorInvocation } from './OpenposeImageProcessorI
import type { ParamFloatInvocation } from './ParamFloatInvocation';
import type { ParamIntInvocation } from './ParamIntInvocation';
import type { PidiImageProcessorInvocation } from './PidiImageProcessorInvocation';
import type { PipelineModelLoaderInvocation } from './PipelineModelLoaderInvocation';
import type { RandomIntInvocation } from './RandomIntInvocation';
import type { RandomRangeInvocation } from './RandomRangeInvocation';
import type { RangeInvocation } from './RangeInvocation';
@ -56,8 +57,6 @@ import type { RangeOfSizeInvocation } from './RangeOfSizeInvocation';
import type { ResizeLatentsInvocation } from './ResizeLatentsInvocation';
import type { RestoreFaceInvocation } from './RestoreFaceInvocation';
import type { ScaleLatentsInvocation } from './ScaleLatentsInvocation';
import type { SD1ModelLoaderInvocation } from './SD1ModelLoaderInvocation';
import type { SD2ModelLoaderInvocation } from './SD2ModelLoaderInvocation';
import type { ShowImageInvocation } from './ShowImageInvocation';
import type { StepParamEasingInvocation } from './StepParamEasingInvocation';
import type { SubtractInvocation } from './SubtractInvocation';
@ -73,7 +72,7 @@ export type Graph = {
/**
* The nodes in this graph
*/
nodes?: Record<string, (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageResizeInvocation | ImageScaleInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | SD1ModelLoaderInvocation | SD2ModelLoaderInvocation | LoraLoaderInvocation | DynamicPromptInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | FloatLinearRangeInvocation | StepParamEasingInvocation | UpscaleInvocation | RestoreFaceInvocation | InpaintInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageProcessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation)>;
nodes?: Record<string, (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageResizeInvocation | ImageScaleInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | PipelineModelLoaderInvocation | LoraLoaderInvocation | DynamicPromptInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | FloatLinearRangeInvocation | StepParamEasingInvocation | UpscaleInvocation | RestoreFaceInvocation | InpaintInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageProcessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation)>;
/**
* The connections between nodes and their fields in this graph
*/

View File

@ -7,7 +7,7 @@ import type { ImageMetadata } from './ImageMetadata';
import type { ResourceOrigin } from './ResourceOrigin';
/**
* Deserialized image record, enriched for the frontend with URLs.
* Deserialized image record, enriched for the frontend.
*/
export type ImageDTO = {
/**
@ -66,5 +66,9 @@ export type ImageDTO = {
* A limited subset of the image's generation metadata. Retrieve the image's session for full metadata.
*/
metadata?: ImageMetadata;
/**
* The id of the board the image belongs to, if one exists.
*/
board_id?: string;
};

View File

@ -0,0 +1,18 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BaseModelType } from './BaseModelType';
import type { LoRAModelFormat } from './LoRAModelFormat';
import type { ModelError } from './ModelError';
export type LoRAModelConfig = {
name: string;
base_model: BaseModelType;
type: 'lora';
path: string;
description?: string;
model_format: LoRAModelFormat;
error?: ModelError;
};

View File

@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* An enumeration.
*/
export type LoRAModelFormat = 'lycoris' | 'diffusers';

View File

@ -2,16 +2,16 @@
/* tslint:disable */
/* eslint-disable */
import type { invokeai__backend__model_management__models__controlnet__ControlNetModel__Config } from './invokeai__backend__model_management__models__controlnet__ControlNetModel__Config';
import type { invokeai__backend__model_management__models__lora__LoRAModel__Config } from './invokeai__backend__model_management__models__lora__LoRAModel__Config';
import type { invokeai__backend__model_management__models__stable_diffusion__StableDiffusion1Model__CheckpointConfig } from './invokeai__backend__model_management__models__stable_diffusion__StableDiffusion1Model__CheckpointConfig';
import type { invokeai__backend__model_management__models__stable_diffusion__StableDiffusion1Model__DiffusersConfig } from './invokeai__backend__model_management__models__stable_diffusion__StableDiffusion1Model__DiffusersConfig';
import type { invokeai__backend__model_management__models__stable_diffusion__StableDiffusion2Model__CheckpointConfig } from './invokeai__backend__model_management__models__stable_diffusion__StableDiffusion2Model__CheckpointConfig';
import type { invokeai__backend__model_management__models__stable_diffusion__StableDiffusion2Model__DiffusersConfig } from './invokeai__backend__model_management__models__stable_diffusion__StableDiffusion2Model__DiffusersConfig';
import type { invokeai__backend__model_management__models__textual_inversion__TextualInversionModel__Config } from './invokeai__backend__model_management__models__textual_inversion__TextualInversionModel__Config';
import type { invokeai__backend__model_management__models__vae__VaeModel__Config } from './invokeai__backend__model_management__models__vae__VaeModel__Config';
import type { ControlNetModelConfig } from './ControlNetModelConfig';
import type { LoRAModelConfig } from './LoRAModelConfig';
import type { StableDiffusion1ModelCheckpointConfig } from './StableDiffusion1ModelCheckpointConfig';
import type { StableDiffusion1ModelDiffusersConfig } from './StableDiffusion1ModelDiffusersConfig';
import type { StableDiffusion2ModelCheckpointConfig } from './StableDiffusion2ModelCheckpointConfig';
import type { StableDiffusion2ModelDiffusersConfig } from './StableDiffusion2ModelDiffusersConfig';
import type { TextualInversionModelConfig } from './TextualInversionModelConfig';
import type { VaeModelConfig } from './VaeModelConfig';
export type ModelsList = {
models: Record<string, Record<string, Record<string, (invokeai__backend__model_management__models__stable_diffusion__StableDiffusion1Model__DiffusersConfig | invokeai__backend__model_management__models__controlnet__ControlNetModel__Config | invokeai__backend__model_management__models__lora__LoRAModel__Config | invokeai__backend__model_management__models__stable_diffusion__StableDiffusion2Model__DiffusersConfig | invokeai__backend__model_management__models__textual_inversion__TextualInversionModel__Config | invokeai__backend__model_management__models__vae__VaeModel__Config | invokeai__backend__model_management__models__stable_diffusion__StableDiffusion2Model__CheckpointConfig | invokeai__backend__model_management__models__stable_diffusion__StableDiffusion1Model__CheckpointConfig)>>>;
models: Array<(StableDiffusion1ModelCheckpointConfig | StableDiffusion1ModelDiffusersConfig | VaeModelConfig | LoRAModelConfig | ControlNetModelConfig | TextualInversionModelConfig | StableDiffusion2ModelCheckpointConfig | StableDiffusion2ModelDiffusersConfig)>;
};

View File

@ -0,0 +1,28 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BoardDTO } from './BoardDTO';
/**
* Offset-paginated results
*/
export type OffsetPaginatedResults_BoardDTO_ = {
/**
* Items
*/
items: Array<BoardDTO>;
/**
* Offset from which to retrieve items
*/
offset: number;
/**
* Limit of items to get
*/
limit: number;
/**
* Total number of items in result
*/
total: number;
};

View File

@ -0,0 +1,20 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BaseModelType } from './BaseModelType';
/**
* Pipeline model field
*/
export type PipelineModelField = {
/**
* Name of the model
*/
model_name: string;
/**
* Base model
*/
base_model: BaseModelType;
};

View File

@ -2,10 +2,12 @@
/* tslint:disable */
/* eslint-disable */
import type { PipelineModelField } from './PipelineModelField';
/**
* Loading submodels of selected model.
* Loads a pipeline model, outputting its submodels.
*/
export type SD2ModelLoaderInvocation = {
export type PipelineModelLoaderInvocation = {
/**
* The id of this node. Must be unique among all nodes.
*/
@ -14,10 +16,10 @@ export type SD2ModelLoaderInvocation = {
* Whether or not this node is an intermediate node.
*/
is_intermediate?: boolean;
type?: 'sd2_model_loader';
type?: 'pipeline_model_loader';
/**
* Model to load
* The model to load
*/
model_name?: string;
model: PipelineModelField;
};

View File

@ -1,23 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* Loading submodels of selected model.
*/
export type SD1ModelLoaderInvocation = {
/**
* The id of this node. Must be unique among all nodes.
*/
id: string;
/**
* Whether or not this node is an intermediate node.
*/
is_intermediate?: boolean;
type?: 'sd1_model_loader';
/**
* Model to load
*/
model_name?: string;
};

View File

@ -2,14 +2,17 @@
/* tslint:disable */
/* eslint-disable */
import type { BaseModelType } from './BaseModelType';
import type { ModelError } from './ModelError';
import type { ModelVariantType } from './ModelVariantType';
export type invokeai__backend__model_management__models__stable_diffusion__StableDiffusion1Model__CheckpointConfig = {
export type StableDiffusion1ModelCheckpointConfig = {
name: string;
base_model: BaseModelType;
type: 'pipeline';
path: string;
description?: string;
format: 'checkpoint';
default?: boolean;
model_format: 'checkpoint';
error?: ModelError;
vae?: string;
config?: string;

View File

@ -2,14 +2,17 @@
/* tslint:disable */
/* eslint-disable */
import type { BaseModelType } from './BaseModelType';
import type { ModelError } from './ModelError';
import type { ModelVariantType } from './ModelVariantType';
export type invokeai__backend__model_management__models__stable_diffusion__StableDiffusion1Model__DiffusersConfig = {
export type StableDiffusion1ModelDiffusersConfig = {
name: string;
base_model: BaseModelType;
type: 'pipeline';
path: string;
description?: string;
format: 'diffusers';
default?: boolean;
model_format: 'diffusers';
error?: ModelError;
vae?: string;
variant: ModelVariantType;

View File

@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* An enumeration.
*/
export type StableDiffusion1ModelFormat = 'checkpoint' | 'diffusers';

View File

@ -2,15 +2,18 @@
/* tslint:disable */
/* eslint-disable */
import type { BaseModelType } from './BaseModelType';
import type { ModelError } from './ModelError';
import type { ModelVariantType } from './ModelVariantType';
import type { SchedulerPredictionType } from './SchedulerPredictionType';
export type invokeai__backend__model_management__models__stable_diffusion__StableDiffusion2Model__CheckpointConfig = {
export type StableDiffusion2ModelCheckpointConfig = {
name: string;
base_model: BaseModelType;
type: 'pipeline';
path: string;
description?: string;
format: 'checkpoint';
default?: boolean;
model_format: 'checkpoint';
error?: ModelError;
vae?: string;
config?: string;

View File

@ -2,15 +2,18 @@
/* tslint:disable */
/* eslint-disable */
import type { BaseModelType } from './BaseModelType';
import type { ModelError } from './ModelError';
import type { ModelVariantType } from './ModelVariantType';
import type { SchedulerPredictionType } from './SchedulerPredictionType';
export type invokeai__backend__model_management__models__stable_diffusion__StableDiffusion2Model__DiffusersConfig = {
export type StableDiffusion2ModelDiffusersConfig = {
name: string;
base_model: BaseModelType;
type: 'pipeline';
path: string;
description?: string;
format: 'diffusers';
default?: boolean;
model_format: 'diffusers';
error?: ModelError;
vae?: string;
variant: ModelVariantType;

View File

@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* An enumeration.
*/
export type StableDiffusion2ModelFormat = 'checkpoint' | 'diffusers';

View File

@ -0,0 +1,17 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BaseModelType } from './BaseModelType';
import type { ModelError } from './ModelError';
export type TextualInversionModelConfig = {
name: string;
base_model: BaseModelType;
type: 'embedding';
path: string;
description?: string;
model_format: null;
error?: ModelError;
};

View File

@ -0,0 +1,18 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BaseModelType } from './BaseModelType';
import type { ModelError } from './ModelError';
import type { VaeModelFormat } from './VaeModelFormat';
export type VaeModelConfig = {
name: string;
base_model: BaseModelType;
type: 'vae';
path: string;
description?: string;
model_format: VaeModelFormat;
error?: ModelError;
};

View File

@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* An enumeration.
*/
export type VaeModelFormat = 'checkpoint' | 'diffusers';

View File

@ -1,14 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ModelError } from './ModelError';
export type invokeai__backend__model_management__models__controlnet__ControlNetModel__Config = {
path: string;
description?: string;
format: ('checkpoint' | 'diffusers');
default?: boolean;
error?: ModelError;
};

View File

@ -1,14 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ModelError } from './ModelError';
export type invokeai__backend__model_management__models__lora__LoRAModel__Config = {
path: string;
description?: string;
format: ('lycoris' | 'diffusers');
default?: boolean;
error?: ModelError;
};

View File

@ -1,14 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ModelError } from './ModelError';
export type invokeai__backend__model_management__models__textual_inversion__TextualInversionModel__Config = {
path: string;
description?: string;
format: null;
default?: boolean;
error?: ModelError;
};

View File

@ -1,14 +0,0 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ModelError } from './ModelError';
export type invokeai__backend__model_management__models__vae__VaeModel__Config = {
path: string;
description?: string;
format: ('checkpoint' | 'diffusers');
default?: boolean;
error?: ModelError;
};

View File

@ -0,0 +1,247 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BoardChanges } from '../models/BoardChanges';
import type { BoardDTO } from '../models/BoardDTO';
import type { Body_create_board_image } from '../models/Body_create_board_image';
import type { Body_remove_board_image } from '../models/Body_remove_board_image';
import type { OffsetPaginatedResults_BoardDTO_ } from '../models/OffsetPaginatedResults_BoardDTO_';
import type { OffsetPaginatedResults_ImageDTO_ } from '../models/OffsetPaginatedResults_ImageDTO_';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class BoardsService {
/**
* List Boards
* Gets a list of boards
* @returns any Successful Response
* @throws ApiError
*/
public static listBoards({
all,
offset,
limit,
}: {
/**
* Whether to list all boards
*/
all?: boolean,
/**
* The page offset
*/
offset?: number,
/**
* The number of boards per page
*/
limit?: number,
}): CancelablePromise<(OffsetPaginatedResults_BoardDTO_ | Array<BoardDTO>)> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/boards/',
query: {
'all': all,
'offset': offset,
'limit': limit,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Create Board
* Creates a board
* @returns BoardDTO The board was created successfully
* @throws ApiError
*/
public static createBoard({
boardName,
}: {
/**
* The name of the board to create
*/
boardName: string,
}): CancelablePromise<BoardDTO> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/boards/',
query: {
'board_name': boardName,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Get Board
* Gets a board
* @returns BoardDTO Successful Response
* @throws ApiError
*/
public static getBoard({
boardId,
}: {
/**
* The id of board to get
*/
boardId: string,
}): CancelablePromise<BoardDTO> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/boards/{board_id}',
path: {
'board_id': boardId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Delete Board
* Deletes a board
* @returns any Successful Response
* @throws ApiError
*/
public static deleteBoard({
boardId,
}: {
/**
* The id of board to delete
*/
boardId: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/api/v1/boards/{board_id}',
path: {
'board_id': boardId,
},
errors: {
422: `Validation Error`,
},
});
}
/**
* Update Board
* Updates a board
* @returns BoardDTO The board was updated successfully
* @throws ApiError
*/
public static updateBoard({
boardId,
requestBody,
}: {
/**
* The id of board to update
*/
boardId: string,
requestBody: BoardChanges,
}): CancelablePromise<BoardDTO> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/api/v1/boards/{board_id}',
path: {
'board_id': boardId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Create Board Image
* Creates a board_image
* @returns any The image was added to a board successfully
* @throws ApiError
*/
public static createBoardImage({
requestBody,
}: {
requestBody: Body_create_board_image,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/board_images/',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Remove Board Image
* Deletes a board_image
* @returns any The image was removed from the board successfully
* @throws ApiError
*/
public static removeBoardImage({
requestBody,
}: {
requestBody: Body_remove_board_image,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/api/v1/board_images/',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* List Board Images
* Gets a list of images for a board
* @returns OffsetPaginatedResults_ImageDTO_ Successful Response
* @throws ApiError
*/
public static listBoardImages({
boardId,
offset,
limit = 10,
}: {
/**
* The id of the board
*/
boardId: string,
/**
* The page offset
*/
offset?: number,
/**
* The number of boards per page
*/
limit?: number,
}): CancelablePromise<OffsetPaginatedResults_ImageDTO_> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/board_images/{board_id}',
path: {
'board_id': boardId,
},
query: {
'offset': offset,
'limit': limit,
},
errors: {
422: `Validation Error`,
},
});
}
}

View File

@ -25,6 +25,7 @@ export class ImagesService {
imageOrigin,
categories,
isIntermediate,
boardId,
offset,
limit = 10,
}: {
@ -40,6 +41,10 @@ export class ImagesService {
* Whether to list intermediate images
*/
isIntermediate?: boolean,
/**
* The board id to filter by
*/
boardId?: string,
/**
* The page offset
*/
@ -56,6 +61,7 @@ export class ImagesService {
'image_origin': imageOrigin,
'categories': categories,
'is_intermediate': isIntermediate,
'board_id': boardId,
'offset': offset,
'limit': limit,
},

View File

@ -51,6 +51,7 @@ import type { PaginatedResults_GraphExecutionState_ } from '../models/PaginatedR
import type { ParamFloatInvocation } from '../models/ParamFloatInvocation';
import type { ParamIntInvocation } from '../models/ParamIntInvocation';
import type { PidiImageProcessorInvocation } from '../models/PidiImageProcessorInvocation';
import type { PipelineModelLoaderInvocation } from '../models/PipelineModelLoaderInvocation';
import type { RandomIntInvocation } from '../models/RandomIntInvocation';
import type { RandomRangeInvocation } from '../models/RandomRangeInvocation';
import type { RangeInvocation } from '../models/RangeInvocation';
@ -58,8 +59,6 @@ import type { RangeOfSizeInvocation } from '../models/RangeOfSizeInvocation';
import type { ResizeLatentsInvocation } from '../models/ResizeLatentsInvocation';
import type { RestoreFaceInvocation } from '../models/RestoreFaceInvocation';
import type { ScaleLatentsInvocation } from '../models/ScaleLatentsInvocation';
import type { SD1ModelLoaderInvocation } from '../models/SD1ModelLoaderInvocation';
import type { SD2ModelLoaderInvocation } from '../models/SD2ModelLoaderInvocation';
import type { ShowImageInvocation } from '../models/ShowImageInvocation';
import type { StepParamEasingInvocation } from '../models/StepParamEasingInvocation';
import type { SubtractInvocation } from '../models/SubtractInvocation';
@ -175,7 +174,7 @@ export class SessionsService {
* The id of the session
*/
sessionId: string,
requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageResizeInvocation | ImageScaleInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | SD1ModelLoaderInvocation | SD2ModelLoaderInvocation | LoraLoaderInvocation | DynamicPromptInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | FloatLinearRangeInvocation | StepParamEasingInvocation | UpscaleInvocation | RestoreFaceInvocation | InpaintInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageProcessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation),
requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageResizeInvocation | ImageScaleInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | PipelineModelLoaderInvocation | LoraLoaderInvocation | DynamicPromptInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | FloatLinearRangeInvocation | StepParamEasingInvocation | UpscaleInvocation | RestoreFaceInvocation | InpaintInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageProcessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation),
}): CancelablePromise<string> {
return __request(OpenAPI, {
method: 'POST',
@ -212,7 +211,7 @@ export class SessionsService {
* The path to the node in the graph
*/
nodePath: string,
requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageResizeInvocation | ImageScaleInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | SD1ModelLoaderInvocation | SD2ModelLoaderInvocation | LoraLoaderInvocation | DynamicPromptInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | FloatLinearRangeInvocation | StepParamEasingInvocation | UpscaleInvocation | RestoreFaceInvocation | InpaintInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageProcessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation),
requestBody: (LoadImageInvocation | ShowImageInvocation | ImageCropInvocation | ImagePasteInvocation | MaskFromAlphaInvocation | ImageMultiplyInvocation | ImageChannelInvocation | ImageConvertInvocation | ImageBlurInvocation | ImageResizeInvocation | ImageScaleInvocation | ImageLerpInvocation | ImageInverseLerpInvocation | ControlNetInvocation | ImageProcessorInvocation | PipelineModelLoaderInvocation | LoraLoaderInvocation | DynamicPromptInvocation | CompelInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | RandomIntInvocation | ParamIntInvocation | ParamFloatInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | CvInpaintInvocation | RangeInvocation | RangeOfSizeInvocation | RandomRangeInvocation | FloatLinearRangeInvocation | StepParamEasingInvocation | UpscaleInvocation | RestoreFaceInvocation | InpaintInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | CannyImageProcessorInvocation | HedImageProcessorInvocation | LineartImageProcessorInvocation | LineartAnimeImageProcessorInvocation | OpenposeImageProcessorInvocation | MidasDepthImageProcessorInvocation | NormalbaeImageProcessorInvocation | MlsdImageProcessorInvocation | PidiImageProcessorInvocation | ContentShuffleImageProcessorInvocation | ZoeDepthImageProcessorInvocation | MediapipeFaceProcessorInvocation | LatentsToLatentsInvocation),
}): CancelablePromise<GraphExecutionState> {
return __request(OpenAPI, {
method: 'PUT',

Some files were not shown because too many files have changed in this diff Show More