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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>InvokeAI - A Stable Diffusion Toolkit</title> <title>InvokeAI - A Stable Diffusion Toolkit</title>
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" /> <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"> <link rel="stylesheet" href="./assets/index.bb945c0a.css">
</head> </head>

View File

@ -16,6 +16,9 @@ import { createSelector } from '@reduxjs/toolkit';
import { GalleryState } from '../features/gallery/gallerySlice'; import { GalleryState } from '../features/gallery/gallerySlice';
import { OptionsState } from '../features/options/optionsSlice'; import { OptionsState } from '../features/options/optionsSlice';
import { activeTabNameSelector } from '../features/options/optionsSelectors'; import { activeTabNameSelector } from '../features/options/optionsSelectors';
import { SystemState } from '../features/system/systemSlice';
import _ from 'lodash';
import { Model } from './invokeai';
keepGUIAlive(); keepGUIAlive();
@ -23,9 +26,15 @@ const appSelector = createSelector(
[ [
(state: RootState) => state.gallery, (state: RootState) => state.gallery,
(state: RootState) => state.options, (state: RootState) => state.options,
(state: RootState) => state.system,
activeTabNameSelector, activeTabNameSelector,
], ],
(gallery: GalleryState, options: OptionsState, activeTabName) => { (
gallery: GalleryState,
options: OptionsState,
system: SystemState,
activeTabName
) => {
const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } = const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } =
gallery; gallery;
const { const {
@ -34,17 +43,36 @@ const appSelector = createSelector(
shouldPinOptionsPanel, shouldPinOptionsPanel,
} = options; } = 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 { return {
shouldShowGalleryButton: !( modelStatusText,
shouldShowGallery || shouldShowGalleryButton,
(shouldHoldGalleryOpen && !shouldPinGallery) shouldShowOptionsPanelButton,
),
shouldShowOptionsPanelButton:
!(
shouldShowOptionsPanel ||
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
) && ['txt2img', 'img2img', 'inpainting'].includes(activeTabName),
}; };
},
{
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 { useAppDispatch, useAppSelector } from '../../app/store';
import { RootState } from '../../app/store'; import { RootState } from '../../app/store';
import { import {
OptionsState,
setActiveTab, setActiveTab,
setAllParameters, setAllParameters,
setInitialImage, setInitialImage,
@ -26,15 +27,49 @@ import { useToast } from '@chakra-ui/react';
import { FaCopy, FaPaintBrush, FaSeedling } from 'react-icons/fa'; import { FaCopy, FaPaintBrush, FaSeedling } from 'react-icons/fa';
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice'; import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
import { hoverableImageSelector } from './gallerySliceSelectors'; 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( 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 { return {
isProcessing: system.isProcessing, isProcessing,
isConnected: system.isConnected, isConnected,
isGFPGANAvailable: system.isGFPGANAvailable, isGFPGANAvailable,
isESRGANAvailable: system.isESRGANAvailable, isESRGANAvailable,
upscalingLevel,
facetoolStrength,
intermediateImage,
shouldShowImageDetails,
activeTabName,
}; };
}, },
{ {
@ -54,29 +89,20 @@ type CurrentImageButtonsProps = {
*/ */
const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => { const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { activeTabName } = useAppSelector(hoverableImageSelector); const {
isProcessing,
const shouldShowImageDetails = useAppSelector( isConnected,
(state: RootState) => state.options.shouldShowImageDetails isGFPGANAvailable,
); isESRGANAvailable,
upscalingLevel,
facetoolStrength,
intermediateImage,
shouldShowImageDetails,
activeTabName,
} = useAppSelector(systemSelector);
const toast = useToast(); 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 = () => { const handleClickUseAsInitialImage = () => {
dispatch(setInitialImage(image)); dispatch(setInitialImage(image));
dispatch(setActiveTab(1)); dispatch(setActiveTab(1));

View File

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

View File

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

View File

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

View File

@ -1,9 +1,15 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import { RootState } from '../../app/store'; import { RootState } from '../../app/store';
import { tabMap } from '../tabs/InvokeTabs'; import { tabMap } from '../tabs/InvokeTabs';
import { OptionsState } from './optionsSlice'; import { OptionsState } from './optionsSlice';
export const activeTabNameSelector = createSelector( export const activeTabNameSelector = createSelector(
(state: RootState) => state.options, (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 { Tooltip } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import { MouseEvent, ReactNode, useCallback, useRef } from 'react'; import { MouseEvent, ReactNode, useCallback, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs'; import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
@ -33,6 +34,11 @@ const optionsPanelSelector = createSelector(
shouldPinOptionsPanel, shouldPinOptionsPanel,
optionsPanelScrollPosition, optionsPanelScrollPosition,
}; };
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
} }
); );