mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): revise generation mode logic
- Canvas generation mode is replace with a boolean `sendToCanvas` flag. When off, images generated on the canvas go to the gallery. When on, they get added to the staging area. - When an image result is received, if its destination is the canvas, staging is automatically started. - Updated queue list to show the destination column. - Added `IconSwitch` component to represent binary choices, used for the new `sendToCanvas` flag and image viewer toggle. - Remove the queue actions menu in `QueueControls`. Move the queue count badge to the cancel button. - Redo layout of `QueueControls` to prevent duplicate queue count badges. - Fix issue where gallery and options panels could show thru transparent regions of queue tab. - Disable panel hotkeys when on mm/queue tabs.
This commit is contained in:
parent
2f81d1ac83
commit
e1122c541d
@ -164,10 +164,10 @@
|
|||||||
"alpha": "Alpha",
|
"alpha": "Alpha",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
"tab": "Tab",
|
"tab": "Tab",
|
||||||
"viewing": "Viewing",
|
"view": "View",
|
||||||
"viewingDesc": "Review images in a large gallery view",
|
"viewDesc": "Review images in a large gallery view",
|
||||||
"editing": "Editing",
|
"edit": "Edit",
|
||||||
"editingDesc": "Edit on the Control Layers canvas",
|
"editDesc": "Edit on the Canvas",
|
||||||
"comparing": "Comparing",
|
"comparing": "Comparing",
|
||||||
"comparingDesc": "Comparing two images",
|
"comparingDesc": "Comparing two images",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
@ -328,9 +328,13 @@
|
|||||||
"completedIn": "Completed in",
|
"completedIn": "Completed in",
|
||||||
"batch": "Batch",
|
"batch": "Batch",
|
||||||
"origin": "Origin",
|
"origin": "Origin",
|
||||||
"originCanvas": "Canvas",
|
"destination": "Destination",
|
||||||
"originWorkflows": "Workflows",
|
"upscaling": "Upscaling",
|
||||||
"originOther": "Other",
|
"canvas": "Canvas",
|
||||||
|
"generation": "Generation",
|
||||||
|
"workflows": "Workflows",
|
||||||
|
"other": "Other",
|
||||||
|
"gallery": "Gallery",
|
||||||
"batchFieldValues": "Batch Field Values",
|
"batchFieldValues": "Batch Field Values",
|
||||||
"item": "Item",
|
"item": "Item",
|
||||||
"session": "Session",
|
"session": "Session",
|
||||||
@ -1695,6 +1699,10 @@
|
|||||||
"inpaintMask": "Inpaint Mask",
|
"inpaintMask": "Inpaint Mask",
|
||||||
"regionalGuidance": "Regional Guidance",
|
"regionalGuidance": "Regional Guidance",
|
||||||
"ipAdapter": "IP Adapter",
|
"ipAdapter": "IP Adapter",
|
||||||
|
"sendToGallery": "Send To Gallery",
|
||||||
|
"sendToGalleryDesc": "Generations will be sent to the gallery.",
|
||||||
|
"sendToCanvas": "Send To Canvas",
|
||||||
|
"sendToCanvasDesc": "Generations will be staged onto the canvas.",
|
||||||
"rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
|
"rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)",
|
||||||
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
|
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
|
||||||
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
|
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
|
||||||
|
@ -31,7 +31,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
|
|
||||||
let didStartStaging = false;
|
let didStartStaging = false;
|
||||||
|
|
||||||
if (!state.canvasSession.isStaging && state.canvasSession.mode === 'compose') {
|
if (!state.canvasSession.isStaging && state.canvasSession.sendToCanvas) {
|
||||||
dispatch(sessionStartedStaging());
|
dispatch(sessionStartedStaging());
|
||||||
didStartStaging = true;
|
didStartStaging = true;
|
||||||
}
|
}
|
||||||
@ -70,7 +70,11 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
|
|
||||||
const { g, noise, posCond } = buildGraphResult.value;
|
const { g, noise, posCond } = buildGraphResult.value;
|
||||||
|
|
||||||
const prepareBatchResult = withResult(() => prepareLinearUIBatch(state, g, prepend, noise, posCond));
|
const destination = state.canvasSession.sendToCanvas ? 'canvas' : 'gallery';
|
||||||
|
|
||||||
|
const prepareBatchResult = withResult(() =>
|
||||||
|
prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination)
|
||||||
|
);
|
||||||
|
|
||||||
if (isErr(prepareBatchResult)) {
|
if (isErr(prepareBatchResult)) {
|
||||||
log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch');
|
log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch');
|
||||||
|
@ -32,6 +32,7 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
|
|||||||
workflow: builtWorkflow,
|
workflow: builtWorkflow,
|
||||||
runs: state.params.iterations,
|
runs: state.params.iterations,
|
||||||
origin: 'workflows',
|
origin: 'workflows',
|
||||||
|
destination: 'gallery',
|
||||||
},
|
},
|
||||||
prepend: action.payload.prepend,
|
prepend: action.payload.prepend,
|
||||||
};
|
};
|
||||||
|
@ -16,7 +16,7 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
|
|||||||
|
|
||||||
const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state);
|
const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state);
|
||||||
|
|
||||||
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond);
|
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond, 'upscaling', 'gallery');
|
||||||
|
|
||||||
const req = dispatch(
|
const req = dispatch(
|
||||||
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
|
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
|
||||||
|
104
invokeai/frontend/web/src/common/components/IconSwitch.tsx
Normal file
104
invokeai/frontend/web/src/common/components/IconSwitch.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import { Box, Flex, IconButton, Tooltip, useToken } from '@invoke-ai/ui-library';
|
||||||
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
type IconSwitchProps = {
|
||||||
|
isChecked: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
iconChecked: ReactElement;
|
||||||
|
tooltipChecked?: ReactNode;
|
||||||
|
iconUnchecked: ReactElement;
|
||||||
|
tooltipUnchecked?: ReactNode;
|
||||||
|
ariaLabel: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSx = (padding: string | number): SystemStyleObject => ({
|
||||||
|
transition: 'left 0.1s ease-in-out, transform 0.1s ease-in-out',
|
||||||
|
'&[data-checked="true"]': {
|
||||||
|
left: `calc(100% - ${padding})`,
|
||||||
|
transform: 'translateX(-100%)',
|
||||||
|
},
|
||||||
|
'&[data-checked="false"]': {
|
||||||
|
left: padding,
|
||||||
|
transform: 'translateX(0)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const IconSwitch = memo(
|
||||||
|
({
|
||||||
|
isChecked,
|
||||||
|
onChange,
|
||||||
|
iconChecked,
|
||||||
|
tooltipChecked,
|
||||||
|
iconUnchecked,
|
||||||
|
tooltipUnchecked,
|
||||||
|
ariaLabel,
|
||||||
|
}: IconSwitchProps) => {
|
||||||
|
const onUncheck = useCallback(() => {
|
||||||
|
onChange(false);
|
||||||
|
}, [onChange]);
|
||||||
|
const onCheck = useCallback(() => {
|
||||||
|
onChange(true);
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
const gap = useToken('space', 1.5);
|
||||||
|
const sx = useMemo(() => getSx(gap), [gap]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
position="relative"
|
||||||
|
bg='base.800'
|
||||||
|
borderRadius="base"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
h="full"
|
||||||
|
p={gap}
|
||||||
|
gap={gap}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
borderRadius="base"
|
||||||
|
bg="invokeBlue.400"
|
||||||
|
w={12}
|
||||||
|
top={gap}
|
||||||
|
bottom={gap}
|
||||||
|
data-checked={isChecked}
|
||||||
|
sx={sx}
|
||||||
|
/>
|
||||||
|
<Tooltip hasArrow label={tooltipUnchecked}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
fontSize={16}
|
||||||
|
icon={iconUnchecked}
|
||||||
|
onClick={onUncheck}
|
||||||
|
variant={!isChecked ? 'solid' : 'ghost'}
|
||||||
|
colorScheme={!isChecked ? 'invokeBlue' : 'base'}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
data-checked={!isChecked}
|
||||||
|
w={12}
|
||||||
|
alignSelf="stretch"
|
||||||
|
h="auto"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip hasArrow label={tooltipChecked}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
fontSize={16}
|
||||||
|
icon={iconChecked}
|
||||||
|
onClick={onCheck}
|
||||||
|
variant={isChecked ? 'solid' : 'ghost'}
|
||||||
|
colorScheme={isChecked ? 'invokeBlue' : 'base'}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
data-checked={isChecked}
|
||||||
|
w={12}
|
||||||
|
alignSelf="stretch"
|
||||||
|
h="auto"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
IconSwitch.displayName = 'IconSwitch';
|
@ -1,29 +0,0 @@
|
|||||||
import { Button, ButtonGroup } from '@invoke-ai/ui-library';
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { selectCanvasSessionSlice, sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice';
|
|
||||||
import { memo, useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const selectCanvasMode = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.mode);
|
|
||||||
|
|
||||||
export const CanvasModeSwitcher = memo(() => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const mode = useAppSelector(selectCanvasMode);
|
|
||||||
const onClickGenerate = useCallback(() => dispatch(sessionModeChanged({ mode: 'generate' })), [dispatch]);
|
|
||||||
const onClickCompose = useCallback(() => dispatch(sessionModeChanged({ mode: 'compose' })), [dispatch]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ButtonGroup variant="outline">
|
|
||||||
<Button onClick={onClickGenerate} colorScheme={mode === 'generate' ? 'invokeBlue' : 'base'}>
|
|
||||||
{t('controlLayers.generateMode')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onClickCompose} colorScheme={mode === 'compose' ? 'invokeBlue' : 'base'}>
|
|
||||||
{t('controlLayers.composeMode')}
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
CanvasModeSwitcher.displayName = 'CanvasModeSwitcher';
|
|
@ -0,0 +1,59 @@
|
|||||||
|
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { IconSwitch } from 'common/components/IconSwitch';
|
||||||
|
import { selectIsComposing, sessionSendToCanvasChanged } from 'features/controlLayers/store/canvasSessionSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
const TooltipSendToGallery = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column">
|
||||||
|
<Text fontWeight="semibold">{t('controlLayers.sendToGallery')}</Text>
|
||||||
|
<Text fontWeight="normal">{t('controlLayers.sendToGalleryDesc')}</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
TooltipSendToGallery.displayName = 'TooltipSendToGallery';
|
||||||
|
|
||||||
|
const TooltipSendToCanvas = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column">
|
||||||
|
<Text fontWeight="semibold">{t('controlLayers.sendToCanvas')}</Text>
|
||||||
|
<Text fontWeight="normal">{t('controlLayers.sendToCanvasDesc')}</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
TooltipSendToCanvas.displayName = 'TooltipSendToCanvas';
|
||||||
|
|
||||||
|
export const CanvasSendToToggle = memo(() => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const isComposing = useAppSelector(selectIsComposing);
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(isChecked: boolean) => {
|
||||||
|
dispatch(sessionSendToCanvasChanged(isChecked));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconSwitch
|
||||||
|
isChecked={isComposing}
|
||||||
|
onChange={onChange}
|
||||||
|
iconUnchecked={<PiImageBold />}
|
||||||
|
tooltipUnchecked={<TooltipSendToGallery />}
|
||||||
|
iconChecked={<PiPaintBrushBold />}
|
||||||
|
tooltipChecked={<TooltipSendToCanvas />}
|
||||||
|
ariaLabel="Toggle canvas mode"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CanvasSendToToggle.displayName = 'CanvasSendToToggle';
|
@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable i18next/no-literal-string */
|
/* eslint-disable i18next/no-literal-string */
|
||||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||||
import { CanvasModeSwitcher } from 'features/controlLayers/components/CanvasModeSwitcher';
|
|
||||||
import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton';
|
import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton';
|
||||||
import { CanvasScale } from 'features/controlLayers/components/CanvasScale';
|
import { CanvasScale } from 'features/controlLayers/components/CanvasScale';
|
||||||
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
|
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
|
||||||
@ -28,7 +27,6 @@ export const ControlLayersToolbar = memo(() => {
|
|||||||
<CanvasResetViewButton />
|
<CanvasResetViewButton />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<ToolFillColorPicker />
|
<ToolFillColorPicker />
|
||||||
<CanvasModeSwitcher />
|
|
||||||
<CanvasSettingsPopover />
|
<CanvasSettingsPopover />
|
||||||
<ViewerToggle />
|
<ViewerToggle />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { createAction, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
import { createAction, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
import { canvasSlice } from 'features/controlLayers/store/canvasSlice';
|
import { canvasSlice } from 'features/controlLayers/store/canvasSlice';
|
||||||
import type { SessionMode, StagingAreaImage } from 'features/controlLayers/store/types';
|
import type { StagingAreaImage } from 'features/controlLayers/store/types';
|
||||||
|
|
||||||
export type CanvasSessionState = {
|
export type CanvasSessionState = {
|
||||||
mode: SessionMode;
|
sendToCanvas: boolean;
|
||||||
isStaging: boolean;
|
isStaging: boolean;
|
||||||
stagedImages: StagingAreaImage[];
|
stagedImages: StagingAreaImage[];
|
||||||
selectedStagedImageIndex: number;
|
selectedStagedImageIndex: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: CanvasSessionState = {
|
const initialState: CanvasSessionState = {
|
||||||
mode: 'generate',
|
sendToCanvas: false,
|
||||||
isStaging: false,
|
isStaging: false,
|
||||||
stagedImages: [],
|
stagedImages: [],
|
||||||
selectedStagedImageIndex: 0,
|
selectedStagedImageIndex: 0,
|
||||||
@ -27,6 +27,7 @@ export const canvasSessionSlice = createSlice({
|
|||||||
},
|
},
|
||||||
sessionImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
|
sessionImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
|
||||||
const { stagingAreaImage } = action.payload;
|
const { stagingAreaImage } = action.payload;
|
||||||
|
state.isStaging = true;
|
||||||
state.stagedImages.push(stagingAreaImage);
|
state.stagedImages.push(stagingAreaImage);
|
||||||
state.selectedStagedImageIndex = state.stagedImages.length - 1;
|
state.selectedStagedImageIndex = state.stagedImages.length - 1;
|
||||||
},
|
},
|
||||||
@ -50,9 +51,8 @@ export const canvasSessionSlice = createSlice({
|
|||||||
state.stagedImages = [];
|
state.stagedImages = [];
|
||||||
state.selectedStagedImageIndex = 0;
|
state.selectedStagedImageIndex = 0;
|
||||||
},
|
},
|
||||||
sessionModeChanged: (state, action: PayloadAction<{ mode: SessionMode }>) => {
|
sessionSendToCanvasChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
const { mode } = action.payload;
|
state.sendToCanvas = action.payload;
|
||||||
state.mode = mode;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -64,7 +64,7 @@ export const {
|
|||||||
sessionStagingAreaReset,
|
sessionStagingAreaReset,
|
||||||
sessionNextStagedImageSelected,
|
sessionNextStagedImageSelected,
|
||||||
sessionPrevStagedImageSelected,
|
sessionPrevStagedImageSelected,
|
||||||
sessionModeChanged,
|
sessionSendToCanvasChanged,
|
||||||
} = canvasSessionSlice.actions;
|
} = canvasSessionSlice.actions;
|
||||||
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
@ -85,3 +85,7 @@ export const sessionStagingAreaImageAccepted = createAction<{ index: number }>(
|
|||||||
export const selectCanvasSessionSlice = (s: RootState) => s.canvasSession;
|
export const selectCanvasSessionSlice = (s: RootState) => s.canvasSession;
|
||||||
|
|
||||||
export const selectIsStaging = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.isStaging);
|
export const selectIsStaging = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.isStaging);
|
||||||
|
export const selectIsComposing = createSelector(
|
||||||
|
selectCanvasSessionSlice,
|
||||||
|
(canvasSession) => canvasSession.sendToCanvas
|
||||||
|
);
|
||||||
|
@ -1,57 +1,60 @@
|
|||||||
import { ButtonGroup, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library';
|
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { IconSwitch } from 'common/components/IconSwitch';
|
||||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
import { memo } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiEyeBold, PiPencilBold } from 'react-icons/pi';
|
import { PiEyeBold, PiPencilBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const ViewerToggle = memo(() => {
|
const TooltipEdit = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column">
|
||||||
|
<Text fontWeight="semibold">{t('common.edit')}</Text>
|
||||||
|
<Text fontWeight="normal">{t('common.editDesc')}</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
TooltipEdit.displayName = 'TooltipEdit';
|
||||||
|
|
||||||
|
const TooltipView = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column">
|
||||||
|
<Text fontWeight="semibold">{t('common.view')}</Text>
|
||||||
|
<Text fontWeight="normal">{t('common.viewDesc')}</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
TooltipView.displayName = 'TooltipView';
|
||||||
|
|
||||||
|
export const ViewerToggle = memo(() => {
|
||||||
const imageViewer = useImageViewer();
|
const imageViewer = useImageViewer();
|
||||||
useHotkeys('z', imageViewer.onToggle, [imageViewer]);
|
useHotkeys('z', imageViewer.onToggle, [imageViewer]);
|
||||||
useHotkeys('esc', imageViewer.onClose, [imageViewer]);
|
useHotkeys('esc', imageViewer.onClose, [imageViewer]);
|
||||||
|
const onChange = useCallback(
|
||||||
|
(isChecked: boolean) => {
|
||||||
|
if (isChecked) {
|
||||||
|
imageViewer.onClose();
|
||||||
|
} else {
|
||||||
|
imageViewer.onOpen();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[imageViewer]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={4} alignItems="center" justifyContent="center">
|
<IconSwitch
|
||||||
<ButtonGroup size="md">
|
isChecked={!imageViewer.isOpen}
|
||||||
<Tooltip
|
onChange={onChange}
|
||||||
hasArrow
|
iconUnchecked={<PiEyeBold />}
|
||||||
label={
|
tooltipUnchecked={<TooltipView />}
|
||||||
<Flex flexDir="column">
|
iconChecked={<PiPencilBold />}
|
||||||
<Text fontWeight="semibold">{t('common.viewing')}</Text>
|
tooltipChecked={<TooltipEdit />}
|
||||||
<Text fontWeight="normal">{t('common.viewingDesc')}</Text>
|
ariaLabel="Toggle viewer"
|
||||||
</Flex>
|
/>
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
icon={<PiEyeBold />}
|
|
||||||
onClick={imageViewer.onOpen}
|
|
||||||
variant={imageViewer.isOpen ? 'solid' : 'outline'}
|
|
||||||
colorScheme={imageViewer.isOpen ? 'invokeBlue' : 'base'}
|
|
||||||
aria-label={t('common.viewing')}
|
|
||||||
w={12}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
hasArrow
|
|
||||||
label={
|
|
||||||
<Flex flexDir="column">
|
|
||||||
<Text fontWeight="semibold">{t('common.editing')}</Text>
|
|
||||||
<Text fontWeight="normal">{t('common.editingDesc')}</Text>
|
|
||||||
</Flex>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
icon={<PiPencilBold />}
|
|
||||||
onClick={imageViewer.onClose}
|
|
||||||
variant={!imageViewer.isOpen ? 'solid' : 'outline'}
|
|
||||||
colorScheme={!imageViewer.isOpen ? 'invokeBlue' : 'base'}
|
|
||||||
aria-label={t('common.editing')}
|
|
||||||
w={12}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</ButtonGroup>
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import 'reactflow/dist/style.css';
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||||
import QueueControls from 'features/queue/components/QueueControls';
|
|
||||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||||
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
||||||
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
||||||
@ -34,7 +33,6 @@ const NodeEditorPanelGroup = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||||
<QueueControls />
|
|
||||||
<Flex w="full" justifyContent="space-between" alignItems="center" gap="4" padding={1}>
|
<Flex w="full" justifyContent="space-between" alignItems="center" gap="4" padding={1}>
|
||||||
<Flex justifyContent="space-between" alignItems="center" gap="4">
|
<Flex justifyContent="space-between" alignItems="center" gap="4">
|
||||||
<WorkflowLibraryButton />
|
<WorkflowLibraryButton />
|
||||||
|
@ -10,7 +10,9 @@ export const prepareLinearUIBatch = (
|
|||||||
g: Graph,
|
g: Graph,
|
||||||
prepend: boolean,
|
prepend: boolean,
|
||||||
noise: Invocation<'noise'>,
|
noise: Invocation<'noise'>,
|
||||||
posCond: Invocation<'compel' | 'sdxl_compel_prompt'>
|
posCond: Invocation<'compel' | 'sdxl_compel_prompt'>,
|
||||||
|
origin: 'generation' | 'workflows' | 'upscaling',
|
||||||
|
destination: 'canvas' | 'gallery'
|
||||||
): BatchConfig => {
|
): BatchConfig => {
|
||||||
const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.params;
|
const { iterations, model, shouldRandomizeSeed, seed, shouldConcatPrompts } = state.params;
|
||||||
const { prompts, seedBehaviour } = state.dynamicPrompts;
|
const { prompts, seedBehaviour } = state.dynamicPrompts;
|
||||||
@ -103,7 +105,8 @@ export const prepareLinearUIBatch = (
|
|||||||
graph: g.getGraph(),
|
graph: g.getGraph(),
|
||||||
runs: 1,
|
runs: 1,
|
||||||
data,
|
data,
|
||||||
origin: 'canvas',
|
origin,
|
||||||
|
destination,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ export const addInpaint = async (
|
|||||||
const canvas = selectCanvasSlice(state);
|
const canvas = selectCanvasSlice(state);
|
||||||
|
|
||||||
const { bbox } = canvas;
|
const { bbox } = canvas;
|
||||||
const { mode } = canvasSession;
|
const { sendToCanvas: isComposing } = canvasSession;
|
||||||
|
|
||||||
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
||||||
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
|
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
|
||||||
@ -99,7 +99,7 @@ export const addInpaint = async (
|
|||||||
g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
|
g.addEdge(resizeImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
|
||||||
g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
|
g.addEdge(resizeMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
|
||||||
|
|
||||||
if (mode === 'generate') {
|
if (!isComposing) {
|
||||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ export const addInpaint = async (
|
|||||||
|
|
||||||
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
|
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
|
||||||
|
|
||||||
if (mode === 'generate') {
|
if (!isComposing) {
|
||||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export const addOutpaint = async (
|
|||||||
const canvas = selectCanvasSlice(state);
|
const canvas = selectCanvasSlice(state);
|
||||||
|
|
||||||
const { bbox } = canvas;
|
const { bbox } = canvas;
|
||||||
const { mode } = canvasSession;
|
const { sendToCanvas: isComposing } = canvasSession;
|
||||||
|
|
||||||
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
const initialImage = await manager.compositor.getCompositeRasterLayerImageDTO(bbox.rect);
|
||||||
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
|
const maskImage = await manager.compositor.getCompositeInpaintMaskImageDTO(bbox.rect);
|
||||||
@ -123,7 +123,7 @@ export const addOutpaint = async (
|
|||||||
g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
|
g.addEdge(resizeOutputImageToOriginalSize, 'image', canvasPasteBack, 'generated_image');
|
||||||
g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
|
g.addEdge(resizeOutputMaskToOriginalSize, 'image', canvasPasteBack, 'mask');
|
||||||
|
|
||||||
if (mode === 'generate') {
|
if (!isComposing) {
|
||||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +173,7 @@ export const addOutpaint = async (
|
|||||||
g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask');
|
g.addEdge(createGradientMask, 'expanded_mask_area', canvasPasteBack, 'mask');
|
||||||
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
|
g.addEdge(l2i, 'image', canvasPasteBack, 'generated_image');
|
||||||
|
|
||||||
if (mode === 'generate') {
|
if (!isComposing) {
|
||||||
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
canvasPasteBack.source_image = { image_name: initialImage.image_name };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,7 +282,7 @@ export const buildSD1Graph = async (
|
|||||||
canvasOutput = addWatermarker(g, canvasOutput);
|
canvasOutput = addWatermarker(g, canvasOutput);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave;
|
const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave;
|
||||||
|
|
||||||
g.updateNode(canvasOutput, {
|
g.updateNode(canvasOutput, {
|
||||||
id: getPrefixedId('canvas_output'),
|
id: getPrefixedId('canvas_output'),
|
||||||
|
@ -285,7 +285,7 @@ export const buildSDXLGraph = async (
|
|||||||
canvasOutput = addWatermarker(g, canvasOutput);
|
canvasOutput = addWatermarker(g, canvasOutput);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldSaveToGallery = canvasSession.mode === 'generate' || canvasSettings.autoSave;
|
const shouldSaveToGallery = !canvasSession.sendToCanvas || canvasSettings.autoSave;
|
||||||
|
|
||||||
g.updateNode(canvasOutput, {
|
g.updateNode(canvasOutput, {
|
||||||
id: getPrefixedId('canvas_output'),
|
id: getPrefixedId('canvas_output'),
|
||||||
|
@ -1,67 +1,39 @@
|
|||||||
import type { IconButtonProps } from '@invoke-ai/ui-library';
|
|
||||||
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
|
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
|
||||||
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
|
||||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||||
import { memo } from 'react';
|
import { memo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiTrashSimpleBold, PiXBold } from 'react-icons/pi';
|
import { PiTrashSimpleBold, PiXBold } from 'react-icons/pi';
|
||||||
|
|
||||||
type ClearQueueButtonProps = Omit<IconButtonProps, 'aria-label'>;
|
export const ClearQueueIconButton = memo((_) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
export const ClearAllQueueIconButton = memo((props: ClearQueueButtonProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dialogState = useClearQueueConfirmationAlertDialog();
|
const clearQueue = useClearQueue();
|
||||||
const { isLoading, isDisabled } = useClearQueue();
|
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
isLoading={isLoading}
|
|
||||||
aria-label={t('queue.clear')}
|
|
||||||
tooltip={t('queue.clearTooltip')}
|
|
||||||
icon={<PiTrashSimpleBold size="16px" />}
|
|
||||||
colorScheme="error"
|
|
||||||
onClick={dialogState.setTrue}
|
|
||||||
data-testid={t('queue.clear')}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ClearAllQueueIconButton.displayName = 'ClearAllQueueIconButton';
|
|
||||||
|
|
||||||
const ClearSingleQueueItemIconButton = memo((props: ClearQueueButtonProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { cancelQueueItem, isLoading, isDisabled } = useCancelCurrentQueueItem();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
isLoading={isLoading}
|
|
||||||
aria-label={t('queue.cancel')}
|
|
||||||
tooltip={t('queue.cancelTooltip')}
|
|
||||||
icon={<PiXBold size="16px" />}
|
|
||||||
colorScheme="error"
|
|
||||||
onClick={cancelQueueItem}
|
|
||||||
data-testid={t('queue.cancel')}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ClearSingleQueueItemIconButton.displayName = 'ClearSingleQueueItemIconButton';
|
|
||||||
|
|
||||||
export const ClearQueueIconButton = memo((props: ClearQueueButtonProps) => {
|
|
||||||
// Show the single item clear button when shift is pressed
|
// Show the single item clear button when shift is pressed
|
||||||
// Otherwise show the clear queue button
|
// Otherwise show the clear queue button
|
||||||
const shift = useShiftModifier();
|
const shift = useShiftModifier();
|
||||||
|
|
||||||
if (shift) {
|
return (
|
||||||
return <ClearAllQueueIconButton {...props} />;
|
<>
|
||||||
}
|
<IconButton
|
||||||
|
ref={ref}
|
||||||
return <ClearSingleQueueItemIconButton {...props} />;
|
size="lg"
|
||||||
|
isDisabled={shift ? clearQueue.isDisabled : cancelCurrentQueueItem.isDisabled}
|
||||||
|
isLoading={shift ? clearQueue.isLoading : cancelCurrentQueueItem.isLoading}
|
||||||
|
aria-label={shift ? t('queue.clear') : t('queue.cancel')}
|
||||||
|
tooltip={shift ? t('queue.clearTooltip') : t('queue.cancelTooltip')}
|
||||||
|
icon={shift ? <PiTrashSimpleBold /> : <PiXBold />}
|
||||||
|
colorScheme="error"
|
||||||
|
onClick={shift ? clearQueue.openDialog : cancelCurrentQueueItem.cancelQueueItem}
|
||||||
|
data-testid={shift ? t('queue.clear') : t('queue.cancel')}
|
||||||
|
/>
|
||||||
|
{/* The badge is dynamically positioned, needs a ref to the target element */}
|
||||||
|
<QueueCountBadge targetRef={ref} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ClearQueueIconButton.displayName = 'ClearQueueIconButton';
|
ClearQueueIconButton.displayName = 'ClearQueueIconButton';
|
||||||
|
@ -15,7 +15,7 @@ export const InvokeQueueBackButton = memo(() => {
|
|||||||
const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading);
|
const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex pos="relative" flexGrow={1} minW="240px">
|
<Flex pos="relative" w="192px">
|
||||||
<QueueIterationsNumberInput />
|
<QueueIterationsNumberInput />
|
||||||
<QueueButtonTooltip>
|
<QueueButtonTooltip>
|
||||||
<Button
|
<Button
|
||||||
|
@ -1,25 +1,27 @@
|
|||||||
import { ButtonGroup, Flex, Spacer } from '@invoke-ai/ui-library';
|
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle';
|
||||||
import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
|
import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
|
||||||
import QueueFrontButton from 'features/queue/components/QueueFrontButton';
|
import QueueFrontButton from 'features/queue/components/QueueFrontButton';
|
||||||
import ProgressBar from 'features/system/components/ProgressBar';
|
import ProgressBar from 'features/system/components/ProgressBar';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { memo, useRef } from 'react';
|
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { InvokeQueueBackButton } from './InvokeQueueBackButton';
|
import { InvokeQueueBackButton } from './InvokeQueueBackButton';
|
||||||
import { QueueActionsMenuButton } from './QueueActionsMenuButton';
|
|
||||||
|
|
||||||
const QueueControls = () => {
|
const QueueControls = () => {
|
||||||
const isPrependEnabled = useFeatureStatus('prependQueue');
|
const isPrependEnabled = useFeatureStatus('prependQueue');
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const tab = useAppSelector(selectActiveTab);
|
||||||
return (
|
return (
|
||||||
<Flex ref={containerRef} w="full" position="relative" borderRadius="base" gap={2} flexDir="column">
|
<Flex w="full" position="relative" borderRadius="base" gap={2} flexDir="column">
|
||||||
<ButtonGroup size="lg" isAttached={false}>
|
<Flex gap={2}>
|
||||||
{isPrependEnabled && <QueueFrontButton />}
|
{isPrependEnabled && <QueueFrontButton />}
|
||||||
<InvokeQueueBackButton />
|
<InvokeQueueBackButton />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<QueueActionsMenuButton containerRef={containerRef} />
|
{tab === 'generation' && <CanvasSendToToggle />}
|
||||||
<ClearQueueIconButton />
|
<ClearQueueIconButton />
|
||||||
</ButtonGroup>
|
</Flex>
|
||||||
<ProgressBar />
|
<ProgressBar />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
import { Badge, Portal } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { $isParametersPanelOpen } from 'features/ui/store/uiSlice';
|
||||||
|
import type { RefObject } from 'react';
|
||||||
|
import { memo, useEffect, useState } from 'react';
|
||||||
|
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
targetRef: RefObject<HTMLDivElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QueueCountBadge = memo(({ targetRef }: Props) => {
|
||||||
|
const [badgePos, setBadgePos] = useState<{ x: string; y: string } | null>(null);
|
||||||
|
const isParametersPanelOpen = useStore($isParametersPanelOpen);
|
||||||
|
const { queueSize } = useGetQueueStatusQuery(undefined, {
|
||||||
|
selectFromResult: (res) => ({
|
||||||
|
queueSize: res.data ? res.data.queue.pending + res.data.queue.in_progress : 0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!targetRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = targetRef.current;
|
||||||
|
const parent = target.parentElement;
|
||||||
|
|
||||||
|
if (!parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cb = () => {
|
||||||
|
if (!$isParametersPanelOpen.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { x, y } = target.getBoundingClientRect();
|
||||||
|
setBadgePos({ x: `${x - 7}px`, y: `${y - 5}px` });
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(cb);
|
||||||
|
resizeObserver.observe(parent);
|
||||||
|
cb();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, [targetRef]);
|
||||||
|
|
||||||
|
if (queueSize === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!badgePos) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isParametersPanelOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Badge
|
||||||
|
pos="absolute"
|
||||||
|
insetInlineStart={badgePos.x}
|
||||||
|
insetBlockStart={badgePos.y}
|
||||||
|
colorScheme="invokeYellow"
|
||||||
|
zIndex="docked"
|
||||||
|
shadow="dark-lg"
|
||||||
|
userSelect="none"
|
||||||
|
>
|
||||||
|
{queueSize}
|
||||||
|
</Badge>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
QueueCountBadge.displayName = 'QueueCountBadge';
|
@ -1,6 +1,7 @@
|
|||||||
import type { ChakraProps, CollapseProps } from '@invoke-ai/ui-library';
|
import type { ChakraProps, CollapseProps } from '@invoke-ai/ui-library';
|
||||||
import { ButtonGroup, Collapse, Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
import { ButtonGroup, Collapse, Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
||||||
import QueueStatusBadge from 'features/queue/components/common/QueueStatusBadge';
|
import QueueStatusBadge from 'features/queue/components/common/QueueStatusBadge';
|
||||||
|
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
|
||||||
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
|
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
|
||||||
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
|
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
|
||||||
import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps';
|
import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps';
|
||||||
@ -52,6 +53,7 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
|
|||||||
|
|
||||||
const isCanceled = useMemo(() => ['canceled', 'completed', 'failed'].includes(item.status), [item.status]);
|
const isCanceled = useMemo(() => ['canceled', 'completed', 'failed'].includes(item.status), [item.status]);
|
||||||
const originText = useOriginText(item.origin);
|
const originText = useOriginText(item.origin);
|
||||||
|
const destinationText = useDestinationText(item.destination);
|
||||||
|
|
||||||
const icon = useMemo(() => <PiXBold />, []);
|
const icon = useMemo(() => <PiXBold />, []);
|
||||||
return (
|
return (
|
||||||
@ -76,6 +78,11 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
|
|||||||
{originText}
|
{originText}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<Flex w={COLUMN_WIDTHS.destination} flexShrink={0}>
|
||||||
|
<Text overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" alignItems="center">
|
||||||
|
{destinationText}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
<Flex w={COLUMN_WIDTHS.time} alignItems="center" flexShrink={0}>
|
<Flex w={COLUMN_WIDTHS.time} alignItems="center" flexShrink={0}>
|
||||||
{executionTime || '-'}
|
{executionTime || '-'}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Button, ButtonGroup, Flex, Heading, Spinner, Text } from '@invoke-ai/ui-library';
|
import { Button, ButtonGroup, Flex, Heading, Spinner, Text } from '@invoke-ai/ui-library';
|
||||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||||
|
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
|
||||||
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
|
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
|
||||||
import { useCancelBatch } from 'features/queue/hooks/useCancelBatch';
|
import { useCancelBatch } from 'features/queue/hooks/useCancelBatch';
|
||||||
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
|
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
|
||||||
@ -17,7 +18,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
||||||
const { session_id, batch_id, item_id, origin } = queueItemDTO;
|
const { session_id, batch_id, item_id, origin, destination } = queueItemDTO;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled } = useCancelBatch(batch_id);
|
const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled } = useCancelBatch(batch_id);
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
|||||||
const { data: queueItem } = useGetQueueItemQuery(item_id);
|
const { data: queueItem } = useGetQueueItemQuery(item_id);
|
||||||
|
|
||||||
const originText = useOriginText(origin);
|
const originText = useOriginText(origin);
|
||||||
|
const destinationText = useDestinationText(destination);
|
||||||
|
|
||||||
const statusAndTiming = useMemo(() => {
|
const statusAndTiming = useMemo(() => {
|
||||||
if (!queueItem) {
|
if (!queueItem) {
|
||||||
@ -54,6 +56,7 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
|
|||||||
>
|
>
|
||||||
<QueueItemData label={t('queue.status')} data={statusAndTiming} />
|
<QueueItemData label={t('queue.status')} data={statusAndTiming} />
|
||||||
<QueueItemData label={t('queue.origin')} data={originText} />
|
<QueueItemData label={t('queue.origin')} data={originText} />
|
||||||
|
<QueueItemData label={t('queue.destination')} data={destinationText} />
|
||||||
<QueueItemData label={t('queue.item')} data={item_id} />
|
<QueueItemData label={t('queue.item')} data={item_id} />
|
||||||
<QueueItemData label={t('queue.batch')} data={batch_id} />
|
<QueueItemData label={t('queue.batch')} data={batch_id} />
|
||||||
<QueueItemData label={t('queue.session')} data={session_id} />
|
<QueueItemData label={t('queue.session')} data={session_id} />
|
||||||
|
@ -25,6 +25,9 @@ const QueueListHeader = () => {
|
|||||||
<Flex ps={0.5} w={COLUMN_WIDTHS.origin} alignItems="center">
|
<Flex ps={0.5} w={COLUMN_WIDTHS.origin} alignItems="center">
|
||||||
<Text variant="subtext">{t('queue.origin')}</Text>
|
<Text variant="subtext">{t('queue.origin')}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<Flex ps={0.5} w={COLUMN_WIDTHS.destination} alignItems="center">
|
||||||
|
<Text variant="subtext">{t('queue.destination')}</Text>
|
||||||
|
</Flex>
|
||||||
<Flex ps={0.5} w={COLUMN_WIDTHS.time} alignItems="center">
|
<Flex ps={0.5} w={COLUMN_WIDTHS.time} alignItems="center">
|
||||||
<Text variant="subtext">{t('queue.time')}</Text>
|
<Text variant="subtext">{t('queue.time')}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -4,7 +4,8 @@ export const COLUMN_WIDTHS = {
|
|||||||
statusDot: 2,
|
statusDot: 2,
|
||||||
time: '4rem',
|
time: '4rem',
|
||||||
origin: '5rem',
|
origin: '5rem',
|
||||||
|
destination: '6rem',
|
||||||
batchId: '5rem',
|
batchId: '5rem',
|
||||||
fieldValues: 'auto',
|
fieldValues: 'auto',
|
||||||
actions: 'auto',
|
actions: 'auto',
|
||||||
};
|
} as const;
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { SessionQueueItemDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
export const useDestinationText = (destination: SessionQueueItemDTO['destination']) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (destination === 'canvas') {
|
||||||
|
return t('queue.canvas');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destination === 'gallery') {
|
||||||
|
return t('queue.gallery');
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('queue.other');
|
||||||
|
};
|
@ -4,13 +4,13 @@ import type { SessionQueueItemDTO } from 'services/api/types';
|
|||||||
export const useOriginText = (origin: SessionQueueItemDTO['origin']) => {
|
export const useOriginText = (origin: SessionQueueItemDTO['origin']) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (origin === 'canvas') {
|
if (origin === 'generation') {
|
||||||
return t('queue.originCanvas');
|
return t('queue.generation');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (origin === 'workflows') {
|
if (origin === 'workflows') {
|
||||||
return t('queue.originWorkflows');
|
return t('queue.workflows');
|
||||||
}
|
}
|
||||||
|
|
||||||
return t('queue.originOther');
|
return t('queue.other');
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { $isConnected } from 'app/hooks/useSocketIO';
|
import { $isConnected } from 'app/hooks/useSocketIO';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { useClearQueueConfirmationAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||||
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
|
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
@ -10,6 +11,7 @@ import { useClearQueueMutation, useGetQueueStatusQuery } from 'services/api/endp
|
|||||||
export const useClearQueue = () => {
|
export const useClearQueue = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const dialog = useClearQueueConfirmationAlertDialog();
|
||||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
const { data: queueStatus } = useGetQueueStatusQuery();
|
||||||
const isConnected = useStore($isConnected);
|
const isConnected = useStore($isConnected);
|
||||||
const [trigger, { isLoading }] = useClearQueueMutation({
|
const [trigger, { isLoading }] = useClearQueueMutation({
|
||||||
@ -41,5 +43,5 @@ export const useClearQueue = () => {
|
|||||||
|
|
||||||
const isDisabled = useMemo(() => !isConnected || !queueStatus?.queue.total, [isConnected, queueStatus?.queue.total]);
|
const isDisabled = useMemo(() => !isConnected || !queueStatus?.queue.total, [isConnected, queueStatus?.queue.total]);
|
||||||
|
|
||||||
return { clearQueue, isLoading, queueStatus, isDisabled };
|
return { clearQueue, openDialog: dialog.setTrue, isLoading, queueStatus, isDisabled };
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||||
@ -7,6 +7,7 @@ import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent
|
|||||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||||
import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer';
|
import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
||||||
|
import QueueControls from 'features/queue/components/QueueControls';
|
||||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
||||||
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
||||||
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
|
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
|
||||||
@ -89,8 +90,14 @@ export const AppContent = memo(() => {
|
|||||||
|
|
||||||
const galleryPanel = usePanel(galleryPanelUsePanelOptions);
|
const galleryPanel = usePanel(galleryPanelUsePanelOptions);
|
||||||
|
|
||||||
useHotkeys('g', galleryPanel.toggle, [galleryPanel.toggle]);
|
useHotkeys('g', galleryPanel.toggle, { enabled: shouldShowGalleryPanel }, [
|
||||||
useHotkeys(['t', 'o'], optionsPanel.toggle, [optionsPanel.toggle]);
|
galleryPanel.toggle,
|
||||||
|
shouldShowGalleryPanel,
|
||||||
|
]);
|
||||||
|
useHotkeys(['t', 'o'], optionsPanel.toggle, { enabled: shouldShowOptionsPanel }, [
|
||||||
|
optionsPanel.toggle,
|
||||||
|
shouldShowOptionsPanel,
|
||||||
|
]);
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+r',
|
'shift+r',
|
||||||
() => {
|
() => {
|
||||||
@ -133,21 +140,26 @@ export const AppContent = memo(() => {
|
|||||||
storage={panelStorage}
|
storage={panelStorage}
|
||||||
>
|
>
|
||||||
<Panel order={0} collapsible style={panelStyles} {...optionsPanel.panelProps}>
|
<Panel order={0} collapsible style={panelStyles} {...optionsPanel.panelProps}>
|
||||||
<TabMountGate tab="generation">
|
<Flex flexDir="column" w="full" h="full" gap={2}>
|
||||||
<TabVisibilityGate tab="generation">
|
<QueueControls />
|
||||||
<ParametersPanelTextToImage />
|
<Box position="relative" w="full" h="full">
|
||||||
</TabVisibilityGate>
|
<TabMountGate tab="generation">
|
||||||
</TabMountGate>
|
<TabVisibilityGate tab="generation">
|
||||||
<TabMountGate tab="upscaling">
|
<ParametersPanelTextToImage />
|
||||||
<TabVisibilityGate tab="upscaling">
|
</TabVisibilityGate>
|
||||||
<ParametersPanelUpscale />
|
</TabMountGate>
|
||||||
</TabVisibilityGate>
|
<TabMountGate tab="upscaling">
|
||||||
</TabMountGate>
|
<TabVisibilityGate tab="upscaling">
|
||||||
<TabMountGate tab="workflows">
|
<ParametersPanelUpscale />
|
||||||
<TabVisibilityGate tab="workflows">
|
</TabVisibilityGate>
|
||||||
<NodeEditorPanelGroup />
|
</TabMountGate>
|
||||||
</TabVisibilityGate>
|
<TabMountGate tab="workflows">
|
||||||
</TabMountGate>
|
<TabVisibilityGate tab="workflows">
|
||||||
|
<NodeEditorPanelGroup />
|
||||||
|
</TabVisibilityGate>
|
||||||
|
</TabMountGate>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
</Panel>
|
</Panel>
|
||||||
<ResizeHandle id="options-main-handle" orientation="vertical" {...optionsPanel.resizeHandleProps} />
|
<ResizeHandle id="options-main-handle" orientation="vertical" {...optionsPanel.resizeHandleProps} />
|
||||||
<Panel id="main-panel" order={1} minSize={20} style={panelStyles}>
|
<Panel id="main-panel" order={1} minSize={20} style={panelStyles}>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
import { ButtonGroup, Flex, Icon, IconButton, Portal, spinAnimation } from '@invoke-ai/ui-library';
|
import { ButtonGroup, Flex, Icon, IconButton, Portal, spinAnimation } from '@invoke-ai/ui-library';
|
||||||
import CancelCurrentQueueItemIconButton from 'features/queue/components/CancelCurrentQueueItemIconButton';
|
import CancelCurrentQueueItemIconButton from 'features/queue/components/CancelCurrentQueueItemIconButton';
|
||||||
import { ClearAllQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
|
|
||||||
import { QueueButtonTooltip } from 'features/queue/components/QueueButtonTooltip';
|
import { QueueButtonTooltip } from 'features/queue/components/QueueButtonTooltip';
|
||||||
|
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||||
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
||||||
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
|
import type { UsePanelReturn } from 'features/ui/hooks/usePanel';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCircleNotchBold, PiSlidersHorizontalBold } from 'react-icons/pi';
|
import { PiCircleNotchBold, PiSlidersHorizontalBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
import { RiSparklingFill } from 'react-icons/ri';
|
import { RiSparklingFill } from 'react-icons/ri';
|
||||||
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ type Props = {
|
|||||||
const FloatingSidePanelButtons = (props: Props) => {
|
const FloatingSidePanelButtons = (props: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { queueBack, isLoading, isDisabled } = useQueueBack();
|
const { queueBack, isLoading, isDisabled } = useQueueBack();
|
||||||
|
const clearQueue = useClearQueue();
|
||||||
const { data: queueStatus } = useGetQueueStatusQuery();
|
const { data: queueStatus } = useGetQueueStatusQuery();
|
||||||
|
|
||||||
const queueButtonIcon = useMemo(() => {
|
const queueButtonIcon = useMemo(() => {
|
||||||
@ -71,7 +72,17 @@ const FloatingSidePanelButtons = (props: Props) => {
|
|||||||
</QueueButtonTooltip>
|
</QueueButtonTooltip>
|
||||||
<CancelCurrentQueueItemIconButton sx={floatingButtonStyles} />
|
<CancelCurrentQueueItemIconButton sx={floatingButtonStyles} />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<ClearAllQueueIconButton sx={floatingButtonStyles} />
|
<IconButton
|
||||||
|
isDisabled={clearQueue.isDisabled}
|
||||||
|
isLoading={clearQueue.isLoading}
|
||||||
|
aria-label={t('queue.clear')}
|
||||||
|
tooltip={t('queue.clearTooltip')}
|
||||||
|
icon={<PiTrashSimpleBold />}
|
||||||
|
colorScheme="error"
|
||||||
|
onClick={clearQueue.openDialog}
|
||||||
|
data-testid={t('queue.clear')}
|
||||||
|
sx={floatingButtonStyles}
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,6 @@ import { selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
|
|||||||
import { selectEntityCount } from 'features/controlLayers/store/selectors';
|
import { selectEntityCount } from 'features/controlLayers/store/selectors';
|
||||||
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
||||||
import QueueControls from 'features/queue/components/QueueControls';
|
|
||||||
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
||||||
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
|
import { CompositingSettingsAccordion } from 'features/settingsAccordions/components/CompositingSettingsAccordion/CompositingSettingsAccordion';
|
||||||
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
|
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
|
||||||
@ -64,7 +63,6 @@ const ParametersPanelTextToImage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" h="full" flexDir="column" gap={2}>
|
<Flex w="full" h="full" flexDir="column" gap={2}>
|
||||||
<QueueControls />
|
|
||||||
<StylePresetMenuTrigger />
|
<StylePresetMenuTrigger />
|
||||||
<Flex w="full" h="full" position="relative">
|
<Flex w="full" h="full" position="relative">
|
||||||
<Box position="absolute" top={0} left={0} right={0} bottom={0} ref={ref}>
|
<Box position="absolute" top={0} left={0} right={0} bottom={0} ref={ref}>
|
||||||
|
@ -2,7 +2,6 @@ import { Box, Flex } from '@invoke-ai/ui-library';
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
||||||
import QueueControls from 'features/queue/components/QueueControls';
|
|
||||||
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
import { AdvancedSettingsAccordion } from 'features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion';
|
||||||
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
|
import { GenerationSettingsAccordion } from 'features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion';
|
||||||
import { UpscaleSettingsAccordion } from 'features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion';
|
import { UpscaleSettingsAccordion } from 'features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleSettingsAccordion';
|
||||||
@ -23,7 +22,6 @@ const ParametersPanelUpscale = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" h="full" flexDir="column" gap={2}>
|
<Flex w="full" h="full" flexDir="column" gap={2}>
|
||||||
<QueueControls />
|
|
||||||
<StylePresetMenuTrigger />
|
<StylePresetMenuTrigger />
|
||||||
<Flex w="full" h="full" position="relative">
|
<Flex w="full" h="full" position="relative">
|
||||||
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
||||||
|
@ -3,30 +3,33 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import type { TabName } from 'features/ui/store/uiTypes';
|
import type { TabName } from 'features/ui/store/uiTypes';
|
||||||
import { memo, type ReactElement, useCallback } from 'react';
|
import { forwardRef, memo, type ReactElement, useCallback } from 'react';
|
||||||
|
|
||||||
export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: ReactElement; label: string }) => {
|
export const TabButton = memo(
|
||||||
const dispatch = useAppDispatch();
|
forwardRef(({ tab, icon, label }: { tab: TabName; icon: ReactElement; label: string }, ref) => {
|
||||||
const activeTabName = useAppSelector(selectActiveTab);
|
const dispatch = useAppDispatch();
|
||||||
const onClick = useCallback(() => {
|
const activeTabName = useAppSelector(selectActiveTab);
|
||||||
dispatch(setActiveTab(tab));
|
const onClick = useCallback(() => {
|
||||||
}, [dispatch, tab]);
|
dispatch(setActiveTab(tab));
|
||||||
|
}, [dispatch, tab]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={label} placement="end">
|
<Tooltip label={label} placement="end">
|
||||||
<IconButton
|
<IconButton
|
||||||
p={0}
|
ref={ref}
|
||||||
onClick={onClick}
|
p={0}
|
||||||
icon={icon}
|
onClick={onClick}
|
||||||
size="md"
|
icon={icon}
|
||||||
fontSize="24px"
|
size="md"
|
||||||
variant="appTab"
|
fontSize="24px"
|
||||||
data-selected={activeTabName === tab}
|
variant="appTab"
|
||||||
aria-label={label}
|
data-selected={activeTabName === tab}
|
||||||
data-testid={label}
|
aria-label={label}
|
||||||
/>
|
data-testid={label}
|
||||||
</Tooltip>
|
/>
|
||||||
);
|
</Tooltip>
|
||||||
});
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
TabButton.displayName = 'TabButton';
|
TabButton.displayName = 'TabButton';
|
||||||
|
@ -5,7 +5,7 @@ import { memo } from 'react';
|
|||||||
|
|
||||||
const ModelManagerTab = () => {
|
const ModelManagerTab = () => {
|
||||||
return (
|
return (
|
||||||
<Flex w="full" h="full" gap="2">
|
<Flex layerStyle="body" w="full" h="full" gap="2">
|
||||||
<ModelManager />
|
<ModelManager />
|
||||||
<ModelPane />
|
<ModelPane />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -4,7 +4,7 @@ import { memo } from 'react';
|
|||||||
|
|
||||||
const QueueTab = () => {
|
const QueueTab = () => {
|
||||||
return (
|
return (
|
||||||
<Flex w="full" h="full">
|
<Flex layerStyle="body" w="full" h="full">
|
||||||
<QueueTabContent />
|
<QueueTabContent />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -13,7 +13,7 @@ import { getCategories, getListImagesUrl } from 'services/api/util';
|
|||||||
|
|
||||||
const log = logger('events');
|
const log = logger('events');
|
||||||
|
|
||||||
const isCanvasOutput = (data: S['InvocationCompleteEvent']) => {
|
const isCanvasOutputNode = (data: S['InvocationCompleteEvent']) => {
|
||||||
return data.invocation_source_id.split(':')[0] === 'canvas_output';
|
return data.invocation_source_id.split(':')[0] === 'canvas_output';
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -114,25 +114,19 @@ export const buildOnInvocationComplete = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOriginCanvas = async (data: S['InvocationCompleteEvent']) => {
|
const handleOriginCanvas = async (data: S['InvocationCompleteEvent']) => {
|
||||||
const session = getState().canvasSession;
|
|
||||||
|
|
||||||
const imageDTO = await getResultImageDTO(data);
|
const imageDTO = await getResultImageDTO(data);
|
||||||
|
|
||||||
if (!imageDTO) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.mode === 'compose') {
|
if (data.destination === 'canvas') {
|
||||||
if (session.isStaging && isCanvasOutput(data)) {
|
if (isCanvasOutputNode(data)) {
|
||||||
if (data.result.type === 'canvas_v2_mask_and_crop_output') {
|
if (data.result.type === 'canvas_v2_mask_and_crop_output') {
|
||||||
const { offset_x, offset_y } = data.result;
|
const { offset_x, offset_y } = data.result;
|
||||||
if (session.isStaging) {
|
dispatch(sessionImageStaged({ stagingAreaImage: { imageDTO, offsetX: offset_x, offsetY: offset_y } }));
|
||||||
dispatch(sessionImageStaged({ stagingAreaImage: { imageDTO, offsetX: offset_x, offsetY: offset_y } }));
|
|
||||||
}
|
|
||||||
} else if (data.result.type === 'image_output') {
|
} else if (data.result.type === 'image_output') {
|
||||||
if (session.isStaging) {
|
dispatch(sessionImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
|
||||||
dispatch(sessionImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -96,7 +96,7 @@ export const setEventListeners = ({ socket, dispatch, getState, setIsConnected }
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('invocation_denoise_progress', (data) => {
|
socket.on('invocation_denoise_progress', (data) => {
|
||||||
const { invocation_source_id, invocation, step, total_steps, progress_image, origin, percentage, session_id } =
|
const { invocation_source_id, invocation, step, total_steps, progress_image, origin, destination, percentage, session_id } =
|
||||||
data;
|
data;
|
||||||
|
|
||||||
if (cancellations.has(session_id)) {
|
if (cancellations.has(session_id)) {
|
||||||
@ -122,7 +122,7 @@ export const setEventListeners = ({ socket, dispatch, getState, setIsConnected }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (origin === 'canvas') {
|
if (origin === 'canvas' && destination === 'canvas') {
|
||||||
$lastCanvasProgressEvent.set(data);
|
$lastCanvasProgressEvent.set(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user