From 870cc5b73371ccfaa29727ec5df28d279f8e29a1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 2 Jan 2024 20:43:25 +1100 Subject: [PATCH] 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 --- invokeai/frontend/web/public/locales/en.json | 3 +- .../listeners/promptChanged.ts | 32 +++++++++++++++---- .../common/components/InvMenu/InvMenuItem.tsx | 24 ++------------ .../src/common/hooks/useIsReadyToEnqueue.ts | 5 +-- .../ShowDynamicPromptsPreviewButton.tsx | 17 +++++++++- .../util/getShouldProcessPrompt.ts | 3 ++ .../components/InvokeQueueBackButton.tsx | 10 ++++-- .../queue/components/QueueButtonTooltip.tsx | 15 +++++++-- invokeai/frontend/web/src/theme/animations.ts | 12 +++++++ 9 files changed, 85 insertions(+), 36 deletions(-) create mode 100644 invokeai/frontend/web/src/features/dynamicPrompts/util/getShouldProcessPrompt.ts create mode 100644 invokeai/frontend/web/src/theme/animations.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index acdef31681..7bae282710 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -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", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts index 131926e628..cb51adce14 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/promptChanged.ts @@ -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( diff --git a/invokeai/frontend/web/src/common/components/InvMenu/InvMenuItem.tsx b/invokeai/frontend/web/src/common/components/InvMenu/InvMenuItem.tsx index 87715e191c..687730c464 100644 --- a/invokeai/frontend/web/src/common/components/InvMenu/InvMenuItem.tsx +++ b/invokeai/frontend/web/src/common/components/InvMenu/InvMenuItem.tsx @@ -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( (props: InvMenuItemProps, ref) => { @@ -30,13 +18,7 @@ export const InvMenuItem = memo( return ( - ) : ( - icon - ) - } + icon={isLoading ? : icon} isDisabled={isLoading || isDisabled} data-destructive={isDestructive} {...rest} diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 35d90be2f0..d7cc127047 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -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')); } diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton.tsx index 55fa1068a5..c560c81d7d 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton.tsx @@ -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 ( - + { aria-label={t('dynamicPrompts.showDynamicPrompts')} icon={} onClick={onOpen} + sx={isLoading ? loadingStyles : undefined} /> ); diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/util/getShouldProcessPrompt.ts b/invokeai/frontend/web/src/features/dynamicPrompts/util/getShouldProcessPrompt.ts new file mode 100644 index 0000000000..56fa32fb67 --- /dev/null +++ b/invokeai/frontend/web/src/features/dynamicPrompts/util/getShouldProcessPrompt.ts @@ -0,0 +1,3 @@ +const hasOpenCloseCurlyBracesRegex = /.*\{.*\}.*/; +export const getShouldProcessPrompt = (prompt: string): boolean => + hasOpenCloseCurlyBracesRegex.test(prompt); diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx index a139e435d2..d156e63977 100644 --- a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx @@ -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(() => { } tooltip={} @@ -83,8 +87,10 @@ export const InvokeQueueBackButton = memo(() => { size="lg" w="calc(100% - 60px)" flexShrink={0} + justifyContent="space-between" + spinnerPlacement="end" > - {invoke} + {invoke} diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index 013493ff63..5c09599477 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -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 ( diff --git a/invokeai/frontend/web/src/theme/animations.ts b/invokeai/frontend/web/src/theme/animations.ts new file mode 100644 index 0000000000..f5a1b897ec --- /dev/null +++ b/invokeai/frontend/web/src/theme/animations.ts @@ -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`;