Fixes re-renders triggered by typing prompt

This commit is contained in:
psychedelicious 2022-10-31 22:36:15 +11:00
parent 7b329b7c91
commit c40278dae7
12 changed files with 721 additions and 684 deletions

517
frontend/dist/assets/index.86b555db.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>InvokeAI - A Stable Diffusion Toolkit</title>
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
<script type="module" crossorigin src="./assets/index.fb948d0a.js"></script>
<script type="module" crossorigin src="./assets/index.86b555db.js"></script>
<link rel="stylesheet" href="./assets/index.bb945c0a.css">
</head>

View File

@ -16,6 +16,9 @@ import { createSelector } from '@reduxjs/toolkit';
import { GalleryState } from '../features/gallery/gallerySlice';
import { OptionsState } from '../features/options/optionsSlice';
import { activeTabNameSelector } from '../features/options/optionsSelectors';
import { SystemState } from '../features/system/systemSlice';
import _ from 'lodash';
import { Model } from './invokeai';
keepGUIAlive();
@ -23,9 +26,15 @@ const appSelector = createSelector(
[
(state: RootState) => state.gallery,
(state: RootState) => state.options,
(state: RootState) => state.system,
activeTabNameSelector,
],
(gallery: GalleryState, options: OptionsState, activeTabName) => {
(
gallery: GalleryState,
options: OptionsState,
system: SystemState,
activeTabName
) => {
const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } =
gallery;
const {
@ -34,17 +43,36 @@ const appSelector = createSelector(
shouldPinOptionsPanel,
} = options;
const modelStatusText = _.reduce(
system.model_list,
(acc: string, cur: Model, key: string) => {
if (cur.status === 'active') acc = key;
return acc;
},
''
);
const shouldShowGalleryButton = !(
shouldShowGallery ||
(shouldHoldGalleryOpen && !shouldPinGallery)
);
const shouldShowOptionsPanelButton =
!(
shouldShowOptionsPanel ||
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
) && ['txt2img', 'img2img', 'inpainting'].includes(activeTabName);
return {
shouldShowGalleryButton: !(
shouldShowGallery ||
(shouldHoldGalleryOpen && !shouldPinGallery)
),
shouldShowOptionsPanelButton:
!(
shouldShowOptionsPanel ||
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
) && ['txt2img', 'img2img', 'inpainting'].includes(activeTabName),
modelStatusText,
shouldShowGalleryButton,
shouldShowOptionsPanelButton,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);

View File

@ -0,0 +1,83 @@
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import { RootState } from '../../app/store';
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
import { OptionsState } from '../../features/options/optionsSlice';
import { SystemState } from '../../features/system/systemSlice';
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
import { validateSeedWeights } from '../../common/util/seedWeightPairs';
export const readinessSelector = createSelector(
[
(state: RootState) => state.options,
(state: RootState) => state.system,
(state: RootState) => state.inpainting,
activeTabNameSelector,
],
(
options: OptionsState,
system: SystemState,
inpainting: InpaintingState,
activeTabName
) => {
const {
prompt,
shouldGenerateVariations,
seedWeights,
maskPath,
initialImage,
seed,
} = options;
const { isProcessing, isConnected } = system;
const { imageToInpaint } = inpainting;
// Cannot generate without a prompt
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
return false;
}
if (activeTabName === 'img2img' && !initialImage) {
return false;
}
if (activeTabName === 'inpainting' && !imageToInpaint) {
return false;
}
// Cannot generate with a mask without img2img
if (maskPath && !initialImage) {
return false;
}
// TODO: job queue
// Cannot generate if already processing an image
if (isProcessing) {
return false;
}
// Cannot generate if not connected
if (!isConnected) {
return false;
}
// Cannot generate variations without valid seed weights
if (
shouldGenerateVariations &&
(!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
) {
return false;
}
// All good
return true;
},
{
memoizeOptions: {
equalityCheck: _.isEqual,
resultEqualityCheck: _.isEqual,
},
}
);

View File

@ -1,115 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import { useMemo } from 'react';
import { useAppSelector } from '../../app/store';
import { RootState } from '../../app/store';
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
import { OptionsState } from '../../features/options/optionsSlice';
import { SystemState } from '../../features/system/systemSlice';
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
import { validateSeedWeights } from '../util/seedWeightPairs';
export const useCheckParametersSelector = createSelector(
[
(state: RootState) => state.options,
(state: RootState) => state.system,
(state: RootState) => state.inpainting,
activeTabNameSelector
],
(options: OptionsState, system: SystemState, inpainting: InpaintingState, activeTabName) => {
return {
// options
prompt: options.prompt,
shouldGenerateVariations: options.shouldGenerateVariations,
seedWeights: options.seedWeights,
maskPath: options.maskPath,
initialImage: options.initialImage,
seed: options.seed,
activeTabName,
// system
isProcessing: system.isProcessing,
isConnected: system.isConnected,
// inpainting
hasInpaintingImage: Boolean(inpainting.imageToInpaint),
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
/**
* Checks relevant pieces of state to confirm generation will not deterministically fail.
* This is used to prevent the 'Generate' button from being clicked.
*/
const useCheckParameters = (): boolean => {
const {
prompt,
shouldGenerateVariations,
seedWeights,
maskPath,
initialImage,
seed,
activeTabName,
isProcessing,
isConnected,
hasInpaintingImage,
} = useAppSelector(useCheckParametersSelector);
return useMemo(() => {
// Cannot generate without a prompt
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
return false;
}
if (activeTabName === 'img2img' && !initialImage) {
return false;
}
if (activeTabName === 'inpainting' && !hasInpaintingImage) {
return false;
}
// Cannot generate with a mask without img2img
if (maskPath && !initialImage) {
return false;
}
// TODO: job queue
// Cannot generate if already processing an image
if (isProcessing) {
return false;
}
// Cannot generate if not connected
if (!isConnected) {
return false;
}
// Cannot generate variations without valid seed weights
if (
shouldGenerateVariations &&
(!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
) {
return false;
}
// All good
return true;
}, [
prompt,
maskPath,
isProcessing,
initialImage,
isConnected,
shouldGenerateVariations,
seedWeights,
seed,
activeTabName,
hasInpaintingImage,
]);
};
export default useCheckParameters;

View File

@ -6,6 +6,7 @@ import * as InvokeAI from '../../app/invokeai';
import { useAppDispatch, useAppSelector } from '../../app/store';
import { RootState } from '../../app/store';
import {
OptionsState,
setActiveTab,
setAllParameters,
setInitialImage,
@ -26,15 +27,49 @@ import { useToast } from '@chakra-ui/react';
import { FaCopy, FaPaintBrush, FaSeedling } from 'react-icons/fa';
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
import { hoverableImageSelector } from './gallerySliceSelectors';
import { GalleryState } from './gallerySlice';
import { activeTabNameSelector } from '../options/optionsSelectors';
const intermediateImageSelector = createSelector(
(state: RootState) => state.gallery,
(gallery: GalleryState) => gallery.intermediateImage,
{
memoizeOptions: {
resultEqualityCheck: (a, b) =>
(a === undefined && b === undefined) || a.uuid === b.uuid,
},
}
);
const systemSelector = createSelector(
(state: RootState) => state.system,
(system: SystemState) => {
[
(state: RootState) => state.system,
(state: RootState) => state.options,
intermediateImageSelector,
activeTabNameSelector,
],
(
system: SystemState,
options: OptionsState,
intermediateImage,
activeTabName
) => {
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
system;
const { upscalingLevel, facetoolStrength, shouldShowImageDetails } =
options;
return {
isProcessing: system.isProcessing,
isConnected: system.isConnected,
isGFPGANAvailable: system.isGFPGANAvailable,
isESRGANAvailable: system.isESRGANAvailable,
isProcessing,
isConnected,
isGFPGANAvailable,
isESRGANAvailable,
upscalingLevel,
facetoolStrength,
intermediateImage,
shouldShowImageDetails,
activeTabName,
};
},
{
@ -54,29 +89,20 @@ type CurrentImageButtonsProps = {
*/
const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
const dispatch = useAppDispatch();
const { activeTabName } = useAppSelector(hoverableImageSelector);
const shouldShowImageDetails = useAppSelector(
(state: RootState) => state.options.shouldShowImageDetails
);
const {
isProcessing,
isConnected,
isGFPGANAvailable,
isESRGANAvailable,
upscalingLevel,
facetoolStrength,
intermediateImage,
shouldShowImageDetails,
activeTabName,
} = useAppSelector(systemSelector);
const toast = useToast();
const intermediateImage = useAppSelector(
(state: RootState) => state.gallery.intermediateImage
);
const upscalingLevel = useAppSelector(
(state: RootState) => state.options.upscalingLevel
);
const facetoolStrength = useAppSelector(
(state: RootState) => state.options.facetoolStrength
);
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
useAppSelector(systemSelector);
const handleClickUseAsInitialImage = () => {
dispatch(setInitialImage(image));
dispatch(setActiveTab(1));

View File

@ -1,14 +1,12 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { BsImageFill, BsPlayFill } from 'react-icons/bs';
import { FaPlay, FaPlayCircle } from 'react-icons/fa';
import { IoPlay } from 'react-icons/io5';
import { FaPlay } from 'react-icons/fa';
import { readinessSelector } from '../../../app/selectors/readinessSelector';
import { generateImage } from '../../../app/socketio/actions';
import { useAppDispatch, useAppSelector } from '../../../app/store';
import IAIButton, {
IAIButtonProps,
} from '../../../common/components/IAIButton';
import IAIIconButton from '../../../common/components/IAIIconButton';
import useCheckParameters from '../../../common/hooks/useCheckParameters';
import { activeTabNameSelector } from '../optionsSelectors';
interface InvokeButton extends Omit<IAIButtonProps, 'label'> {
@ -18,8 +16,7 @@ interface InvokeButton extends Omit<IAIButtonProps, 'label'> {
export default function InvokeButton(props: InvokeButton) {
const { iconButton = false, ...rest } = props;
const dispatch = useAppDispatch();
const isReady = useCheckParameters();
const isReady = useAppSelector(readinessSelector);
const activeTabName = useAppSelector(activeTabNameSelector);
const handleClickGenerate = () => {

View File

@ -1,13 +1,18 @@
import { createSelector } from '@reduxjs/toolkit';
import { FaRecycle } from 'react-icons/fa';
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
import IAIIconButton from '../../../common/components/IAIIconButton';
import { setShouldLoopback } from '../optionsSlice';
import { OptionsState, setShouldLoopback } from '../optionsSlice';
const loopbackSelector = createSelector(
(state: RootState) => state.options,
(options: OptionsState) => options.shouldLoopback
);
const LoopbackButton = () => {
const dispatch = useAppDispatch();
const { shouldLoopback } = useAppSelector(
(state: RootState) => state.options
);
const shouldLoopback = useAppSelector(loopbackSelector);
return (
<IAIIconButton
aria-label="Loopback"

View File

@ -6,9 +6,9 @@ import { generateImage } from '../../../app/socketio/actions';
import { OptionsState, setPrompt } from '../optionsSlice';
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import useCheckParameters from '../../../common/hooks/useCheckParameters';
import { useHotkeys } from 'react-hotkeys-hook';
import { activeTabNameSelector } from '../optionsSelectors';
import { readinessSelector } from '../../../app/selectors/readinessSelector';
const promptInputSelector = createSelector(
[(state: RootState) => state.options, activeTabNameSelector],
@ -29,10 +29,11 @@ const promptInputSelector = createSelector(
* Prompt input text area.
*/
const PromptInput = () => {
const promptRef = useRef<HTMLTextAreaElement>(null);
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
const dispatch = useAppDispatch();
const isReady = useCheckParameters();
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
const isReady = useAppSelector(readinessSelector);
const promptRef = useRef<HTMLTextAreaElement>(null);
const handleChangePrompt = (e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(setPrompt(e.target.value));

View File

@ -1,9 +1,15 @@
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import { RootState } from '../../app/store';
import { tabMap } from '../tabs/InvokeTabs';
import { OptionsState } from './optionsSlice';
export const activeTabNameSelector = createSelector(
(state: RootState) => state.options,
(options: OptionsState) => tabMap[options.activeTab]
(options: OptionsState) => tabMap[options.activeTab],
{
memoizeOptions: {
equalityCheck: _.isEqual,
},
}
);

View File

@ -1,5 +1,6 @@
import { Tooltip } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import { MouseEvent, ReactNode, useCallback, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
@ -33,6 +34,11 @@ const optionsPanelSelector = createSelector(
shouldPinOptionsPanel,
optionsPanelScrollPosition,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);