Merge branch 'main' into doc_updates_23

This commit is contained in:
Lincoln Stein 2023-07-09 13:25:58 -04:00 committed by GitHub
commit 2ad95f961c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 280 additions and 108 deletions

View File

@ -22,6 +22,7 @@ app_config.parse_args()
logger = InvokeAILogger.getLogger(config=app_config)
import invokeai.frontend.web as web_dir
import mimetypes
from .api.dependencies import ApiDependencies
from .api.routers import sessions, models, images, boards, board_images, app_info
@ -31,7 +32,12 @@ from .invocations.baseinvocation import BaseInvocation
import torch
if torch.backends.mps.is_available():
import invokeai.backend.util.mps_fixes
# fix for windows mimetypes registry entries being borked
# see https://github.com/invoke-ai/InvokeAI/discussions/3684#discussioncomment-6391352
mimetypes.add_type('application/javascript', '.js')
mimetypes.add_type('text/css', '.css')
# Create the app
# TODO: create this all in a method so configuration/etc. can be passed in?
app = FastAPI(title="Invoke AI", docs_url=None, redoc_url=None)

View File

@ -115,6 +115,7 @@ const ParamEmbeddingPopover = (props: Props) => {
nothingFound="No matching Embeddings"
itemComponent={IAIMantineSelectItemWithTooltip}
disabled={data.length === 0}
onDropdownClose={onClose}
filter={(value, item: SelectItem) =>
item.label
?.toLowerCase()

View File

@ -57,6 +57,7 @@ const selector = createSelector(
images,
allImagesTotal,
isLoading,
isFetching,
categories,
selectedBoardId,
};
@ -82,8 +83,14 @@ const ImageGalleryGrid = () => {
},
});
const { images, isLoading, allImagesTotal, categories, selectedBoardId } =
useAppSelector(selector);
const {
images,
isLoading,
isFetching,
allImagesTotal,
categories,
selectedBoardId,
} = useAppSelector(selector);
const { selectedBoard } = useListAllBoardsQuery(undefined, {
selectFromResult: ({ data }) => ({
@ -176,7 +183,7 @@ const ImageGalleryGrid = () => {
<IAIButton
onClick={handleLoadMoreImages}
isDisabled={!areMoreAvailable}
isLoading={isLoading}
isLoading={isFetching}
loadingText="Loading"
flexShrink={0}
>

View File

@ -1,17 +1,18 @@
import { ChakraProps, Flex, Grid, IconButton } from '@chakra-ui/react';
import { ChakraProps, Flex, Grid, IconButton, Spinner } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { clamp, isEqual } from 'lodash-es';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
imageSelected,
selectFilteredImages,
selectImagesById,
} from 'features/gallery/store/gallerySlice';
import { clamp, isEqual } from 'lodash-es';
import { useCallback, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { selectFilteredImages } from 'features/gallery/store/gallerySlice';
import { useTranslation } from 'react-i18next';
import { FaAngleDoubleRight, FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { receivedPageOfImages } from 'services/api/thunks/image';
const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = {
height: '100%',
@ -26,6 +27,7 @@ const nextPrevButtonStyles: ChakraProps['sx'] = {
export const nextPrevImageButtonsSelector = createSelector(
[stateSelector, selectFilteredImages],
(state, filteredImages) => {
const { total, isFetching } = state.gallery;
const lastSelectedImage =
state.gallery.selection[state.gallery.selection.length - 1];
@ -63,6 +65,8 @@ export const nextPrevImageButtonsSelector = createSelector(
isOnFirstImage: currentImageIndex === 0,
isOnLastImage:
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
areMoreImagesAvailable: total > imagesLength,
isFetching,
nextImage,
prevImage,
nextImageId,
@ -80,8 +84,14 @@ const NextPrevImageButtons = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { isOnFirstImage, isOnLastImage, nextImageId, prevImageId } =
useAppSelector(nextPrevImageButtonsSelector);
const {
isOnFirstImage,
isOnLastImage,
nextImageId,
prevImageId,
areMoreImagesAvailable,
isFetching,
} = useAppSelector(nextPrevImageButtonsSelector);
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
useState<boolean>(false);
@ -102,6 +112,14 @@ const NextPrevImageButtons = () => {
nextImageId && dispatch(imageSelected(nextImageId));
}, [dispatch, nextImageId]);
const handleLoadMoreImages = useCallback(() => {
dispatch(
receivedPageOfImages({
is_intermediate: false,
})
);
}, [dispatch]);
useHotkeys(
'left',
() => {
@ -113,9 +131,21 @@ const NextPrevImageButtons = () => {
useHotkeys(
'right',
() => {
handleNextImage();
if (isOnLastImage && areMoreImagesAvailable && !isFetching) {
handleLoadMoreImages();
return;
}
if (!isOnLastImage) {
handleNextImage();
}
},
[nextImageId]
[
nextImageId,
isOnLastImage,
areMoreImagesAvailable,
handleLoadMoreImages,
isFetching,
]
);
return (
@ -164,6 +194,34 @@ const NextPrevImageButtons = () => {
sx={nextPrevButtonStyles}
/>
)}
{shouldShowNextPrevButtons &&
isOnLastImage &&
areMoreImagesAvailable &&
!isFetching && (
<IconButton
aria-label={t('accessibility.loadMore')}
icon={<FaAngleDoubleRight size={64} />}
variant="unstyled"
onClick={handleLoadMoreImages}
boxSize={16}
sx={nextPrevButtonStyles}
/>
)}
{shouldShowNextPrevButtons &&
isOnLastImage &&
areMoreImagesAvailable &&
isFetching && (
<Flex
sx={{
w: 16,
h: 16,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Spinner opacity={0.5} size="xl" />
</Flex>
)}
</Grid>
</Flex>
);

View File

@ -28,6 +28,12 @@ const selector = createSelector(
};
});
data.push({
label: 'Progress Image',
value: 'progress_image',
description: 'Displays the progress image in the Node Editor',
});
return { data };
},
defaultSelectorOptions

View File

@ -1,14 +1,15 @@
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCallback } from 'react';
import {
Background,
OnConnect,
OnConnectEnd,
OnConnectStart,
OnEdgesChange,
OnNodesChange,
ReactFlow,
OnConnectStart,
OnConnectEnd,
} from 'reactflow';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RootState } from 'app/store/store';
import {
connectionEnded,
connectionMade,
@ -16,15 +17,18 @@ import {
edgesChanged,
nodesChanged,
} from '../store/nodesSlice';
import { useCallback } from 'react';
import { InvocationComponent } from './InvocationComponent';
import TopLeftPanel from './panels/TopLeftPanel';
import TopRightPanel from './panels/TopRightPanel';
import TopCenterPanel from './panels/TopCenterPanel';
import ProgressImageNode from './ProgressImageNode';
import BottomLeftPanel from './panels/BottomLeftPanel.tsx';
import MinimapPanel from './panels/MinimapPanel';
import TopCenterPanel from './panels/TopCenterPanel';
import TopLeftPanel from './panels/TopLeftPanel';
import TopRightPanel from './panels/TopRightPanel';
const nodeTypes = { invocation: InvocationComponent };
const nodeTypes = {
invocation: InvocationComponent,
progress_image: ProgressImageNode,
};
export const Flow = () => {
const dispatch = useAppDispatch();

View File

@ -1,15 +1,15 @@
import { Flex, Heading, Tooltip, Icon } from '@chakra-ui/react';
import { InvocationTemplate } from 'features/nodes/types/types';
import { Flex, Heading, Icon, Tooltip } from '@chakra-ui/react';
import { memo } from 'react';
import { FaInfoCircle } from 'react-icons/fa';
interface IAINodeHeaderProps {
nodeId: string;
template: InvocationTemplate;
nodeId?: string;
title?: string;
description?: string;
}
const IAINodeHeader = (props: IAINodeHeaderProps) => {
const { nodeId, template } = props;
const { nodeId, title, description } = props;
return (
<Flex
sx={{
@ -31,15 +31,10 @@ const IAINodeHeader = (props: IAINodeHeaderProps) => {
_dark: { color: 'base.100' },
}}
>
{template.title}
{title}
</Heading>
</Tooltip>
<Tooltip
label={template.description}
placement="top"
hasArrow
shouldWrapChildren
>
<Tooltip label={description} placement="top" hasArrow shouldWrapChildren>
<Icon
sx={{
h: 'min-content',

View File

@ -1,64 +1,16 @@
import { NodeProps } from 'reactflow';
import { Box, Flex, Icon, useToken } from '@chakra-ui/react';
import { Flex, Icon } from '@chakra-ui/react';
import { FaExclamationCircle } from 'react-icons/fa';
import { InvocationTemplate, InvocationValue } from '../types/types';
import { NodeProps } from 'reactflow';
import { InvocationValue } from '../types/types';
import { memo, PropsWithChildren, useMemo } from 'react';
import IAINodeOutputs from './IAINode/IAINodeOutputs';
import IAINodeInputs from './IAINode/IAINodeInputs';
import IAINodeHeader from './IAINode/IAINodeHeader';
import IAINodeResizer from './IAINode/IAINodeResizer';
import { RootState } from 'app/store/store';
import { AnyInvocationType } from 'services/events/types';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { NODE_MIN_WIDTH } from 'app/constants';
type InvocationComponentWrapperProps = PropsWithChildren & {
selected: boolean;
};
const InvocationComponentWrapper = (props: InvocationComponentWrapperProps) => {
const [nodeSelectedOutline, nodeShadow] = useToken('shadows', [
'nodeSelectedOutline',
'dark-lg',
]);
return (
<Box
sx={{
position: 'relative',
borderRadius: 'md',
minWidth: NODE_MIN_WIDTH,
shadow: props.selected
? `${nodeSelectedOutline}, ${nodeShadow}`
: `${nodeShadow}`,
}}
>
{props.children}
</Box>
);
};
const makeTemplateSelector = (type: AnyInvocationType) =>
createSelector(
[(state: RootState) => state.nodes],
(nodes) => {
const template = nodes.invocationTemplates[type];
if (!template) {
return;
}
return template;
},
{
memoizeOptions: {
resultEqualityCheck: (
a: InvocationTemplate | undefined,
b: InvocationTemplate | undefined
) => a !== undefined && b !== undefined && a.type === b.type,
},
}
);
import { memo, useMemo } from 'react';
import { makeTemplateSelector } from '../store/util/makeTemplateSelector';
import IAINodeHeader from './IAINode/IAINodeHeader';
import IAINodeInputs from './IAINode/IAINodeInputs';
import IAINodeOutputs from './IAINode/IAINodeOutputs';
import IAINodeResizer from './IAINode/IAINodeResizer';
import NodeWrapper from './NodeWrapper';
export const InvocationComponent = memo((props: NodeProps<InvocationValue>) => {
const { id: nodeId, data, selected } = props;
@ -70,7 +22,7 @@ export const InvocationComponent = memo((props: NodeProps<InvocationValue>) => {
if (!template) {
return (
<InvocationComponentWrapper selected={selected}>
<NodeWrapper selected={selected}>
<Flex sx={{ alignItems: 'center', justifyContent: 'center' }}>
<Icon
as={FaExclamationCircle}
@ -82,13 +34,17 @@ export const InvocationComponent = memo((props: NodeProps<InvocationValue>) => {
></Icon>
<IAINodeResizer />
</Flex>
</InvocationComponentWrapper>
</NodeWrapper>
);
}
return (
<InvocationComponentWrapper selected={selected}>
<IAINodeHeader nodeId={nodeId} template={template} />
<NodeWrapper selected={selected}>
<IAINodeHeader
nodeId={nodeId}
title={template.title}
description={template.description}
/>
<Flex
sx={{
flexDirection: 'column',
@ -102,7 +58,7 @@ export const InvocationComponent = memo((props: NodeProps<InvocationValue>) => {
<IAINodeInputs nodeId={nodeId} inputs={inputs} template={template} />
</Flex>
<IAINodeResizer />
</InvocationComponentWrapper>
</NodeWrapper>
);
});

View File

@ -0,0 +1,32 @@
import { Box, useToken } from '@chakra-ui/react';
import { NODE_MIN_WIDTH } from 'app/constants';
import { PropsWithChildren } from 'react';
type NodeWrapperProps = PropsWithChildren & {
selected: boolean;
};
const NodeWrapper = (props: NodeWrapperProps) => {
const [nodeSelectedOutline, nodeShadow] = useToken('shadows', [
'nodeSelectedOutline',
'dark-lg',
]);
return (
<Box
sx={{
position: 'relative',
borderRadius: 'md',
minWidth: NODE_MIN_WIDTH,
shadow: props.selected
? `${nodeSelectedOutline}, ${nodeShadow}`
: `${nodeShadow}`,
}}
>
{props.children}
</Box>
);
};
export default NodeWrapper;

View File

@ -0,0 +1,64 @@
import { Flex, Image } from '@chakra-ui/react';
import { NodeProps } from 'reactflow';
import { InvocationValue } from '../types/types';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { memo } from 'react';
import IAINodeHeader from './IAINode/IAINodeHeader';
import IAINodeResizer from './IAINode/IAINodeResizer';
import NodeWrapper from './NodeWrapper';
const ProgressImageNode = (props: NodeProps<InvocationValue>) => {
const progressImage = useAppSelector((state) => state.system.progressImage);
const { selected } = props;
return (
<NodeWrapper selected={selected}>
<IAINodeHeader
title="Progress Image"
description="Displays the progress image in the Node Editor"
/>
<Flex
sx={{
flexDirection: 'column',
borderBottomRadius: 'md',
p: 2,
bg: 'base.200',
_dark: { bg: 'base.800' },
}}
>
{progressImage ? (
<Image
src={progressImage.dataURL}
sx={{
w: 'full',
h: 'full',
objectFit: 'contain',
}}
/>
) : (
<Flex
sx={{
w: 'full',
h: 'full',
minW: 32,
minH: 32,
alignItems: 'center',
justifyContent: 'center',
}}
>
<IAINoContentFallback />
</Flex>
)}
</Flex>
<IAINodeResizer
maxHeight={progressImage?.height ?? 512}
maxWidth={progressImage?.width ?? 512}
/>
</NodeWrapper>
);
};
export default memo(ProgressImageNode);

View File

@ -5,6 +5,7 @@ import { memo, useCallback } from 'react';
import { Panel } from 'reactflow';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
import NodeInvokeButton from '../ui/NodeInvokeButton';
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
const TopCenterPanel = () => {
const dispatch = useAppDispatch();
@ -17,6 +18,7 @@ const TopCenterPanel = () => {
<Panel position="top-center">
<HStack>
<NodeInvokeButton />
<CancelButton />
<IAIButton onClick={handleReloadSchema}>Reload Schema</IAIButton>
</HStack>
</Panel>

View File

@ -24,7 +24,23 @@ export const useBuildInvocation = () => {
const flow = useReactFlow();
return useCallback(
(type: AnyInvocationType) => {
(type: AnyInvocationType | 'progress_image') => {
if (type === 'progress_image') {
const { x, y } = flow.project({
x: window.innerWidth / 2.5,
y: window.innerHeight / 8,
});
const node: Node = {
id: 'progress_image',
type: 'progress_image',
position: { x: x, y: y },
data: {},
};
return node;
}
const template = invocationTemplates[type];
if (template === undefined) {

View File

@ -0,0 +1,24 @@
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { InvocationTemplate } from 'features/nodes/types/types';
import { AnyInvocationType } from 'services/events/types';
export const makeTemplateSelector = (type: AnyInvocationType) =>
createSelector(
[(state: RootState) => state.nodes],
(nodes) => {
const template = nodes.invocationTemplates[type];
if (!template) {
return;
}
return template;
},
{
memoizeOptions: {
resultEqualityCheck: (
a: InvocationTemplate | undefined,
b: InvocationTemplate | undefined
) => a !== undefined && b !== undefined && a.type === b.type,
},
}
);

View File

@ -54,8 +54,10 @@ export const parseFieldValue = (field: InputFieldValue) => {
export const buildNodesGraph = (state: RootState): Graph => {
const { nodes, edges } = state.nodes;
const filteredNodes = nodes.filter((n) => n.type !== 'progress_image');
// Reduce the node editor nodes into invocation graph nodes
const parsedNodes = nodes.reduce<NonNullable<Graph['nodes']>>(
const parsedNodes = filteredNodes.reduce<NonNullable<Graph['nodes']>>(
(nodesAccumulator, node, nodeIndex) => {
const { id, data } = node;
const { type, inputs } = data;

View File

@ -10,20 +10,19 @@ const ParamModelandVAEandScheduler = () => {
return (
<Flex gap={3} w="full" flexWrap={isVaeEnabled ? 'wrap' : 'nowrap'}>
<Box w="full">
<ModelSelect />
</Box>
<Flex gap={3} w="full">
<Box w="full">
<ModelSelect />
</Box>
{isVaeEnabled && (
<Box w="full">
<VAESelect />
</Box>
)}
<Box w="full">
<ParamScheduler />
</Box>
</Flex>
<Box w="full">
<ParamScheduler />
</Box>
</Flex>
);
};