From 711fe91b24d89a7609f2359f4e112f306416e4e3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:42:07 +1000 Subject: [PATCH] feat(ui): add on first progress autoswitch mode --- .../SimpleSession/QueueItemPreviewMini.tsx | 12 +-- .../SimpleSession/StagingAreaHeader.tsx | 22 +----- .../components/SimpleSession/context.tsx | 23 ++++-- .../StagingArea/SimpleStagingAreaToolbar.tsx | 2 + .../SimpleStagingAreaToolbarMenu.tsx | 17 +++++ .../StagingArea/StagingAreaToolbar.tsx | 4 +- .../StagingArea/StagingAreaToolbarMenu.tsx | 20 +++++ .../StagingAreaToolbarMenuAutoSwitch.tsx | 34 +++++++++ ...agingAreaToolbarMenuNewLayerFromImage.tsx} | 75 ++++++++----------- 9 files changed, 125 insertions(+), 84 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbarMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx rename invokeai/frontend/web/src/features/controlLayers/components/StagingArea/{StagingAreaToolbarSaveAsMenu.tsx => StagingAreaToolbarMenuNewLayerFromImage.tsx} (68%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx index 3393d7a18e..5851178b0f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/QueueItemPreviewMini.tsx @@ -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 ( - + {imageDTO && } {!imageLoaded && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx index b048d7dd59..0f4828c93f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/StagingAreaHeader.tsx @@ -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) => { - ctx.$autoSwitch.set(e.target.checked); - }, - [ctx.$autoSwitch] - ); - return ( Review Session - - Auto-switch - - - ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx index 172a8188ce..3f7a608b7b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/context.tsx @@ -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; export type ProgressData = { itemId: number; @@ -86,7 +90,7 @@ type CanvasSessionContextValue = { $selectedItem: Atom; $selectedItemIndex: Atom; $selectedItemOutputImageName: Atom; - $autoSwitch: WritableAtom; + $autoSwitch: WritableAtom; $lastLoadedItemId: WritableAtom; 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('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); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx index 26511981f3..3fa5e3e0ca 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbar.tsx @@ -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(() => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbarMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbarMenu.tsx new file mode 100644 index 0000000000..60b3a39203 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/SimpleStagingAreaToolbarMenu.tsx @@ -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 ( + + } colorScheme="invokeBlue" /> + + + + + ); +}); + +SimpleStagingAreaToolbarMenu.displayName = 'SimpleStagingAreaToolbarMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index 63926e766d..6b57e5cf93 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -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(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenu.tsx new file mode 100644 index 0000000000..73710521da --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenu.tsx @@ -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 ( + + } colorScheme="invokeBlue" /> + + + + + + + ); +}); + +StagingAreaToolbarMenu.displayName = 'StagingAreaToolbarMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx new file mode 100644 index 0000000000..567e6beb5e --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuAutoSwitch.tsx @@ -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 ( + + + Off + + + First Progress + + + Completed + + + ); +}); + +StagingAreaToolbarMenuAutoSwitch.displayName = 'StagingAreaToolbarMenuAutoSwitch'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage.tsx similarity index 68% rename from invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage.tsx index 455eb97319..54ab1578db 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveAsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarMenuNewLayerFromImage.tsx @@ -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 ( - - } - colorScheme="invokeBlue" + + } + onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={!imageName || !shouldShowStagedImage} - /> - - } - onClickCapture={onClickNewInpaintMaskFromImage} - isDisabled={!imageName || !shouldShowStagedImage} - > - {t('controlLayers.inpaintMask')} - - } - onClickCapture={onClickNewRegionalGuidanceFromImage} - isDisabled={!imageName || !shouldShowStagedImage} - > - {t('controlLayers.regionalGuidance')} - - } - onClickCapture={onClickNewControlLayerFromImage} - isDisabled={!imageName || !shouldShowStagedImage} - > - {t('controlLayers.controlLayer')} - - } - onClickCapture={onClickNewRasterLayerFromImage} - isDisabled={!imageName || !shouldShowStagedImage} - > - {t('controlLayers.rasterLayer')} - - - + > + {t('controlLayers.inpaintMask')} + + } + onClickCapture={onClickNewRegionalGuidanceFromImage} + isDisabled={!imageName || !shouldShowStagedImage} + > + {t('controlLayers.regionalGuidance')} + + } + onClickCapture={onClickNewControlLayerFromImage} + isDisabled={!imageName || !shouldShowStagedImage} + > + {t('controlLayers.controlLayer')} + + } + onClickCapture={onClickNewRasterLayerFromImage} + isDisabled={!imageName || !shouldShowStagedImage} + > + {t('controlLayers.rasterLayer')} + + ); }); -StagingAreaToolbarSaveAsMenu.displayName = 'StagingAreaToolbarSaveAsMenu'; +StagingAreaToolbarNewLayerFromImageMenuItems.displayName = 'StagingAreaToolbarNewLayerFromImageMenuItems';