feat(ui): wip refactor socket events

This commit is contained in:
psychedelicious
2023-04-05 18:01:32 +10:00
parent 4e2358cb09
commit 760b4b938c
25 changed files with 395 additions and 398 deletions

View File

@ -14,6 +14,7 @@
import { InvokeTabName } from 'features/ui/store/tabMap'; import { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types'; import { IRect } from 'konva/lib/types';
import { ImageMetadata, ImageType } from 'services/api';
/** /**
* TODO: * TODO:
@ -132,12 +133,10 @@ export declare type _Image = {
*/ */
export declare type Image = { export declare type Image = {
name: string; name: string;
type: ImageType;
url: string; url: string;
thumbnail: string; thumbnail: string;
width: number; metadata: ImageMetadata;
height: number;
timestamp: number;
metadata?: Metadata;
}; };
// GalleryImages is an array of Image. // GalleryImages is an array of Image.

View File

@ -5,32 +5,39 @@ import {
InvocationErrorEvent, InvocationErrorEvent,
InvocationStartedEvent, InvocationStartedEvent,
} from 'services/events/types'; } from 'services/events/types';
/**
* We can't use redux-toolkit's createSlice() to make these actions,
* because they have no associated reducer. They only exist to dispatch
* requests to the server via socketio. These actions will be handled
* by the middleware.
*/
export const emitSubscribe = createAction<string>('socketio/subscribe'); type SocketioPayload = {
export const emitUnsubscribe = createAction<string>('socketio/unsubscribe');
type Timestamp = {
timestamp: Date; timestamp: Date;
}; };
export const socketioConnected = createAction<SocketioPayload>(
'socketio/socketioConnected'
);
export const socketioDisconnected = createAction<SocketioPayload>(
'socketio/socketioDisconnected'
);
export const socketioSubscribed = createAction<
SocketioPayload & { sessionId: string }
>('socketio/socketioSubscribed');
export const socketioUnsubscribed = createAction<
SocketioPayload & { sessionId: string }
>('socketio/socketioUnsubscribed');
export const invocationStarted = createAction< export const invocationStarted = createAction<
{ data: InvocationStartedEvent } & Timestamp SocketioPayload & { data: InvocationStartedEvent }
>('socketio/invocationStarted'); >('socketio/invocationStarted');
export const invocationComplete = createAction< export const invocationComplete = createAction<
{ data: InvocationCompleteEvent } & Timestamp SocketioPayload & { data: InvocationCompleteEvent }
>('socketio/invocationComplete'); >('socketio/invocationComplete');
export const invocationError = createAction< export const invocationError = createAction<
{ data: InvocationErrorEvent } & Timestamp SocketioPayload & { data: InvocationErrorEvent }
>('socketio/invocationError'); >('socketio/invocationError');
export const generatorProgress = createAction< export const generatorProgress = createAction<
{ data: GeneratorProgressEvent } & Timestamp SocketioPayload & { data: GeneratorProgressEvent }
>('socketio/generatorProgress'); >('socketio/generatorProgress');

View File

@ -1,15 +0,0 @@
import { Socket } from 'socket.io-client';
const makeSocketIOEmitters = (socketio: Socket) => {
return {
emitSubscribe: (sessionId: string) => {
socketio.emit('subscribe', { session: sessionId });
},
emitUnsubscribe: (sessionId: string) => {
socketio.emit('unsubscribe', { session: sessionId });
},
};
};
export default makeSocketIOEmitters;

View File

@ -1,192 +0,0 @@
import { MiddlewareAPI } from '@reduxjs/toolkit';
import dateFormat from 'dateformat';
import i18n from 'i18n';
import { v4 as uuidv4 } from 'uuid';
import {
addLogEntry,
errorOccurred,
setCurrentStatus,
setIsCancelable,
setIsConnected,
setIsProcessing,
socketioConnected,
socketioDisconnected,
} from 'features/system/store/systemSlice';
import {
addImage,
clearIntermediateImage,
setIntermediateImage,
} from 'features/gallery/store/gallerySlice';
import type { AppDispatch, RootState } from 'app/store';
import {
GeneratorProgressEvent,
InvocationCompleteEvent,
InvocationErrorEvent,
InvocationStartedEvent,
} from 'services/events/types';
import {
setProgress,
setProgressImage,
setSessionId,
setStatus,
STATUS,
} from 'services/apiSlice';
import { emitUnsubscribe, invocationComplete } from './actions';
import { resultAdded } from 'features/gallery/store/resultsSlice';
import {
receivedResultImagesPage,
receivedUploadImagesPage,
} from 'services/thunks/gallery';
import { deserializeImageField } from 'services/util/deserializeImageField';
/**
* Returns an object containing listener callbacks
*/
const makeSocketIOListeners = (
store: MiddlewareAPI<AppDispatch, RootState>
) => {
const { dispatch, getState } = store;
return {
/**
* Callback to run when we receive a 'connect' event.
*/
onConnect: () => {
try {
dispatch(socketioConnected());
// fetch more images, but only if we don't already have images
if (!getState().results.ids.length) {
dispatch(receivedResultImagesPage());
}
if (!getState().uploads.ids.length) {
dispatch(receivedUploadImagesPage());
}
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'disconnect' event.
*/
onDisconnect: () => {
try {
dispatch(socketioDisconnected());
dispatch(emitUnsubscribe(getState().api.sessionId));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Disconnected from server`,
level: 'warning',
})
);
} catch (e) {
console.error(e);
}
},
onInvocationStarted: (data: InvocationStartedEvent) => {
console.log('invocation_started', data);
dispatch(setStatus(STATUS.busy));
},
/**
* Callback to run when we receive a 'generationResult' event.
*/
onInvocationComplete: (data: InvocationCompleteEvent) => {
console.log('invocation_complete', data);
try {
dispatch(invocationComplete({ data, timestamp: new Date() }));
const sessionId = data.graph_execution_state_id;
if (data.result.type === 'image') {
// const resultImage = deserializeImageField(data.result.image);
// dispatch(resultAdded(resultImage));
// // need to update the type for this or figure out how to get these values
// dispatch(
// addImage({
// category: 'result',
// image: {
// uuid: uuidv4(),
// url: resultImage.url,
// thumbnail: '',
// width: 512,
// height: 512,
// category: 'result',
// name: resultImage.name,
// mtime: new Date().getTime(),
// },
// })
// );
// dispatch(setIsProcessing(false));
// dispatch(setIsCancelable(false));
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Generated: ${data.result.image.image_name}`,
// })
// );
dispatch(emitUnsubscribe(sessionId));
// dispatch(setSessionId(''));
}
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'progressUpdate' event.
* TODO: Add additional progress phases
*/
onGeneratorProgress: (data: GeneratorProgressEvent) => {
try {
console.log('generator_progress', data);
dispatch(setProgress(data.step / data.total_steps));
if (data.progress_image) {
dispatch(
setIntermediateImage({
// need to update the type for this or figure out how to get these values
category: 'result',
uuid: uuidv4(),
mtime: new Date().getTime(),
url: data.progress_image.dataURL,
thumbnail: '',
...data.progress_image,
})
);
}
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'progressUpdate' event.
*/
onInvocationError: (data: InvocationErrorEvent) => {
const { error } = data;
try {
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Server error: ${error}`,
level: 'error',
})
);
dispatch(errorOccurred());
dispatch(clearIntermediateImage());
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'galleryImages' event.
*/
};
};
export default makeSocketIOListeners;

View File

@ -1,16 +1,26 @@
import { Middleware } from '@reduxjs/toolkit'; import { Middleware, MiddlewareAPI } from '@reduxjs/toolkit';
import { io } from 'socket.io-client'; import { io } from 'socket.io-client';
import makeSocketIOEmitters from './emitters';
import makeSocketIOListeners from './listeners';
import { import {
GeneratorProgressEvent, GeneratorProgressEvent,
InvocationCompleteEvent, InvocationCompleteEvent,
InvocationErrorEvent, InvocationErrorEvent,
InvocationStartedEvent, InvocationStartedEvent,
} from 'services/events/types'; } from 'services/events/types';
import { invocationComplete } from './actions'; import {
generatorProgress,
invocationComplete,
invocationError,
invocationStarted,
socketioConnected,
socketioDisconnected,
socketioSubscribed,
} from './actions';
import {
receivedResultImagesPage,
receivedUploadImagesPage,
} from 'services/thunks/gallery';
import { AppDispatch, RootState } from 'app/store';
const socket_url = `ws://${window.location.host}`; const socket_url = `ws://${window.location.host}`;
@ -22,64 +32,62 @@ const socketio = io(socket_url, {
export const socketioMiddleware = () => { export const socketioMiddleware = () => {
let areListenersSet = false; let areListenersSet = false;
const middleware: Middleware = (store) => (next) => (action) => { const middleware: Middleware =
const { emitSubscribe, emitUnsubscribe } = makeSocketIOEmitters(socketio); (store: MiddlewareAPI<AppDispatch, RootState>) => (next) => (action) => {
const { dispatch, getState } = store;
const timestamp = new Date();
const { if (!areListenersSet) {
onConnect, socketio.on('connect', () => {
onDisconnect, dispatch(socketioConnected({ timestamp }));
onInvocationStarted,
onGeneratorProgress,
onInvocationError,
onInvocationComplete,
} = makeSocketIOListeners(store);
if (!areListenersSet) { if (!getState().results.ids.length) {
socketio.on('connect', () => onConnect()); dispatch(receivedResultImagesPage());
socketio.on('disconnect', () => onDisconnect()); }
}
areListenersSet = true; if (!getState().uploads.ids.length) {
dispatch(receivedUploadImagesPage());
}
});
// use the action's match() function for type narrowing and safety socketio.on('disconnect', () => {
if (invocationComplete.match(action)) { dispatch(socketioDisconnected({ timestamp }));
emitUnsubscribe(action.payload.data.graph_execution_state_id); socketio.removeAllListeners();
socketio.removeAllListeners(); });
}
/**
* Handle redux actions caught by middleware.
*/
switch (action.type) {
case 'socketio/subscribe': {
emitSubscribe(action.payload);
socketio.on('invocation_started', (data: InvocationStartedEvent) =>
onInvocationStarted(data)
);
socketio.on('generator_progress', (data: GeneratorProgressEvent) =>
onGeneratorProgress(data)
);
socketio.on('invocation_error', (data: InvocationErrorEvent) =>
onInvocationError(data)
);
socketio.on('invocation_complete', (data: InvocationCompleteEvent) =>
onInvocationComplete(data)
);
break;
} }
// case 'socketio/unsubscribe': { areListenersSet = true;
// emitUnsubscribe(action.payload);
// socketio.removeAllListeners(); if (invocationComplete.match(action)) {
// break; socketio.emit('unsubscribe', {
// } session: action.payload.data.graph_execution_state_id,
} });
next(action); socketio.removeAllListeners();
}; }
if (socketioSubscribed.match(action)) {
socketio.emit('subscribe', { session: action.payload.sessionId });
socketio.on('invocation_started', (data: InvocationStartedEvent) => {
dispatch(invocationStarted({ data, timestamp }));
});
socketio.on('generator_progress', (data: GeneratorProgressEvent) => {
dispatch(generatorProgress({ data, timestamp }));
});
socketio.on('invocation_error', (data: InvocationErrorEvent) => {
dispatch(invocationError({ data, timestamp }));
});
socketio.on('invocation_complete', (data: InvocationCompleteEvent) => {
dispatch(invocationComplete({ data, timestamp }));
});
}
next(action);
};
return middleware; return middleware;
}; };

View File

@ -34,8 +34,9 @@ import type { RootState } from 'app/store';
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
import { import {
clearInitialImage, clearInitialImage,
initialImageSelected,
setInfillMethod, setInfillMethod,
setInitialImage, // setInitialImage,
setMaskPath, setMaskPath,
} from 'features/parameters/store/generationSlice'; } from 'features/parameters/store/generationSlice';
import { tabMap } from 'features/ui/store/tabMap'; import { tabMap } from 'features/ui/store/tabMap';
@ -146,7 +147,8 @@ const makeSocketIOListeners = (
const activeTabName = tabMap[activeTab]; const activeTabName = tabMap[activeTab];
switch (activeTabName) { switch (activeTabName) {
case 'img2img': { case 'img2img': {
dispatch(setInitialImage(newImage)); dispatch(initialImageSelected(newImage.uuid));
// dispatch(setInitialImage(newImage));
break; break;
} }
} }

View File

@ -7,6 +7,7 @@ import {
} from 'services/api'; } from 'services/api';
import { _Image } from 'app/invokeai'; import { _Image } from 'app/invokeai';
import { initialImageSelector } from 'features/parameters/store/generationSelectors';
// fe todo fix model type (frontend uses null, backend uses undefined) // fe todo fix model type (frontend uses null, backend uses undefined)
// fe todo update front end to store to have whole image field (vs just name) // fe todo update front end to store to have whole image field (vs just name)
@ -66,10 +67,16 @@ export function buildImg2ImgNode(
seamless, seamless,
img2imgStrength: strength, img2imgStrength: strength,
shouldFitToWidthHeight: fit, shouldFitToWidthHeight: fit,
initialImage,
shouldRandomizeSeed, shouldRandomizeSeed,
} = generation; } = generation;
const initialImage = initialImageSelector(state);
if (!initialImage) {
// TODO: handle this
throw 'no initial image';
}
return { return {
type: 'img2img', type: 'img2img',
prompt, prompt,
@ -83,8 +90,8 @@ export function buildImg2ImgNode(
model, model,
progress_images: shouldDisplayInProgressType === 'full-res', progress_images: shouldDisplayInProgressType === 'full-res',
image: { image: {
image_name: (initialImage as _Image).name!, image_name: initialImage.name,
image_type: 'result', image_type: 'results',
}, },
strength, strength,
fit, fit,
@ -107,7 +114,7 @@ export function buildFacetoolNode(
image_name: image_name:
(typeof initialImage === 'string' ? initialImage : initialImage?.url) || (typeof initialImage === 'string' ? initialImage : initialImage?.url) ||
'', '',
image_type: 'result', image_type: 'results',
}, },
strength, strength,
}; };
@ -130,7 +137,7 @@ export function buildUpscaleNode(
image_name: image_name:
(typeof initialImage === 'string' ? initialImage : initialImage?.url) || (typeof initialImage === 'string' ? initialImage : initialImage?.url) ||
'', '',
image_type: 'result', image_type: 'results',
}, },
strength, strength,
level, level,

View File

@ -14,8 +14,9 @@ import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings'; import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings';
import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings'; import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings';
import { import {
initialImageSelected,
setAllParameters, setAllParameters,
setInitialImage, // setInitialImage,
setSeed, setSeed,
} from 'features/parameters/store/generationSlice'; } from 'features/parameters/store/generationSlice';
import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors'; import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors';
@ -129,8 +130,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const handleClickUseAsInitialImage = () => { const handleClickUseAsInitialImage = () => {
if (!currentImage) return; if (!currentImage) return;
if (isLightboxOpen) dispatch(setIsLightboxOpen(false)); if (isLightboxOpen) dispatch(setIsLightboxOpen(false));
dispatch(setInitialImage(currentImage)); dispatch(initialImageSelected(currentImage.uuid));
dispatch(setActiveTab('img2img')); // dispatch(setInitialImage(currentImage));
// dispatch(setActiveTab('img2img'));
}; };
const handleCopyImage = async () => { const handleCopyImage = async () => {

View File

@ -1,29 +1,44 @@
import { Box, Flex, Image } from '@chakra-ui/react'; import { Box, Flex, Image } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/storeHooks'; import { useAppSelector } from 'app/storeHooks';
import { GalleryState } from 'features/gallery/store/gallerySlice'; import { systemSelector } from 'features/system/store/systemSelectors';
import { uiSelector } from 'features/ui/store/uiSelectors'; import { uiSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { APP_METADATA_HEIGHT } from 'theme/util/constants'; import { APP_METADATA_HEIGHT } from 'theme/util/constants';
import { import { selectedImageSelector } from '../store/gallerySelectors';
gallerySelector,
selectedImageSelector,
} from '../store/gallerySelectors';
import CurrentImageFallback from './CurrentImageFallback'; import CurrentImageFallback from './CurrentImageFallback';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from './NextPrevImageButtons'; import NextPrevImageButtons from './NextPrevImageButtons';
export const imagesSelector = createSelector( export const imagesSelector = createSelector(
[gallerySelector, uiSelector, selectedImageSelector], [uiSelector, selectedImageSelector, systemSelector],
(gallery: GalleryState, ui, selectedImage) => { (ui, selectedImage, system) => {
const { currentImage, intermediateImage } = gallery;
const { shouldShowImageDetails } = ui; const { shouldShowImageDetails } = ui;
const { progressImage } = system;
// TODO: Clean this up, this is really gross
const imageToDisplay = progressImage
? {
url: progressImage.dataURL,
width: progressImage.width,
height: progressImage.height,
isProgressImage: true,
image: progressImage,
}
: selectedImage
? {
url: selectedImage.url,
width: selectedImage.metadata.width,
height: selectedImage.metadata.height,
isProgressImage: false,
image: selectedImage,
}
: null;
return { return {
imageToDisplay: intermediateImage ? intermediateImage : selectedImage,
isIntermediate: Boolean(intermediateImage),
shouldShowImageDetails, shouldShowImageDetails,
imageToDisplay,
}; };
}, },
{ {
@ -34,7 +49,7 @@ export const imagesSelector = createSelector(
); );
export default function CurrentImagePreview() { export default function CurrentImagePreview() {
const { shouldShowImageDetails, imageToDisplay, isIntermediate } = const { shouldShowImageDetails, imageToDisplay } =
useAppSelector(imagesSelector); useAppSelector(imagesSelector);
console.log(imageToDisplay); console.log(imageToDisplay);
return ( return (
@ -52,34 +67,42 @@ export default function CurrentImagePreview() {
src={imageToDisplay.url} src={imageToDisplay.url}
width={imageToDisplay.width} width={imageToDisplay.width}
height={imageToDisplay.height} height={imageToDisplay.height}
fallback={!isIntermediate ? <CurrentImageFallback /> : undefined} fallback={
!imageToDisplay.isProgressImage ? (
<CurrentImageFallback />
) : undefined
}
sx={{ sx={{
objectFit: 'contain', objectFit: 'contain',
maxWidth: '100%', maxWidth: '100%',
maxHeight: '100%', maxHeight: '100%',
height: 'auto', height: 'auto',
position: 'absolute', position: 'absolute',
imageRendering: isIntermediate ? 'pixelated' : 'initial', imageRendering: imageToDisplay.isProgressImage
? 'pixelated'
: 'initial',
borderRadius: 'base', borderRadius: 'base',
}} }}
/> />
)} )}
{!shouldShowImageDetails && <NextPrevImageButtons />} {!shouldShowImageDetails && <NextPrevImageButtons />}
{shouldShowImageDetails && imageToDisplay && ( {shouldShowImageDetails &&
<Box imageToDisplay &&
sx={{ 'metadata' in imageToDisplay.image && (
position: 'absolute', <Box
top: '0', sx={{
width: '100%', position: 'absolute',
height: '100%', top: '0',
borderRadius: 'base', width: '100%',
overflow: 'scroll', height: '100%',
maxHeight: APP_METADATA_HEIGHT, borderRadius: 'base',
}} overflow: 'scroll',
> maxHeight: APP_METADATA_HEIGHT,
{/* <ImageMetadataViewer image={imageToDisplay} /> */} }}
</Box> >
)} <ImageMetadataViewer image={imageToDisplay.image} />
</Box>
)}
</Flex> </Flex>
); );
} }

View File

@ -14,9 +14,9 @@ import {
setCurrentImage, setCurrentImage,
} from 'features/gallery/store/gallerySlice'; } from 'features/gallery/store/gallerySlice';
import { import {
initialImageSelected,
setAllImageToImageParameters, setAllImageToImageParameters,
setAllParameters, setAllParameters,
setInitialImage,
setSeed, setSeed,
} from 'features/parameters/store/generationSlice'; } from 'features/parameters/store/generationSlice';
import { DragEvent, memo, useState } from 'react'; import { DragEvent, memo, useState } from 'react';
@ -72,10 +72,9 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleMouseOut = () => setIsHovered(false); const handleMouseOut = () => setIsHovered(false);
const handleUsePrompt = () => { const handleUsePrompt = () => {
if (image.metadata?.image?.prompt) { if (image.metadata?.sd_metadata?.prompt) {
setBothPrompts(image.metadata?.image?.prompt); setBothPrompts(image.metadata?.sd_metadata?.prompt);
} }
toast({ toast({
title: t('toast.promptSet'), title: t('toast.promptSet'),
status: 'success', status: 'success',
@ -85,7 +84,8 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}; };
const handleUseSeed = () => { const handleUseSeed = () => {
image.metadata && dispatch(setSeed(image.metadata.image.seed)); image.metadata.sd_metadata &&
dispatch(setSeed(image.metadata.sd_metadata.image.seed));
toast({ toast({
title: t('toast.seedSet'), title: t('toast.seedSet'),
status: 'success', status: 'success',
@ -95,16 +95,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}; };
const handleSendToImageToImage = () => { const handleSendToImageToImage = () => {
// dispatch(setInitialImage(image)); dispatch(initialImageSelected(image.name));
if (activeTabName !== 'img2img') {
dispatch(setActiveTab('img2img'));
}
toast({
title: t('toast.sentToImageToImage'),
status: 'success',
duration: 2500,
isClosable: true,
});
}; };
const handleSendToCanvas = () => { const handleSendToCanvas = () => {
@ -125,7 +116,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}; };
const handleUseAllParameters = () => { const handleUseAllParameters = () => {
metadata && dispatch(setAllParameters(metadata)); metadata.sd_metadata && dispatch(setAllParameters(metadata.sd_metadata));
toast({ toast({
title: t('toast.parametersSet'), title: t('toast.parametersSet'),
status: 'success', status: 'success',
@ -135,11 +126,13 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}; };
const handleUseInitialImage = async () => { const handleUseInitialImage = async () => {
if (metadata?.image?.init_image_path) { if (metadata.sd_metadata?.image?.init_image_path) {
const response = await fetch(metadata.image.init_image_path); const response = await fetch(
metadata.sd_metadata?.image?.init_image_path
);
if (response.ok) { if (response.ok) {
dispatch(setActiveTab('img2img')); dispatch(setActiveTab('img2img'));
dispatch(setAllImageToImageParameters(metadata)); dispatch(setAllImageToImageParameters(metadata?.sd_metadata));
toast({ toast({
title: t('toast.initialImageSet'), title: t('toast.initialImageSet'),
status: 'success', status: 'success',
@ -160,7 +153,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleSelectImage = () => { const handleSelectImage = () => {
dispatch(imageSelected(image.name)); dispatch(imageSelected(image.name));
// dispatch(setCurrentImage(image));
}; };
const handleDragStart = (e: DragEvent<HTMLDivElement>) => { const handleDragStart = (e: DragEvent<HTMLDivElement>) => {
@ -183,28 +175,30 @@ const HoverableImage = memo((props: HoverableImageProps) => {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClickCapture={handleUsePrompt} onClickCapture={handleUsePrompt}
isDisabled={image?.metadata?.image?.prompt === undefined} isDisabled={image?.metadata?.sd_metadata?.prompt === undefined}
> >
{t('parameters.usePrompt')} {t('parameters.usePrompt')}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClickCapture={handleUseSeed} onClickCapture={handleUseSeed}
isDisabled={image?.metadata?.image?.seed === undefined} isDisabled={image?.metadata?.sd_metadata?.seed === undefined}
> >
{t('parameters.useSeed')} {t('parameters.useSeed')}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClickCapture={handleUseAllParameters} onClickCapture={handleUseAllParameters}
isDisabled={ isDisabled={
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type) !['txt2img', 'img2img'].includes(
image?.metadata?.sd_metadata?.type
)
} }
> >
{t('parameters.useAll')} {t('parameters.useAll')}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClickCapture={handleUseInitialImage} onClickCapture={handleUseInitialImage}
isDisabled={image?.metadata?.image?.type !== 'img2img'} isDisabled={image?.metadata?.sd_metadata?.type !== 'img2img'}
> >
{t('parameters.useInitImg')} {t('parameters.useInitImg')}
</MenuItem> </MenuItem>

View File

@ -18,7 +18,7 @@ import {
setCfgScale, setCfgScale,
setHeight, setHeight,
setImg2imgStrength, setImg2imgStrength,
setInitialImage, // setInitialImage,
setMaskPath, setMaskPath,
setPerlin, setPerlin,
setSampler, setSampler,
@ -113,14 +113,14 @@ const MetadataItem = ({
}; };
type ImageMetadataViewerProps = { type ImageMetadataViewerProps = {
image: InvokeAI._Image; image: InvokeAI.Image;
}; };
// TODO: I don't know if this is needed. // TODO: I don't know if this is needed.
const memoEqualityCheck = ( const memoEqualityCheck = (
prev: ImageMetadataViewerProps, prev: ImageMetadataViewerProps,
next: ImageMetadataViewerProps next: ImageMetadataViewerProps
) => prev.image.uuid === next.image.uuid; ) => prev.image.name === next.image.name;
// TODO: Show more interesting information in this component. // TODO: Show more interesting information in this component.
@ -137,8 +137,8 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
dispatch(setShouldShowImageDetails(false)); dispatch(setShouldShowImageDetails(false));
}); });
const metadata = image?.metadata?.image || {}; const metadata = image?.metadata.sd_metadata || {};
const dreamPrompt = image?.dreamPrompt; const dreamPrompt = image?.metadata.sd_metadata?.dreamPrompt;
const { const {
cfg_scale, cfg_scale,
@ -160,6 +160,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
type, type,
variations, variations,
width, width,
model_weights,
} = metadata; } = metadata;
const { t } = useTranslation(); const { t } = useTranslation();
@ -193,8 +194,8 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
{Object.keys(metadata).length > 0 ? ( {Object.keys(metadata).length > 0 ? (
<> <>
{type && <MetadataItem label="Generation type" value={type} />} {type && <MetadataItem label="Generation type" value={type} />}
{image.metadata?.model_weights && ( {model_weights && (
<MetadataItem label="Model" value={image.metadata.model_weights} /> <MetadataItem label="Model" value={model_weights} />
)} )}
{['esrgan', 'gfpgan'].includes(type) && ( {['esrgan', 'gfpgan'].includes(type) && (
<MetadataItem label="Original image" value={orig_path} /> <MetadataItem label="Original image" value={orig_path} />
@ -288,14 +289,14 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
onClick={() => dispatch(setHeight(height))} onClick={() => dispatch(setHeight(height))}
/> />
)} )}
{init_image_path && ( {/* {init_image_path && (
<MetadataItem <MetadataItem
label="Initial image" label="Initial image"
value={init_image_path} value={init_image_path}
isLink isLink
onClick={() => dispatch(setInitialImage(init_image_path))} onClick={() => dispatch(setInitialImage(init_image_path))}
/> />
)} )} */}
{mask_image_path && ( {mask_image_path && (
<MetadataItem <MetadataItem
label="Mask image" label="Mask image"

View File

@ -7,8 +7,16 @@ import {
uiSelector, uiSelector,
} from 'features/ui/store/uiSelectors'; } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { selectResultsAll, selectResultsEntities } from './resultsSlice'; import {
import { selectUploadsAll, selectUploadsEntities } from './uploadsSlice'; selectResultsAll,
selectResultsById,
selectResultsEntities,
} from './resultsSlice';
import {
selectUploadsAll,
selectUploadsById,
selectUploadsEntities,
} from './uploadsSlice';
export const gallerySelector = (state: RootState) => state.gallery; export const gallerySelector = (state: RootState) => state.gallery;

View File

@ -3,13 +3,14 @@ import { Image } from 'app/invokeai';
import { invocationComplete } from 'app/nodesSocketio/actions'; import { invocationComplete } from 'app/nodesSocketio/actions';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { socketioConnected } from 'features/system/store/systemSlice';
import { import {
receivedResultImagesPage, receivedResultImagesPage,
IMAGES_PER_PAGE, IMAGES_PER_PAGE,
} from 'services/thunks/gallery'; } from 'services/thunks/gallery';
import { isImageOutput } from 'services/types/guards'; import { isImageOutput } from 'services/types/guards';
import { deserializeImageField } from 'services/util/deserializeImageField'; import { deserializeImageField } from 'services/util/deserializeImageField';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
// import { deserializeImageField } from 'services/util/deserializeImageField';
import { setCurrentCategory } from './gallerySlice'; import { setCurrentCategory } from './gallerySlice';
// use `createEntityAdapter` to create a slice for results images // use `createEntityAdapter` to create a slice for results images
@ -21,7 +22,7 @@ export const resultsAdapter = createEntityAdapter<Image>({
// `(item) => item.id`, but for our result images, the `name` is the unique identifier. // `(item) => item.id`, but for our result images, the `name` is the unique identifier.
selectId: (image) => image.name, selectId: (image) => image.name,
// Order all images by their time (in descending order) // Order all images by their time (in descending order)
sortComparer: (a, b) => b.timestamp - a.timestamp, sortComparer: (a, b) => b.metadata.timestamp - a.metadata.timestamp,
}); });
// This type is intersected with the Entity type to create the shape of the state // This type is intersected with the Entity type to create the shape of the state
@ -61,7 +62,9 @@ const resultsSlice = createSlice({
builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => { builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => {
const { items, page, pages } = action.payload; const { items, page, pages } = action.payload;
const resultImages = items.map((image) => deserializeImageField(image)); const resultImages = items.map((image) =>
deserializeImageResponse(image)
);
// use the adapter reducer to append all the results to state // use the adapter reducer to append all the results to state
resultsAdapter.addMany(state, resultImages); resultsAdapter.addMany(state, resultImages);

View File

@ -7,10 +7,11 @@ import {
IMAGES_PER_PAGE, IMAGES_PER_PAGE,
} from 'services/thunks/gallery'; } from 'services/thunks/gallery';
import { deserializeImageField } from 'services/util/deserializeImageField'; import { deserializeImageField } from 'services/util/deserializeImageField';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
export const uploadsAdapter = createEntityAdapter<Image>({ export const uploadsAdapter = createEntityAdapter<Image>({
selectId: (image) => image.name, selectId: (image) => image.name,
sortComparer: (a, b) => b.timestamp - a.timestamp, sortComparer: (a, b) => b.metadata.timestamp - a.metadata.timestamp,
}); });
type AdditionalUploadsState = { type AdditionalUploadsState = {
@ -38,7 +39,7 @@ const uploadsSlice = createSlice({
builder.addCase(receivedUploadImagesPage.fulfilled, (state, action) => { builder.addCase(receivedUploadImagesPage.fulfilled, (state, action) => {
const { items, page, pages } = action.payload; const { items, page, pages } = action.payload;
const images = items.map((image) => deserializeImageField(image)); const images = items.map((image) => deserializeImageResponse(image));
uploadsAdapter.addMany(state, images); uploadsAdapter.addMany(state, images);

View File

@ -1,5 +1,11 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import {
selectResultsById,
selectResultsEntities,
} from 'features/gallery/store/resultsSlice';
import { selectUploadsById } from 'features/gallery/store/uploadsSlice';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
export const generationSelector = (state: RootState) => state.generation; export const generationSelector = (state: RootState) => state.generation;
@ -15,3 +21,15 @@ export const mayGenerateMultipleImagesSelector = createSelector(
}, },
} }
); );
export const initialImageSelector = createSelector(
[(state: RootState) => state, generationSelector],
(state, generation) => {
const { initialImage: initialImageName } = generation;
return (
selectResultsById(state, initialImageName as string) ??
selectUploadsById(state, initialImageName as string)
);
}
);

View File

@ -317,12 +317,12 @@ export const generationSlice = createSlice({
setShouldRandomizeSeed: (state, action: PayloadAction<boolean>) => { setShouldRandomizeSeed: (state, action: PayloadAction<boolean>) => {
state.shouldRandomizeSeed = action.payload; state.shouldRandomizeSeed = action.payload;
}, },
setInitialImage: ( // setInitialImage: (
state, // state,
action: PayloadAction<InvokeAI._Image | string> // action: PayloadAction<InvokeAI._Image | string>
) => { // ) => {
state.initialImage = action.payload; // state.initialImage = action.payload;
}, // },
clearInitialImage: (state) => { clearInitialImage: (state) => {
state.initialImage = undefined; state.initialImage = undefined;
}, },
@ -353,6 +353,9 @@ export const generationSlice = createSlice({
setVerticalSymmetrySteps: (state, action: PayloadAction<number>) => { setVerticalSymmetrySteps: (state, action: PayloadAction<number>) => {
state.verticalSymmetrySteps = action.payload; state.verticalSymmetrySteps = action.payload;
}, },
initialImageSelected: (state, action: PayloadAction<string>) => {
state.initialImage = action.payload;
},
}, },
}); });
@ -368,7 +371,7 @@ export const {
setHeight, setHeight,
setImg2imgStrength, setImg2imgStrength,
setInfillMethod, setInfillMethod,
setInitialImage, // setInitialImage,
setIterations, setIterations,
setMaskPath, setMaskPath,
setParameter, setParameter,
@ -394,6 +397,7 @@ export const {
setShouldUseSymmetry, setShouldUseSymmetry,
setHorizontalSymmetrySteps, setHorizontalSymmetrySteps,
setVerticalSymmetrySteps, setVerticalSymmetrySteps,
initialImageSelected,
} = generationSlice.actions; } = generationSlice.actions;
export default generationSlice.reducer; export default generationSlice.reducer;

View File

@ -1,13 +1,22 @@
import { ExpandedIndex, UseToastOptions } from '@chakra-ui/react'; import { ExpandedIndex, StatHelpText, UseToastOptions } from '@chakra-ui/react';
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/invokeai'; import * as InvokeAI from 'app/invokeai';
import { invocationComplete } from 'app/nodesSocketio/actions'; import {
generatorProgress,
invocationComplete,
invocationError,
invocationStarted,
socketioConnected,
socketioDisconnected,
} from 'app/nodesSocketio/actions';
import { resultAdded } from 'features/gallery/store/resultsSlice'; import { resultAdded } from 'features/gallery/store/resultsSlice';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import i18n from 'i18n'; import i18n from 'i18n';
import { isImageOutput } from 'services/types/guards'; import { isImageOutput } from 'services/types/guards';
import { ProgressImage } from 'services/events/types';
import { initialImageSelected } from 'features/parameters/store/generationSlice';
export type LogLevel = 'info' | 'warning' | 'error'; export type LogLevel = 'info' | 'warning' | 'error';
@ -61,6 +70,7 @@ export interface SystemState
cancelType: CancelType; cancelType: CancelType;
cancelAfter: number | null; cancelAfter: number | null;
}; };
progressImage: ProgressImage | null;
} }
const initialSystemState: SystemState = { const initialSystemState: SystemState = {
@ -103,6 +113,7 @@ const initialSystemState: SystemState = {
cancelType: 'immediate', cancelType: 'immediate',
cancelAfter: null, cancelAfter: null,
}, },
progressImage: null,
}; };
export const systemSlice = createSlice({ export const systemSlice = createSlice({
@ -276,21 +287,63 @@ export const systemSlice = createSlice({
setCancelAfter: (state, action: PayloadAction<number | null>) => { setCancelAfter: (state, action: PayloadAction<number | null>) => {
state.cancelOptions.cancelAfter = action.payload; state.cancelOptions.cancelAfter = action.payload;
}, },
socketioConnected: (state) => { // socketioConnected: (state) => {
state.isConnected = true; // state.isConnected = true;
state.currentStatus = i18n.t('common.statusConnected'); // state.currentStatus = i18n.t('common.statusConnected');
}, // },
socketioDisconnected: (state) => { // socketioDisconnected: (state) => {
state.isConnected = false; // state.isConnected = false;
state.currentStatus = i18n.t('common.statusDisconnected'); // state.currentStatus = i18n.t('common.statusDisconnected');
}, // },
}, },
extraReducers(builder) { extraReducers(builder) {
builder.addCase(socketioConnected, (state, action) => {
const { timestamp } = action.payload;
state.isConnected = true;
state.currentStatus = i18n.t('common.statusConnected');
state.log.push({
timestamp: dateFormat(timestamp, 'isoDateTime'),
message: `Connected to server`,
level: 'info',
});
});
builder.addCase(socketioDisconnected, (state, action) => {
const { timestamp } = action.payload;
state.isConnected = false;
state.currentStatus = i18n.t('common.statusDisconnected');
state.log.push({
timestamp: dateFormat(timestamp, 'isoDateTime'),
message: `Disconnected from server`,
level: 'warning',
});
});
builder.addCase(invocationStarted, (state, action) => {
state.isProcessing = true;
state.currentStatusHasSteps = false;
});
builder.addCase(generatorProgress, (state, action) => {
const { step, total_steps, progress_image } = action.payload.data;
state.currentStatusHasSteps = true;
state.currentStep = step + 1; // TODO: step starts at -1, think this is a bug
state.totalSteps = total_steps;
state.progressImage = progress_image ?? null;
});
builder.addCase(invocationComplete, (state, action) => { builder.addCase(invocationComplete, (state, action) => {
const { data, timestamp } = action.payload; const { data, timestamp } = action.payload;
state.isProcessing = false;
state.isCancelable = false;
state.isProcessing = false;
state.currentStep = 0;
state.totalSteps = 0;
state.progressImage = null;
// TODO: handle logging for other invocation types
if (isImageOutput(data.result)) { if (isImageOutput(data.result)) {
state.log.push({ state.log.push({
timestamp: dateFormat(timestamp, 'isoDateTime'), timestamp: dateFormat(timestamp, 'isoDateTime'),
@ -299,6 +352,29 @@ export const systemSlice = createSlice({
}); });
} }
}); });
builder.addCase(invocationError, (state, action) => {
const { data, timestamp } = action.payload;
state.log.push({
timestamp: dateFormat(timestamp, 'isoDateTime'),
message: `Server error: ${data.error}`,
level: 'error',
});
state.wasErrorSeen = true;
state.progressImage = null;
state.isProcessing = false;
});
builder.addCase(initialImageSelected, (state) => {
state.toastQueue.push({
title: i18n.t('toast.sentToImageToImage'),
status: 'success',
duration: 2500,
isClosable: true,
});
});
}, },
}); });
@ -334,8 +410,8 @@ export const {
setOpenModel, setOpenModel,
setCancelType, setCancelType,
setCancelAfter, setCancelAfter,
socketioConnected, // socketioConnected,
socketioDisconnected, // socketioDisconnected,
} = systemSlice.actions; } = systemSlice.actions;
export default systemSlice.reducer; export default systemSlice.reducer;

View File

@ -1,7 +1,7 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react'; import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { setInitialImage } from 'features/parameters/store/generationSlice'; import { initialImageSelected } from 'features/parameters/store/generationSlice';
import { import {
activeTabNameSelector, activeTabNameSelector,
uiSelector, uiSelector,
@ -47,7 +47,7 @@ const InvokeWorkarea = (props: InvokeWorkareaProps) => {
const image = getImageByUuid(uuid); const image = getImageByUuid(uuid);
if (!image) return; if (!image) return;
if (activeTabName === 'img2img') { if (activeTabName === 'img2img') {
dispatch(setInitialImage(image)); dispatch(initialImageSelected(image.uuid));
} else if (activeTabName === 'unifiedCanvas') { } else if (activeTabName === 'unifiedCanvas') {
dispatch(setInitialCanvasImage(image)); dispatch(setInitialCanvasImage(image));
} }

View File

@ -1,14 +1,12 @@
import { Flex, Image, Text, useToast } from '@chakra-ui/react'; import { Flex, Image, Text, useToast } from '@chakra-ui/react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import ImageUploaderIconButton from 'common/components/ImageUploaderIconButton'; import ImageUploaderIconButton from 'common/components/ImageUploaderIconButton';
import { initialImageSelector } from 'features/parameters/store/generationSelectors';
import { clearInitialImage } from 'features/parameters/store/generationSlice'; import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export default function InitImagePreview() { export default function InitImagePreview() {
const initialImage = useAppSelector( const initialImage = useAppSelector(initialImageSelector);
(state: RootState) => state.generation.initialImage
);
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -0,0 +1,13 @@
import { InvokeTabName, tabMap } from './tabMap';
import { UIState } from './uiTypes';
export const setActiveTabReducer = (
state: UIState,
newActiveTab: number | InvokeTabName
) => {
if (typeof newActiveTab === 'number') {
state.activeTab = newActiveTab;
} else {
state.activeTab = tabMap.indexOf(newActiveTab);
}
};

View File

@ -1,5 +1,7 @@
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import { initialImageSelected } from 'features/parameters/store/generationSlice';
import { setActiveTabReducer } from './extraReducers';
import { InvokeTabName, tabMap } from './tabMap'; import { InvokeTabName, tabMap } from './tabMap';
import { AddNewModelType, UIState } from './uiTypes'; import { AddNewModelType, UIState } from './uiTypes';
@ -25,11 +27,7 @@ export const uiSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
setActiveTab: (state, action: PayloadAction<number | InvokeTabName>) => { setActiveTab: (state, action: PayloadAction<number | InvokeTabName>) => {
if (typeof action.payload === 'number') { setActiveTabReducer(state, action.payload);
state.activeTab = action.payload;
} else {
state.activeTab = tabMap.indexOf(action.payload);
}
}, },
setCurrentTheme: (state, action: PayloadAction<string>) => { setCurrentTheme: (state, action: PayloadAction<string>) => {
state.currentTheme = action.payload; state.currentTheme = action.payload;
@ -93,6 +91,13 @@ export const uiSlice = createSlice({
} }
}, },
}, },
extraReducers(builder) {
builder.addCase(initialImageSelected, (state) => {
if (tabMap[state.activeTab] !== 'img2img') {
setActiveTabReducer(state, 'img2img');
}
});
},
}); });
export const { export const {

View File

@ -1,7 +1,7 @@
import { isFulfilled, Middleware, MiddlewareAPI } from '@reduxjs/toolkit'; import { isFulfilled, Middleware, MiddlewareAPI } from '@reduxjs/toolkit';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { emitSubscribe } from 'app/nodesSocketio/actions'; import { socketioSubscribed } from 'app/nodesSocketio/actions';
import { AppDispatch, RootState } from 'app/store'; import { AppDispatch, RootState } from 'app/store';
import { setSessionId } from './apiSlice'; import { setSessionId } from './apiSlice';
import { uploadImage } from './thunks/image'; import { uploadImage } from './thunks/image';
@ -10,7 +10,7 @@ import * as InvokeAI from 'app/invokeai';
import { addImage } from 'features/gallery/store/gallerySlice'; import { addImage } from 'features/gallery/store/gallerySlice';
import { tabMap } from 'features/ui/store/tabMap'; import { tabMap } from 'features/ui/store/tabMap';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { setInitialImage } from 'features/parameters/store/generationSlice'; import { initialImageSelected as initialImageSet } from 'features/parameters/store/generationSlice';
/** /**
* `redux-toolkit` provides nice matching utilities, which can be used as type guards * `redux-toolkit` provides nice matching utilities, which can be used as type guards
@ -24,12 +24,14 @@ export const invokeMiddleware: Middleware =
(store: MiddlewareAPI<AppDispatch, RootState>) => (next) => (action) => { (store: MiddlewareAPI<AppDispatch, RootState>) => (next) => (action) => {
const { dispatch, getState } = store; const { dispatch, getState } = store;
const timestamp = new Date();
if (isFulfilledCreateSession(action)) { if (isFulfilledCreateSession(action)) {
const sessionId = action.payload.id; const sessionId = action.payload.id;
console.log('createSession.fulfilled'); console.log('createSession.fulfilled');
dispatch(setSessionId(sessionId)); dispatch(setSessionId(sessionId));
dispatch(emitSubscribe(sessionId)); dispatch(socketioSubscribed({ sessionId, timestamp }));
dispatch(invokeSession({ sessionId })); dispatch(invokeSession({ sessionId }));
} else if (isFulfilledUploadImage(action)) { } else if (isFulfilledUploadImage(action)) {
const uploadLocation = action.payload; const uploadLocation = action.payload;
@ -54,7 +56,8 @@ export const invokeMiddleware: Middleware =
if (activeTabName === 'unifiedCanvas') { if (activeTabName === 'unifiedCanvas') {
dispatch(setInitialCanvasImage(newImage)); dispatch(setInitialCanvasImage(newImage));
} else if (activeTabName === 'img2img') { } else if (activeTabName === 'img2img') {
dispatch(setInitialImage(newImage)); // dispatch(setInitialImage(newImage));
dispatch(initialImageSet(newImage.uuid));
} }
} else { } else {
next(action); next(action);

View File

@ -27,6 +27,10 @@ export const extractTimestampFromImageName = (imageName: string) => {
return Number(timestamp); return Number(timestamp);
}; };
/**
* Process ImageField objects. These come from `invocation_complete` events and do not contain all the data we need.
* This is a WIP on the server side.
*/
export const deserializeImageField = (image: ImageField): Image => { export const deserializeImageField = (image: ImageField): Image => {
const name = image.image_name; const name = image.image_name;
const type = image.image_type; const type = image.image_type;
@ -37,10 +41,13 @@ export const deserializeImageField = (image: ImageField): Image => {
return { return {
name, name,
type,
url, url,
thumbnail, thumbnail,
timestamp, metadata: {
height: 512, timestamp,
width: 512, height: 512, // TODO: need the server to give this to us
width: 512,
},
}; };
}; };

View File

@ -0,0 +1,22 @@
import { Image } from 'app/invokeai';
import { ImageResponse } from 'services/api';
/**
* Process ImageReponse objects, which we get from the `list_images` endpoint.
*/
export const deserializeImageResponse = (
imageResponse: ImageResponse
): Image => {
const { image_name, image_type, image_url, metadata, thumbnail_url } =
imageResponse;
// TODO: parse metadata - just leaving it as-is for now
return {
name: image_name,
type: image_type,
url: image_url,
thumbnail: thumbnail_url,
metadata,
};
};

View File

@ -9,7 +9,9 @@ const { defineMultiStyleConfig, definePartsStyle } =
const invokeAIFilledTrack = defineStyle((_props) => ({ const invokeAIFilledTrack = defineStyle((_props) => ({
bg: 'accent.600', bg: 'accent.600',
transition: 'width 0.2s ease-in-out', // TODO: the animation is nice but looks weird bc it is substantially longer than each step
// so we get to 100% long before it finishes
// transition: 'width 0.2s ease-in-out',
_indeterminate: { _indeterminate: {
bgGradient: bgGradient:
'linear(to-r, transparent 0%, accent.600 50%, transparent 100%);', 'linear(to-r, transparent 0%, accent.600 50%, transparent 100%);',