mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-14 01:34:59 -05:00
feat(ui): improved automatic tab/panel switching on user actions
This commit is contained in:
@@ -2358,6 +2358,7 @@
|
||||
"newGlobalReferenceImage": "New Global Reference Image",
|
||||
"newRegionalReferenceImage": "New Regional Reference Image",
|
||||
"newControlLayer": "New Control Layer",
|
||||
"newResizedControlLayer": "New Resized Control Layer",
|
||||
"newRasterLayer": "New Raster Layer",
|
||||
"newInpaintMask": "New Inpaint Mask",
|
||||
"newRegionalGuidance": "New Regional Guidance",
|
||||
|
||||
@@ -12,6 +12,10 @@ const addControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.
|
||||
const addRegionalGuidanceReferenceImageFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
|
||||
type: 'regional_guidance_with_reference_image',
|
||||
});
|
||||
const addResizedControlLayerFromImageDndTargetData = newCanvasEntityFromImageDndTarget.getData({
|
||||
type: 'control_layer',
|
||||
withResize: true,
|
||||
});
|
||||
|
||||
export const CanvasDropArea = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
@@ -45,7 +49,6 @@ export const CanvasDropArea = memo(() => {
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
<GridItem position="relative">
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasEntityFromImageDndTarget}
|
||||
@@ -54,6 +57,14 @@ export const CanvasDropArea = memo(() => {
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem position="relative">
|
||||
<DndDropTarget
|
||||
dndTarget={newCanvasEntityFromImageDndTarget}
|
||||
dndTargetData={addResizedControlLayerFromImageDndTargetData}
|
||||
label={t('controlLayers.canvasContextMenu.newResizedControlLayer')}
|
||||
isDisabled={isBusy}
|
||||
/>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -29,6 +29,8 @@ export const CanvasLaunchpadPanel = memo(() => {
|
||||
as="a"
|
||||
variant="link"
|
||||
href="https://support.invoke.ai/support/solutions/articles/151000216086-model-guide"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="sm"
|
||||
>
|
||||
{t('ui.launchpad.modelGuideLink')}
|
||||
|
||||
@@ -25,6 +25,8 @@ export const GenerateLaunchpadPanel = memo(() => {
|
||||
as="a"
|
||||
variant="link"
|
||||
href="https://support.invoke.ai/support/solutions/articles/151000216086-model-guide"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="sm"
|
||||
>
|
||||
Check out our Model Guide.
|
||||
|
||||
@@ -352,7 +352,10 @@ const _newCanvasEntity = buildTypeAndKey('new-canvas-entity-from-image');
|
||||
type NewCanvasEntityFromImageDndTargetData = DndData<
|
||||
typeof _newCanvasEntity.type,
|
||||
typeof _newCanvasEntity.key,
|
||||
{ type: CanvasEntityType | 'regional_guidance_with_reference_image' }
|
||||
{
|
||||
type: CanvasEntityType | 'regional_guidance_with_reference_image';
|
||||
withResize?: boolean;
|
||||
}
|
||||
>;
|
||||
export const newCanvasEntityFromImageDndTarget: DndTarget<
|
||||
NewCanvasEntityFromImageDndTargetData,
|
||||
@@ -368,9 +371,9 @@ export const newCanvasEntityFromImageDndTarget: DndTarget<
|
||||
return true;
|
||||
},
|
||||
handler: ({ sourceData, targetData, dispatch, getState }) => {
|
||||
const { type } = targetData.payload;
|
||||
const { type, withResize } = targetData.payload;
|
||||
const { imageDTO } = sourceData.payload;
|
||||
createNewCanvasEntityFromImage({ type, imageDTO, dispatch, getState });
|
||||
createNewCanvasEntityFromImage({ type, imageDTO, withResize, dispatch, getState });
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
@@ -381,7 +384,7 @@ type NewCanvasFromImageDndTargetData = DndData<
|
||||
typeof _newCanvas.type,
|
||||
typeof _newCanvas.key,
|
||||
{
|
||||
type: CanvasEntityType | 'regional_guidance_with_reference_image' | 'reference_image';
|
||||
type: CanvasEntityType | 'regional_guidance_with_reference_image';
|
||||
withResize?: boolean;
|
||||
withInpaintMask?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { AppDispatch, AppGetState } from 'app/store/store';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import {
|
||||
getDefaultRefImageConfig,
|
||||
getDefaultRegionalGuidanceRefImageConfig,
|
||||
} from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { getDefaultRegionalGuidanceRefImageConfig } 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';
|
||||
@@ -17,7 +14,7 @@ import {
|
||||
rgAdded,
|
||||
rgRefImageImageChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { refImageAdded, refImageImageChanged } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { refImageImageChanged } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { selectBboxModelBase, selectBboxRect } from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
@@ -37,6 +34,8 @@ import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { imageDTOToFile, imagesApi, uploadImage } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { Equals } from 'tsafe';
|
||||
@@ -76,22 +75,39 @@ export const setComparisonImage = (arg: { image_name: string; dispatch: AppDispa
|
||||
dispatch(imageToCompareChanged(image_name));
|
||||
};
|
||||
|
||||
export const createNewCanvasEntityFromImage = (arg: {
|
||||
export const createNewCanvasEntityFromImage = async (arg: {
|
||||
imageDTO: ImageDTO;
|
||||
type: CanvasEntityType | 'regional_guidance_with_reference_image';
|
||||
withResize?: boolean;
|
||||
dispatch: AppDispatch;
|
||||
getState: AppGetState;
|
||||
overrides?: Partial<Pick<CanvasEntityState, 'isEnabled' | 'isLocked' | 'name' | 'position'>>;
|
||||
}) => {
|
||||
const { type, imageDTO, dispatch, getState, overrides: _overrides } = arg;
|
||||
const { type, imageDTO, dispatch, getState, withResize, overrides: _overrides } = arg;
|
||||
const state = getState();
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(state);
|
||||
const { x, y, width, height } = selectBboxRect(state);
|
||||
|
||||
let imageObject: CanvasImageState;
|
||||
|
||||
if (withResize && (width !== imageDTO.width || height !== imageDTO.height)) {
|
||||
const resizedImageDTO = await uploadImage({
|
||||
file: await imageDTOToFile(imageDTO),
|
||||
image_category: 'general',
|
||||
is_intermediate: true,
|
||||
silent: true,
|
||||
resize_to: { width, height },
|
||||
});
|
||||
imageObject = imageDTOToImageObject(resizedImageDTO);
|
||||
} else {
|
||||
imageObject = imageDTOToImageObject(imageDTO);
|
||||
}
|
||||
|
||||
const overrides = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
..._overrides,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'raster_layer': {
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
@@ -122,6 +138,8 @@ export const createNewCanvasEntityFromImage = (arg: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -137,7 +155,7 @@ export const createNewCanvasEntityFromImage = (arg: {
|
||||
*/
|
||||
export const newCanvasFromImage = async (arg: {
|
||||
imageDTO: ImageDTO;
|
||||
type: CanvasEntityType | 'regional_guidance_with_reference_image' | 'reference_image';
|
||||
type: CanvasEntityType | 'regional_guidance_with_reference_image';
|
||||
withResize?: boolean;
|
||||
withInpaintMask?: boolean;
|
||||
dispatch: AppDispatch;
|
||||
@@ -244,17 +262,6 @@ export const newCanvasFromImage = async (arg: {
|
||||
dispatch(canvasClearHistory());
|
||||
break;
|
||||
}
|
||||
case 'reference_image': {
|
||||
const config = deepClone(getDefaultRefImageConfig(getState));
|
||||
config.image = imageDTOToImageWithDims(imageDTO);
|
||||
dispatch(canvasReset());
|
||||
dispatch(refImageAdded({ overrides: { config } }));
|
||||
if (withInpaintMask) {
|
||||
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
|
||||
}
|
||||
dispatch(canvasClearHistory());
|
||||
break;
|
||||
}
|
||||
case 'regional_guidance_with_reference_image': {
|
||||
const config = getDefaultRegionalGuidanceRefImageConfig(getState);
|
||||
config.image = imageDTOToImageWithDims(imageDTO);
|
||||
@@ -270,6 +277,9 @@ export const newCanvasFromImage = async (arg: {
|
||||
default:
|
||||
assert<Equals<typeof type, never>>(false);
|
||||
}
|
||||
|
||||
// Switch to the Canvas panel when creating a new canvas from image
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
};
|
||||
|
||||
export const replaceCanvasEntityObjectsWithImage = (arg: {
|
||||
|
||||
@@ -62,8 +62,14 @@ export const useInvoke = () => {
|
||||
|
||||
const enqueueBack = useCallback(() => {
|
||||
enqueue(false, false);
|
||||
if (tabName === 'generate' || tabName === 'workflows' || tabName === 'upscaling') {
|
||||
if (tabName === 'generate' || tabName === 'upscaling') {
|
||||
navigationApi.focusPanel(tabName, VIEWER_PANEL_ID);
|
||||
} else if (tabName === 'workflows') {
|
||||
// Only switch to viewer if the workflow editor is not currently active
|
||||
const workspace = navigationApi.getPanel('workflows', WORKSPACE_PANEL_ID);
|
||||
if (!workspace?.api.isActive) {
|
||||
navigationApi.focusPanel(tabName, VIEWER_PANEL_ID);
|
||||
}
|
||||
} else if (tabName === 'canvas') {
|
||||
navigationApi.focusPanel(tabName, WORKSPACE_PANEL_ID);
|
||||
}
|
||||
@@ -71,8 +77,14 @@ export const useInvoke = () => {
|
||||
|
||||
const enqueueFront = useCallback(() => {
|
||||
enqueue(true, false);
|
||||
if (tabName === 'generate' || tabName === 'workflows' || tabName === 'upscaling') {
|
||||
if (tabName === 'generate' || tabName === 'upscaling') {
|
||||
navigationApi.focusPanel(tabName, VIEWER_PANEL_ID);
|
||||
} else if (tabName === 'workflows') {
|
||||
// Only switch to viewer if the workflow editor is not currently active
|
||||
const workspace = navigationApi.getPanel('workflows', WORKSPACE_PANEL_ID);
|
||||
if (!workspace?.api.isActive) {
|
||||
navigationApi.focusPanel(tabName, VIEWER_PANEL_ID);
|
||||
}
|
||||
} else if (tabName === 'canvas') {
|
||||
navigationApi.focusPanel(tabName, WORKSPACE_PANEL_ID);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { useWorkflowLibraryModal } from 'features/nodes/store/workflowLibraryModal';
|
||||
import { workflowModeChanged } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -24,6 +26,8 @@ export const useNewWorkflow = () => {
|
||||
dispatch(workflowModeChanged('edit'));
|
||||
workflowLibraryModal.close();
|
||||
|
||||
navigationApi.focusPanel('workflows', WORKSPACE_PANEL_ID);
|
||||
|
||||
toast({
|
||||
id: 'NEW_WORKFLOW_CREATED',
|
||||
title: t('workflows.newWorkflowCreated'),
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { getIsFormEmpty } from 'features/nodes/components/sidePanel/builder/form-manipulation';
|
||||
import { $nodeExecutionStates } from 'features/nodes/hooks/useNodeExecutionState';
|
||||
import { $templates, workflowLoaded } from 'features/nodes/store/nodesSlice';
|
||||
import { $needsFit } from 'features/nodes/store/reactFlowInstance';
|
||||
import { workflowModeChanged } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error';
|
||||
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { VIEWER_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { serializeError } from 'serialize-error';
|
||||
@@ -49,7 +51,6 @@ export const useValidateAndLoadWorkflow = () => {
|
||||
origin: 'file' | 'image' | 'object' | 'library'
|
||||
): Promise<WorkflowV3 | null> => {
|
||||
try {
|
||||
await navigationApi.focusPanel('workflows', WORKSPACE_PANEL_ID);
|
||||
const templates = $templates.get();
|
||||
const { workflow, warnings } = await validateWorkflow({
|
||||
workflow: unvalidatedWorkflow,
|
||||
@@ -68,6 +69,17 @@ export const useValidateAndLoadWorkflow = () => {
|
||||
|
||||
$nodeExecutionStates.set({});
|
||||
dispatch(workflowLoaded(workflow));
|
||||
|
||||
// If the form is empty, assume the user is editing a new workflow.
|
||||
if (getIsFormEmpty(workflow.form)) {
|
||||
dispatch(workflowModeChanged('edit'));
|
||||
navigationApi.focusPanel('workflows', WORKSPACE_PANEL_ID);
|
||||
} else {
|
||||
// Else assume they want to use the linear view of the workflow.
|
||||
dispatch(workflowModeChanged('view'));
|
||||
navigationApi.focusPanel('workflows', VIEWER_PANEL_ID);
|
||||
}
|
||||
|
||||
if (!warnings.length) {
|
||||
toast({
|
||||
id: 'WORKFLOW_LOADED',
|
||||
|
||||
Reference in New Issue
Block a user