Skip to content

Commit e73150c

Browse files
feat(ui): improved automatic tab/panel switching on user actions
1 parent f2426c3 commit e73150c

File tree

9 files changed

+87
-30
lines changed

9 files changed

+87
-30
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2358,6 +2358,7 @@
23582358
"newGlobalReferenceImage": "New Global Reference Image",
23592359
"newRegionalReferenceImage": "New Regional Reference Image",
23602360
"newControlLayer": "New Control Layer",
2361+
"newResizedControlLayer": "New Resized Control Layer",
23612362
"newRasterLayer": "New Raster Layer",
23622363
"newInpaintMask": "New Inpaint Mask",
23632364
"newRegionalGuidance": "New Regional Guidance",

invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ const addControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.
1212
const addRegionalGuidanceReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
1313
type: 'regional_guidance_with_reference_image',
1414
});
15+
const addResizedControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
16+
type: 'control_layer',
17+
withResize: true,
18+
});
1519

1620
export const CanvasDropArea = memo(() => {
1721
const { t } = useTranslation();
@@ -45,7 +49,6 @@ export const CanvasDropArea = memo(() => {
4549
isDisabled={isBusy}
4650
/>
4751
</GridItem>
48-
4952
<GridItem position="relative">
5053
<DndDropTarget
5154
dndTarget={newCanvasEntityFromImageDndTarget}
@@ -54,6 +57,14 @@ export const CanvasDropArea = memo(() => {
5457
isDisabled={isBusy}
5558
/>
5659
</GridItem>
60+
<GridItem position="relative">
61+
<DndDropTarget
62+
dndTarget={newCanvasEntityFromImageDndTarget}
63+
dndTargetData={addResizedControlLayerFromImageDndTargetData}
64+
label={t('controlLayers.canvasContextMenu.newResizedControlLayer')}
65+
isDisabled={isBusy}
66+
/>
67+
</GridItem>
5768
</Grid>
5869
</>
5970
);

invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/CanvasLaunchpadPanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export const CanvasLaunchpadPanel = memo(() => {
2929
as="a"
3030
variant="link"
3131
href="https://support.invoke.ai/support/solutions/articles/151000216086-model-guide"
32+
target="_blank"
33+
rel="noopener noreferrer"
3234
size="sm"
3335
>
3436
{t('ui.launchpad.modelGuideLink')}

invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export const GenerateLaunchpadPanel = memo(() => {
2525
as="a"
2626
variant="link"
2727
href="https://support.invoke.ai/support/solutions/articles/151000216086-model-guide"
28+
target="_blank"
29+
rel="noopener noreferrer"
2830
size="sm"
2931
>
3032
Check out our Model Guide.

invokeai/frontend/web/src/features/dnd/dnd.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,10 @@ const _newCanvasEntity = buildTypeAndKey('new-canvas-entity-from-image');
352352
type NewCanvasEntityFromImageDndTargetData = DndData<
353353
typeof _newCanvasEntity.type,
354354
typeof _newCanvasEntity.key,
355-
{ type: CanvasEntityType | 'regional_guidance_with_reference_image' }
355+
{
356+
type: CanvasEntityType | 'regional_guidance_with_reference_image';
357+
withResize?: boolean;
358+
}
356359
>;
357360
export const newCanvasEntityFromImageDndTarget: DndTarget<
358361
NewCanvasEntityFromImageDndTargetData,
@@ -368,9 +371,9 @@ export const newCanvasEntityFromImageDndTarget: DndTarget<
368371
return true;
369372
},
370373
handler: ({ sourceData, targetData, dispatch, getState }) => {
371-
const { type } = targetData.payload;
374+
const { type, withResize } = targetData.payload;
372375
const { imageDTO } = sourceData.payload;
373-
createNewCanvasEntityFromImage({ type, imageDTO, dispatch, getState });
376+
createNewCanvasEntityFromImage({ type, imageDTO, withResize, dispatch, getState });
374377
},
375378
};
376379
//#endregion
@@ -381,7 +384,7 @@ type NewCanvasFromImageDndTargetData = DndData<
381384
typeof _newCanvas.type,
382385
typeof _newCanvas.key,
383386
{
384-
type: CanvasEntityType | 'regional_guidance_with_reference_image' | 'reference_image';
387+
type: CanvasEntityType | 'regional_guidance_with_reference_image';
385388
withResize?: boolean;
386389
withInpaintMask?: boolean;
387390
}

invokeai/frontend/web/src/features/imageActions/actions.ts

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import type { AppDispatch, AppGetState } from 'app/store/store';
22
import { deepClone } from 'common/util/deepClone';
3-
import {
4-
getDefaultRefImageConfig,
5-
getDefaultRegionalGuidanceRefImageConfig,
6-
} from 'features/controlLayers/hooks/addLayerHooks';
3+
import { getDefaultRegionalGuidanceRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
74
import { CanvasEntityTransformer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityTransformer';
85
import { getPrefixedId } from 'features/controlLayers/konva/util';
96
import { canvasReset } from 'features/controlLayers/store/actions';
@@ -17,7 +14,7 @@ import {
1714
rgAdded,
1815
rgRefImageImageChanged,
1916
} from 'features/controlLayers/store/canvasSlice';
20-
import { refImageAdded, refImageImageChanged } from 'features/controlLayers/store/refImagesSlice';
17+
import { refImageImageChanged } from 'features/controlLayers/store/refImagesSlice';
2118
import { selectBboxModelBase, selectBboxRect } from 'features/controlLayers/store/selectors';
2219
import type {
2320
CanvasControlLayerState,
@@ -37,6 +34,8 @@ import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
3734
import type { FieldIdentifier } from 'features/nodes/types/field';
3835
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
3936
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
37+
import { navigationApi } from 'features/ui/layouts/navigation-api';
38+
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
4039
import { imageDTOToFile, imagesApi, uploadImage } from 'services/api/endpoints/images';
4140
import type { ImageDTO } from 'services/api/types';
4241
import type { Equals } from 'tsafe';
@@ -76,22 +75,39 @@ export const setComparisonImage = (arg: { image_name: string; dispatch: AppDispa
7675
dispatch(imageToCompareChanged(image_name));
7776
};
7877

79-
export const createNewCanvasEntityFromImage = (arg: {
78+
export const createNewCanvasEntityFromImage = async (arg: {
8079
imageDTO: ImageDTO;
8180
type: CanvasEntityType | 'regional_guidance_with_reference_image';
81+
withResize?: boolean;
8282
dispatch: AppDispatch;
8383
getState: AppGetState;
8484
overrides?: Partial<Pick<CanvasEntityState, 'isEnabled' | 'isLocked' | 'name' | 'position'>>;
8585
}) => {
86-
const { type, imageDTO, dispatch, getState, overrides: _overrides } = arg;
86+
const { type, imageDTO, dispatch, getState, withResize, overrides: _overrides } = arg;
8787
const state = getState();
88-
const imageObject = imageDTOToImageObject(imageDTO);
89-
const { x, y } = selectBboxRect(state);
88+
const { x, y, width, height } = selectBboxRect(state);
89+
90+
let imageObject: CanvasImageState;
91+
92+
if (withResize && (width !== imageDTO.width || height !== imageDTO.height)) {
93+
const resizedImageDTO = await uploadImage({
94+
file: await imageDTOToFile(imageDTO),
95+
image_category: 'general',
96+
is_intermediate: true,
97+
silent: true,
98+
resize_to: { width, height },
99+
});
100+
imageObject = imageDTOToImageObject(resizedImageDTO);
101+
} else {
102+
imageObject = imageDTOToImageObject(imageDTO);
103+
}
104+
90105
const overrides = {
91106
objects: [imageObject],
92107
position: { x, y },
93108
..._overrides,
94109
};
110+
95111
switch (type) {
96112
case 'raster_layer': {
97113
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
@@ -122,6 +138,8 @@ export const createNewCanvasEntityFromImage = (arg: {
122138
break;
123139
}
124140
}
141+
142+
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
125143
};
126144

127145
/**
@@ -137,7 +155,7 @@ export const createNewCanvasEntityFromImage = (arg: {
137155
*/
138156
export const newCanvasFromImage = async (arg: {
139157
imageDTO: ImageDTO;
140-
type: CanvasEntityType | 'regional_guidance_with_reference_image' | 'reference_image';
158+
type: CanvasEntityType | 'regional_guidance_with_reference_image';
141159
withResize?: boolean;
142160
withInpaintMask?: boolean;
143161
dispatch: AppDispatch;
@@ -244,17 +262,6 @@ export const newCanvasFromImage = async (arg: {
244262
dispatch(canvasClearHistory());
245263
break;
246264
}
247-
case 'reference_image': {
248-
const config = deepClone(getDefaultRefImageConfig(getState));
249-
config.image = imageDTOToImageWithDims(imageDTO);
250-
dispatch(canvasReset());
251-
dispatch(refImageAdded({ overrides: { config } }));
252-
if (withInpaintMask) {
253-
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
254-
}
255-
dispatch(canvasClearHistory());
256-
break;
257-
}
258265
case 'regional_guidance_with_reference_image': {
259266
const config = getDefaultRegionalGuidanceRefImageConfig(getState);
260267
config.image = imageDTOToImageWithDims(imageDTO);
@@ -270,6 +277,9 @@ export const newCanvasFromImage = async (arg: {
270277
default:
271278
assert<Equals<typeof type, never>>(false);
272279
}
280+
281+
// Switch to the Canvas panel when creating a new canvas from image
282+
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
273283
};
274284

275285
export const replaceCanvasEntityObjectsWithImage = (arg: {

invokeai/frontend/web/src/features/queue/hooks/useInvoke.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,29 @@ export const useInvoke = () => {
6262

6363
const enqueueBack = useCallback(() => {
6464
enqueue(false, false);
65-
if (tabName === 'generate' || tabName === 'workflows' || tabName === 'upscaling') {
65+
if (tabName === 'generate' || tabName === 'upscaling') {
6666
navigationApi.focusPanel(tabName, VIEWER_PANEL_ID);
67+
} else if (tabName === 'workflows') {
68+
// Only switch to viewer if the workflow editor is not currently active
69+
const workspace = navigationApi.getPanel('workflows', WORKSPACE_PANEL_ID);
70+
if (!workspace?.api.isActive) {
71+
navigationApi.focusPanel(tabName, VIEWER_PANEL_ID);
72+
}
6773
} else if (tabName === 'canvas') {
6874
navigationApi.focusPanel(tabName, WORKSPACE_PANEL_ID);
6975
}
7076
}, [enqueue, tabName]);
7177

7278
const enqueueFront = useCallback(() => {
7379
enqueue(true, false);
74-
if (tabName === 'generate' || tabName === 'workflows' || tabName === 'upscaling') {
80+
if (tabName === 'generate' || tabName === 'upscaling') {
7581
navigationApi.focusPanel(tabName, VIEWER_PANEL_ID);
82+
} else if (tabName === 'workflows') {
83+
// Only switch to viewer if the workflow editor is not currently active
84+
const workspace = navigationApi.getPanel('workflows', WORKSPACE_PANEL_ID);
85+
if (!workspace?.api.isActive) {
86+
navigationApi.focusPanel(tabName, VIEWER_PANEL_ID);
87+
}
7688
} else if (tabName === 'canvas') {
7789
navigationApi.focusPanel(tabName, WORKSPACE_PANEL_ID);
7890
}

invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
77
import { useWorkflowLibraryModal } from 'features/nodes/store/workflowLibraryModal';
88
import { workflowModeChanged } from 'features/nodes/store/workflowLibrarySlice';
99
import { toast } from 'features/toast/toast';
10+
import { navigationApi } from 'features/ui/layouts/navigation-api';
11+
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
1012
import { memo, useCallback } from 'react';
1113
import { useTranslation } from 'react-i18next';
1214

@@ -24,6 +26,8 @@ export const useNewWorkflow = () => {
2426
dispatch(workflowModeChanged('edit'));
2527
workflowLibraryModal.close();
2628

29+
navigationApi.focusPanel('workflows', WORKSPACE_PANEL_ID);
30+
2731
toast({
2832
id: 'NEW_WORKFLOW_CREATED',
2933
title: t('workflows.newWorkflowCreated'),

invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { logger } from 'app/logging/logger';
22
import { useAppDispatch } from 'app/store/storeHooks';
3+
import { getIsFormEmpty } from 'features/nodes/components/sidePanel/builder/form-manipulation';
34
import { $nodeExecutionStates } from 'features/nodes/hooks/useNodeExecutionState';
45
import { $templates, workflowLoaded } from 'features/nodes/store/nodesSlice';
56
import { $needsFit } from 'features/nodes/store/reactFlowInstance';
7+
import { workflowModeChanged } from 'features/nodes/store/workflowLibrarySlice';
68
import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error';
79
import type { WorkflowV3 } from 'features/nodes/types/workflow';
810
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
911
import { toast } from 'features/toast/toast';
1012
import { navigationApi } from 'features/ui/layouts/navigation-api';
11-
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
13+
import { VIEWER_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
1214
import { useCallback } from 'react';
1315
import { useTranslation } from 'react-i18next';
1416
import { serializeError } from 'serialize-error';
@@ -49,7 +51,6 @@ export const useValidateAndLoadWorkflow = () => {
4951
origin: 'file' | 'image' | 'object' | 'library'
5052
): Promise<WorkflowV3 | null> => {
5153
try {
52-
await navigationApi.focusPanel('workflows', WORKSPACE_PANEL_ID);
5354
const templates = $templates.get();
5455
const { workflow, warnings } = await validateWorkflow({
5556
workflow: unvalidatedWorkflow,
@@ -68,6 +69,17 @@ export const useValidateAndLoadWorkflow = () => {
6869

6970
$nodeExecutionStates.set({});
7071
dispatch(workflowLoaded(workflow));
72+
73+
// If the form is empty, assume the user is editing a new workflow.
74+
if (getIsFormEmpty(workflow.form)) {
75+
dispatch(workflowModeChanged('edit'));
76+
navigationApi.focusPanel('workflows', WORKSPACE_PANEL_ID);
77+
} else {
78+
// Else assume they want to use the linear view of the workflow.
79+
dispatch(workflowModeChanged('view'));
80+
navigationApi.focusPanel('workflows', VIEWER_PANEL_ID);
81+
}
82+
7183
if (!warnings.length) {
7284
toast({
7385
id: 'WORKFLOW_LOADED',

0 commit comments

Comments
 (0)