Adds hints when unable to invoke

- Popover on Invoke button indicates why exactly it is disabled, e.g. prompt is empty, something else is processing, etc. 
- There may be more than one reason; all are displayed.
This commit is contained in:
psychedelicious 2022-11-01 23:52:07 +11:00 committed by Lincoln Stein
parent 0eef74bc00
commit 871a8a5375
16 changed files with 606 additions and 557 deletions

517
frontend/dist/assets/index.3a0c23f9.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

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,8 +6,8 @@
<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.784f40b4.js"></script> <script type="module" crossorigin src="./assets/index.3a0c23f9.js"></script>
<link rel="stylesheet" href="./assets/index.e29f3ddc.css"> <link rel="stylesheet" href="./assets/index.c1f2c49a.css">
</head> </head>
<body> <body>

View File

@ -1,8 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect } from 'react';
import ProgressBar from '../features/system/ProgressBar'; import ProgressBar from '../features/system/ProgressBar';
import SiteHeader from '../features/system/SiteHeader'; import SiteHeader from '../features/system/SiteHeader';
import Console from '../features/system/Console'; import Console from '../features/system/Console';
import Loading from '../Loading';
import { useAppDispatch } from './store'; import { useAppDispatch } from './store';
import { requestSystemConfig } from './socketio/actions'; import { requestSystemConfig } from './socketio/actions';
import { keepGUIAlive } from './utils'; import { keepGUIAlive } from './utils';
@ -16,9 +15,10 @@ 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 { readinessChanged, SystemState } from '../features/system/systemSlice';
import _ from 'lodash'; import _ from 'lodash';
import { Model } from './invokeai'; import { Model } from './invokeai';
import { readinessSelector } from './selectors/readinessSelector';
keepGUIAlive(); keepGUIAlive();
@ -79,17 +79,20 @@ const appSelector = createSelector(
const App = () => { const App = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [isReady, setIsReady] = useState<boolean>(false); // const [isReady, setIsReady] = useState<boolean>(false);
const { isReady, reasonsWhyNotReady } = useAppSelector(readinessSelector);
const { shouldShowGalleryButton, shouldShowOptionsPanelButton } = const { shouldShowGalleryButton, shouldShowOptionsPanelButton } =
useAppSelector(appSelector); useAppSelector(appSelector);
useEffect(() => { useEffect(() => {
dispatch(requestSystemConfig()); dispatch(requestSystemConfig());
setIsReady(true);
}, [dispatch]); }, [dispatch]);
return isReady ? ( useEffect(() => {
dispatch(readinessChanged({ isReady, reasonsWhyNotReady }));
}, [dispatch, isReady, reasonsWhyNotReady]);
return (
<div className="App"> <div className="App">
<ImageUploader> <ImageUploader>
<ProgressBar /> <ProgressBar />
@ -104,8 +107,6 @@ const App = () => {
{shouldShowOptionsPanelButton && <FloatingOptionsPanelButtons />} {shouldShowOptionsPanelButton && <FloatingOptionsPanelButtons />}
</ImageUploader> </ImageUploader>
</div> </div>
) : (
<Loading />
); );
}; };

View File

@ -1,6 +1,6 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash'; import _ from 'lodash';
import { RootState } from '../../app/store'; import { RootState } from '../store';
import { activeTabNameSelector } from '../../features/options/optionsSelectors'; import { activeTabNameSelector } from '../../features/options/optionsSelectors';
import { OptionsState } from '../../features/options/optionsSlice'; import { OptionsState } from '../../features/options/optionsSlice';
@ -25,7 +25,7 @@ export const readinessSelector = createSelector(
prompt, prompt,
shouldGenerateVariations, shouldGenerateVariations,
seedWeights, seedWeights,
maskPath, // maskPath,
initialImage, initialImage,
seed, seed,
} = options; } = options;
@ -34,33 +34,49 @@ export const readinessSelector = createSelector(
const { imageToInpaint } = inpainting; const { imageToInpaint } = inpainting;
let isReady = true;
const reasonsWhyNotReady: string[] = [];
// Cannot generate without a prompt // Cannot generate without a prompt
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) { if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
return false; isReady = false;
reasonsWhyNotReady.push('Missing a prompt.');
} }
if (activeTabName === 'img2img' && !initialImage) { if (activeTabName === 'img2img' && !initialImage) {
return false; isReady = false;
reasonsWhyNotReady.push(
'On ImageToImage tab, but no initial image is selected.'
);
} }
if (activeTabName === 'inpainting' && !imageToInpaint) { if (activeTabName === 'inpainting' && !imageToInpaint) {
return false; isReady = false;
reasonsWhyNotReady.push(
'On Inpainting tab, but no initial image is selected.'
);
} }
// Cannot generate with a mask without img2img // // We don't use mask paths now.
if (maskPath && !initialImage) { // // Cannot generate with a mask without img2img
return false; // if (maskPath && !initialImage) {
} // isReady = false;
// reasonsWhyNotReady.push(
// 'On ImageToImage tab, but no mask is provided.'
// );
// }
// TODO: job queue // TODO: job queue
// Cannot generate if already processing an image // Cannot generate if already processing an image
if (isProcessing) { if (isProcessing) {
return false; isReady = false;
reasonsWhyNotReady.push('System is already processing something.');
} }
// Cannot generate if not connected // Cannot generate if not connected
if (!isConnected) { if (!isConnected) {
return false; isReady = false;
reasonsWhyNotReady.push('System is disconnected.');
} }
// Cannot generate variations without valid seed weights // Cannot generate variations without valid seed weights
@ -68,11 +84,12 @@ export const readinessSelector = createSelector(
shouldGenerateVariations && shouldGenerateVariations &&
(!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1) (!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
) { ) {
return false; isReady = false;
reasonsWhyNotReady.push('Seed-weight pairs are badly formatted.');
} }
// All good // All good
return true; return { isReady, reasonsWhyNotReady };
}, },
{ {
memoizeOptions: { memoizeOptions: {

View File

@ -4,12 +4,14 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
Box, Box,
BoxProps,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { PopoverProps } from '@chakra-ui/react'; import { PopoverProps } from '@chakra-ui/react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
type IAIPopoverProps = PopoverProps & { type IAIPopoverProps = PopoverProps & {
triggerComponent: ReactNode; triggerComponent: ReactNode;
triggerContainerProps?: BoxProps;
children: ReactNode; children: ReactNode;
styleClass?: string; styleClass?: string;
hasArrow?: boolean; hasArrow?: boolean;
@ -18,15 +20,17 @@ type IAIPopoverProps = PopoverProps & {
const IAIPopover = (props: IAIPopoverProps) => { const IAIPopover = (props: IAIPopoverProps) => {
const { const {
triggerComponent, triggerComponent,
triggerContainerProps,
children, children,
styleClass, styleClass,
hasArrow = true, hasArrow = true,
...rest ...rest
} = props; } = props;
return ( return (
<Popover {...rest}> <Popover {...rest}>
<PopoverTrigger> <PopoverTrigger>
<Box>{triggerComponent}</Box> <Box {...triggerContainerProps}>{triggerComponent}</Box>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className={`invokeai__popover-content ${styleClass}`}> <PopoverContent className={`invokeai__popover-content ${styleClass}`}>
{hasArrow && <PopoverArrow className={'invokeai__popover-arrow'} />} {hasArrow && <PopoverArrow className={'invokeai__popover-arrow'} />}

View File

@ -1,3 +1,4 @@
import { ListItem, UnorderedList } from '@chakra-ui/react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { FaPlay } from 'react-icons/fa'; import { FaPlay } from 'react-icons/fa';
import { readinessSelector } from '../../../app/selectors/readinessSelector'; import { readinessSelector } from '../../../app/selectors/readinessSelector';
@ -7,6 +8,7 @@ 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 IAIPopover from '../../../common/components/IAIPopover';
import { activeTabNameSelector } from '../optionsSelectors'; import { activeTabNameSelector } from '../optionsSelectors';
interface InvokeButton extends Omit<IAIButtonProps, 'label'> { interface InvokeButton extends Omit<IAIButtonProps, 'label'> {
@ -16,7 +18,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 = useAppSelector(readinessSelector); const { isReady, reasonsWhyNotReady } = useAppSelector(readinessSelector);
const activeTabName = useAppSelector(activeTabNameSelector); const activeTabName = useAppSelector(activeTabNameSelector);
const handleClickGenerate = () => { const handleClickGenerate = () => {
@ -33,7 +35,7 @@ export default function InvokeButton(props: InvokeButton) {
[isReady, activeTabName] [isReady, activeTabName]
); );
return iconButton ? ( const buttonComponent = iconButton ? (
<IAIIconButton <IAIIconButton
aria-label="Invoke" aria-label="Invoke"
type="submit" type="submit"
@ -56,4 +58,22 @@ export default function InvokeButton(props: InvokeButton) {
{...rest} {...rest}
/> />
); );
return isReady ? (
buttonComponent
) : (
<IAIPopover
trigger="hover"
triggerContainerProps={{ style: { flexGrow: 4 } }}
triggerComponent={buttonComponent}
>
{reasonsWhyNotReady && (
<UnorderedList>
{reasonsWhyNotReady.map((reason, i) => (
<ListItem key={i}>{reason}</ListItem>
))}
</UnorderedList>
)}
</IAIPopover>
);
} }

View File

@ -7,6 +7,7 @@
.invoke-btn { .invoke-btn {
flex-grow: 1; flex-grow: 1;
width: 100%;
svg { svg {
width: 18px !important; width: 18px !important;
height: 18px !important; height: 18px !important;

View File

@ -31,7 +31,7 @@ const promptInputSelector = createSelector(
const PromptInput = () => { const PromptInput = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { prompt, activeTabName } = useAppSelector(promptInputSelector); const { prompt, activeTabName } = useAppSelector(promptInputSelector);
const isReady = useAppSelector(readinessSelector); const { isReady } = useAppSelector(readinessSelector);
const promptRef = useRef<HTMLTextAreaElement>(null); const promptRef = useRef<HTMLTextAreaElement>(null);

View File

@ -15,6 +15,11 @@ export interface Log {
[index: number]: LogEntry; [index: number]: LogEntry;
} }
export type ReadinessPayload = {
isReady: boolean;
reasonsWhyNotReady: string[];
};
export interface SystemState export interface SystemState
extends InvokeAI.SystemStatus, extends InvokeAI.SystemStatus,
InvokeAI.SystemConfig { InvokeAI.SystemConfig {
@ -36,6 +41,8 @@ export interface SystemState
shouldDisplayGuides: boolean; shouldDisplayGuides: boolean;
wasErrorSeen: boolean; wasErrorSeen: boolean;
isCancelable: boolean; isCancelable: boolean;
isReady: boolean;
reasonsWhyNotReady: string[];
} }
const initialSystemState = { const initialSystemState = {
@ -65,6 +72,8 @@ const initialSystemState = {
hasError: false, hasError: false,
wasErrorSeen: true, wasErrorSeen: true,
isCancelable: true, isCancelable: true,
isReady: false,
reasonsWhyNotReady: [],
}; };
const initialState: SystemState = initialSystemState; const initialState: SystemState = initialSystemState;
@ -178,6 +187,11 @@ export const systemSlice = createSlice({
state.isProcessing = true; state.isProcessing = true;
state.currentStatusHasSteps = false; state.currentStatusHasSteps = false;
}, },
readinessChanged: (state, action: PayloadAction<ReadinessPayload>) => {
const { isReady, reasonsWhyNotReady } = action.payload;
state.isReady = isReady;
state.reasonsWhyNotReady = reasonsWhyNotReady;
},
}, },
}); });
@ -200,6 +214,7 @@ export const {
setModelList, setModelList,
setIsCancelable, setIsCancelable,
modelChangeRequested, modelChangeRequested,
readinessChanged,
} = systemSlice.actions; } = systemSlice.actions;
export default systemSlice.reducer; export default systemSlice.reducer;

View File

@ -34,10 +34,7 @@ import InpaintingBoundingBoxPreview, {
} from './components/InpaintingBoundingBoxPreview'; } from './components/InpaintingBoundingBoxPreview';
import { KonvaEventObject } from 'konva/lib/Node'; import { KonvaEventObject } from 'konva/lib/Node';
import KeyboardEventManager from './components/KeyboardEventManager'; import KeyboardEventManager from './components/KeyboardEventManager';
import { Icon, IconButton, Tooltip, useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import { FaLock, FaUnlock } from 'react-icons/fa';
import { MdInvertColors, MdInvertColorsOff } from 'react-icons/md';
import { BiHide, BiShow } from 'react-icons/bi';
import InpaintingCanvasStatusIcons from './InpaintingCanvasStatusIcons'; import InpaintingCanvasStatusIcons from './InpaintingCanvasStatusIcons';
// Use a closure allow other components to use these things... not ideal... // Use a closure allow other components to use these things... not ideal...
@ -60,9 +57,6 @@ const InpaintingCanvas = () => {
shouldShowBoundingBox, shouldShowBoundingBox,
shouldShowBoundingBoxFill, shouldShowBoundingBoxFill,
isDrawing, isDrawing,
shouldLockBoundingBox,
boundingBoxDimensions,
// isTransformingBoundingBox,
isMouseOverBoundingBox, isMouseOverBoundingBox,
isModifyingBoundingBox, isModifyingBoundingBox,
stageCursor, stageCursor,

View File

@ -5,7 +5,7 @@ import { KonvaEventObject } from 'konva/lib/Node';
import { Box } from 'konva/lib/shapes/Transformer'; import { Box } from 'konva/lib/shapes/Transformer';
import { Vector2d } from 'konva/lib/types'; import { Vector2d } from 'konva/lib/types';
import _ from 'lodash'; import _ from 'lodash';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { Group, Rect, Transformer } from 'react-konva'; import { Group, Rect, Transformer } from 'react-konva';
import { import {
RootState, RootState,
@ -13,7 +13,6 @@ import {
useAppSelector, useAppSelector,
} from '../../../../app/store'; } from '../../../../app/store';
import { roundToMultiple } from '../../../../common/util/roundDownToMultiple'; import { roundToMultiple } from '../../../../common/util/roundDownToMultiple';
import { stageRef } from '../InpaintingCanvas';
import { import {
InpaintingState, InpaintingState,
setBoundingBoxCoordinate, setBoundingBoxCoordinate,

View File

@ -10,9 +10,7 @@ import { activeTabNameSelector } from '../../../options/optionsSelectors';
import { OptionsState } from '../../../options/optionsSlice'; import { OptionsState } from '../../../options/optionsSlice';
import { import {
InpaintingState, InpaintingState,
setIsDrawing,
setIsSpacebarHeld, setIsSpacebarHeld,
setNeedsCache,
setShouldLockBoundingBox, setShouldLockBoundingBox,
toggleTool, toggleTool,
} from '../inpaintingSlice'; } from '../inpaintingSlice';