experiment(ui): add generate tab

This commit is contained in:
psychedelicious
2025-06-12 14:24:57 +10:00
parent 8d1ab0a2e5
commit 3bb446c08f
27 changed files with 181 additions and 107 deletions

View File

@@ -2,8 +2,9 @@ import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { withResultAsync } from 'common/util/result';
import { canvasReset } from 'features/controlLayers/store/actions';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
@@ -90,9 +91,8 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
const overrides: Partial<CanvasRasterLayerState> = {
objects: [imageObject],
};
store.dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
store.dispatch(canvasReset());
store.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
store.dispatch(setActiveTab('canvas'));
store.dispatch(sentImageToCanvas());
$imageViewer.set(false);
toast({
@@ -116,9 +116,9 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
return;
}
const metadata = getImageMetadataResult.value;
store.dispatch(canvasReset());
// This shows a toast
await parseAndRecallAllMetadata(metadata, true);
store.dispatch(setActiveTab('canvas'));
},
[store, t]
);
@@ -162,15 +162,13 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
switch (destination) {
case 'generation':
// Go to the canvas tab, open the image viewer, and enable send-to-gallery mode
store.dispatch(canvasSessionTypeChanged({ type: 'simple' }));
store.dispatch(setActiveTab('canvas'));
store.dispatch(paramsReset());
store.dispatch(activeTabCanvasRightPanelChanged('gallery'));
$imageViewer.set(true);
break;
case 'canvas':
// Go to the canvas tab, close the image viewer, and disable send-to-gallery mode
store.dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
store.dispatch(setActiveTab('canvas'));
store.dispatch(canvasReset());
$imageViewer.set(false);
break;
case 'workflows':

View File

@@ -6,8 +6,10 @@ import { extractMessageFromAssertionError } from 'common/util/extractMessageFrom
import { withResult, withResultAsync } from 'common/util/result';
import { parseify } from 'common/util/serialize';
import {
canvasSessionGenerationStarted,
canvasSessionIdCreated,
generateSessionIdCreated,
selectCanvasSessionId,
selectGenerateSessionId,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
@@ -22,6 +24,7 @@ import { buildSD3Graph } from 'features/nodes/util/graph/generation/buildSD3Grap
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
import { toast } from 'features/toast/toast';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { serializeError } from 'serialize-error';
import { enqueueMutationFixedCacheKeyOptions, queueApi } from 'services/api/endpoints/queue';
import { assert, AssertionError } from 'tsafe';
@@ -36,12 +39,27 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
effect: async (action, { getState, dispatch }) => {
log.debug('Enqueue requested');
if (!selectCanvasSessionId(getState())) {
dispatch(canvasSessionGenerationStarted());
const tab = selectActiveTab(getState());
let sessionId = null;
if (tab === 'generate') {
sessionId = selectGenerateSessionId(getState());
if (!sessionId) {
dispatch(generateSessionIdCreated());
sessionId = selectGenerateSessionId(getState());
}
} else if (tab === 'canvas') {
sessionId = selectCanvasSessionId(getState());
if (!sessionId) {
dispatch(canvasSessionIdCreated());
sessionId = selectCanvasSessionId(getState());
}
} else {
log.warn(`Enqueue requested in unsupported tab ${tab}`);
return;
}
const state = getState();
const destination = state.canvasSession.id;
const destination = sessionId;
assert(destination !== null);
const { prepend } = action.payload;

View File

@@ -2,7 +2,7 @@ import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text }
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
selectSystemShouldConfirmOnNewSession,
shouldConfirmOnNewSessionToggled,
@@ -20,7 +20,7 @@ export const useNewGallerySession = () => {
const newSessionDialog = useNewGallerySessionDialog();
const newGallerySessionImmediate = useCallback(() => {
dispatch(canvasSessionTypeChanged({ type: 'simple' }));
dispatch(generateSessionReset());
dispatch(activeTabCanvasRightPanelChanged('gallery'));
}, [dispatch]);
@@ -41,7 +41,7 @@ export const useNewCanvasSession = () => {
const newSessionDialog = useNewCanvasSessionDialog();
const newCanvasSessionImmediate = useCallback(() => {
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(canvasSessionReset());
dispatch(activeTabCanvasRightPanelChanged('layers'));
}, [dispatch]);

View File

@@ -6,13 +6,22 @@ import { InitialStateAddAStyleReference } from 'features/controlLayers/component
import { InitialStateEditImageCard } from 'features/controlLayers/components/SimpleSession/InitialStateEditImageCard';
import { InitialStateGenerateFromText } from 'features/controlLayers/components/SimpleSession/InitialStateGenerateFromText';
import { InitialStateUseALayoutImageCard } from 'features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
export const InitialState = memo(() => {
const dispatch = useAppDispatch();
const newCanvasSession = useCallback(() => {
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(setActiveTab('canvas'));
toast({
title: 'Switched to Canvas',
description: 'You are in advanced mode yadda yadda.',
status: 'info',
position: 'top',
// isClosable: false,
duration: 5000,
});
}, [dispatch]);
return (

View File

@@ -1,28 +1,31 @@
import type { GridItemProps } from '@invoke-ai/ui-library';
import { Button, GridItem } from '@invoke-ai/ui-library';
import { Button, forwardRef, GridItem } from '@invoke-ai/ui-library';
import { memo } from 'react';
export const InitialStateButtonGridItem = memo(({ children, ...rest }: GridItemProps) => {
return (
<GridItem
as={Button}
variant="outline"
display="flex"
position="relative"
flexDir="column"
alignItems="center"
borderWidth={1}
borderRadius="base"
p={2}
pt={6}
gap={2}
w="full"
h="full"
{...rest}
>
{children}
</GridItem>
);
});
export const InitialStateButtonGridItem = memo(
forwardRef(({ children, ...rest }: GridItemProps, ref) => {
return (
<GridItem
ref={ref}
as={Button}
variant="outline"
display="flex"
position="relative"
flexDir="column"
alignItems="center"
borderWidth={1}
borderRadius="base"
p={2}
pt={6}
gap={2}
w="full"
h="full"
{...rest}
>
{children}
</GridItem>
);
})
);
InitialStateButtonGridItem.displayName = 'InitialStateButtonGridItem';

View File

@@ -1,6 +1,5 @@
/* eslint-disable i18next/no-literal-string */
import { Flex, Heading, Spacer } from '@invoke-ai/ui-library';
import { StartOverButton } from 'features/controlLayers/components/StartOverButton';
import { memo } from 'react';
export const StagingAreaHeader = memo(() => {
@@ -8,7 +7,6 @@ export const StagingAreaHeader = memo(() => {
<Flex gap={2} w="full" alignItems="center" px={2}>
<Heading size="sm">Review Session</Heading>
<Spacer />
<StartOverButton />
</Flex>
);
});

View File

@@ -5,7 +5,7 @@ import { useIsRegionFocused } from 'common/hooks/focus';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { canvasSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageNameToImageObject } from 'features/controlLayers/store/util';
@@ -40,7 +40,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
};
dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
dispatch(canvasSessionGenerationFinished());
dispatch(canvasSessionReset());
deleteQueueItemsByDestination.trigger(ctx.session.id);
}, [
selectedItemImageDTO,

View File

@@ -1,7 +1,7 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDeleteQueueItemsByDestination } from 'features/queue/hooks/useDeleteQueueItemsByDestination';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -15,8 +15,13 @@ export const StagingAreaToolbarDiscardAllButton = memo(({ isDisabled }: { isDisa
const discardAll = useCallback(() => {
deleteQueueItemsByDestination.trigger(ctx.session.id);
dispatch(canvasSessionGenerationFinished());
}, [deleteQueueItemsByDestination, ctx.session.id, dispatch]);
if (ctx.session.type === 'advanced') {
dispatch(canvasSessionReset());
} else {
// ctx.session.type === 'simple'
dispatch(generateSessionReset());
}
}, [deleteQueueItemsByDestination, ctx.session.id, ctx.session.type, dispatch]);
return (
<IconButton

View File

@@ -2,7 +2,7 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -23,9 +23,14 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i
await deleteQueueItem.trigger(selectedItemId);
const itemCount = ctx.$itemCount.get();
if (itemCount <= 1) {
dispatch(canvasSessionGenerationFinished());
if (ctx.session.type === 'advanced') {
dispatch(canvasSessionReset());
} else {
// ctx.session.type === 'simple'
dispatch(generateSessionReset());
}
}
}, [selectedItemId, ctx.$itemCount, deleteQueueItem, dispatch]);
}, [selectedItemId, deleteQueueItem, ctx.$itemCount, ctx.session.type, dispatch]);
return (
<IconButton

View File

@@ -1,15 +1,16 @@
/* eslint-disable i18next/no-literal-string */
import { Button } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { $simpleId } from 'features/ui/components/MainPanelContent';
import { memo, useCallback } from 'react';
export const StartOverButton = memo(() => {
const dispatch = useAppDispatch();
const startOver = useCallback(() => {
dispatch(canvasSessionTypeChanged({ type: 'simple' }));
}, [dispatch]);
// dispatch(canvasSessionTypeChanged({ type: 'simple' }));
$simpleId.set(null);
}, []);
return (
<Button size="sm" variant="link" alignSelf="stretch" onClick={startOver} px={2}>

View File

@@ -1,7 +1,6 @@
/* eslint-disable i18next/no-literal-string */
import { Divider, Flex, Heading } from '@invoke-ai/ui-library';
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
import { StartOverButton } from 'features/controlLayers/components/StartOverButton';
import { ToolColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { ToolSettings } from 'features/controlLayers/components/Tool/ToolSettings';
import { CanvasToolbarFitBboxToLayersButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarFitBboxToLayersButton';
@@ -51,7 +50,6 @@ export const CanvasToolbar = memo(() => {
<CanvasSettingsPopover />
</Flex>
<Divider orientation="vertical" />
<StartOverButton />
</Flex>
);
});

View File

@@ -6,7 +6,6 @@ import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasReset } from 'features/controlLayers/store/actions';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
import {
selectAllEntities,
@@ -1595,7 +1594,6 @@ export const canvasSlice = createSlice({
syncScaledSize(state);
}
});
builder.addCase(canvasSessionTypeChanged, (state) => resetState(state));
},
});

View File

@@ -5,13 +5,13 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasReset } from 'features/controlLayers/store/actions';
type CanvasStagingAreaState = {
type: 'simple' | 'advanced';
id: string | null;
generateSessionId: string | null;
canvasSessionId: string | null;
};
const INITIAL_STATE: CanvasStagingAreaState = {
type: 'simple',
id: null,
generateSessionId: null,
canvasSessionId: null,
};
const getInitialState = (): CanvasStagingAreaState => deepClone(INITIAL_STATE);
@@ -20,30 +20,39 @@ export const canvasSessionSlice = createSlice({
name: 'canvasSession',
initialState: getInitialState(),
reducers: {
canvasSessionTypeChanged: (state, action: PayloadAction<{ type: CanvasStagingAreaState['type'] }>) => {
const { type } = action.payload;
state.type = type;
state.id = null;
},
canvasSessionGenerationStarted: {
generateSessionIdCreated: {
reducer: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.id = id;
state.generateSessionId = id;
},
prepare: () => ({
payload: { id: getPrefixedId('generate') },
}),
},
generateSessionReset: (state) => {
state.generateSessionId = null;
},
canvasSessionIdCreated: {
reducer: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.canvasSessionId = id;
},
prepare: () => ({
payload: { id: getPrefixedId('canvas') },
}),
},
canvasSessionGenerationFinished: (state) => {
state.id = null;
canvasSessionReset: (state) => {
state.canvasSessionId = null;
},
},
extraReducers(builder) {
builder.addCase(canvasReset, () => getInitialState());
builder.addCase(canvasReset, (state) => {
state.canvasSessionId = null;
});
},
});
export const { canvasSessionTypeChanged, canvasSessionGenerationStarted, canvasSessionGenerationFinished } =
export const { generateSessionIdCreated, generateSessionReset, canvasSessionIdCreated, canvasSessionReset } =
canvasSessionSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -60,6 +69,9 @@ export const canvasStagingAreaPersistConfig: PersistConfig<CanvasStagingAreaStat
export const selectCanvasSessionSlice = (s: RootState) => s[canvasSessionSlice.name];
export const selectIsStaging = createSelector(selectCanvasSessionSlice, ({ id }) => id !== null);
export const selectCanvasSessionType = createSelector(selectCanvasSessionSlice, ({ type }) => type);
export const selectCanvasSessionId = createSelector(selectCanvasSessionSlice, ({ id }) => id);
export const selectCanvasSessionId = createSelector(selectCanvasSessionSlice, ({ canvasSessionId }) => canvasSessionId);
export const selectGenerateSessionId = createSelector(
selectCanvasSessionSlice,
({ generateSessionId }) => generateSessionId
);
export const selectIsStaging = createSelector(selectCanvasSessionId, (canvasSessionId) => canvasSessionId !== null);

View File

@@ -1,7 +1,7 @@
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import type { LoRA } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { LoRAModelConfig } from 'services/api/types';
@@ -64,7 +64,7 @@ export const lorasSlice = createSlice({
},
},
extraReducers(builder) {
builder.addCase(canvasSessionTypeChanged, () => {
builder.addCase(paramsReset, () => {
// When a new session is requested, clear all LoRAs
return deepClone(initialState);
});

View File

@@ -2,7 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import type { ParameterHRFMethod, ParameterStrength } from 'features/parameters/types/parameterSchemas';
interface HRFState {
@@ -34,7 +34,7 @@ export const hrfSlice = createSlice({
},
},
extraReducers(builder) {
builder.addCase(canvasSessionTypeChanged, () => {
builder.addCase(paramsReset, () => {
return deepClone(initialHRFState);
});
},

View File

@@ -6,6 +6,7 @@ import {
} from 'features/controlLayers/hooks/addLayerHooks';
import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasReset } from 'features/controlLayers/store/actions';
import {
bboxChangedFromCanvas,
canvasClearHistory,
@@ -16,7 +17,6 @@ import {
rgAdded,
rgRefImageImageChanged,
} from 'features/controlLayers/store/canvasSlice';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { refImageAdded, refImageImageChanged } from 'features/controlLayers/store/refImagesSlice';
import { selectBboxModelBase, selectBboxRect } from 'features/controlLayers/store/selectors';
import type {
@@ -185,7 +185,7 @@ export const newCanvasFromImage = async (arg: {
objects: [imageObject],
} satisfies Partial<CanvasRasterLayerState>;
addFitOnLayerInitCallback(overrides.id);
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
@@ -202,7 +202,7 @@ export const newCanvasFromImage = async (arg: {
controlAdapter: deepClone(initialControlNet),
} satisfies Partial<CanvasControlLayerState>;
addFitOnLayerInitCallback(overrides.id);
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(controlLayerAdded({ overrides, isSelected: true }));
@@ -218,7 +218,7 @@ export const newCanvasFromImage = async (arg: {
objects: [imageObject],
} satisfies Partial<CanvasInpaintMaskState>;
addFitOnLayerInitCallback(overrides.id);
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
@@ -234,7 +234,7 @@ export const newCanvasFromImage = async (arg: {
objects: [imageObject],
} satisfies Partial<CanvasRegionalGuidanceState>;
addFitOnLayerInitCallback(overrides.id);
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(canvasReset());
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
dispatch(rgAdded({ overrides, isSelected: true }));
@@ -247,7 +247,7 @@ export const newCanvasFromImage = async (arg: {
case 'reference_image': {
const config = deepClone(getDefaultRefImageConfig(getState));
config.image = imageDTOToImageWithDims(imageDTO);
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(canvasReset());
dispatch(refImageAdded({ overrides: { config } }));
if (withInpaintMask) {
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
@@ -259,7 +259,7 @@ export const newCanvasFromImage = async (arg: {
const config = getDefaultRegionalGuidanceRefImageConfig(getState);
config.image = imageDTOToImageWithDims(imageDTO);
const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), config }];
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
dispatch(canvasReset());
dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true }));
if (withInpaintMask) {
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));

View File

@@ -1,13 +1,13 @@
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from 'app/store/store';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import type { CanvasState, ParamsState } from 'features/controlLayers/store/types';
import type { BoardField } from 'features/nodes/types/common';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
import { selectStylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { pick } from 'lodash-es';
import { selectListStylePresetsRequestState } from 'services/api/endpoints/stylePresets';
import type { Invocation, S } from 'services/api/types';
@@ -36,9 +36,9 @@ export const getBoardField = (state: RootState): BoardField | undefined => {
export const selectCanvasOutputFields = (state: RootState) => {
// Advanced session means working on canvas - images are not saved to gallery or added to a board.
// Simple session means working in YOLO mode - images are saved to gallery & board.
const type = selectCanvasSessionType(state);
const is_intermediate = type === 'advanced';
const board = type === 'advanced' ? undefined : getBoardField(state);
const tab = selectActiveTab(state);
const is_intermediate = tab === 'canvas';
const board = tab === 'canvas' ? undefined : getBoardField(state);
return {
is_intermediate,

View File

@@ -44,7 +44,7 @@ export const useInvoke = () => {
return;
}
if (tabName === 'canvas') {
if (tabName === 'canvas' || tabName === 'generate') {
dispatch(enqueueRequestedCanvas({ prepend }));
return;
}

View File

@@ -2,7 +2,7 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { canvasSessionGenerationStarted } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
import { atom } from 'nanostores';
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
@@ -29,7 +29,7 @@ export const stylePresetSlice = createSlice({
},
},
extraReducers(builder) {
builder.addCase(canvasSessionGenerationStarted, () => {
builder.addCase(paramsReset, () => {
return deepClone(initialState);
});
builder.addMatcher(stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled, (state, action) => {

View File

@@ -2,7 +2,6 @@ import { ButtonGroup, Flex, Icon, IconButton, spinAnimation, Tooltip, useShiftMo
import { useAppSelector } from 'app/store/storeHooks';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
@@ -22,11 +21,10 @@ import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
export const FloatingLeftPanelButtons = memo((props: { onToggle: () => void }) => {
const tab = useAppSelector(selectActiveTab);
const type = useAppSelector(selectCanvasSessionType);
return (
<Flex pos="absolute" transform="translate(0, -50%)" top="50%" insetInlineStart={2} direction="column" gap={2}>
{tab === 'canvas' && type === 'advanced' && (
{tab === 'canvas' && (
<CanvasManagerProviderGate>
<ToolChooser />
</CanvasManagerProviderGate>

View File

@@ -15,6 +15,7 @@ export const LeftPanelContent = memo(() => {
<Flex flexDir="column" w="full" h="full" gap={2}>
<QueueControls />
<Box position="relative" w="full" h="full">
{tab === 'generate' && <ParametersPanelTextToImage />}
{tab === 'canvas' && <ParametersPanelTextToImage />}
{tab === 'upscaling' && <ParametersPanelUpscale />}
{tab === 'workflows' && <WorkflowsTabLeftPanel />}

View File

@@ -1,19 +1,30 @@
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasMainPanelContent } from 'features/controlLayers/components/CanvasMainPanelContent';
import { AdvancedSession } from 'features/controlLayers/components/AdvancedSession/AdvancedSession';
import { SimpleSession } from 'features/controlLayers/components/SimpleSession/SimpleSession';
import { selectCanvasSessionId, selectGenerateSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
import QueueTab from 'features/ui/components/tabs/QueueTab';
import { WorkflowsMainPanel } from 'features/ui/components/tabs/WorkflowsTabContent';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { atom } from 'nanostores';
import { memo } from 'react';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
export const $simpleId = atom<string | null>(null);
export const $advancedId = atom<string | null>(null);
export const MainPanelContent = memo(() => {
const tab = useAppSelector(selectActiveTab);
const generateId = useAppSelector(selectGenerateSessionId);
const canvasId = useAppSelector(selectCanvasSessionId);
if (tab === 'generate') {
return <SimpleSession id={generateId} />;
}
if (tab === 'canvas') {
return <CanvasMainPanelContent />;
return <AdvancedSession id={canvasId} />;
}
if (tab === 'upscaling') {
return <ImageViewer />;

View File

@@ -4,7 +4,6 @@ import { useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { BoardsListPanelContent } from 'features/gallery/components/BoardsListPanelContent';
import { Gallery } from 'features/gallery/components/Gallery';
import { GalleryTopBar } from 'features/gallery/components/GalleryTopBar';
@@ -29,7 +28,6 @@ export const RightPanelContent = memo(() => {
const boardSearchText = useAppSelector(selectBoardSearchText);
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
const imperativePanelGroupRef = useRef<ImperativePanelGroupHandle>(null);
const type = useAppSelector(selectCanvasSessionType);
const tab = useAppSelector(selectActiveTab);
const boardsListPanelOptions = useMemo<UsePanelOptions>(
@@ -79,7 +77,7 @@ export const RightPanelContent = memo(() => {
<Panel order={1} id="gallery-wrapper-panel" collapsible {...galleryPanel.panelProps}>
<Gallery />
</Panel>
{tab === 'canvas' && type === 'advanced' && (
{tab === 'canvas' && (
<>
<HorizontalResizeHandle id="gallery-panel-to-layers-handle" {...galleryPanel.resizeHandleProps} />
<Panel order={2} id="canvas-layers-panel" collapsible {...canvasLayersPanel.panelProps}>

View File

@@ -1,3 +1,4 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
@@ -5,6 +6,12 @@ import { setActiveTab } from 'features/ui/store/uiSlice';
import type { TabName } from 'features/ui/store/uiTypes';
import { forwardRef, memo, type ReactElement, useCallback } from 'react';
const sx: SystemStyleObject = {
'&[data-selected=true]': {
svg: { fill: 'invokeYellow.300' },
},
};
export const TabButton = memo(
forwardRef(({ tab, icon, label }: { tab: TabName; icon: ReactElement; label: string }, ref) => {
const dispatch = useAppDispatch();
@@ -26,6 +33,7 @@ export const TabButton = memo(
data-selected={activeTabName === tab}
aria-label={label}
data-testid={label}
sx={sx}
/>
</Tooltip>
);

View File

@@ -8,7 +8,7 @@ import { VideosModalButton } from 'features/system/components/VideosModal/Videos
import { TabMountGate } from 'features/ui/components/TabMountGate';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiBoundingBoxBold, PiCubeBold, PiFlowArrowBold, PiFrameCornersBold, PiQueueBold } from 'react-icons/pi';
import { PiBoundingBoxBold, PiCubeBold, PiFlowArrowBold, PiFrameCornersBold, PiQueueBold, PiTextAaBold } from 'react-icons/pi';
import { Notifications } from './Notifications';
import { TabButton } from './TabButton';
@@ -21,6 +21,9 @@ export const VerticalNavBar = memo(() => {
<Flex flexDir="column" alignItems="center" py={2} gap={4} minW={0}>
<InvokeAILogoComponent />
<Flex gap={4} pt={6} h="full" flexDir="column">
<TabMountGate tab="generate">
<TabButton tab="generate" icon={<PiTextAaBold />} label="Generate" />
</TabMountGate>
<TabMountGate tab="canvas">
<TabButton tab="canvas" icon={<PiBoundingBoxBold />} label={t('ui.tabs.canvas')} />
</TabMountGate>

View File

@@ -1,7 +1,8 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { canvasReset } from 'features/controlLayers/store/actions';
import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
import type { Dimensions } from 'features/controlLayers/store/types';
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
import { atom } from 'nanostores';
@@ -56,9 +57,18 @@ export const uiSlice = createSlice({
builder.addCase(workflowLoaded, (state) => {
state.activeTab = 'workflows';
});
builder.addCase(canvasSessionTypeChanged, (state) => {
builder.addCase(canvasReset, (state) => {
state.activeTab = 'canvas';
});
builder.addCase(canvasSessionReset, (state) => {
state.activeTab = 'canvas';
});
builder.addCase(generateSessionReset, (state) => {
state.activeTab = 'generate';
});
// builder.addCase(canvasSessionTypeChanged, (state) => {
// state.activeTab = 'canvas';
// });
},
});
@@ -98,12 +108,12 @@ export const uiPersistConfig: PersistConfig<UIState> = {
persistDenylist: ['shouldShowImageDetails'],
};
const TABS_WITH_LEFT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows'] as const;
const TABS_WITH_LEFT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows', 'generate'] as const;
export const LEFT_PANEL_MIN_SIZE_PX = 400;
export const $isLeftPanelOpen = atom(true);
export const selectWithLeftPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_LEFT_PANEL.includes(ui.activeTab));
const TABS_WITH_RIGHT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows'] as const;
const TABS_WITH_RIGHT_PANEL: TabName[] = ['canvas', 'upscaling', 'workflows', 'generate'] as const;
export const RIGHT_PANEL_MIN_SIZE_PX = 390;
export const $isRightPanelOpen = atom(true);
export const selectWithRightPanel = createSelector(selectUiSlice, (ui) => TABS_WITH_RIGHT_PANEL.includes(ui.activeTab));

View File

@@ -1,6 +1,6 @@
import type { Dimensions } from 'features/controlLayers/store/types';
export type TabName = 'canvas' | 'upscaling' | 'workflows' | 'models' | 'queue';
export type TabName = 'generate' | 'canvas' | 'upscaling' | 'workflows' | 'models' | 'queue';
export type CanvasRightPanelTabName = 'layers' | 'gallery';
export interface UIState {