Merge branch 'main' of https://github.com/invoke-ai/InvokeAI into responsive-ui

This commit is contained in:
SammCheese
2023-04-22 08:27:00 +02:00
329 changed files with 13081 additions and 1437 deletions

View File

@@ -6,19 +6,18 @@ import {
Box,
Flex,
} from '@chakra-ui/react';
import { Feature } from 'app/features';
import GuideIcon from 'common/components/GuideIcon';
import { ReactNode } from 'react';
import { ParametersAccordionItem } from '../ParametersAccordion';
export interface InvokeAccordionItemProps {
header: string;
content: ReactNode;
feature?: Feature;
additionalHeaderComponents?: ReactNode;
}
type InvokeAccordionItemProps = {
accordionItem: ParametersAccordionItem;
};
export default function InvokeAccordionItem(props: InvokeAccordionItemProps) {
const { header, feature, content, additionalHeaderComponents } = props;
export default function InvokeAccordionItem({
accordionItem,
}: InvokeAccordionItemProps) {
const { header, feature, content, additionalHeaderComponents } =
accordionItem;
return (
<AccordionItem>
@@ -32,7 +31,7 @@ export default function InvokeAccordionItem(props: InvokeAccordionItemProps) {
<AccordionIcon />
</Flex>
</AccordionButton>
<AccordionPanel>{content}</AccordionPanel>
<AccordionPanel p={4}>{content}</AccordionPanel>
</AccordionItem>
);
}

View File

@@ -115,9 +115,7 @@ const InfillAndScalingSettings = () => {
onChange={handleChangeBoundingBoxScaleMethod}
/>
<IAISlider
isInputDisabled={!isManual}
isResetDisabled={!isManual}
isSliderDisabled={!isManual}
isDisabled={!isManual}
label={t('parameters.scaledWidth')}
min={64}
max={1024}
@@ -132,9 +130,7 @@ const InfillAndScalingSettings = () => {
handleReset={handleResetScaledWidth}
/>
<IAISlider
isInputDisabled={!isManual}
isResetDisabled={!isManual}
isSliderDisabled={!isManual}
isDisabled={!isManual}
label={t('parameters.scaledHeight')}
min={64}
max={1024}
@@ -155,9 +151,7 @@ const InfillAndScalingSettings = () => {
onChange={(e) => dispatch(setInfillMethod(e.target.value))}
/>
<IAISlider
isInputDisabled={infillMethod !== 'tile'}
isResetDisabled={infillMethod !== 'tile'}
isSliderDisabled={infillMethod !== 'tile'}
isDisabled={infillMethod !== 'tile'}
label={t('parameters.tileSize')}
min={16}
max={64}

View File

@@ -18,9 +18,7 @@ export default function CodeformerFidelity() {
return (
<IAISlider
isSliderDisabled={!isGFPGANAvailable}
isInputDisabled={!isGFPGANAvailable}
isResetDisabled={!isGFPGANAvailable}
isDisabled={!isGFPGANAvailable}
label={t('parameters.codeformerFidelity')}
step={0.05}
min={0}

View File

@@ -18,9 +18,7 @@ export default function FaceRestoreStrength() {
return (
<IAISlider
isSliderDisabled={!isGFPGANAvailable}
isInputDisabled={!isGFPGANAvailable}
isResetDisabled={!isGFPGANAvailable}
isDisabled={!isGFPGANAvailable}
label={t('parameters.strength')}
step={0.05}
min={0}

View File

@@ -12,6 +12,10 @@ export default function ImageFit() {
(state: RootState) => state.generation.shouldFitToWidthHeight
);
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
const handleChangeFit = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldFitToWidthHeight(e.target.checked));
@@ -19,6 +23,7 @@ export default function ImageFit() {
return (
<IAISwitch
isDisabled={!isImageToImageEnabled}
label={t('parameters.imageFit')}
isChecked={shouldFitToWidthHeight}
onChange={handleChangeFit}

View File

@@ -1,13 +1,15 @@
import { VStack } from '@chakra-ui/react';
import { Flex, Image, VStack } from '@chakra-ui/react';
import ImageFit from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageFit';
import ImageToImageStrength from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength';
import { useTranslation } from 'react-i18next';
import InitialImagePreview from './InitialImagePreview';
export default function ImageToImageSettings() {
const { t } = useTranslation();
return (
<VStack gap={2} alignItems="stretch">
<InitialImagePreview />
<ImageToImageStrength label={t('parameters.img2imgStrength')} />
<ImageFit />
</VStack>

View File

@@ -14,6 +14,9 @@ export default function ImageToImageStrength(props: ImageToImageStrengthProps) {
const img2imgStrength = useAppSelector(
(state: RootState) => state.generation.img2imgStrength
);
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
const dispatch = useAppDispatch();
@@ -37,6 +40,7 @@ export default function ImageToImageStrength(props: ImageToImageStrengthProps) {
inputWidth={22}
withReset
handleReset={handleImg2ImgStrengthReset}
isDisabled={!isImageToImageEnabled}
/>
);
}

View File

@@ -0,0 +1,24 @@
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISwitch from 'common/components/IAISwitch';
import { isImageToImageEnabledChanged } from 'features/parameters/store/generationSlice';
import { ChangeEvent } from 'react';
export default function ImageToImageToggle() {
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
const dispatch = useAppDispatch();
const handleChange = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(isImageToImageEnabledChanged(e.target.checked));
return (
<IAISwitch
isChecked={isImageToImageEnabled}
width="auto"
onChange={handleChange}
/>
);
}

View File

@@ -0,0 +1,155 @@
import { Box, Flex, Image, Spinner, Text } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder';
import { useGetUrl } from 'common/util/getUrl';
import useGetImageByNameAndType from 'features/gallery/hooks/useGetImageByName';
import { selectResultsById } from 'features/gallery/store/resultsSlice';
import {
clearInitialImage,
initialImageSelected,
} from 'features/parameters/store/generationSlice';
import { addToast } from 'features/system/store/systemSlice';
import { isEqual } from 'lodash';
import { DragEvent, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ImageType } from 'services/api';
import ImageToImageOverlay from 'common/components/ImageToImageOverlay';
const initialImagePreviewSelector = createSelector(
[(state: RootState) => state],
(state) => {
const { initialImage } = state.generation;
const image = selectResultsById(state, initialImage as string);
return {
initialImage: image,
};
},
{ memoizeOptions: { resultEqualityCheck: isEqual } }
);
const InitialImagePreview = () => {
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
const { initialImage } = useAppSelector(initialImagePreviewSelector);
const { getUrl } = useGetUrl();
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [isLoaded, setIsLoaded] = useState(false);
const getImageByNameAndType = useGetImageByNameAndType();
const onError = () => {
dispatch(
addToast({
title: t('toast.parametersFailed'),
description: t('toast.parametersFailedDesc'),
status: 'error',
isClosable: true,
})
);
dispatch(clearInitialImage());
setIsLoaded(false);
};
const handleDrop = useCallback(
(e: DragEvent<HTMLDivElement>) => {
setIsLoaded(false);
const name = e.dataTransfer.getData('invokeai/imageName');
const type = e.dataTransfer.getData('invokeai/imageType') as ImageType;
if (!name || !type) {
return;
}
const image = getImageByNameAndType(name, type);
if (!image) {
return;
}
dispatch(initialImageSelected(image.name));
},
[getImageByNameAndType, dispatch]
);
return (
<Flex
sx={{
height: 'full',
width: 'full',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
}}
onDrop={handleDrop}
>
<Box
sx={{
height: 'full',
width: 'full',
opacity: isImageToImageEnabled ? 1 : 0.5,
filter: isImageToImageEnabled ? 'none' : 'auto',
blur: '5px',
position: 'relative',
}}
>
{initialImage?.url && (
<>
<Image
sx={{
fit: 'contain',
borderRadius: 'base',
}}
src={getUrl(initialImage?.url)}
onError={onError}
onLoad={() => {
setIsLoaded(true);
}}
fallback={
<Flex
sx={{ h: 36, alignItems: 'center', justifyContent: 'center' }}
>
<Spinner color="grey" w="5rem" h="5rem" />
</Flex>
}
/>
{isLoaded && (
<ImageToImageOverlay
setIsLoaded={setIsLoaded}
image={initialImage}
/>
)}
</>
)}
{!initialImage?.url && <SelectImagePlaceholder />}
</Box>
{!isImageToImageEnabled && (
<Flex
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
}}
>
<Text
fontWeight={500}
fontSize="md"
userSelect="none"
color="base.200"
>
Image to Image is Disabled
</Text>
</Flex>
)}
</Flex>
);
};
export default InitialImagePreview;

View File

@@ -51,9 +51,7 @@ export const HiresStrength = () => {
// inputWidth={22}
withReset
handleReset={handleHiResStrengthReset}
isSliderDisabled={!hiresFix}
isInputDisabled={!hiresFix}
isResetDisabled={!hiresFix}
isDisabled={!hiresFix}
/>
);
};

View File

@@ -30,9 +30,7 @@ export default function UpscaleDenoisingStrength() {
withSliderMarks
withInput
withReset
isSliderDisabled={!isESRGANAvailable}
isInputDisabled={!isESRGANAvailable}
isResetDisabled={!isESRGANAvailable}
isDisabled={!isESRGANAvailable}
/>
);
}

View File

@@ -27,9 +27,7 @@ export default function UpscaleStrength() {
withSliderMarks
withInput
withReset
isSliderDisabled={!isESRGANAvailable}
isInputDisabled={!isESRGANAvailable}
isResetDisabled={!isESRGANAvailable}
isDisabled={!isESRGANAvailable}
/>
);
}

View File

@@ -24,9 +24,7 @@ export default function VariationAmount() {
step={0.01}
min={0}
max={1}
isSliderDisabled={!shouldGenerateVariations}
isInputDisabled={!shouldGenerateVariations}
isResetDisabled={!shouldGenerateVariations}
isDisabled={!shouldGenerateVariations}
onChange={(v) => dispatch(setVariationAmount(v))}
handleReset={() => dispatch(setVariationAmount(0.1))}
withInput

View File

@@ -19,9 +19,7 @@ export default function MainHeight() {
return shouldUseSliders ? (
<IAISlider
isSliderDisabled={activeTabName === 'unifiedCanvas'}
isInputDisabled={activeTabName === 'unifiedCanvas'}
isResetDisabled={activeTabName === 'unifiedCanvas'}
isDisabled={activeTabName === 'unifiedCanvas'}
label={t('parameters.height')}
value={height}
min={64}

View File

@@ -19,9 +19,7 @@ export default function MainWidth() {
return shouldUseSliders ? (
<IAISlider
isSliderDisabled={activeTabName === 'unifiedCanvas'}
isInputDisabled={activeTabName === 'unifiedCanvas'}
isResetDisabled={activeTabName === 'unifiedCanvas'}
isDisabled={activeTabName === 'unifiedCanvas'}
label={t('parameters.width')}
value={width}
min={64}

View File

@@ -1,63 +1,92 @@
import { Accordion } from '@chakra-ui/react';
import { RootState } from 'app/store';
import { createSelector } from '@reduxjs/toolkit';
import { Feature } from 'app/features';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { setOpenAccordions } from 'features/system/store/systemSlice';
import { ReactElement } from 'react';
import InvokeAccordionItem, {
InvokeAccordionItemProps,
} from './AccordionItems/InvokeAccordionItem';
import { tabMap } from 'features/ui/store/tabMap';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { openAccordionItemsChanged } from 'features/ui/store/uiSlice';
import { filter } from 'lodash';
import { ReactNode, useCallback } from 'react';
import InvokeAccordionItem from './AccordionItems/InvokeAccordionItem';
type ParametersAccordionType = {
[parametersAccordionKey: string]: InvokeAccordionItemProps;
const parametersAccordionSelector = createSelector([uiSelector], (uiSlice) => {
const {
activeTab,
openLinearAccordionItems,
openUnifiedCanvasAccordionItems,
disabledParameterPanels,
} = uiSlice;
let openAccordions: number[] = [];
if (tabMap[activeTab] === 'linear') {
openAccordions = openLinearAccordionItems;
}
if (tabMap[activeTab] === 'unifiedCanvas') {
openAccordions = openUnifiedCanvasAccordionItems;
}
return {
openAccordions,
disabledParameterPanels,
};
});
export type ParametersAccordionItem = {
name: string;
header: string;
content: ReactNode;
feature?: Feature;
additionalHeaderComponents?: ReactNode;
};
type ParametersAccordionsType = {
accordionInfo: ParametersAccordionType;
export type ParametersAccordionItems = {
[parametersAccordionKey: string]: ParametersAccordionItem;
};
type ParametersAccordionProps = {
accordionItems: ParametersAccordionItems;
};
/**
* Main container for generation and processing parameters.
*/
const ParametersAccordion = (props: ParametersAccordionsType) => {
const { accordionInfo } = props;
const openAccordions = useAppSelector(
(state: RootState) => state.system.openAccordions
const ParametersAccordion = ({ accordionItems }: ParametersAccordionProps) => {
const { openAccordions, disabledParameterPanels } = useAppSelector(
parametersAccordionSelector
);
const dispatch = useAppDispatch();
/**
* Stores accordion state in redux so preferred UI setup is retained.
*/
const handleChangeAccordionState = (openAccordions: number | number[]) =>
dispatch(setOpenAccordions(openAccordions));
const renderAccordions = () => {
const accordionsToRender: ReactElement[] = [];
if (accordionInfo) {
Object.keys(accordionInfo).forEach((key) => {
const { header, feature, content, additionalHeaderComponents } =
accordionInfo[key];
accordionsToRender.push(
<InvokeAccordionItem
key={key}
header={header}
feature={feature}
content={content}
additionalHeaderComponents={additionalHeaderComponents}
/>
);
});
}
return accordionsToRender;
const handleChangeAccordionState = (openAccordions: number | number[]) => {
dispatch(
openAccordionItemsChanged(
Array.isArray(openAccordions) ? openAccordions : [openAccordions]
)
);
};
// Render function for accordion items
const renderAccordionItems = useCallback(() => {
// Filter out disabled accordions
const filteredAccordionItems = filter(
accordionItems,
(item) => disabledParameterPanels.indexOf(item.name) === -1
);
return filteredAccordionItems.map((accordionItem) => (
<InvokeAccordionItem
key={accordionItem.name}
accordionItem={accordionItem}
/>
));
}, [disabledParameterPanels, accordionItems]);
return (
<Accordion
defaultIndex={openAccordions}
allowMultiple
reduceMotion
onChange={handleChangeAccordionState}
sx={{
display: 'flex',
@@ -65,7 +94,7 @@ const ParametersAccordion = (props: ParametersAccordionsType) => {
gap: 2,
}}
>
{renderAccordions()}
{renderAccordionItems()}
</Accordion>
);
};

View File

@@ -1,5 +1,4 @@
import { createSelector } from '@reduxjs/toolkit';
import { cancelProcessing } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIIconButton, {
IAIIconButtonProps,
@@ -9,16 +8,36 @@ import {
SystemState,
setCancelAfter,
setCancelType,
cancelScheduled,
cancelTypeChanged,
CancelType,
} from 'features/system/store/systemSlice';
import { isEqual } from 'lodash';
import { useEffect, useCallback, memo } from 'react';
import { ButtonSpinner, ButtonGroup } from '@chakra-ui/react';
import {
ButtonSpinner,
ButtonGroup,
Menu,
MenuButton,
MenuList,
MenuOptionGroup,
MenuItemOption,
IconButton,
} from '@chakra-ui/react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { MdCancel, MdCancelScheduleSend } from 'react-icons/md';
import {
MdArrowDropDown,
MdArrowDropUp,
MdCancel,
MdCancelScheduleSend,
} from 'react-icons/md';
import IAISimpleMenu from 'common/components/IAISimpleMenu';
import { sessionCanceled } from 'services/thunks/session';
import { FaChevronDown } from 'react-icons/fa';
import { BiChevronDown } from 'react-icons/bi';
const cancelButtonSelector = createSelector(
systemSelector,
@@ -29,8 +48,11 @@ const cancelButtonSelector = createSelector(
isCancelable: system.isCancelable,
currentIteration: system.currentIteration,
totalIterations: system.totalIterations,
cancelType: system.cancelOptions.cancelType,
cancelAfter: system.cancelOptions.cancelAfter,
// cancelType: system.cancelOptions.cancelType,
// cancelAfter: system.cancelOptions.cancelAfter,
sessionId: system.sessionId,
cancelType: system.cancelType,
isCancelScheduled: system.isCancelScheduled,
};
},
{
@@ -56,16 +78,34 @@ const CancelButton = (
currentIteration,
totalIterations,
cancelType,
cancelAfter,
isCancelScheduled,
// cancelAfter,
sessionId,
} = useAppSelector(cancelButtonSelector);
const handleClickCancel = useCallback(() => {
dispatch(cancelProcessing());
dispatch(setCancelAfter(null));
}, [dispatch]);
if (!sessionId) {
return;
}
if (cancelType === 'scheduled') {
dispatch(cancelScheduled());
return;
}
dispatch(sessionCanceled({ sessionId }));
}, [dispatch, sessionId, cancelType]);
const { t } = useTranslation();
const isCancelScheduled = cancelAfter === null ? false : true;
const handleCancelTypeChanged = useCallback(
(value: string | string[]) => {
const newCancelType = Array.isArray(value) ? value[0] : value;
dispatch(cancelTypeChanged(newCancelType as CancelType));
},
[dispatch]
);
// const isCancelScheduled = cancelAfter === null ? false : true;
useHotkeys(
'shift+x',
@@ -77,22 +117,22 @@ const CancelButton = (
[isConnected, isProcessing, isCancelable]
);
useEffect(() => {
if (cancelAfter !== null && cancelAfter < currentIteration) {
handleClickCancel();
}
}, [cancelAfter, currentIteration, handleClickCancel]);
// useEffect(() => {
// if (cancelAfter !== null && cancelAfter < currentIteration) {
// handleClickCancel();
// }
// }, [cancelAfter, currentIteration, handleClickCancel]);
const cancelMenuItems = [
{
item: t('parameters.cancel.immediate'),
onClick: () => dispatch(setCancelType('immediate')),
},
{
item: t('parameters.cancel.schedule'),
onClick: () => dispatch(setCancelType('scheduled')),
},
];
// const cancelMenuItems = [
// {
// item: t('parameters.cancel.immediate'),
// onClick: () => dispatch(cancelTypeChanged('immediate')),
// },
// {
// item: t('parameters.cancel.schedule'),
// onClick: () => dispatch(cancelTypeChanged('scheduled')),
// },
// ];
return (
<ButtonGroup isAttached width={btnGroupWidth}>
@@ -121,29 +161,40 @@ const CancelButton = (
? t('parameters.cancel.isScheduled')
: t('parameters.cancel.schedule')
}
isDisabled={
!isConnected ||
!isProcessing ||
!isCancelable ||
currentIteration === totalIterations
}
onClick={() => {
// If a cancel request has already been made, and the user clicks again before the next iteration has been processed, stop the request.
if (isCancelScheduled) dispatch(setCancelAfter(null));
else dispatch(setCancelAfter(currentIteration));
}}
isDisabled={!isConnected || !isProcessing || !isCancelable}
onClick={handleClickCancel}
colorScheme="error"
{...rest}
/>
)}
<IAISimpleMenu
menuItems={cancelMenuItems}
iconTooltip={t('parameters.cancel.setType')}
menuButtonProps={{
colorScheme: 'error',
minWidth: 5,
}}
/>
<Menu closeOnSelect={false}>
<MenuButton
as={IconButton}
tooltip={t('parameters.cancel.setType')}
aria-label={t('parameters.cancel.setType')}
icon={<BiChevronDown />}
paddingX={0}
paddingY={0}
colorScheme="error"
minWidth={5}
/>
<MenuList minWidth="240px">
<MenuOptionGroup
value={cancelType}
title="Cancel Type"
type="radio"
onChange={handleCancelTypeChanged}
>
<MenuItemOption value="immediate">
{t('parameters.cancel.immediate')}
</MenuItemOption>
<MenuItemOption value="scheduled">
{t('parameters.cancel.schedule')}
</MenuItemOption>
</MenuOptionGroup>
</MenuList>
</Menu>
</ButtonGroup>
);
};

View File

@@ -11,6 +11,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaPlay } from 'react-icons/fa';
import { linearGraphBuilt, sessionCreated } from 'services/thunks/session';
interface InvokeButton
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
@@ -24,7 +25,8 @@ export default function InvokeButton(props: InvokeButton) {
const activeTabName = useAppSelector(activeTabNameSelector);
const handleClickGenerate = () => {
dispatch(generateImage(activeTabName));
// dispatch(generateImage(activeTabName));
dispatch(linearGraphBuilt());
};
const { t } = useTranslation();