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.
This commit is contained in:
psychedelicious
2024-11-19 10:49:06 -06:00
parent cead2c4445
commit ea8787c8ff
14 changed files with 548 additions and 254 deletions

View File

@@ -263,7 +263,8 @@
"iterations_one": "Iteration", "iterations_one": "Iteration",
"iterations_other": "Iterations", "iterations_other": "Iterations",
"generations_one": "Generation", "generations_one": "Generation",
"generations_other": "Generations" "generations_other": "Generations",
"batchSize": "Batch Size"
}, },
"invocationCache": { "invocationCache": {
"invocationCache": "Invocation Cache", "invocationCache": "Invocation Cache",

View File

@@ -4,7 +4,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { buildAdHocPostProcessingGraph } from 'features/nodes/util/graph/buildAdHocPostProcessingGraph'; import { buildAdHocPostProcessingGraph } from 'features/nodes/util/graph/buildAdHocPostProcessingGraph';
import { toast } from 'features/toast/toast'; import { toast } from 'features/toast/toast';
import { t } from 'i18next'; 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 { BatchConfig, ImageDTO } from 'services/api/types';
import type { JsonObject } from 'type-fest'; import type { JsonObject } from 'type-fest';
@@ -32,9 +32,7 @@ export const addAdHocPostProcessingRequestedListener = (startAppListening: AppSt
try { try {
const req = dispatch( const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, { queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, enqueueMutationFixedCacheKeyOptions)
fixedCacheKey: 'enqueueBatch',
})
); );
const enqueueResult = await req.unwrap(); const enqueueResult = await req.unwrap();

View File

@@ -13,7 +13,7 @@ import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGr
import type { Graph } from 'features/nodes/util/graph/generation/Graph'; import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { toast } from 'features/toast/toast'; import { toast } from 'features/toast/toast';
import { serializeError } from 'serialize-error'; 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 type { Invocation } from 'services/api/types';
import { assert, AssertionError } from 'tsafe'; import { assert, AssertionError } from 'tsafe';
import type { JsonObject } from 'type-fest'; import type { JsonObject } from 'type-fest';
@@ -91,9 +91,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
} }
const req = dispatch( const req = dispatch(
queueApi.endpoints.enqueueBatch.initiate(prepareBatchResult.value, { queueApi.endpoints.enqueueBatch.initiate(prepareBatchResult.value, enqueueMutationFixedCacheKeyOptions)
fixedCacheKey: 'enqueueBatch',
})
); );
req.reset(); req.reset();

View File

@@ -6,7 +6,7 @@ import { isImageFieldCollectionInputInstance } from 'features/nodes/types/field'
import { isInvocationNode } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation';
import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph'; import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph';
import { buildWorkflowWithValidation } from 'features/nodes/util/workflow/buildWorkflow'; 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'; import type { Batch, BatchConfig } from 'services/api/types';
const log = logger('workflows'); const log = logger('workflows');
@@ -70,11 +70,7 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
prepend: action.payload.prepend, prepend: action.payload.prepend,
}; };
const req = dispatch( const req = dispatch(queueApi.endpoints.enqueueBatch.initiate(batchConfig, enqueueMutationFixedCacheKeyOptions));
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
fixedCacheKey: 'enqueueBatch',
})
);
try { try {
await req.unwrap(); await req.unwrap();
} finally { } finally {

View File

@@ -2,7 +2,7 @@ import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildMultidiffusionUpscaleGraph } from 'features/nodes/util/graph/buildMultidiffusionUpscaleGraph'; 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) => { export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening) => {
startAppListening({ startAppListening({
@@ -16,11 +16,7 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond, 'upscaling', 'gallery'); const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond, 'upscaling', 'gallery');
const req = dispatch( const req = dispatch(queueApi.endpoints.enqueueBatch.initiate(batchConfig, enqueueMutationFixedCacheKeyOptions));
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
fixedCacheKey: 'enqueueBatch',
})
);
try { try {
await req.unwrap(); await req.unwrap();
} finally { } finally {

View File

@@ -51,7 +51,7 @@ import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { atom, computed } from 'nanostores'; import { atom, computed } from 'nanostores';
import type { Logger } from 'roarr'; import type { Logger } from 'roarr';
import { getImageDTO } from 'services/api/endpoints/images'; 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 type { BatchConfig, ImageDTO, S } from 'services/api/types';
import { QueueError } from 'services/events/errors'; import { QueueError } from 'services/events/errors';
import type { Param0 } from 'tsafe'; import type { Param0 } from 'tsafe';
@@ -402,7 +402,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
queueApi.endpoints.enqueueBatch.initiate(batch, { 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 // Use the same cache key for all enqueueBatch requests, so that all consumers of this query get the same status
// updates. // updates.
fixedCacheKey: 'enqueueBatch', ...enqueueMutationFixedCacheKeyOptions,
// We do not need RTK to track this request in the store // We do not need RTK to track this request in the store
track: false, track: false,
}) })

View File

@@ -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<Props>) => {
return (
<Tooltip label={<TooltipContent prepend={prepend} />} maxW={512} {...rest}>
{children}
</Tooltip>
);
};
const TooltipContent = memo(({ prepend = false }: { prepend?: boolean }) => {
const activeTab = useAppSelector(selectActiveTab);
if (activeTab === 'canvas') {
return <CanvasTabTooltipContent prepend={prepend} />;
}
if (activeTab === 'workflows') {
return <WorkflowsTabTooltipContent prepend={prepend} />;
}
if (activeTab === 'upscaling') {
return <UpscaleTabTooltipContent prepend={prepend} />;
}
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 (
<Flex flexDir="column" gap={1}>
<IsReadyText isReady={isReady} prepend={prepend} />
<QueueCountPredictionCanvasOrUpscaleTab />
{reasons.length > 0 && (
<>
<StyledDivider />
<ReasonsList reasons={reasons} />
</>
)}
<StyledDivider />
<AddingToText />
</Flex>
);
});
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 (
<Flex flexDir="column" gap={1}>
<IsReadyText isReady={isReady} prepend={prepend} />
<QueueCountPredictionCanvasOrUpscaleTab />
{reasons.length > 0 && (
<>
<StyledDivider />
<ReasonsList reasons={reasons} />
</>
)}
<StyledDivider />
<AddingToText />
</Flex>
);
});
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 (
<Flex flexDir="column" gap={1}>
<IsReadyText isReady={isReady} prepend={prepend} />
<QueueCountPredictionWorkflowsTab />
{reasons.length > 0 && (
<>
<StyledDivider />
<ReasonsList reasons={reasons} />
</>
)}
<StyledDivider />
<AddingToText />
</Flex>
);
});
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>{text}</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>{text}</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 fontWeight="semibold">{text}</Text>;
});
IsReadyText.displayName = 'IsReadyText';
const ReasonsList = memo(({ reasons }: { reasons: Reason[] }) => {
return (
<UnorderedList>
{reasons.map((reason, i) => (
<ReasonListItem key={`${reason.content}.${i}`} reason={reason} />
))}
</UnorderedList>
);
});
ReasonsList.displayName = 'ReasonsList';
const ReasonListItem = memo(({ reason }: { reason: Reason }) => {
return (
<ListItem>
<span>
{reason.prefix && (
<Text as="span" fontWeight="semibold">
{reason.prefix}:{' '}
</Text>
)}
<Text as="span">{reason.content}</Text>
</span>
</ListItem>
);
});
ReasonListItem.displayName = 'ReasonListItem';
const StyledDivider = memo(() => <Divider opacity={0.2} borderColor="base.900" />);
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 (
<Text fontStyle="oblique 10deg">
{addingTo}{' '}
<Text as="span" fontWeight="semibold">
{destination}
</Text>
</Text>
);
});
AddingToText.displayName = 'AddingToText';

View File

@@ -6,7 +6,7 @@ import { useInvoke } from 'features/queue/hooks/useInvoke';
import { memo } from 'react'; import { memo } from 'react';
import { PiLightningFill, PiSparkleFill } from 'react-icons/pi'; import { PiLightningFill, PiSparkleFill } from 'react-icons/pi';
import { QueueButtonTooltip } from './QueueButtonTooltip'; import { InvokeButtonTooltip } from './InvokeButtonTooltip/InvokeButtonTooltip';
const invoke = 'Invoke'; const invoke = 'Invoke';
@@ -18,7 +18,7 @@ export const InvokeButton = memo(() => {
return ( return (
<Flex pos="relative" w="200px"> <Flex pos="relative" w="200px">
<QueueIterationsNumberInput /> <QueueIterationsNumberInput />
<QueueButtonTooltip prepend={shift}> <InvokeButtonTooltip prepend={shift}>
<Button <Button
onClick={shift ? queue.queueFront : queue.queueBack} onClick={shift ? queue.queueFront : queue.queueBack}
isLoading={queue.isLoading || isLoadingDynamicPrompts} isLoading={queue.isLoading || isLoadingDynamicPrompts}
@@ -36,7 +36,7 @@ export const InvokeButton = memo(() => {
<span>{invoke}</span> <span>{invoke}</span>
<Spacer /> <Spacer />
</Button> </Button>
</QueueButtonTooltip> </InvokeButtonTooltip>
</Flex> </Flex>
); );
}); });

View File

@@ -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<Props>) => {
return (
<Tooltip label={<TooltipContent prepend={prepend} />} maxW={512} {...rest}>
{children}
</Tooltip>
);
};
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 (
<Flex flexDir="column" gap={1}>
<Text fontWeight="semibold">{label}</Text>
<Text>{queueCountPredictionLabel}</Text>
{reasons.length > 0 && (
<>
<Divider opacity={0.2} borderColor="base.900" />
<UnorderedList>
{reasons.map((reason, i) => (
<ListItem key={`${reason.content}.${i}`}>
<span>
{reason.prefix && (
<Text as="span" fontWeight="semibold">
{reason.prefix}:{' '}
</Text>
)}
<Text as="span">{reason.content}</Text>
</span>
</ListItem>
))}
</UnorderedList>
</>
)}
<Divider opacity={0.2} borderColor="base.900" />
<Text fontStyle="oblique 10deg">
{addingTo}{' '}
<Text as="span" fontWeight="semibold">
{destination}
</Text>
</Text>
</Flex>
);
});
TooltipContent.displayName = 'QueueButtonTooltipContent';

View File

@@ -1,17 +1,63 @@
import { useStore } from '@nanostores/react';
import { enqueueRequested } from 'app/store/actions'; import { enqueueRequested } from 'app/store/actions';
import { $true } from 'app/store/nanostores/util';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; 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 { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback } from 'react'; import { useCallback, useMemo } from 'react';
import { useEnqueueBatchMutation } from 'services/api/endpoints/queue'; import { enqueueMutationFixedCacheKeyOptions, useEnqueueBatchMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useInvoke = () => { export const useInvoke = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const tabName = useAppSelector(selectActiveTab); const tabName = useAppSelector(selectActiveTab);
const { isReady } = useIsReadyToEnqueue(); const isConnected = useStore($isConnected);
const [_, { isLoading }] = useEnqueueBatchMutation({ const canvasManager = useCanvasManagerSafe();
fixedCacheKey: 'enqueueBatch', 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(() => { const queueBack = useCallback(() => {
if (!isReady) { if (!isReady) {
return; return;

View File

@@ -1,4 +1,5 @@
import { import {
enqueueMutationFixedCacheKeyOptions,
useCancelQueueItemMutation, useCancelQueueItemMutation,
// useCancelByBatchIdsMutation, // useCancelByBatchIdsMutation,
useClearQueueMutation, useClearQueueMutation,
@@ -9,9 +10,9 @@ import {
} from 'services/api/endpoints/queue'; } from 'services/api/endpoints/queue';
export const useIsQueueMutationInProgress = () => { export const useIsQueueMutationInProgress = () => {
const [_triggerEnqueueBatch, { isLoading: isLoadingEnqueueBatch }] = useEnqueueBatchMutation({ const [_triggerEnqueueBatch, { isLoading: isLoadingEnqueueBatch }] = useEnqueueBatchMutation(
fixedCacheKey: 'enqueueBatch', enqueueMutationFixedCacheKeyOptions
}); );
const [_triggerResumeProcessor, { isLoading: isLoadingResumeProcessor }] = useResumeProcessorMutation({ const [_triggerResumeProcessor, { isLoading: isLoadingResumeProcessor }] = useResumeProcessorMutation({
fixedCacheKey: 'resumeProcessor', fixedCacheKey: 'resumeProcessor',
}); });

View File

@@ -1,9 +1,5 @@
import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { $true } from 'app/store/nanostores/util';
import { useAppSelector } from 'app/store/storeHooks';
import type { AppConfig } from 'app/types/invokeai'; import type { AppConfig } from 'app/types/invokeai';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import type { ParamsState } from 'features/controlLayers/store/paramsSlice'; import type { ParamsState } from 'features/controlLayers/store/paramsSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; 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 type { DynamicPromptsState } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { $templates } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors'; import { selectNodesSlice } from 'features/nodes/store/selectors';
import type { NodesState, Templates } from 'features/nodes/store/types'; import type { NodesState, Templates } from 'features/nodes/store/types';
import type { WorkflowSettingsState } from 'features/nodes/store/workflowSettingsSlice'; 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 type { UpscaleState } from 'features/parameters/store/upscaleSlice';
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice'; import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
import { selectConfigSlice } from 'features/system/store/configSlice'; 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 i18n from 'i18next';
import { forEach, upperFirst } from 'lodash-es'; import { forEach, upperFirst } from 'lodash-es';
import { useMemo } from 'react';
import { getConnectedEdges } from 'reactflow'; 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 = { const LAYER_TYPE_TO_TKEY = {
reference_image: 'controlLayers.referenceImage', reference_image: 'controlLayers.referenceImage',
@@ -37,15 +36,22 @@ const LAYER_TYPE_TO_TKEY = {
control_layer: 'controlLayers.controlLayer', control_layer: 'controlLayers.controlLayer',
} as const; } as const;
type Reason = { prefix?: string; content: string }; export type Reason = { prefix?: string; content: string };
const handleWorkflowsTab = (arg: { const disconnectedReason = (t: typeof i18n.t) => ({ content: t('parameters.invoke.systemDisconnected') });
reasons: Reason[];
const getReasonsWhyCannotEnqueueWorkflowsTab = (arg: {
isConnected: boolean;
nodes: NodesState; nodes: NodesState;
workflowSettings: WorkflowSettingsState; workflowSettings: WorkflowSettingsState;
templates: Templates; templates: Templates;
}) => { }): Reason[] => {
const { reasons, nodes, workflowSettings, templates } = arg; const { isConnected, nodes, workflowSettings, templates } = arg;
const reasons: Reason[] = [];
if (!isConnected) {
reasons.push(disconnectedReason(i18n.t));
}
if (workflowSettings.shouldValidateGraph) { if (workflowSettings.shouldValidateGraph) {
if (!nodes.nodes.length) { if (!nodes.nodes.length) {
@@ -121,15 +127,22 @@ const handleWorkflowsTab = (arg: {
}); });
}); });
} }
return reasons;
}; };
const handleUpscalingTab = (arg: { const getReasonsWhyCannotEnqueueUpscaleTab = (arg: {
reasons: Reason[]; isConnected: boolean;
upscale: UpscaleState; upscale: UpscaleState;
config: AppConfig; config: AppConfig;
params: ParamsState; 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) { if (!upscale.upscaleInitialImage) {
reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') }); reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') });
@@ -160,10 +173,12 @@ const handleUpscalingTab = (arg: {
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') }); reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
} }
} }
return reasons;
}; };
const handleCanvasTab = (arg: { const getReasonsWhyCannotEnqueueCanvasTab = (arg: {
reasons: Reason[]; isConnected: boolean;
canvas: CanvasState; canvas: CanvasState;
params: ParamsState; params: ParamsState;
dynamicPrompts: DynamicPromptsState; dynamicPrompts: DynamicPromptsState;
@@ -174,7 +189,7 @@ const handleCanvasTab = (arg: {
canvasIsSelectingObject: boolean; canvasIsSelectingObject: boolean;
}) => { }) => {
const { const {
reasons, isConnected,
canvas, canvas,
params, params,
dynamicPrompts, dynamicPrompts,
@@ -184,8 +199,12 @@ const handleCanvasTab = (arg: {
canvasIsCompositing, canvasIsCompositing,
canvasIsSelectingObject, canvasIsSelectingObject,
} = arg; } = arg;
const { model, positivePrompt } = params; const { model, positivePrompt } = params;
const reasons: Reason[] = [];
if (!isConnected) {
reasons.push(disconnectedReason(i18n.t));
}
if (canvasIsFiltering) { if (canvasIsFiltering) {
reasons.push({ content: i18n.t('parameters.invoke.canvasIsFiltering') }); reasons.push({ content: i18n.t('parameters.invoke.canvasIsFiltering') });
@@ -353,10 +372,11 @@ const handleCanvasTab = (arg: {
reasons.push({ prefix, content }); reasons.push({ prefix, content });
} }
}); });
return reasons;
}; };
const createSelector = (arg: { export const buildSelectReasonsWhyCannotEnqueueCanvasTab = (arg: {
templates: Templates;
isConnected: boolean; isConnected: boolean;
canvasIsFiltering: boolean; canvasIsFiltering: boolean;
canvasIsTransforming: boolean; canvasIsTransforming: boolean;
@@ -365,7 +385,6 @@ const createSelector = (arg: {
canvasIsSelectingObject: boolean; canvasIsSelectingObject: boolean;
}) => { }) => {
const { const {
templates,
isConnected, isConnected,
canvasIsFiltering, canvasIsFiltering,
canvasIsTransforming, canvasIsTransforming,
@@ -373,79 +392,127 @@ const createSelector = (arg: {
canvasIsCompositing, canvasIsCompositing,
canvasIsSelectingObject, canvasIsSelectingObject,
} = arg; } = 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 return createSelector(
if (!isConnected) { selectCanvasSlice,
reasons.push({ content: i18n.t('parameters.invoke.systemDisconnected') }); selectParamsSlice,
} selectDynamicPromptsSlice,
(canvas, params, dynamicPrompts) =>
if (activeTabName === 'workflows') { getReasonsWhyCannotEnqueueCanvasTab({
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,
isConnected, isConnected,
canvas,
params,
dynamicPrompts,
canvasIsFiltering, canvasIsFiltering,
canvasIsTransforming, canvasIsTransforming,
canvasIsRasterizing, canvasIsRasterizing,
canvasIsCompositing, canvasIsCompositing,
canvasIsSelectingObject, 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)
);

View File

@@ -4,7 +4,7 @@ import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser'
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; 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 { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useInvoke } from 'features/queue/hooks/useInvoke'; import { useInvoke } from 'features/queue/hooks/useInvoke';
import type { UsePanelReturn } from 'features/ui/hooks/usePanel'; import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
@@ -62,7 +62,7 @@ const FloatingSidePanelButtons = (props: Props) => {
flexGrow={1} flexGrow={1}
/> />
</Tooltip> </Tooltip>
<QueueButtonTooltip prepend={shift} placement="end"> <InvokeButtonTooltip prepend={shift} placement="end">
<IconButton <IconButton
aria-label={t('queue.queueBack')} aria-label={t('queue.queueBack')}
onClick={shift ? queue.queueFront : queue.queueBack} onClick={shift ? queue.queueFront : queue.queueBack}
@@ -72,7 +72,7 @@ const FloatingSidePanelButtons = (props: Props) => {
colorScheme="invokeYellow" colorScheme="invokeYellow"
flexGrow={1} flexGrow={1}
/> />
</QueueButtonTooltip> </InvokeButtonTooltip>
<Tooltip label={t('queue.cancelTooltip')} placement="end"> <Tooltip label={t('queue.cancelTooltip')} placement="end">
<IconButton <IconButton
isDisabled={cancelCurrent.isDisabled} isDisabled={cancelCurrent.isDisabled}

View File

@@ -425,3 +425,7 @@ const resetListQueryData = (
// we have to manually kick off another query to get the first page and re-initialize the list // we have to manually kick off another query to get the first page and re-initialize the list
dispatch(queueApi.endpoints.listQueueItems.initiate(undefined)); dispatch(queueApi.endpoints.listQueueItems.initiate(undefined));
}; };
export const enqueueMutationFixedCacheKeyOptions = {
fixedCacheKey: 'enqueueBatch',
} as const;