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

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

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

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

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

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

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

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

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

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

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

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