feat(ui): dynamic prompts loading ux

- Prompt must have an open curly brace followed by a close curly brace to enable dynamic prompts processing
- If a the given prompt already had a dynamic prompt cached, do not re-process
- If processing is not needed, user may invoke immediately
- Invoke button shows loading state when dynamic prompts are processing, tooltip says generating
- Dynamic prompts preview icon in prompt box shows loading state when processing, tooltip says generating
This commit is contained in:
psychedelicious 2024-01-02 20:43:25 +11:00 committed by Kent Keirsey
parent 0b4eb888c5
commit 870cc5b733
9 changed files with 85 additions and 36 deletions

View File

@ -1218,7 +1218,8 @@
"perIterationDesc": "Use a different seed for each iteration",
"perPromptLabel": "Seed per Image",
"perPromptDesc": "Use a different seed for each image"
}
},
"loading": "Generating Dynamic Prompts..."
},
"sdxl": {
"cfgScale": "CFG Scale",

View File

@ -8,6 +8,7 @@ import {
parsingErrorChanged,
promptsChanged,
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { setPositivePrompt } from 'features/parameters/store/generationSlice';
import { utilitiesApi } from 'services/api/endpoints/utilities';
import { appSocketConnected } from 'services/events/actions';
@ -29,20 +30,39 @@ export const addDynamicPromptsListener = () => {
action,
{ dispatch, getState, cancelActiveListeners, delay }
) => {
// debounce request
cancelActiveListeners();
await delay(1000);
const state = getState();
const { positivePrompt } = state.generation;
const { maxPrompts } = state.dynamicPrompts;
if (state.config.disabledFeatures.includes('dynamicPrompting')) {
return;
}
const { positivePrompt } = state.generation;
const { maxPrompts } = state.dynamicPrompts;
const cachedPrompts = utilitiesApi.endpoints.dynamicPrompts.select({
prompt: positivePrompt,
max_prompts: maxPrompts,
})(getState()).data;
dispatch(isLoadingChanged(true));
if (cachedPrompts) {
dispatch(promptsChanged(cachedPrompts.prompts));
return;
}
if (!getShouldProcessPrompt(state.generation.positivePrompt)) {
if (state.dynamicPrompts.isLoading) {
dispatch(isLoadingChanged(false));
}
dispatch(promptsChanged([state.generation.positivePrompt]));
return;
}
if (!state.dynamicPrompts.isLoading) {
dispatch(isLoadingChanged(true));
}
// debounce request
await delay(1000);
try {
const req = dispatch(

View File

@ -1,22 +1,10 @@
import { SpinnerIcon } from '@chakra-ui/icons';
import {
forwardRef,
keyframes,
MenuItem as ChakraMenuItem,
} from '@chakra-ui/react';
import { forwardRef, MenuItem as ChakraMenuItem } from '@chakra-ui/react';
import { memo } from 'react';
import { spinAnimation } from 'theme/animations';
import type { InvMenuItemProps } from './types';
const spin = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
export const InvMenuItem = memo(
forwardRef<InvMenuItemProps, typeof ChakraMenuItem>(
(props: InvMenuItemProps, ref) => {
@ -30,13 +18,7 @@ export const InvMenuItem = memo(
return (
<ChakraMenuItem
ref={ref}
icon={
isLoading ? (
<SpinnerIcon animation={`${spin} 1s linear infinite`} />
) : (
icon
)
}
icon={isLoading ? <SpinnerIcon animation={spinAnimation} /> : icon}
isDisabled={isLoading || isDisabled}
data-destructive={isDestructive}
{...rest}

View File

@ -3,6 +3,7 @@ import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { selectControlAdapterAll } from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import i18n from 'i18next';
@ -22,7 +23,7 @@ const selector = createMemoizedSelector(
},
activeTabName
) => {
const { initialImage, model } = generation;
const { initialImage, model, positivePrompt } = generation;
const { isConnected } = system;
@ -87,7 +88,7 @@ const selector = createMemoizedSelector(
});
}
} else {
if (dynamicPrompts.prompts.length === 0) {
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
reasons.push(i18n.t('parameters.invoke.noPrompts'));
}

View File

@ -1,15 +1,29 @@
import type { SystemStyleObject } from '@chakra-ui/styled-system';
import { useAppSelector } from 'app/store/storeHooks';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { useDynamicPromptsModal } from 'features/dynamicPrompts/hooks/useDynamicPromptsModal';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { BsBracesAsterisk } from 'react-icons/bs';
import { spinAnimation } from 'theme/animations';
const loadingStyles: SystemStyleObject = {
svg: { animation: spinAnimation },
};
export const ShowDynamicPromptsPreviewButton = memo(() => {
const { t } = useTranslation();
const isLoading = useAppSelector((state) => state.dynamicPrompts.isLoading);
const { isOpen, onOpen } = useDynamicPromptsModal();
return (
<InvTooltip label={t('dynamicPrompts.showDynamicPrompts')}>
<InvTooltip
label={
isLoading
? t('dynamicPrompts.loading')
: t('dynamicPrompts.showDynamicPrompts')
}
>
<InvIconButton
size="sm"
variant="promptOverlay"
@ -17,6 +31,7 @@ export const ShowDynamicPromptsPreviewButton = memo(() => {
aria-label={t('dynamicPrompts.showDynamicPrompts')}
icon={<BsBracesAsterisk />}
onClick={onOpen}
sx={isLoading ? loadingStyles : undefined}
/>
</InvTooltip>
);

View File

@ -0,0 +1,3 @@
const hasOpenCloseCurlyBracesRegex = /.*\{.*\}.*/;
export const getShouldProcessPrompt = (prompt: string): boolean =>
hasOpenCloseCurlyBracesRegex.test(prompt);

View File

@ -41,6 +41,9 @@ const selector = createMemoizedSelector([stateSelector], (state) => {
});
export const InvokeQueueBackButton = memo(() => {
const isLoadingDynamicPrompts = useAppSelector(
(state) => state.dynamicPrompts.isLoading
);
const { queueBack, isLoading, isDisabled } = useQueueBack();
const { iterations, step, fineStep } = useAppSelector(selector);
const dispatch = useAppDispatch();
@ -73,7 +76,8 @@ export const InvokeQueueBackButton = memo(() => {
</IAIInformationalPopover>
<InvButton
onClick={queueBack}
isLoading={isLoading}
isLoading={isLoading || isLoadingDynamicPrompts}
loadingText={invoke}
isDisabled={isDisabled}
rightIcon={<IoSparkles />}
tooltip={<QueueButtonTooltip />}
@ -83,8 +87,10 @@ export const InvokeQueueBackButton = memo(() => {
size="lg"
w="calc(100% - 60px)"
flexShrink={0}
justifyContent="space-between"
spinnerPlacement="end"
>
{invoke}
<span>{invoke}</span>
<Spacer />
</InvButton>
</Flex>

View File

@ -4,6 +4,7 @@ import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { InvText } from 'common/components/InvText/wrapper';
import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useEnqueueBatchMutation } from 'services/api/endpoints/queue';
@ -15,8 +16,10 @@ const tooltipSelector = createMemoizedSelector(
[stateSelector],
({ gallery, dynamicPrompts, generation }) => {
const { autoAddBoardId } = gallery;
const promptsCount = dynamicPrompts.prompts.length;
const { iterations } = generation;
const { iterations, positivePrompt } = generation;
const promptsCount = getShouldProcessPrompt(positivePrompt)
? dynamicPrompts.prompts.length
: 1;
return {
autoAddBoardId,
promptsCount,
@ -32,6 +35,9 @@ type Props = {
export const QueueButtonTooltip = memo(({ prepend = false }: Props) => {
const { t } = useTranslation();
const { isReady, reasons } = useIsReadyToEnqueue();
const isLoadingDynamicPrompts = useAppSelector(
(state) => state.dynamicPrompts.isLoading
);
const { autoAddBoardId, promptsCount, iterations } =
useAppSelector(tooltipSelector);
const autoAddBoardName = useBoardName(autoAddBoardId);
@ -43,6 +49,9 @@ export const QueueButtonTooltip = memo(({ prepend = false }: Props) => {
if (isLoading) {
return t('queue.enqueueing');
}
if (isLoadingDynamicPrompts) {
return t('dynamicPrompts.loading');
}
if (isReady) {
if (prepend) {
return t('queue.queueFront');
@ -50,7 +59,7 @@ export const QueueButtonTooltip = memo(({ prepend = false }: Props) => {
return t('queue.queueBack');
}
return t('queue.notReady');
}, [isLoading, isReady, prepend, t]);
}, [isLoading, isLoadingDynamicPrompts, isReady, prepend, t]);
return (
<Flex flexDir="column" gap={1}>

View File

@ -0,0 +1,12 @@
import { keyframes } from '@emotion/react';
export const spinKeyframes = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
export const spinAnimation = `${spinKeyframes} 0.45s linear infinite`;