feat(ui): add on first progress autoswitch mode

This commit is contained in:
psychedelicious
2025-06-10 12:42:07 +10:00
parent 2f26657c17
commit 711fe91b24
9 changed files with 125 additions and 84 deletions

View File

@@ -42,10 +42,6 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) =
ctx.$selectedItemId.set(item.item_id);
}, [ctx.$selectedItemId, item.item_id]);
const onDoubleClick = useCallback(() => {
ctx.$autoSwitch.set(item.status === 'in_progress');
}, [ctx.$autoSwitch, item.status]);
const onLoad = useCallback(() => {
setImageLoaded(true);
if (ctx.$progressData.get()[item.item_id]) {
@@ -54,13 +50,7 @@ export const QueueItemPreviewMini = memo(({ item, isSelected, number }: Props) =
}, [ctx.$lastLoadedItemId, ctx.$progressData, item.item_id]);
return (
<Flex
id={getQueueItemElementId(item.item_id)}
sx={sx}
data-selected={isSelected}
onClick={onClick}
onDoubleClick={onDoubleClick}
>
<Flex id={getQueueItemElementId(item.item_id)} sx={sx} data-selected={isSelected} onClick={onClick}>
<QueueItemStatusLabel status={item.status} position="absolute" margin="auto" />
{imageDTO && <DndImage imageDTO={imageDTO} onLoad={onLoad} asThumbnail />}
{!imageLoaded && <QueueItemProgressImage itemId={item.item_id} position="absolute" />}

View File

@@ -1,31 +1,13 @@
/* eslint-disable i18next/no-literal-string */
import { Divider, Flex, FormControl, FormLabel, Heading, Spacer, Switch } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { Flex, Heading, Spacer } from '@invoke-ai/ui-library';
import { StartOverButton } from 'features/controlLayers/components/StartOverButton';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { memo } from 'react';
export const StagingAreaHeader = memo(() => {
const ctx = useCanvasSessionContext();
const autoSwitch = useStore(ctx.$autoSwitch);
const onChangeAutoSwitch = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
ctx.$autoSwitch.set(e.target.checked);
},
[ctx.$autoSwitch]
);
return (
<Flex gap={2} w="full" alignItems="center" px={2}>
<Heading size="sm">Review Session</Heading>
<Spacer />
<FormControl w="min-content" me={2}>
<FormLabel m={0}>Auto-switch</FormLabel>
<Switch size="sm" isChecked={autoSwitch} onChange={onChangeAutoSwitch} />
</FormControl>
<Divider orientation="vertical" />
<StartOverButton />
</Flex>
);

View File

@@ -12,6 +12,10 @@ import { queueApi } from 'services/api/endpoints/queue';
import type { S } from 'services/api/types';
import { $socket } from 'services/events/stores';
import { assert } from 'tsafe';
import { z } from 'zod';
export const zAutoSwitchMode = z.enum(['off', 'first_progress', 'completed']);
export type AutoSwitchMode = z.infer<typeof zAutoSwitchMode>;
export type ProgressData = {
itemId: number;
@@ -86,7 +90,7 @@ type CanvasSessionContextValue = {
$selectedItem: Atom<S['SessionQueueItem'] | null>;
$selectedItemIndex: Atom<number | null>;
$selectedItemOutputImageName: Atom<string | null>;
$autoSwitch: WritableAtom<boolean>;
$autoSwitch: WritableAtom<AutoSwitchMode>;
$lastLoadedItemId: WritableAtom<number | null>;
selectNext: () => void;
selectPrev: () => void;
@@ -121,7 +125,7 @@ export const CanvasSessionContextProvider = memo(
/**
* Whether auto-switch is enabled.
*/
const $autoSwitch = useState(() => atom(true))[0];
const $autoSwitch = useState(() => atom<AutoSwitchMode>('first_progress'))[0];
/**
* An internal flag used to work around race conditions with auto-switch switching to queue items before their
@@ -184,15 +188,15 @@ export const CanvasSessionContextProvider = memo(
* image recorded.
*/
const $selectedItemOutputImageName = useState(() =>
computed([$selectedItem], (selectedItem) => {
if (selectedItem === null) {
computed([$selectedItemId, $progressData], (selectedItemId, progressData) => {
if (selectedItemId === null) {
return null;
}
const outputImageName = getOutputImageName(selectedItem);
if (outputImageName === null) {
const datum = progressData[selectedItemId];
if (!datum) {
return null;
}
return outputImageName;
return datum.outputImageName;
})
)[0];
@@ -269,6 +273,9 @@ export const CanvasSessionContextProvider = memo(
return;
}
setProgress($progressData, data);
if ($autoSwitch.get() === 'first_progress') {
$selectedItemId.set(data.item_id);
}
};
socket.on('invocation_progress', onProgress);
@@ -391,7 +398,7 @@ export const CanvasSessionContextProvider = memo(
if (lastLoadedItemId === null) {
return;
}
if ($autoSwitch.get()) {
if ($autoSwitch.get() === 'completed') {
$selectedItemId.set(lastLoadedItemId);
}
$lastLoadedItemId.set(null);

View File

@@ -1,4 +1,5 @@
import { ButtonGroup } from '@invoke-ai/ui-library';
import { SimpleStagingAreaToolbarMenu } from 'features/controlLayers/components/StagingArea/SimpleStagingAreaToolbarMenu';
import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
import { StagingAreaToolbarImageCountButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton';
@@ -16,6 +17,7 @@ export const SimpleStagingAreaToolbar = memo(() => {
</ButtonGroup>
<ButtonGroup borderRadius="base" shadow="dark-lg">
<StagingAreaToolbarDiscardSelectedButton />
<SimpleStagingAreaToolbarMenu />
<StagingAreaToolbarDiscardAllButton />
</ButtonGroup>
</>

View File

@@ -0,0 +1,17 @@
import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { StagingAreaToolbarMenuAutoSwitch } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch';
import { memo } from 'react';
import { PiDotsThreeBold } from 'react-icons/pi';
export const SimpleStagingAreaToolbarMenu = memo(() => {
return (
<Menu>
<MenuButton as={IconButton} icon={<PiDotsThreeBold />} colorScheme="invokeBlue" />
<MenuList>
<StagingAreaToolbarMenuAutoSwitch />
</MenuList>
</Menu>
);
});
SimpleStagingAreaToolbarMenu.displayName = 'SimpleStagingAreaToolbarMenu';

View File

@@ -6,9 +6,9 @@ import { StagingAreaToolbarAcceptButton } from 'features/controlLayers/component
import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
import { StagingAreaToolbarImageCountButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton';
import { StagingAreaToolbarMenu } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarMenu';
import { StagingAreaToolbarNextButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton';
import { StagingAreaToolbarPrevButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton';
import { StagingAreaToolbarSaveAsMenu } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu';
import { StagingAreaToolbarSaveSelectedToGalleryButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton';
import { StagingAreaToolbarToggleShowResultsButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
@@ -42,7 +42,7 @@ export const StagingAreaToolbar = memo(() => {
<StagingAreaToolbarAcceptButton />
<StagingAreaToolbarToggleShowResultsButton />
<StagingAreaToolbarSaveSelectedToGalleryButton />
<StagingAreaToolbarSaveAsMenu />
<StagingAreaToolbarMenu />
<StagingAreaToolbarDiscardSelectedButton isDisabled={!shouldShowStagedImage} />
<StagingAreaToolbarDiscardAllButton isDisabled={!shouldShowStagedImage} />
</ButtonGroup>

View File

@@ -0,0 +1,20 @@
import { IconButton, Menu, MenuButton, MenuDivider, MenuList } from '@invoke-ai/ui-library';
import { StagingAreaToolbarMenuAutoSwitch } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch';
import { StagingAreaToolbarNewLayerFromImageMenuItems } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage';
import { memo } from 'react';
import { PiDotsThreeBold } from 'react-icons/pi';
export const StagingAreaToolbarMenu = memo(() => {
return (
<Menu>
<MenuButton as={IconButton} icon={<PiDotsThreeBold />} colorScheme="invokeBlue" />
<MenuList>
<StagingAreaToolbarMenuAutoSwitch />
<MenuDivider />
<StagingAreaToolbarNewLayerFromImageMenuItems />
</MenuList>
</Menu>
);
});
StagingAreaToolbarMenu.displayName = 'StagingAreaToolbarMenu';

View File

@@ -0,0 +1,34 @@
/* eslint-disable i18next/no-literal-string */
import { MenuItemOption, MenuOptionGroup } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCanvasSessionContext, zAutoSwitchMode } from 'features/controlLayers/components/SimpleSession/context';
import { memo, useCallback } from 'react';
export const StagingAreaToolbarMenuAutoSwitch = memo(() => {
const ctx = useCanvasSessionContext();
const autoSwitch = useStore(ctx.$autoSwitch);
const onChange = useCallback(
(val: string | string[]) => {
const newAutoSwitch = zAutoSwitchMode.parse(val);
ctx.$autoSwitch.set(newAutoSwitch);
},
[ctx.$autoSwitch]
);
return (
<MenuOptionGroup value={autoSwitch} onChange={onChange} title="Auto Switch" type="radio">
<MenuItemOption value="off" closeOnSelect={false}>
Off
</MenuItemOption>
<MenuItemOption value="first_progress" closeOnSelect={false}>
First Progress
</MenuItemOption>
<MenuItemOption value="completed" closeOnSelect={false}>
Completed
</MenuItemOption>
</MenuOptionGroup>
);
});
StagingAreaToolbarMenuAutoSwitch.displayName = 'StagingAreaToolbarMenuAutoSwitch';

View File

@@ -1,4 +1,4 @@
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { MenuGroup, MenuItem } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/nanostores/store';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
@@ -8,12 +8,11 @@ import { createNewCanvasEntityFromImage } from 'features/imageActions/actions';
import { toast } from 'features/toast/toast';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDotsThreeBold } from 'react-icons/pi';
import { copyImage } from 'services/api/endpoints/images';
const uploadImageArg = { image_category: 'general', is_intermediate: true, silent: true } as const;
export const StagingAreaToolbarSaveAsMenu = memo(() => {
export const StagingAreaToolbarNewLayerFromImageMenuItems = memo(() => {
const canvasManager = useCanvasManager();
const { t } = useTranslation();
const ctx = useCanvasSessionContext();
@@ -97,47 +96,37 @@ export const StagingAreaToolbarSaveAsMenu = memo(() => {
}, [imageName, store, toastSentToCanvas]);
return (
<Menu>
<MenuButton
as={IconButton}
aria-label={t('controlLayers.newLayerFromImage')}
tooltip={t('controlLayers.newLayerFromImage')}
icon={<PiDotsThreeBold />}
colorScheme="invokeBlue"
<MenuGroup title="New Layer From Image">
<MenuItem
icon={<NewLayerIcon />}
onClickCapture={onClickNewInpaintMaskFromImage}
isDisabled={!imageName || !shouldShowStagedImage}
/>
<MenuList>
<MenuItem
icon={<NewLayerIcon />}
onClickCapture={onClickNewInpaintMaskFromImage}
isDisabled={!imageName || !shouldShowStagedImage}
>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem
icon={<NewLayerIcon />}
onClickCapture={onClickNewRegionalGuidanceFromImage}
isDisabled={!imageName || !shouldShowStagedImage}
>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem
icon={<NewLayerIcon />}
onClickCapture={onClickNewControlLayerFromImage}
isDisabled={!imageName || !shouldShowStagedImage}
>
{t('controlLayers.controlLayer')}
</MenuItem>
<MenuItem
icon={<NewLayerIcon />}
onClickCapture={onClickNewRasterLayerFromImage}
isDisabled={!imageName || !shouldShowStagedImage}
>
{t('controlLayers.rasterLayer')}
</MenuItem>
</MenuList>
</Menu>
>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem
icon={<NewLayerIcon />}
onClickCapture={onClickNewRegionalGuidanceFromImage}
isDisabled={!imageName || !shouldShowStagedImage}
>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem
icon={<NewLayerIcon />}
onClickCapture={onClickNewControlLayerFromImage}
isDisabled={!imageName || !shouldShowStagedImage}
>
{t('controlLayers.controlLayer')}
</MenuItem>
<MenuItem
icon={<NewLayerIcon />}
onClickCapture={onClickNewRasterLayerFromImage}
isDisabled={!imageName || !shouldShowStagedImage}
>
{t('controlLayers.rasterLayer')}
</MenuItem>
</MenuGroup>
);
});
StagingAreaToolbarSaveAsMenu.displayName = 'StagingAreaToolbarSaveAsMenu';
StagingAreaToolbarNewLayerFromImageMenuItems.displayName = 'StagingAreaToolbarNewLayerFromImageMenuItems';