refactor(ui): canvas flow (wip)

This commit is contained in:
psychedelicious
2025-05-23 19:07:26 +10:00
parent aa3b2106d4
commit c0428ee7ef
14 changed files with 297 additions and 83 deletions

View File

@@ -5,6 +5,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { extractMessageFromAssertionError } from 'common/util/extractMessageFromAssertionError';
import { withResult, withResultAsync } from 'common/util/result';
import { parseify } from 'common/util/serialize';
import { canvasSessionStarted, selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildChatGPT4oGraph } from 'features/nodes/util/graph/generation/buildChatGPT4oGraph';
@@ -115,6 +116,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
try {
await req.unwrap();
if (!selectCanvasSessionType(state)) {
dispatch(canvasSessionStarted({ sessionType: 'simple' }));
}
log.debug(parseify({ batchConfig: prepareBatchResult.value }), 'Enqueued batch');
} catch (error) {
log.error({ error: serializeError(error as Error) }, 'Failed to enqueue batch');

View File

@@ -9,7 +9,7 @@ import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/contr
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
import {
canvasStagingAreaPersistConfig,
canvasStagingAreaSlice,
canvasSessionSlice,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
@@ -65,7 +65,7 @@ const allReducers = {
[stylePresetSlice.name]: stylePresetSlice.reducer,
[paramsSlice.name]: paramsSlice.reducer,
[canvasSettingsSlice.name]: canvasSettingsSlice.reducer,
[canvasStagingAreaSlice.name]: canvasStagingAreaSlice.reducer,
[canvasSessionSlice.name]: canvasSessionSlice.reducer,
[lorasSlice.name]: lorasSlice.reducer,
[workflowLibrarySlice.name]: workflowLibrarySlice.reducer,
};

View File

@@ -1,5 +1,6 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Button, ContextMenu, Flex, IconButton, Image, Menu, MenuButton, MenuList, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper';
import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask';
@@ -18,17 +19,32 @@ import { StagingAreaToolbar } from 'features/controlLayers/components/StagingAre
import { CanvasToolbar } from 'features/controlLayers/components/Toolbar/CanvasToolbar';
import { Transform } from 'features/controlLayers/components/Transform/Transform';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { newCanvasSessionRequested } from 'features/controlLayers/store/actions';
import { canvasReset, newAdvancedCanvasSessionRequested } from 'features/controlLayers/store/actions';
import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice';
import {
selectCanvasSessionType,
selectIsStaging,
selectSelectedImage,
selectStagedImageIndex,
selectStagedImages,
stagingAreaImageSelected,
stagingAreaImageStaged,
stagingAreaNextStagedImageSelected,
stagingAreaPrevStagedImageSelected,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { selectIsCanvasEmpty, selectIsSessionStarted } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { isImageField, type ProgressImage } from 'features/nodes/types/common';
import { isCanvasOutputEvent } from 'features/nodes/util/graph/graphBuilderUtils';
import type { Atom } from 'nanostores';
import { atom } from 'nanostores';
import { memo, useCallback, useEffect, useState } from 'react';
import { flushSync } from 'react-dom';
import { useHotkeys } from 'react-hotkeys-hook';
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import { assert } from 'tsafe';
import { getImageDTOSafe } from 'services/api/endpoints/images';
import type { ImageDTO, S } from 'services/api/types';
import { $socket } from 'services/events/stores';
import type { Equals } from 'tsafe';
import { assert, objectEntries } from 'tsafe';
import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress';
@@ -50,22 +66,21 @@ const MenuContent = memo(() => {
MenuContent.displayName = 'MenuContent';
export const CanvasMainPanelContent = memo(() => {
const isCanvasEmpty = useAppSelector(selectIsCanvasEmpty);
const isSessionStarted = useAppSelector(selectIsSessionStarted);
const sessionType = useAppSelector(selectCanvasSessionType);
if (!isSessionStarted) {
if (sessionType === null) {
return <NoActiveSession />;
}
if (isSessionStarted && isCanvasEmpty) {
if (sessionType === 'simple') {
return <SimpleActiveSession />;
}
if (isSessionStarted && !isCanvasEmpty) {
if (sessionType === 'advanced') {
return <CanvasActiveSession />;
}
assert(false);
assert<Equals<never, typeof sessionType>>(false, 'Unexpected sessionType');
});
CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
@@ -73,7 +88,7 @@ CanvasMainPanelContent.displayName = 'CanvasMainPanelContent';
const NoActiveSession = memo(() => {
const dispatch = useAppDispatch();
const newSesh = useCallback(() => {
dispatch(newCanvasSessionRequested());
dispatch(newAdvancedCanvasSessionRequested());
}, [dispatch]);
return (
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
@@ -108,26 +123,189 @@ const NoActiveSession = memo(() => {
);
});
NoActiveSession.displayName = 'NoActiveSession';
type EphemeralProgressImage = { sessionId: string; image: ProgressImage };
const SimpleActiveSession = memo(() => {
const dispatch = useAppDispatch();
const isStaging = useAppSelector(selectIsStaging);
const selectedImage = useAppSelector(selectSelectedImage);
const stagedImages = useAppSelector(selectStagedImages);
const socket = useStore($socket);
const [$progressImage] = useState(() => atom<EphemeralProgressImage | null>(null));
useEffect(() => {
if (!socket) {
return;
}
const onInvocationProgress = (event: S['InvocationProgressEvent']) => {
if (!event) {
return;
}
if (event.origin !== 'canvas') {
return;
}
if (!event.image) {
return;
}
$progressImage.set({ sessionId: event.session_id, image: event.image });
};
const onInvocationComplete = async (event: S['InvocationCompleteEvent']) => {
const progressImage = $progressImage.get();
if (!progressImage) {
return;
}
if (progressImage.sessionId !== event.session_id) {
return;
}
if (!isCanvasOutputEvent(event)) {
return;
}
let imageDTO: ImageDTO | null = null;
for (const [_name, value] of objectEntries(event.result)) {
if (isImageField(value)) {
imageDTO = await getImageDTOSafe(value.image_name);
break;
}
}
if (!imageDTO) {
return;
}
flushSync(() => {
dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
});
$progressImage.set(null);
};
const onQueueItemStatusChanged = (event: S['QueueItemStatusChangedEvent']) => {
const progressImage = $progressImage.get();
if (!progressImage) {
return;
}
if (progressImage.sessionId !== event.session_id) {
return;
}
if (event.status !== 'canceled' && event.status !== 'failed') {
return;
}
$progressImage.set(null);
};
console.log('SUB session preview image listeners');
socket.on('invocation_progress', onInvocationProgress);
socket.on('invocation_complete', onInvocationComplete);
socket.on('queue_item_status_changed', onQueueItemStatusChanged);
return () => {
console.log('UNSUB session preview image listeners');
socket.off('invocation_progress', onInvocationProgress);
socket.off('invocation_complete', onInvocationComplete);
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
};
}, [$progressImage, dispatch, socket]);
const onReset = useCallback(() => {
dispatch(canvasReset());
}, [dispatch]);
const selectNext = useCallback(() => {
dispatch(stagingAreaNextStagedImageSelected());
}, [dispatch]);
useHotkeys(['right'], selectNext, { preventDefault: true }, [selectNext]);
const selectPrev = useCallback(() => {
dispatch(stagingAreaPrevStagedImageSelected());
}, [dispatch]);
useHotkeys(['left'], selectPrev, { preventDefault: true }, [selectPrev]);
return (
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
<Text fontSize="lg" fontWeight="bold">
Simple Session (staging view) {isStaging && 'STAGING'}
</Text>
{selectedImage && <Image src={selectedImage.imageDTO.image_url} />}
<Flex gap={2} maxW="full" overflow="scroll">
{stagedImages.map(({ imageDTO }) => (
<Image key={imageDTO.image_name} maxW={108} src={imageDTO.thumbnail_url} />
))}
<Flex>
<Text fontSize="lg" fontWeight="bold">
Simple Session (staging view) {isStaging && 'STAGING'}
</Text>
<Button onClick={onReset}>reset</Button>
</Flex>
<SelectedImage $progressImage={$progressImage} />
<SessionImages />
</Flex>
);
});
SimpleActiveSession.displayName = 'SimpleActiveSession';
const SelectedImage = memo(({ $progressImage }: { $progressImage: Atom<EphemeralProgressImage | null> }) => {
const progressImage = useStore($progressImage);
const selectedImage = useAppSelector(selectSelectedImage);
if (progressImage) {
return (
<Flex alignItems="center" justifyContent="center" minH={0} minW={0}>
<Image
objectFit="contain"
maxH="full"
maxW="full"
src={progressImage.image.dataURL}
width={progressImage.image.width}
height={progressImage.image.height}
/>
</Flex>
);
}
if (selectedImage) {
return (
<Flex alignItems="center" justifyContent="center" minH={0} minW={0}>
<Image
objectFit="contain"
maxH="full"
maxW="full"
src={selectedImage.imageDTO.image_url}
width={selectedImage.imageDTO.width}
height={selectedImage.imageDTO.height}
/>
</Flex>
);
}
return <Text>No images</Text>;
});
SelectedImage.displayName = 'SelectedImage';
const SessionImages = memo(() => {
const stagedImages = useAppSelector(selectStagedImages);
return (
<Flex gap={2} h={108} maxW="full" overflow="scroll">
{stagedImages.map(({ imageDTO }, index) => (
<SessionImage key={imageDTO.image_name} index={index} imageDTO={imageDTO} />
))}
</Flex>
);
});
SessionImages.displayName = 'SessionImages';
const sx = {
'&[data-is-selected="false"]': {
opacity: 0.5,
},
} satisfies SystemStyleObject;
const SessionImage = memo(({ index, imageDTO }: { index: number; imageDTO: ImageDTO }) => {
const dispatch = useAppDispatch();
const selectedImageIndex = useAppSelector(selectStagedImageIndex);
const onClick = useCallback(() => {
dispatch(stagingAreaImageSelected({ index }));
}, [dispatch, index]);
return (
<Image
maxW={108}
src={imageDTO.image_url}
fallbackSrc={imageDTO.thumbnail_url}
onClick={onClick}
data-is-selected={selectedImageIndex === index}
sx={sx}
/>
);
});
SessionImage.displayName = 'SessionImage';
const CanvasActiveSession = memo(() => {
const dynamicGrid = useAppSelector(selectDynamicGrid);
const showHUD = useAppSelector(selectShowHUD);

View File

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

View File

@@ -78,7 +78,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
const { x, y } = this.manager.stateApi.getBbox().rect;
const shouldShowStagedImage = this.$shouldShowStagedImage.get();
this.selectedImage = stagingArea.stagedImages[stagingArea.selectedStagedImageIndex] ?? null;
this.selectedImage = stagingArea.images[stagingArea.selectedImageIndex] ?? null;
this.konva.group.position({ x, y });
if (this.selectedImage) {

View File

@@ -2,6 +2,6 @@ import { createAction, isAnyOf } from '@reduxjs/toolkit';
// Needed to split this from canvasSlice.ts to avoid circular dependencies
export const canvasReset = createAction('canvas/canvasReset');
export const newGallerySessionRequested = createAction('canvas/newGallerySessionRequested');
export const newCanvasSessionRequested = createAction('canvas/newCanvasSessionRequested');
export const newSessionRequested = isAnyOf(newGallerySessionRequested, newCanvasSessionRequested);
export const newSimpleCanvasSessionRequested = createAction('canvas/newSimpleCanvasSessionRequested');
export const newAdvancedCanvasSessionRequested = createAction('canvas/newAdvancedCanvasSessionRequested');
export const newSessionRequested = isAnyOf(newSimpleCanvasSessionRequested, newAdvancedCanvasSessionRequested);

View File

@@ -1,7 +1,7 @@
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { newCanvasSessionRequested, newGallerySessionRequested } from 'features/controlLayers/store/actions';
import { newAdvancedCanvasSessionRequested, newSimpleCanvasSessionRequested } from 'features/controlLayers/store/actions';
import type { RgbaColor } from 'features/controlLayers/store/types';
type CanvasSettingsState = {
@@ -158,10 +158,10 @@ export const canvasSettingsSlice = createSlice({
},
},
extraReducers(builder) {
builder.addCase(newGallerySessionRequested, (state) => {
builder.addCase(newSimpleCanvasSessionRequested, (state) => {
state.sendToCanvas = false;
});
builder.addCase(newCanvasSessionRequested, (state) => {
builder.addCase(newAdvancedCanvasSessionRequested, (state) => {
state.sendToCanvas = true;
});
},

View File

@@ -7,8 +7,8 @@ import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMul
import { getPrefixedId } from 'features/controlLayers/konva/util';
import {
canvasReset,
newCanvasSessionRequested,
newGallerySessionRequested,
newAdvancedCanvasSessionRequested,
newSimpleCanvasSessionRequested,
} from 'features/controlLayers/store/actions';
import { modelChanged } from 'features/controlLayers/store/paramsSlice';
import {
@@ -1806,12 +1806,11 @@ export const canvasSlice = createSlice({
syncScaledSize(state);
}
});
builder.addCase(newGallerySessionRequested, (state) => {
builder.addCase(newSimpleCanvasSessionRequested, (state) => {
return resetState(state);
});
builder.addCase(newCanvasSessionRequested, (state) => {
builder.addCase(newAdvancedCanvasSessionRequested, (state) => {
const newState = resetState(state);
newState.isSessionStarted = true;
return newState;
});
},

View File

@@ -1,51 +1,73 @@
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { canvasReset } from 'features/controlLayers/store/actions';
import {
canvasReset,
newAdvancedCanvasSessionRequested,
newSimpleCanvasSessionRequested,
} from 'features/controlLayers/store/actions';
import type { StagingAreaImage } from 'features/controlLayers/store/types';
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
import { newSessionRequested } from './actions';
type CanvasStagingAreaState = {
stagedImages: StagingAreaImage[];
selectedStagedImageIndex: number;
sessionType: 'simple' | 'advanced' | null;
images: StagingAreaImage[];
selectedImageIndex: number;
};
const initialState: CanvasStagingAreaState = {
stagedImages: [],
selectedStagedImageIndex: 0,
sessionType: null,
images: [],
selectedImageIndex: 0,
};
export const canvasStagingAreaSlice = createSlice({
name: 'canvasStagingArea',
export const canvasSessionSlice = createSlice({
name: 'canvasSession',
initialState,
reducers: {
stagingAreaImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
const { stagingAreaImage } = action.payload;
state.stagedImages.push(stagingAreaImage);
state.selectedStagedImageIndex = state.stagedImages.length - 1;
state.images.push(stagingAreaImage);
state.selectedImageIndex = state.images.length - 1;
},
stagingAreaImageSelected: (state, action: PayloadAction<{ index: number }>) => {
const { index } = action.payload;
state.selectedImageIndex = index;
},
stagingAreaNextStagedImageSelected: (state) => {
state.selectedStagedImageIndex = (state.selectedStagedImageIndex + 1) % state.stagedImages.length;
state.selectedImageIndex = (state.selectedImageIndex + 1) % state.images.length;
},
stagingAreaPrevStagedImageSelected: (state) => {
state.selectedStagedImageIndex =
(state.selectedStagedImageIndex - 1 + state.stagedImages.length) % state.stagedImages.length;
state.selectedImageIndex = (state.selectedImageIndex - 1 + state.images.length) % state.images.length;
},
stagingAreaStagedImageDiscarded: (state, action: PayloadAction<{ index: number }>) => {
const { index } = action.payload;
state.stagedImages.splice(index, 1);
state.selectedStagedImageIndex = Math.min(state.selectedStagedImageIndex, state.stagedImages.length - 1);
state.images.splice(index, 1);
state.selectedImageIndex = Math.min(state.selectedImageIndex, state.images.length - 1);
},
stagingAreaReset: (state) => {
state.stagedImages = [];
state.selectedStagedImageIndex = 0;
state.images = [];
state.selectedImageIndex = 0;
},
canvasSessionStarted: (state, action: PayloadAction<{ sessionType: CanvasStagingAreaState['sessionType'] }>) => {
const { sessionType } = action.payload;
state.sessionType = sessionType;
state.images = [];
state.selectedImageIndex = 0;
},
},
extraReducers(builder) {
builder.addCase(canvasReset, () => deepClone(initialState));
builder.addMatcher(newSessionRequested, () => deepClone(initialState));
builder.addCase(newSimpleCanvasSessionRequested, () => {
const state = deepClone(initialState);
state.sessionType === 'simple';
return state;
});
builder.addCase(newAdvancedCanvasSessionRequested, () => {
const state = deepClone(initialState);
state.sessionType === 'advanced';
return state;
});
},
});
@@ -53,9 +75,11 @@ export const {
stagingAreaImageStaged,
stagingAreaStagedImageDiscarded,
stagingAreaReset,
stagingAreaImageSelected,
stagingAreaNextStagedImageSelected,
stagingAreaPrevStagedImageSelected,
} = canvasStagingAreaSlice.actions;
canvasSessionStarted,
} = canvasSessionSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
@@ -63,13 +87,13 @@ const migrate = (state: any): any => {
};
export const canvasStagingAreaPersistConfig: PersistConfig<CanvasStagingAreaState> = {
name: canvasStagingAreaSlice.name,
name: canvasSessionSlice.name,
initialState,
migrate,
persistDenylist: [],
};
export const selectCanvasStagingAreaSlice = (s: RootState) => s.canvasStagingArea;
export const selectCanvasStagingAreaSlice = (s: RootState) => s[canvasSessionSlice.name];
/**
* Selects if we should be staging images. This is true if:
@@ -80,7 +104,7 @@ export const selectIsStaging = createSelector(
selectCanvasQueueCounts,
selectCanvasStagingAreaSlice,
({ data }, staging) => {
if (staging.stagedImages.length > 0) {
if (staging.images.length > 0) {
return true;
}
if (!data) {
@@ -91,17 +115,18 @@ export const selectIsStaging = createSelector(
);
export const selectStagedImageIndex = createSelector(
selectCanvasStagingAreaSlice,
(stagingArea) => stagingArea.selectedStagedImageIndex
(stagingArea) => stagingArea.selectedImageIndex
);
export const selectSelectedImage = createSelector(
[selectCanvasStagingAreaSlice, selectStagedImageIndex],
(stagingArea, index) => stagingArea.stagedImages[index] ?? null
);
export const selectStagedImages = createSelector(
selectCanvasStagingAreaSlice,
(stagingArea) => stagingArea.stagedImages
(stagingArea, index) => stagingArea.images[index] ?? null
);
export const selectStagedImages = createSelector(selectCanvasStagingAreaSlice, (stagingArea) => stagingArea.images);
export const selectImageCount = createSelector(
selectCanvasStagingAreaSlice,
(stagingArea) => stagingArea.stagedImages.length
(stagingArea) => stagingArea.images.length
);
export const selectCanvasSessionType = createSelector(
selectCanvasStagingAreaSlice,
(canvasSession) => canvasSession.sessionType
);

View File

@@ -409,7 +409,6 @@ export const selectCanvasMetadata = createSelector(
}
);
export const selectIsSessionStarted = createCanvasSelector(({ isSessionStarted }) => isSessionStarted);
export const selectIsCanvasEmpty = createCanvasSelector(
({ controlLayers, inpaintMasks, rasterLayers, regionalGuidance }) => {
// Check it all manually - could use lodash isEqual, but this selector will be called very often!

View File

@@ -561,7 +561,6 @@ const zReferenceImages = z.object({
});
const zCanvasState = z.object({
_version: z.literal(3).default(3),
isSessionStarted: z.boolean().default(false),
selectedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null),
inpaintMasks: zInpaintMasks.default({ isHidden: false, entities: [] }),

View File

@@ -8,7 +8,7 @@ import { buildPresetModifiedPrompt } from 'features/stylePresets/hooks/usePreset
import { selectStylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
import { pick } from 'lodash-es';
import { selectListStylePresetsRequestState } from 'services/api/endpoints/stylePresets';
import type { Invocation } from 'services/api/types';
import type { Invocation, S } from 'services/api/types';
import { assert } from 'tsafe';
import type { MainModelLoaderNodes } from './types';
@@ -134,3 +134,7 @@ export const isMainModelWithoutUnet = (modelLoader: Invocation<MainModelLoaderNo
modelLoader.type === 'cogview4_model_loader'
);
};
export const isCanvasOutputEvent = (data: S['InvocationCompleteEvent']) => {
return data.invocation_source_id.split(':')[0] === CANVAS_OUTPUT_PREFIX;
};

View File

@@ -1,12 +1,10 @@
import { logger } from 'app/logging/logger';
import type { AppDispatch, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { stagingAreaImageStaged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
import { isImageField, isImageFieldCollection } from 'features/nodes/types/common';
import { zNodeStatus } from 'features/nodes/types/invocation';
import { CANVAS_OUTPUT_PREFIX } from 'features/nodes/util/graph/graphBuilderUtils';
import type { ApiTagDescription } from 'services/api';
import { boardsApi } from 'services/api/endpoints/boards';
import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images';
@@ -19,10 +17,6 @@ import type { JsonObject } from 'type-fest';
const log = logger('events');
const isCanvasOutputNode = (data: S['InvocationCompleteEvent']) => {
return data.invocation_source_id.split(':')[0] === CANVAS_OUTPUT_PREFIX;
};
const nodeTypeDenylist = ['load_image', 'image'];
export const buildOnInvocationComplete = (getState: () => RootState, dispatch: AppDispatch) => {
@@ -179,12 +173,12 @@ export const buildOnInvocationComplete = (getState: () => RootState, dispatch: A
if (data.destination === 'canvas') {
// TODO(psyche): Can/should we let canvas handle this itself?
if (isCanvasOutputNode(data)) {
if (data.result.type === 'image_output') {
dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
}
addImagesToGallery(data, [imageDTO]);
}
// if (isCanvasOutputEvent(data)) {
// if (data.result.type === 'image_output') {
// dispatch(stagingAreaImageStaged({ stagingAreaImage: { imageDTO, offsetX: 0, offsetY: 0 } }));
// }
// addImagesToGallery(data, [imageDTO]);
// }
} else if (!imageDTO.is_intermediate) {
// Desintaion is gallery
addImagesToGallery(data, [imageDTO]);

View File

@@ -9,6 +9,18 @@ export const $socketOptions = map<Partial<ManagerOptions & SocketOptions>>({});
export const $isConnected = atom<boolean>(false);
export const $lastProgressEvent = atom<S['InvocationProgressEvent'] | null>(null);
export const $progressImage = computed($lastProgressEvent, (val) => val?.image ?? null);
export const $canvasProgressImage = computed($lastProgressEvent, (event) => {
if (!event) {
return null;
}
if (event.origin !== 'canvas') {
return null;
}
if (!event.image) {
return null;
}
return event.image;
});
export const $hasProgressImage = computed($lastProgressEvent, (val) => Boolean(val?.image));
export const $isProgressFromCanvas = computed($lastProgressEvent, (val) => val?.destination === 'canvas');
export const $invocationProgressMessage = computed($lastProgressEvent, (val) => {