From ea8787c8ffe481374a9fea7566adbf8cb9769eae Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:49:06 -0600 Subject: [PATCH] feat(ui): update invoke button tooltip for batching - Split up logic to determine reason why the user cannot invoke for each tab. - Fix issue where the workflows tab would show reasons related to canvas/upscale tab. The tooltip now only shows information relevant to the current tab. - Add calculation for batch size to the queue count prediction. - Use a constant for the enqueue mutation's fixed cache key, instead of a string. Just some typo protection. --- invokeai/frontend/web/public/locales/en.json | 3 +- ...addAdHocPostProcessingRequestedListener.ts | 6 +- .../listeners/enqueueRequestedLinear.ts | 6 +- .../listeners/enqueueRequestedNodes.ts | 8 +- .../listeners/enqueueRequestedUpscale.ts | 8 +- .../konva/CanvasStateApiModule.ts | 4 +- .../InvokeButtonTooltip.tsx | 310 ++++++++++++++++++ .../components/InvokeQueueBackButton.tsx | 6 +- .../queue/components/QueueButtonTooltip.tsx | 123 ------- .../web/src/features/queue/hooks/useInvoke.ts | 60 +++- .../hooks/useIsQueueMutationInProgress.ts | 7 +- .../queue/store/readiness.ts} | 251 ++++++++------ .../FloatingParametersPanelButtons.tsx | 6 +- .../web/src/services/api/endpoints/queue.ts | 4 + 14 files changed, 548 insertions(+), 254 deletions(-) create mode 100644 invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx delete mode 100644 invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx rename invokeai/frontend/web/src/{common/hooks/useIsReadyToEnqueue.ts => features/queue/store/readiness.ts} (73%) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 28b658a785..970bbf2fc1 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -263,7 +263,8 @@ "iterations_one": "Iteration", "iterations_other": "Iterations", "generations_one": "Generation", - "generations_other": "Generations" + "generations_other": "Generations", + "batchSize": "Batch Size" }, "invocationCache": { "invocationCache": "Invocation Cache", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener.ts index cc1d2cbbaa..7ab053c185 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener.ts @@ -4,7 +4,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { buildAdHocPostProcessingGraph } from 'features/nodes/util/graph/buildAdHocPostProcessingGraph'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; -import { queueApi } from 'services/api/endpoints/queue'; +import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue'; import type { BatchConfig, ImageDTO } from 'services/api/types'; import type { JsonObject } from 'type-fest'; @@ -32,9 +32,7 @@ export const addAdHocPostProcessingRequestedListener = (startAppListening: AppSt try { const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, { - fixedCacheKey: 'enqueueBatch', - }) + queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, enqueueMutationFixedCacheKeyOptions) ); const enqueueResult = await req.unwrap(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index f5831f4eba..e28ff039c5 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -13,7 +13,7 @@ import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGr import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { toast } from 'features/toast/toast'; import { serializeError } from 'serialize-error'; -import { queueApi } from 'services/api/endpoints/queue'; +import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue'; import type { Invocation } from 'services/api/types'; import { assert, AssertionError } from 'tsafe'; import type { JsonObject } from 'type-fest'; @@ -91,9 +91,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) } const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(prepareBatchResult.value, { - fixedCacheKey: 'enqueueBatch', - }) + queueApi.endpoints.enqueueBatch.initiate(prepareBatchResult.value, enqueueMutationFixedCacheKeyOptions) ); req.reset(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index 5fb9a0fe46..1964aa7ef2 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -6,7 +6,7 @@ import { isImageFieldCollectionInputInstance } from 'features/nodes/types/field' import { isInvocationNode } from 'features/nodes/types/invocation'; import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph'; import { buildWorkflowWithValidation } from 'features/nodes/util/workflow/buildWorkflow'; -import { queueApi } from 'services/api/endpoints/queue'; +import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue'; import type { Batch, BatchConfig } from 'services/api/types'; const log = logger('workflows'); @@ -70,11 +70,7 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) = prepend: action.payload.prepend, }; - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(batchConfig, { - fixedCacheKey: 'enqueueBatch', - }) - ); + const req = dispatch(queueApi.endpoints.enqueueBatch.initiate(batchConfig, enqueueMutationFixedCacheKeyOptions)); try { await req.unwrap(); } finally { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts index 624e9e54b3..022fc99716 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedUpscale.ts @@ -2,7 +2,7 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { buildMultidiffusionUpscaleGraph } from 'features/nodes/util/graph/buildMultidiffusionUpscaleGraph'; -import { queueApi } from 'services/api/endpoints/queue'; +import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue'; export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening) => { startAppListening({ @@ -16,11 +16,7 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening) const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond, 'upscaling', 'gallery'); - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(batchConfig, { - fixedCacheKey: 'enqueueBatch', - }) - ); + const req = dispatch(queueApi.endpoints.enqueueBatch.initiate(batchConfig, enqueueMutationFixedCacheKeyOptions)); try { await req.unwrap(); } finally { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts index 2b3613717c..432685009d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts @@ -51,7 +51,7 @@ import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import { atom, computed } from 'nanostores'; import type { Logger } from 'roarr'; import { getImageDTO } from 'services/api/endpoints/images'; -import { queueApi } from 'services/api/endpoints/queue'; +import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue'; import type { BatchConfig, ImageDTO, S } from 'services/api/types'; import { QueueError } from 'services/events/errors'; import type { Param0 } from 'tsafe'; @@ -402,7 +402,7 @@ export class CanvasStateApiModule extends CanvasModuleBase { queueApi.endpoints.enqueueBatch.initiate(batch, { // Use the same cache key for all enqueueBatch requests, so that all consumers of this query get the same status // updates. - fixedCacheKey: 'enqueueBatch', + ...enqueueMutationFixedCacheKeyOptions, // We do not need RTK to track this request in the store track: false, }) diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx new file mode 100644 index 0000000000..6d10325af2 --- /dev/null +++ b/invokeai/frontend/web/src/features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip.tsx @@ -0,0 +1,310 @@ +import type { TooltipProps } from '@invoke-ai/ui-library'; +import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { $true } from 'app/store/nanostores/util'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { selectSendToCanvas } from 'features/controlLayers/store/canvasSettingsSlice'; +import { selectIterations } from 'features/controlLayers/store/paramsSlice'; +import { selectDynamicPromptsIsLoading } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; +import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import type { Reason } from 'features/queue/store/readiness'; +import { + buildSelectIsReadyToEnqueueCanvasTab, + buildSelectIsReadyToEnqueueUpscaleTab, + buildSelectIsReadyToEnqueueWorkflowsTab, + buildSelectReasonsWhyCannotEnqueueCanvasTab, + buildSelectReasonsWhyCannotEnqueueUpscaleTab, + buildSelectReasonsWhyCannotEnqueueWorkflowsTab, + selectPromptsCount, + selectWorkflowsBatchSize, +} from 'features/queue/store/readiness'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; +import type { PropsWithChildren } from 'react'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { enqueueMutationFixedCacheKeyOptions, useEnqueueBatchMutation } from 'services/api/endpoints/queue'; +import { useBoardName } from 'services/api/hooks/useBoardName'; +import { $isConnected } from 'services/events/stores'; + +type Props = TooltipProps & { + prepend?: boolean; +}; + +export const InvokeButtonTooltip = ({ prepend, children, ...rest }: PropsWithChildren) => { + return ( + } maxW={512} {...rest}> + {children} + + ); +}; + +const TooltipContent = memo(({ prepend = false }: { prepend?: boolean }) => { + const activeTab = useAppSelector(selectActiveTab); + + if (activeTab === 'canvas') { + return ; + } + + if (activeTab === 'workflows') { + return ; + } + + if (activeTab === 'upscaling') { + return ; + } + + return null; +}); +TooltipContent.displayName = 'TooltipContent'; + +const CanvasTabTooltipContent = memo(({ prepend = false }: { prepend?: boolean }) => { + const isConnected = useStore($isConnected); + const canvasManager = useCanvasManagerSafe(); + const canvasIsFiltering = useStore(canvasManager?.stateApi.$isFiltering ?? $true); + const canvasIsTransforming = useStore(canvasManager?.stateApi.$isTransforming ?? $true); + const canvasIsRasterizing = useStore(canvasManager?.stateApi.$isRasterizing ?? $true); + const canvasIsSelectingObject = useStore(canvasManager?.stateApi.$isSegmenting ?? $true); + const canvasIsCompositing = useStore(canvasManager?.compositor.$isBusy ?? $true); + + const selectIsReady = useMemo( + () => + buildSelectIsReadyToEnqueueCanvasTab({ + isConnected, + canvasIsFiltering, + canvasIsTransforming, + canvasIsRasterizing, + canvasIsSelectingObject, + canvasIsCompositing, + }), + [ + isConnected, + canvasIsCompositing, + canvasIsFiltering, + canvasIsRasterizing, + canvasIsSelectingObject, + canvasIsTransforming, + ] + ); + + const selectReasons = useMemo( + () => + buildSelectReasonsWhyCannotEnqueueCanvasTab({ + isConnected, + canvasIsFiltering, + canvasIsTransforming, + canvasIsRasterizing, + canvasIsSelectingObject, + canvasIsCompositing, + }), + [ + isConnected, + canvasIsCompositing, + canvasIsFiltering, + canvasIsRasterizing, + canvasIsSelectingObject, + canvasIsTransforming, + ] + ); + + const isReady = useAppSelector(selectIsReady); + const reasons = useAppSelector(selectReasons); + + return ( + + + + {reasons.length > 0 && ( + <> + + + + )} + + + + ); +}); +CanvasTabTooltipContent.displayName = 'CanvasTabTooltipContent'; + +const UpscaleTabTooltipContent = memo(({ prepend = false }: { prepend?: boolean }) => { + const isConnected = useStore($isConnected); + + const selectIsReady = useMemo(() => buildSelectIsReadyToEnqueueUpscaleTab({ isConnected }), [isConnected]); + const selectReasons = useMemo(() => buildSelectReasonsWhyCannotEnqueueUpscaleTab({ isConnected }), [isConnected]); + + const isReady = useAppSelector(selectIsReady); + const reasons = useAppSelector(selectReasons); + + return ( + + + + {reasons.length > 0 && ( + <> + + + + )} + + + + ); +}); +UpscaleTabTooltipContent.displayName = 'UpscaleTabTooltipContent'; + +const WorkflowsTabTooltipContent = memo(({ prepend = false }: { prepend?: boolean }) => { + const isConnected = useStore($isConnected); + const templates = useStore($templates); + + const selectIsReady = useMemo( + () => buildSelectIsReadyToEnqueueWorkflowsTab({ isConnected, templates }), + [isConnected, templates] + ); + const selectReasons = useMemo( + () => buildSelectReasonsWhyCannotEnqueueWorkflowsTab({ isConnected, templates }), + [isConnected, templates] + ); + + const isReady = useAppSelector(selectIsReady); + const reasons = useAppSelector(selectReasons); + + return ( + + + + {reasons.length > 0 && ( + <> + + + + )} + + + + ); +}); +WorkflowsTabTooltipContent.displayName = 'WorkflowsTabTooltipContent'; + +const QueueCountPredictionCanvasOrUpscaleTab = memo(() => { + const { t } = useTranslation(); + const promptsCount = useAppSelector(selectPromptsCount); + const iterationsCount = useAppSelector(selectIterations); + + const text = useMemo(() => { + const generationCount = Math.min(promptsCount * iterationsCount, 10000); + const prompts = t('queue.prompts', { count: promptsCount }); + const iterations = t('queue.iterations', { count: iterationsCount }); + const generations = t('queue.generations', { count: generationCount }); + return `${promptsCount} ${prompts} \u00d7 ${iterationsCount} ${iterations} -> ${generationCount} ${generations}`.toLowerCase(); + }, [iterationsCount, promptsCount, t]); + + return {text}; +}); +QueueCountPredictionCanvasOrUpscaleTab.displayName = 'QueueCountPredictionCanvasOrUpscaleTab'; + +const QueueCountPredictionWorkflowsTab = memo(() => { + const { t } = useTranslation(); + const batchSize = useAppSelector(selectWorkflowsBatchSize); + const iterationsCount = useAppSelector(selectIterations); + + const text = useMemo(() => { + const generationCount = Math.min(batchSize * iterationsCount, 10000); + const iterations = t('queue.iterations', { count: iterationsCount }); + const generations = t('queue.generations', { count: generationCount }); + return `${batchSize} ${t('queue.batchSize')} \u00d7 ${iterationsCount} ${iterations} -> ${generationCount} ${generations}`.toLowerCase(); + }, [batchSize, iterationsCount, t]); + + return {text}; +}); +QueueCountPredictionWorkflowsTab.displayName = 'QueueCountPredictionWorkflowsTab'; + +const IsReadyText = memo(({ isReady, prepend }: { isReady: boolean; prepend: boolean }) => { + const { t } = useTranslation(); + const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading); + const [_, enqueueMutation] = useEnqueueBatchMutation(enqueueMutationFixedCacheKeyOptions); + + const text = useMemo(() => { + if (enqueueMutation.isLoading) { + return t('queue.enqueueing'); + } + if (isLoadingDynamicPrompts) { + return t('dynamicPrompts.loading'); + } + if (isReady) { + if (prepend) { + return t('queue.queueFront'); + } + return t('queue.queueBack'); + } + return t('queue.notReady'); + }, [enqueueMutation.isLoading, isLoadingDynamicPrompts, isReady, prepend, t]); + + return {text}; +}); +IsReadyText.displayName = 'IsReadyText'; + +const ReasonsList = memo(({ reasons }: { reasons: Reason[] }) => { + return ( + + {reasons.map((reason, i) => ( + + ))} + + ); +}); +ReasonsList.displayName = 'ReasonsList'; + +const ReasonListItem = memo(({ reason }: { reason: Reason }) => { + return ( + + + {reason.prefix && ( + + {reason.prefix}:{' '} + + )} + {reason.content} + + + ); +}); +ReasonListItem.displayName = 'ReasonListItem'; + +const StyledDivider = memo(() => ); +StyledDivider.displayName = 'StyledDivider'; + +const AddingToText = memo(() => { + const { t } = useTranslation(); + const sendToCanvas = useAppSelector(selectSendToCanvas); + const autoAddBoardId = useAppSelector(selectAutoAddBoardId); + const autoAddBoardName = useBoardName(autoAddBoardId); + + const addingTo = useMemo(() => { + if (sendToCanvas) { + return t('controlLayers.stagingOnCanvas'); + } + return t('parameters.invoke.addingImagesTo'); + }, [sendToCanvas, t]); + + const destination = useMemo(() => { + if (sendToCanvas) { + return t('queue.canvas'); + } + if (autoAddBoardName) { + return autoAddBoardName; + } + return t('boards.uncategorized'); + }, [autoAddBoardName, sendToCanvas, t]); + + return ( + + {addingTo}{' '} + + {destination} + + + ); +}); +AddingToText.displayName = 'AddingToText'; diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx index 54b076bee9..33863d6634 100644 --- a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx @@ -6,7 +6,7 @@ import { useInvoke } from 'features/queue/hooks/useInvoke'; import { memo } from 'react'; import { PiLightningFill, PiSparkleFill } from 'react-icons/pi'; -import { QueueButtonTooltip } from './QueueButtonTooltip'; +import { InvokeButtonTooltip } from './InvokeButtonTooltip/InvokeButtonTooltip'; const invoke = 'Invoke'; @@ -18,7 +18,7 @@ export const InvokeButton = memo(() => { return ( - + - + ); }); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx deleted file mode 100644 index 95ee3ced41..0000000000 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import type { TooltipProps } from '@invoke-ai/ui-library'; -import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue'; -import { selectSendToCanvas } from 'features/controlLayers/store/canvasSettingsSlice'; -import { selectIterations, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; -import { - selectDynamicPromptsIsLoading, - selectDynamicPromptsSlice, -} from 'features/dynamicPrompts/store/dynamicPromptsSlice'; -import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; -import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; -import type { PropsWithChildren } from 'react'; -import { memo, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useEnqueueBatchMutation } from 'services/api/endpoints/queue'; -import { useBoardName } from 'services/api/hooks/useBoardName'; - -const selectPromptsCount = createSelector(selectParamsSlice, selectDynamicPromptsSlice, (params, dynamicPrompts) => - getShouldProcessPrompt(params.positivePrompt) ? dynamicPrompts.prompts.length : 1 -); - -type Props = TooltipProps & { - prepend?: boolean; -}; - -export const QueueButtonTooltip = ({ prepend, children, ...rest }: PropsWithChildren) => { - return ( - } maxW={512} {...rest}> - {children} - - ); -}; - -const TooltipContent = memo(({ prepend = false }: { prepend?: boolean }) => { - const { t } = useTranslation(); - const { isReady, reasons } = useIsReadyToEnqueue(); - const sendToCanvas = useAppSelector(selectSendToCanvas); - const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading); - const promptsCount = useAppSelector(selectPromptsCount); - const iterationsCount = useAppSelector(selectIterations); - const autoAddBoardId = useAppSelector(selectAutoAddBoardId); - const autoAddBoardName = useBoardName(autoAddBoardId); - const [_, { isLoading }] = useEnqueueBatchMutation({ - fixedCacheKey: 'enqueueBatch', - }); - const queueCountPredictionLabel = useMemo(() => { - const generationCount = Math.min(promptsCount * iterationsCount, 10000); - const prompts = t('queue.prompts', { count: promptsCount }); - const iterations = t('queue.iterations', { count: iterationsCount }); - const generations = t('queue.generations', { count: generationCount }); - return `${promptsCount} ${prompts} \u00d7 ${iterationsCount} ${iterations} -> ${generationCount} ${generations}`.toLowerCase(); - }, [iterationsCount, promptsCount, t]); - - const label = useMemo(() => { - if (isLoading) { - return t('queue.enqueueing'); - } - if (isLoadingDynamicPrompts) { - return t('dynamicPrompts.loading'); - } - if (isReady) { - if (prepend) { - return t('queue.queueFront'); - } - return t('queue.queueBack'); - } - return t('queue.notReady'); - }, [isLoading, isLoadingDynamicPrompts, isReady, prepend, t]); - - const addingTo = useMemo(() => { - if (sendToCanvas) { - return t('controlLayers.stagingOnCanvas'); - } - return t('parameters.invoke.addingImagesTo'); - }, [sendToCanvas, t]); - - const destination = useMemo(() => { - if (sendToCanvas) { - return t('queue.canvas'); - } - if (autoAddBoardName) { - return autoAddBoardName; - } - return t('boards.uncategorized'); - }, [autoAddBoardName, sendToCanvas, t]); - - return ( - - {label} - {queueCountPredictionLabel} - {reasons.length > 0 && ( - <> - - - {reasons.map((reason, i) => ( - - - {reason.prefix && ( - - {reason.prefix}:{' '} - - )} - {reason.content} - - - ))} - - - )} - - - {addingTo}{' '} - - {destination} - - - - ); -}); - -TooltipContent.displayName = 'QueueButtonTooltipContent'; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts index feb449739c..0b06aae380 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts @@ -1,17 +1,63 @@ +import { useStore } from '@nanostores/react'; import { enqueueRequested } from 'app/store/actions'; +import { $true } from 'app/store/nanostores/util'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue'; +import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { + buildSelectIsReadyToEnqueueCanvasTab, + buildSelectIsReadyToEnqueueUpscaleTab, + buildSelectIsReadyToEnqueueWorkflowsTab, +} from 'features/queue/store/readiness'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; -import { useCallback } from 'react'; -import { useEnqueueBatchMutation } from 'services/api/endpoints/queue'; +import { useCallback, useMemo } from 'react'; +import { enqueueMutationFixedCacheKeyOptions, useEnqueueBatchMutation } from 'services/api/endpoints/queue'; +import { $isConnected } from 'services/events/stores'; export const useInvoke = () => { const dispatch = useAppDispatch(); const tabName = useAppSelector(selectActiveTab); - const { isReady } = useIsReadyToEnqueue(); - const [_, { isLoading }] = useEnqueueBatchMutation({ - fixedCacheKey: 'enqueueBatch', - }); + const isConnected = useStore($isConnected); + const canvasManager = useCanvasManagerSafe(); + const canvasIsFiltering = useStore(canvasManager?.stateApi.$isFiltering ?? $true); + const canvasIsTransforming = useStore(canvasManager?.stateApi.$isTransforming ?? $true); + const canvasIsRasterizing = useStore(canvasManager?.stateApi.$isRasterizing ?? $true); + const canvasIsSelectingObject = useStore(canvasManager?.stateApi.$isSegmenting ?? $true); + const canvasIsCompositing = useStore(canvasManager?.compositor.$isBusy ?? $true); + const templates = useStore($templates); + + const selectIsReady = useMemo(() => { + if (tabName === 'canvas') { + return buildSelectIsReadyToEnqueueCanvasTab({ + isConnected, + canvasIsFiltering, + canvasIsTransforming, + canvasIsRasterizing, + canvasIsSelectingObject, + canvasIsCompositing, + }); + } + if (tabName === 'upscaling') { + return buildSelectIsReadyToEnqueueUpscaleTab({ isConnected }); + } + if (tabName === 'workflows') { + return buildSelectIsReadyToEnqueueWorkflowsTab({ isConnected, templates }); + } + return () => false; + }, [ + tabName, + isConnected, + canvasIsFiltering, + canvasIsTransforming, + canvasIsRasterizing, + canvasIsSelectingObject, + canvasIsCompositing, + templates, + ]); + + const isReady = useAppSelector(selectIsReady); + + const [_, { isLoading }] = useEnqueueBatchMutation(enqueueMutationFixedCacheKeyOptions); const queueBack = useCallback(() => { if (!isReady) { return; diff --git a/invokeai/frontend/web/src/features/queue/hooks/useIsQueueMutationInProgress.ts b/invokeai/frontend/web/src/features/queue/hooks/useIsQueueMutationInProgress.ts index 302a2cb2c0..1a4fe31f27 100644 --- a/invokeai/frontend/web/src/features/queue/hooks/useIsQueueMutationInProgress.ts +++ b/invokeai/frontend/web/src/features/queue/hooks/useIsQueueMutationInProgress.ts @@ -1,4 +1,5 @@ import { + enqueueMutationFixedCacheKeyOptions, useCancelQueueItemMutation, // useCancelByBatchIdsMutation, useClearQueueMutation, @@ -9,9 +10,9 @@ import { } from 'services/api/endpoints/queue'; export const useIsQueueMutationInProgress = () => { - const [_triggerEnqueueBatch, { isLoading: isLoadingEnqueueBatch }] = useEnqueueBatchMutation({ - fixedCacheKey: 'enqueueBatch', - }); + const [_triggerEnqueueBatch, { isLoading: isLoadingEnqueueBatch }] = useEnqueueBatchMutation( + enqueueMutationFixedCacheKeyOptions + ); const [_triggerResumeProcessor, { isLoading: isLoadingResumeProcessor }] = useResumeProcessorMutation({ fixedCacheKey: 'resumeProcessor', }); diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts similarity index 73% rename from invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts rename to invokeai/frontend/web/src/features/queue/store/readiness.ts index fc188ac0e7..0d14bba73d 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -1,9 +1,5 @@ -import { useStore } from '@nanostores/react'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { $true } from 'app/store/nanostores/util'; -import { useAppSelector } from 'app/store/storeHooks'; +import { createSelector } from '@reduxjs/toolkit'; import type { AppConfig } from 'app/types/invokeai'; -import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import type { ParamsState } from 'features/controlLayers/store/paramsSlice'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; @@ -11,7 +7,6 @@ import type { CanvasState } from 'features/controlLayers/store/types'; import type { DynamicPromptsState } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; -import { $templates } from 'features/nodes/store/nodesSlice'; import { selectNodesSlice } from 'features/nodes/store/selectors'; import type { NodesState, Templates } from 'features/nodes/store/types'; import type { WorkflowSettingsState } from 'features/nodes/store/workflowSettingsSlice'; @@ -21,13 +16,17 @@ import { isInvocationNode } from 'features/nodes/types/invocation'; import type { UpscaleState } from 'features/parameters/store/upscaleSlice'; import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice'; import { selectConfigSlice } from 'features/system/store/configSlice'; -import { selectSystemSlice } from 'features/system/store/systemSlice'; -import { selectActiveTab } from 'features/ui/store/uiSelectors'; import i18n from 'i18next'; import { forEach, upperFirst } from 'lodash-es'; -import { useMemo } from 'react'; import { getConnectedEdges } from 'reactflow'; -import { $isConnected } from 'services/events/stores'; + +/** + * This file contains selectors and utilities for determining the app is ready to enqueue generations. The handling + * differs for each tab (canvas, upscaling, workflows). + * + * For example, the canvas tab needs to check the status of the canvas manager before enqueuing, while the workflows + * tab needs to check the status of the nodes and their connections. + */ const LAYER_TYPE_TO_TKEY = { reference_image: 'controlLayers.referenceImage', @@ -37,15 +36,22 @@ const LAYER_TYPE_TO_TKEY = { control_layer: 'controlLayers.controlLayer', } as const; -type Reason = { prefix?: string; content: string }; +export type Reason = { prefix?: string; content: string }; -const handleWorkflowsTab = (arg: { - reasons: Reason[]; +const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invoke.systemDisconnected') }); + +const getReasonsWhyCannotEnqueueWorkflowsTab = (arg: { + isConnected: boolean; nodes: NodesState; workflowSettings: WorkflowSettingsState; templates: Templates; -}) => { - const { reasons, nodes, workflowSettings, templates } = arg; +}): Reason[] => { + const { isConnected, nodes, workflowSettings, templates } = arg; + const reasons: Reason[] = []; + + if (!isConnected) { + reasons.push(disconnectedReason(i18n.t)); + } if (workflowSettings.shouldValidateGraph) { if (!nodes.nodes.length) { @@ -121,15 +127,22 @@ const handleWorkflowsTab = (arg: { }); }); } + + return reasons; }; -const handleUpscalingTab = (arg: { - reasons: Reason[]; +const getReasonsWhyCannotEnqueueUpscaleTab = (arg: { + isConnected: boolean; upscale: UpscaleState; config: AppConfig; params: ParamsState; }) => { - const { reasons, upscale, config, params } = arg; + const { isConnected, upscale, config, params } = arg; + const reasons: Reason[] = []; + + if (!isConnected) { + reasons.push(disconnectedReason(i18n.t)); + } if (!upscale.upscaleInitialImage) { reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') }); @@ -160,10 +173,12 @@ const handleUpscalingTab = (arg: { reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') }); } } + + return reasons; }; -const handleCanvasTab = (arg: { - reasons: Reason[]; +const getReasonsWhyCannotEnqueueCanvasTab = (arg: { + isConnected: boolean; canvas: CanvasState; params: ParamsState; dynamicPrompts: DynamicPromptsState; @@ -174,7 +189,7 @@ const handleCanvasTab = (arg: { canvasIsSelectingObject: boolean; }) => { const { - reasons, + isConnected, canvas, params, dynamicPrompts, @@ -184,8 +199,12 @@ const handleCanvasTab = (arg: { canvasIsCompositing, canvasIsSelectingObject, } = arg; - const { model, positivePrompt } = params; + const reasons: Reason[] = []; + + if (!isConnected) { + reasons.push(disconnectedReason(i18n.t)); + } if (canvasIsFiltering) { reasons.push({ content: i18n.t('parameters.invoke.canvasIsFiltering') }); @@ -353,10 +372,11 @@ const handleCanvasTab = (arg: { reasons.push({ prefix, content }); } }); + + return reasons; }; -const createSelector = (arg: { - templates: Templates; +export const buildSelectReasonsWhyCannotEnqueueCanvasTab = (arg: { isConnected: boolean; canvasIsFiltering: boolean; canvasIsTransforming: boolean; @@ -365,7 +385,6 @@ const createSelector = (arg: { canvasIsSelectingObject: boolean; }) => { const { - templates, isConnected, canvasIsFiltering, canvasIsTransforming, @@ -373,79 +392,127 @@ const createSelector = (arg: { canvasIsCompositing, canvasIsSelectingObject, } = arg; - return createMemoizedSelector( - [ - selectSystemSlice, - selectNodesSlice, - selectWorkflowSettingsSlice, - selectDynamicPromptsSlice, - selectCanvasSlice, - selectParamsSlice, - selectUpscaleSlice, - selectConfigSlice, - selectActiveTab, - ], - (system, nodes, workflowSettings, dynamicPrompts, canvas, params, upscale, config, activeTabName) => { - const reasons: Reason[] = []; - // Cannot generate if not connected - if (!isConnected) { - reasons.push({ content: i18n.t('parameters.invoke.systemDisconnected') }); - } - - if (activeTabName === 'workflows') { - handleWorkflowsTab({ reasons, nodes, workflowSettings, templates }); - } else if (activeTabName === 'upscaling') { - handleUpscalingTab({ reasons, upscale, config, params }); - } else { - handleCanvasTab({ - reasons, - canvas, - params, - dynamicPrompts, - canvasIsFiltering, - canvasIsTransforming, - canvasIsRasterizing, - canvasIsCompositing, - canvasIsSelectingObject, - }); - } - - return { isReady: !reasons.length, reasons }; - } - ); -}; - -export const useIsReadyToEnqueue = () => { - const templates = useStore($templates); - const isConnected = useStore($isConnected); - const canvasManager = useCanvasManagerSafe(); - const canvasIsFiltering = useStore(canvasManager?.stateApi.$isFiltering ?? $true); - const canvasIsTransforming = useStore(canvasManager?.stateApi.$isTransforming ?? $true); - const canvasIsRasterizing = useStore(canvasManager?.stateApi.$isRasterizing ?? $true); - const canvasIsSelectingObject = useStore(canvasManager?.stateApi.$isSegmenting ?? $true); - const canvasIsCompositing = useStore(canvasManager?.compositor.$isBusy ?? $true); - const selector = useMemo( - () => - createSelector({ - templates, + return createSelector( + selectCanvasSlice, + selectParamsSlice, + selectDynamicPromptsSlice, + (canvas, params, dynamicPrompts) => + getReasonsWhyCannotEnqueueCanvasTab({ isConnected, + canvas, + params, + dynamicPrompts, canvasIsFiltering, canvasIsTransforming, canvasIsRasterizing, canvasIsCompositing, canvasIsSelectingObject, - }), - [ - templates, - isConnected, - canvasIsFiltering, - canvasIsTransforming, - canvasIsRasterizing, - canvasIsCompositing, - canvasIsSelectingObject, - ] + }) ); - const value = useAppSelector(selector); - return value; }; + +export const buildSelectIsReadyToEnqueueCanvasTab = (arg: { + isConnected: boolean; + canvasIsFiltering: boolean; + canvasIsTransforming: boolean; + canvasIsRasterizing: boolean; + canvasIsCompositing: boolean; + canvasIsSelectingObject: boolean; +}) => { + const { + isConnected, + canvasIsFiltering, + canvasIsTransforming, + canvasIsRasterizing, + canvasIsCompositing, + canvasIsSelectingObject, + } = arg; + + return createSelector( + selectCanvasSlice, + selectParamsSlice, + selectDynamicPromptsSlice, + (canvas, params, dynamicPrompts) => + getReasonsWhyCannotEnqueueCanvasTab({ + isConnected, + canvas, + params, + dynamicPrompts, + canvasIsFiltering, + canvasIsTransforming, + canvasIsRasterizing, + canvasIsCompositing, + canvasIsSelectingObject, + }).length === 0 + ); +}; + +export const buildSelectReasonsWhyCannotEnqueueUpscaleTab = (arg: { isConnected: boolean }) => { + const { isConnected } = arg; + return createSelector(selectUpscaleSlice, selectConfigSlice, selectParamsSlice, (upscale, config, params) => + getReasonsWhyCannotEnqueueUpscaleTab({ isConnected, upscale, config, params }) + ); +}; + +export const buildSelectIsReadyToEnqueueUpscaleTab = (arg: { isConnected: boolean }) => { + const { isConnected } = arg; + + return createSelector( + selectUpscaleSlice, + selectConfigSlice, + selectParamsSlice, + (upscale, config, params) => + getReasonsWhyCannotEnqueueUpscaleTab({ isConnected, upscale, config, params }).length === 0 + ); +}; + +export const buildSelectReasonsWhyCannotEnqueueWorkflowsTab = (arg: { isConnected: boolean; templates: Templates }) => { + const { isConnected, templates } = arg; + + return createSelector(selectNodesSlice, selectWorkflowSettingsSlice, (nodes, workflowSettings) => + getReasonsWhyCannotEnqueueWorkflowsTab({ + isConnected, + nodes, + workflowSettings, + templates, + }) + ); +}; + +export const buildSelectIsReadyToEnqueueWorkflowsTab = (arg: { isConnected: boolean; templates: Templates }) => { + const { isConnected, templates } = arg; + + return createSelector( + selectNodesSlice, + selectWorkflowSettingsSlice, + (nodes, workflowSettings) => + getReasonsWhyCannotEnqueueWorkflowsTab({ + isConnected, + nodes, + workflowSettings, + templates, + }).length === 0 + ); +}; + +export const selectPromptsCount = createSelector( + selectParamsSlice, + selectDynamicPromptsSlice, + (params, dynamicPrompts) => (getShouldProcessPrompt(params.positivePrompt) ? dynamicPrompts.prompts.length : 1) +); + +export const selectWorkflowsBatchSize = createSelector(selectNodesSlice, ({ nodes }) => + // The batch size is the product of all batch nodes' collection sizes + nodes.filter(isInvocationNode).reduce((batchSize, node) => { + if (!isImageFieldCollectionInputInstance(node.data.inputs.images)) { + return batchSize; + } + // If the batch size is not set, default to 1 + batchSize = batchSize || 1; + // Multiply the batch size by the number of images in the batch + batchSize = batchSize * (node.data.inputs.images.value?.length ?? 0); + + return batchSize; + }, 0) +); diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx index 1ec9559bad..afdddf587c 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx @@ -4,7 +4,7 @@ import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser' import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; -import { QueueButtonTooltip } from 'features/queue/components/QueueButtonTooltip'; +import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip'; import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem'; import { useInvoke } from 'features/queue/hooks/useInvoke'; import type { UsePanelReturn } from 'features/ui/hooks/usePanel'; @@ -62,7 +62,7 @@ const FloatingSidePanelButtons = (props: Props) => { flexGrow={1} /> - + { colorScheme="invokeYellow" flexGrow={1} /> - +