Compare commits

..

12 Commits

Author SHA1 Message Date
Sergey Borisov
0ebe2c0ebc Add mask to l2l, MaskEdge and ColorCorrect nodes 2023-07-24 14:25:54 +03:00
blessedcoolant
02618a701d fix: Fix app crashing when you upload an incorrect JSON to node editor (#3911)
## What type of PR is this? (check all applicable)

- [x] Bug Fix


## Have you discussed this change with the InvokeAI team?
- [x] Yes, we feel very passionate about this.     

## Description

Uploading an incorrect JSON file to the Node Editor would crash the app.

While this is a much larger problem that we will tackle while refining
the Node Editor, this is a fix that should address 99% of the cases out
there.

When saving an InvokeAI node graph, there are three primary keys.

1. `nodes` - which has all the node related data.
2. `edges` - which has all the edges related data
3. `viewport` - which has all the viewport related data.

So when we load back the JSON, we now check if all three of these keys
exist in the retrieved JSON object. While the `viewport` itself is not a
mandatory key to repopulate the graph, checking for it will allow us to
treat it as an additional check to ensure that the graph was saved from
InvokeAI.

As a result ...

- If you upload an invalid JSON file, the app now warns you that the
JSON is invalid.
- If you upload a JSON of a graph editor that is not InvokeAI, it simply
warns you that you are uploading a non InvokeAI graph.

So effectively, you should not be able to load any graph that is not
generated by ReactFlow.

Here are the edge cases:

- What happens if a user maintains the above key structure but tampers
with the data inside them? Well tested it. Turns out because we validate
and build the graph based on the JSON data, if you tamper with any data
that is needed to rebuild that node, it simply will skip that and load
the rest of the graph with valid data.
- What happens if a user uploads a graph that was made by some other
random ReactFlow app? Well, same as above. Because we do not have to
parse that in our setup, it simply will skip it and only display what
are setup to do.

I think that just about covers 99% of the cases where this could go
wrong. If there's any other edges cases, can add checks if need be. But
can't think of any at the moment.

## Related Tickets & Documents

### Closes
- #3893 
- #3881

## [optional] Are there any post deployment tasks we need to perform?

Yes. Making @psychedelicious a little bit happier. :P
2023-07-24 02:15:46 +12:00
blessedcoolant
af4579b4d4 feat: Add more sanity checks for graph loading 2023-07-23 18:12:25 +12:00
blessedcoolant
35acb5de76 Merge branch 'main' into json-crash-fix 2023-07-23 16:50:36 +12:00
blessedcoolant
225f608556 fix: Add more sanity checks & rename buttons to Graphs 2023-07-23 16:49:52 +12:00
Alexandre Macabies
00d3cd4aed Fix 'Del' hotkey to delete current image. 2023-07-23 14:16:32 +10:00
blessedcoolant
fdc444ed61 fix: Fix app crashing when you upload an incorrect JSON to node editor 2023-07-23 15:24:04 +12:00
blessedcoolant
075f9b3a7a ui: pay back tech debt (#3896)
## What type of PR is this? (check all applicable)

- [x] Refactor
- [ ] Feature
- [ ] Bug Fix
- [ ] Optimization
- [ ] Documentation Update
- [ ] Community Node Submission


## Have you discussed this change with the InvokeAI team?
- [ ] Yes
- [x] No, because: n/a

      
## Have you updated all relevant documentation?
- [ ] Yes
- [x] No n/a


## Description

Big cleanup:
- improve & simplify the app logging
- resolve all TS issues
- resolve all circular dependencies
- fix all lint/format issues

## QA Instructions, Screenshots, Recordings

`yarn lint` passes:


![image](https://github.com/invoke-ai/InvokeAI/assets/4822129/7b763922-f00c-4b17-be23-2432da50f816)
<!-- 
Please provide steps on how to test changes, any hardware or 
software specifications as well as any other pertinent information. 
-->

## Added/updated tests?

- [ ] Yes
- [x] No : n/a

## [optional] Are there any post deployment tasks we need to perform?

bask in the glory of what *should* be a fully-passing frontend lint on
this PR
2023-07-23 13:57:43 +12:00
psychedelicious
c5147d0f57 fix(ui): fix all eslint & prettier issues 2023-07-22 23:45:24 +10:00
psychedelicious
6452d0fc28 fix(ui): fix all circular dependencies 2023-07-22 22:48:39 +10:00
psychedelicious
5468d9a9fc fix(ui): resolve all typescript issues 2023-07-22 21:38:50 +10:00
psychedelicious
75863e7181 feat(ui): logging cleanup
- simplify access to app logger
- spruce up and make consistent log format
- improve messaging
2023-07-22 21:12:51 +10:00
233 changed files with 1904 additions and 3024 deletions

View File

@@ -2,6 +2,7 @@
from typing import Literal, Optional
import cv2
import numpy
from PIL import Image, ImageFilter, ImageOps, ImageChops
from pydantic import BaseModel, Field
@@ -193,13 +194,10 @@ class ImagePasteInvocation(BaseInvocation, PILInvocationConfig):
def invoke(self, context: InvocationContext) -> ImageOutput:
base_image = context.services.images.get_pil_image(self.base_image.image_name)
image = context.services.images.get_pil_image(self.image.image_name)
mask = (
None
if self.mask is None
else ImageOps.invert(
context.services.images.get_pil_image(self.mask.image_name)
)
)
mask = None
if self.mask is not None:
mask = context.services.images.get_pil_image(self.mask.image_name)
mask = ImageOps.invert(mask.convert("L"))
# TODO: probably shouldn't invert mask here... should user be required to do it?
min_x = min(0, self.x)
@@ -650,3 +648,167 @@ class ImageInverseLerpInvocation(BaseInvocation, PILInvocationConfig):
width=image_dto.width,
height=image_dto.height,
)
class MaskEdgeInvocation(BaseInvocation, PILInvocationConfig):
"""Applies an edge mask to an image"""
# fmt: off
type: Literal["mask_edge"] = "mask_edge"
# Inputs
image: Optional[ImageField] = Field(default=None, description="The image to apply the mask to")
edge_size: int = Field(description="The size of the edge")
edge_blur: int = Field(description="The amount of blur on the edge")
low_threshold: int = Field(description="First threshold for the hysteresis procedure in Canny edge detection")
high_threshold: int = Field(description="Second threshold for the hysteresis procedure in Canny edge detection")
# fmt: on
def invoke(self, context: InvocationContext) -> MaskOutput:
mask = context.services.images.get_pil_image(self.image.image_name)
npimg = numpy.asarray(mask, dtype=numpy.uint8)
npgradient = numpy.uint8(
255 * (1.0 - numpy.floor(numpy.abs(0.5 - numpy.float32(npimg) / 255.0) * 2.0))
)
npedge = cv2.Canny(npimg, threshold1=self.low_threshold, threshold2=self.high_threshold)
npmask = npgradient + npedge
npmask = cv2.dilate(
npmask, numpy.ones((3, 3), numpy.uint8), iterations=int(self.edge_size / 2)
)
new_mask = Image.fromarray(npmask)
if self.edge_blur > 0:
new_mask = new_mask.filter(ImageFilter.BoxBlur(self.edge_blur))
new_mask = ImageOps.invert(new_mask)
image_dto = context.services.images.create(
image=new_mask,
image_origin=ResourceOrigin.INTERNAL,
image_category=ImageCategory.MASK,
node_id=self.id,
session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate,
)
return MaskOutput(
mask=ImageField(image_name=image_dto.image_name),
width=image_dto.width,
height=image_dto.height,
)
class ColorCorrectInvocation(BaseInvocation, PILInvocationConfig):
type: Literal["color_correct"] = "color_correct"
init: Optional[ImageField] = Field(default=None, description="Initial image")
result: Optional[ImageField] = Field(default=None, description="Resulted image")
mask: Optional[ImageField] = Field(default=None, description="Mask image")
mask_blur_radius: float = Field(default=8, description="Mask blur radius")
def invoke(self, context: InvocationContext) -> ImageOutput:
pil_init_mask = None
if self.mask is not None:
pil_init_mask = context.services.images.get_pil_image(
self.mask.image_name
).convert("L")
init_image = context.services.images.get_pil_image(
self.init.image_name
)
result = context.services.images.get_pil_image(
self.result.image_name
).convert("RGBA")
#if init_image is None or init_mask is None:
# return result
# Get the original alpha channel of the mask if there is one.
# Otherwise it is some other black/white image format ('1', 'L' or 'RGB')
#pil_init_mask = (
# init_mask.getchannel("A")
# if init_mask.mode == "RGBA"
# else init_mask.convert("L")
#)
pil_init_image = init_image.convert(
"RGBA"
) # Add an alpha channel if one doesn't exist
# Build an image with only visible pixels from source to use as reference for color-matching.
init_rgb_pixels = numpy.asarray(init_image.convert("RGB"), dtype=numpy.uint8)
init_a_pixels = numpy.asarray(pil_init_image.getchannel("A"), dtype=numpy.uint8)
init_mask_pixels = numpy.asarray(pil_init_mask, dtype=numpy.uint8)
# Get numpy version of result
np_image = numpy.asarray(result.convert("RGB"), dtype=numpy.uint8)
# Mask and calculate mean and standard deviation
mask_pixels = init_a_pixels * init_mask_pixels > 0
np_init_rgb_pixels_masked = init_rgb_pixels[mask_pixels, :]
np_image_masked = np_image[mask_pixels, :]
if np_init_rgb_pixels_masked.size > 0:
init_means = np_init_rgb_pixels_masked.mean(axis=0)
init_std = np_init_rgb_pixels_masked.std(axis=0)
gen_means = np_image_masked.mean(axis=0)
gen_std = np_image_masked.std(axis=0)
# Color correct
np_matched_result = np_image.copy()
np_matched_result[:, :, :] = (
(
(
(
np_matched_result[:, :, :].astype(numpy.float32)
- gen_means[None, None, :]
)
/ gen_std[None, None, :]
)
* init_std[None, None, :]
+ init_means[None, None, :]
)
.clip(0, 255)
.astype(numpy.uint8)
)
matched_result = Image.fromarray(np_matched_result, mode="RGB")
else:
matched_result = Image.fromarray(np_image, mode="RGB")
# Blur the mask out (into init image) by specified amount
if self.mask_blur_radius > 0:
nm = numpy.asarray(pil_init_mask, dtype=numpy.uint8)
nmd = cv2.erode(
nm,
kernel=numpy.ones((3, 3), dtype=numpy.uint8),
iterations=int(self.mask_blur_radius / 2),
)
pmd = Image.fromarray(nmd, mode="L")
blurred_init_mask = pmd.filter(ImageFilter.BoxBlur(self.mask_blur_radius))
else:
blurred_init_mask = pil_init_mask
multiplied_blurred_init_mask = ImageChops.multiply(
blurred_init_mask, result.split()[-1]
)
# Paste original on color-corrected generation (using blurred mask)
matched_result.paste(init_image, (0, 0), mask=multiplied_blurred_init_mask)
image_dto = context.services.images.create(
image=matched_result,
image_origin=ResourceOrigin.INTERNAL,
image_category=ImageCategory.GENERAL,
node_id=self.id,
session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate,
)
return ImageOutput(
image=ImageField(image_name=image_dto.image_name),
width=image_dto.width,
height=image_dto.height,
)

View File

@@ -3,6 +3,8 @@
from contextlib import ExitStack
from typing import List, Literal, Optional, Union
import torchvision.transforms as T
from torchvision.transforms.functional import resize as tv_resize
import einops
import torch
from diffusers import ControlNetModel
@@ -394,6 +396,9 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation):
strength: float = Field(
default=0.7, ge=0, le=1,
description="The strength of the latents to use")
mask: Optional[ImageField] = Field(
None, description="Mask",
)
# Schema customisation
class Config(InvocationConfig):
@@ -409,10 +414,25 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation):
},
}
def prep_mask_tensor(self, context, lantents):
if self.mask is None:
return None
mask_image = context.services.images.get_pil_image(self.mask.image_name)
if mask_image.mode != "L":
# FIXME: why do we get passed an RGB image here? We can only use single-channel.
mask_image = mask_image.convert("L")
mask_tensor = image_resized_to_grid_as_tensor(mask_image, normalize=False)
mask_tensor = tv_resize(
mask_tensor, lantents.shape[-2:], T.InterpolationMode.BILINEAR
)
return mask_tensor
@torch.no_grad()
def invoke(self, context: InvocationContext) -> LatentsOutput:
noise = context.services.latents.get(self.noise.latents_name)
latent = context.services.latents.get(self.latents.latents_name)
mask = self.prep_mask_tensor(context, latent)
# Get the source node id (we are invoking the prepared node)
graph_execution_state = context.services.graph_execution_manager.get(
@@ -441,6 +461,7 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation):
noise = noise.to(device=unet.device, dtype=unet.dtype)
latent = latent.to(device=unet.device, dtype=unet.dtype)
mask = mask.to(device=unet.device, dtype=unet.dtype)
scheduler = get_scheduler(
context=context,
@@ -470,6 +491,15 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation):
device=unet.device,
)
def _apply_mask_on_step(step_output, timestep, conditioning_data):
noised_init = scheduler.add_noise(initial_latents, noise, timestep.unsqueeze(0))
step_output.prev_sample = step_output.prev_sample * (1 - mask) + noised_init * mask
return step_output
additional_guidance = []
if mask is not None:
additional_guidance.append(_apply_mask_on_step)
result_latents, result_attention_map_saver = pipeline.latents_from_embeddings(
latents=initial_latents,
timesteps=timesteps,
@@ -477,7 +507,8 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation):
num_inference_steps=self.steps,
conditioning_data=conditioning_data,
control_data=control_data, # list[ControlNetData]
callback=step_callback
callback=step_callback,
additional_guidance=additional_guidance,
)
# https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699

View File

@@ -5,6 +5,10 @@ patches/
stats.html
index.html
.yarn/
.yalc/
*.scss
src/services/api/
src/services/fixtures/*
docs/
static/
src/theme/css/overlayscrollbars.css

View File

@@ -69,6 +69,7 @@
"@mantine/core": "^6.0.14",
"@mantine/form": "^6.0.15",
"@mantine/hooks": "^6.0.14",
"@nanostores/react": "^0.7.1",
"@reduxjs/toolkit": "^1.9.5",
"@roarr/browser-log-writer": "^1.1.5",
"chakra-ui-contextmenu": "^1.0.5",

View File

@@ -102,8 +102,7 @@
"openInNewTab": "Open in New Tab",
"dontAskMeAgain": "Don't ask me again",
"areYouSure": "Are you sure?",
"imagePrompt": "Image Prompt",
"clearNodes": "Are you sure you want to clear all nodes?"
"imagePrompt": "Image Prompt"
},
"gallery": {
"generations": "Generations",
@@ -615,6 +614,11 @@
"initialImageNotSetDesc": "Could not load initial image",
"nodesSaved": "Nodes Saved",
"nodesLoaded": "Nodes Loaded",
"nodesNotValidGraph": "Not a valid InvokeAI Node Graph",
"nodesNotValidJSON": "Not a valid JSON",
"nodesCorruptedGraph": "Cannot load. Graph seems to be corrupted.",
"nodesUnrecognizedTypes": "Cannot load. Graph has unrecognized types",
"nodesBrokenConnections": "Cannot load. Some connections are broken.",
"nodesLoadedFailed": "Failed To Load Nodes",
"nodesCleared": "Nodes Cleared"
},
@@ -700,9 +704,10 @@
},
"nodes": {
"reloadSchema": "Reload Schema",
"saveNodes": "Save Nodes",
"loadNodes": "Load Nodes",
"clearNodes": "Clear Nodes",
"saveGraph": "Save Graph",
"loadGraph": "Load Graph (saved from Node Editor) (Do not copy-paste metadata)",
"clearGraph": "Clear Graph",
"clearGraphDesc": "Are you sure you want to clear all nodes?",
"zoomInNodes": "Zoom In",
"zoomOutNodes": "Zoom Out",
"fitViewportNodes": "Fit View",

View File

@@ -10,7 +10,7 @@ async function main() {
);
const types = await openapiTS(OPENAPI_URL, {
exportType: true,
transform: (schemaObject, metadata) => {
transform: (schemaObject) => {
if ('format' in schemaObject && schemaObject.format === 'binary') {
return schemaObject.nullable ? 'Blob | null' : 'Blob';
}

View File

@@ -14,6 +14,7 @@ import FloatingParametersPanelButtons from 'features/ui/components/FloatingParam
import InvokeTabs from 'features/ui/components/InvokeTabs';
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
import i18n from 'i18n';
import { size } from 'lodash-es';
import { ReactNode, memo, useEffect } from 'react';
import UpdateImageBoardModal from '../../features/gallery/components/Boards/UpdateImageBoardModal';
import GlobalHotkeys from './GlobalHotkeys';
@@ -29,8 +30,7 @@ interface Props {
const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
const language = useAppSelector(languageSelector);
const log = useLogger();
const logger = useLogger();
const dispatch = useAppDispatch();
useEffect(() => {
@@ -38,9 +38,11 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
}, [language]);
useEffect(() => {
log.info({ namespace: 'App', data: config }, 'Received config');
dispatch(configChanged(config));
}, [dispatch, config, log]);
if (size(config)) {
logger.info({ namespace: 'App', config }, 'Received config');
dispatch(configChanged(config));
}
}, [dispatch, config, logger]);
useEffect(() => {
dispatch(appStarted());

View File

@@ -27,7 +27,7 @@ const STYLES: ChakraProps['sx'] = {
const DragPreview = (props: OverlayDragImageProps) => {
if (!props.dragData) {
return;
return null;
}
if (props.dragData.payloadType === 'IMAGE_DTO') {

View File

@@ -39,7 +39,6 @@ const ImageDndContext = (props: ImageDndContextProps) => {
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
console.log('dragEnd', event.active.data.current);
const activeData = event.active.data.current;
const overData = event.over?.data.current;
if (!activeDragData || !overData) {
return;

View File

@@ -11,7 +11,7 @@ import {
useDraggable as useOriginalDraggable,
useDroppable as useOriginalDroppable,
} from '@dnd-kit/core';
import { BoardId } from 'features/gallery/store/gallerySlice';
import { BoardId } from 'features/gallery/store/types';
import { ImageDTO } from 'services/api/types';
type BaseDropData = {

View File

@@ -1,28 +1,10 @@
import { useToast, UseToastOptions } from '@chakra-ui/react';
import { useToast } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { toastQueueSelector } from 'features/system/store/systemSelectors';
import { addToast, clearToastQueue } from 'features/system/store/systemSlice';
import { MakeToastArg, makeToast } from 'features/system/util/makeToast';
import { useCallback, useEffect } from 'react';
export type MakeToastArg = string | UseToastOptions;
/**
* Makes a toast from a string or a UseToastOptions object.
* If a string is passed, the toast will have the status 'info' and will be closable with a duration of 2500ms.
*/
export const makeToast = (arg: MakeToastArg): UseToastOptions => {
if (typeof arg === 'string') {
return {
title: arg,
status: 'info',
isClosable: true,
duration: 2500,
};
}
return { status: 'info', isClosable: true, duration: 2500, ...arg };
};
/**
* Logical component. Watches the toast queue and makes toasts when the queue is not empty.
* @returns null

View File

@@ -1,66 +1,2 @@
// zod needs the array to be `as const` to infer the type correctly
import { SchedulerParam } from 'features/parameters/types/parameterSchemas';
// this is the source of the `SchedulerParam` type, which is generated by zod
export const SCHEDULER_NAMES_AS_CONST = [
'euler',
'deis',
'ddim',
'ddpm',
'dpmpp_2s',
'dpmpp_2m',
'dpmpp_2m_sde',
'dpmpp_sde',
'heun',
'kdpm_2',
'lms',
'pndm',
'unipc',
'euler_k',
'dpmpp_2s_k',
'dpmpp_2m_k',
'dpmpp_2m_sde_k',
'dpmpp_sde_k',
'heun_k',
'lms_k',
'euler_a',
'kdpm_2_a',
] as const;
export const DEFAULT_SCHEDULER_NAME = 'euler';
export const SCHEDULER_NAMES: SchedulerParam[] = [...SCHEDULER_NAMES_AS_CONST];
export const SCHEDULER_LABEL_MAP: Record<SchedulerParam, string> = {
euler: 'Euler',
deis: 'DEIS',
ddim: 'DDIM',
ddpm: 'DDPM',
dpmpp_sde: 'DPM++ SDE',
dpmpp_2s: 'DPM++ 2S',
dpmpp_2m: 'DPM++ 2M',
dpmpp_2m_sde: 'DPM++ 2M SDE',
heun: 'Heun',
kdpm_2: 'KDPM 2',
lms: 'LMS',
pndm: 'PNDM',
unipc: 'UniPC',
euler_k: 'Euler Karras',
dpmpp_sde_k: 'DPM++ SDE Karras',
dpmpp_2s_k: 'DPM++ 2S Karras',
dpmpp_2m_k: 'DPM++ 2M Karras',
dpmpp_2m_sde_k: 'DPM++ 2M SDE Karras',
heun_k: 'Heun Karras',
lms_k: 'LMS Karras',
euler_a: 'Euler Ancestral',
kdpm_2_a: 'KDPM 2 Ancestral',
};
export type Scheduler = (typeof SCHEDULER_NAMES)[number];
export const NUMPY_RAND_MIN = 0;
export const NUMPY_RAND_MAX = 2147483647;
export const NODE_MIN_WIDTH = 250;

View File

@@ -0,0 +1,46 @@
import { createLogWriter } from '@roarr/browser-log-writer';
import { atom } from 'nanostores';
import { Logger, ROARR, Roarr } from 'roarr';
ROARR.write = createLogWriter();
export const BASE_CONTEXT = {};
export const log = Roarr.child(BASE_CONTEXT);
export const $logger = atom<Logger>(Roarr.child(BASE_CONTEXT));
type LoggerNamespace =
| 'images'
| 'models'
| 'config'
| 'canvas'
| 'txt2img'
| 'img2img'
| 'nodes'
| 'system'
| 'socketio'
| 'session';
export const logger = (namespace: LoggerNamespace) =>
$logger.get().child({ namespace });
export const VALID_LOG_LEVELS = [
'trace',
'debug',
'info',
'warn',
'error',
'fatal',
] as const;
export type InvokeLogLevel = (typeof VALID_LOG_LEVELS)[number];
// Translate human-readable log levels to numbers, used for log filtering
export const LOG_LEVEL_MAP: Record<InvokeLogLevel, number> = {
trace: 10,
debug: 20,
info: 30,
warn: 40,
error: 50,
fatal: 60,
};

View File

@@ -1,48 +1,19 @@
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { createLogWriter } from '@roarr/browser-log-writer';
import { useAppSelector } from 'app/store/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors';
import { isEqual } from 'lodash-es';
import { useEffect } from 'react';
import { LogLevelName, ROARR, Roarr } from 'roarr';
import { createLogWriter } from '@roarr/browser-log-writer';
// Base logging context includes only the package name
const baseContext = { package: '@invoke-ai/invoke-ai-ui' };
// Create browser log writer
ROARR.write = createLogWriter();
// Module-scoped logger - can be imported and used anywhere
export let log = Roarr.child(baseContext);
// Translate human-readable log levels to numbers, used for log filtering
export const LOG_LEVEL_MAP: Record<LogLevelName, number> = {
trace: 10,
debug: 20,
info: 30,
warn: 40,
error: 50,
fatal: 60,
};
export const VALID_LOG_LEVELS = [
'trace',
'debug',
'info',
'warn',
'error',
'fatal',
] as const;
export type InvokeLogLevel = (typeof VALID_LOG_LEVELS)[number];
import { ROARR, Roarr } from 'roarr';
import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP } from './logger';
const selector = createSelector(
systemSelector,
(system) => {
const { app_version, consoleLogLevel, shouldLogToConsole } = system;
const { consoleLogLevel, shouldLogToConsole } = system;
return {
version: app_version,
consoleLogLevel,
shouldLogToConsole,
};
@@ -55,8 +26,7 @@ const selector = createSelector(
);
export const useLogger = () => {
const { version, consoleLogLevel, shouldLogToConsole } =
useAppSelector(selector);
const { consoleLogLevel, shouldLogToConsole } = useAppSelector(selector);
// The provided Roarr browser log writer uses localStorage to config logging to console
useEffect(() => {
@@ -78,17 +48,16 @@ export const useLogger = () => {
// Update the module-scoped logger context as needed
useEffect(() => {
// TODO: type this properly
//eslint-disable-next-line @typescript-eslint/no-explicit-any
const newContext: Record<string, any> = {
...baseContext,
...BASE_CONTEXT,
};
if (version) {
newContext.version = version;
}
$logger.set(Roarr.child(newContext));
}, []);
log = Roarr.child(newContext);
}, [version]);
const logger = useStore($logger);
// Use the logger within components - no different than just importing it directly
return log;
return logger;
};

View File

@@ -12,7 +12,7 @@ import { defaultsDeep } from 'lodash-es';
import { UnserializeFunction } from 'redux-remember';
const initialStates: {
[key: string]: any;
[key: string]: object; // TODO: type this properly
} = {
canvas: initialCanvasState,
gallery: initialGalleryState,

View File

@@ -8,10 +8,11 @@ import {
import type { AppDispatch, RootState } from '../../store';
import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener';
import { addFirstListImagesListener } from './listeners/addFirstListImagesListener.ts';
import { addAppConfigReceivedListener } from './listeners/appConfigReceived';
import { addAppStartedListener } from './listeners/appStarted';
import { addBoardIdSelectedListener } from './listeners/boardIdSelected';
import { addDeleteBoardAndImagesFulfilledListener } from './listeners/boardAndImagesDeleted';
import { addBoardIdSelectedListener } from './listeners/boardIdSelected';
import { addCanvasCopiedToClipboardListener } from './listeners/canvasCopiedToClipboard';
import { addCanvasDownloadedAsImageListener } from './listeners/canvasDownloadedAsImage';
import { addCanvasMergedListener } from './listeners/canvasMerged';
@@ -34,10 +35,6 @@ import {
addImageRemovedFromBoardRejectedListener,
} from './listeners/imageRemovedFromBoard';
import { addImageToDeleteSelectedListener } from './listeners/imageToDeleteSelected';
import {
addImageUpdatedFulfilledListener,
addImageUpdatedRejectedListener,
} from './listeners/imageUpdated';
import {
addImageUploadedFulfilledListener,
addImageUploadedRejectedListener,
@@ -69,17 +66,15 @@ import { addGraphExecutionStateCompleteEventListener as addGraphExecutionStateCo
import { addInvocationCompleteEventListener as addInvocationCompleteListener } from './listeners/socketio/socketInvocationComplete';
import { addInvocationErrorEventListener as addInvocationErrorListener } from './listeners/socketio/socketInvocationError';
import { addInvocationStartedEventListener as addInvocationStartedListener } from './listeners/socketio/socketInvocationStarted';
import { addModelLoadEventListener } from './listeners/socketio/socketModelLoad';
import { addSocketSubscribedEventListener as addSocketSubscribedListener } from './listeners/socketio/socketSubscribed';
import { addSocketUnsubscribedEventListener as addSocketUnsubscribedListener } from './listeners/socketio/socketUnsubscribed';
import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSaved';
import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas';
import { addUserInvokedImageToImageListener } from './listeners/userInvokedImageToImage';
import { addUserInvokedNodesListener } from './listeners/userInvokedNodes';
import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage';
import { addModelLoadStartedEventListener } from './listeners/socketio/socketModelLoadStarted';
import { addModelLoadCompletedEventListener } from './listeners/socketio/socketModelLoadCompleted';
import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
import { addFirstListImagesListener } from './listeners/addFirstListImagesListener.ts';
export const listenerMiddleware = createListenerMiddleware();
@@ -109,10 +104,6 @@ export type AppListenerEffect = ListenerEffect<
addImageUploadedFulfilledListener();
addImageUploadedRejectedListener();
// Image updated
addImageUpdatedFulfilledListener();
addImageUpdatedRejectedListener();
// Image selected
addInitialImageSelectedListener();
@@ -161,8 +152,7 @@ addSocketConnectedListener();
addSocketDisconnectedListener();
addSocketSubscribedListener();
addSocketUnsubscribedListener();
addModelLoadStartedEventListener();
addModelLoadCompletedEventListener();
addModelLoadEventListener();
// Session Created
addSessionCreatedPendingListener();

View File

@@ -1,14 +1,13 @@
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { commitStagingAreaImage } from 'features/canvas/store/canvasSlice';
import { sessionCanceled } from 'services/api/thunks/session';
const moduleLog = log.child({ namespace: 'canvas' });
import { startAppListening } from '..';
export const addCommitStagingAreaImageListener = () => {
startAppListening({
actionCreator: commitStagingAreaImage,
effect: async (action, { dispatch, getState }) => {
const log = logger('canvas');
const state = getState();
const { sessionId: session_id, isProcessing } = state.system;
const canvasSessionId = action.payload;
@@ -19,17 +18,15 @@ export const addCommitStagingAreaImageListener = () => {
}
if (!canvasSessionId) {
moduleLog.debug('No canvas session, skipping cancel');
log.debug('No canvas session, skipping cancel');
return;
}
if (canvasSessionId !== session_id) {
moduleLog.debug(
log.debug(
{
data: {
canvasSessionId,
session_id,
},
canvasSessionId,
session_id,
},
'Canvas session does not match global session, skipping cancel'
);

View File

@@ -1,8 +1,6 @@
import { createAction } from '@reduxjs/toolkit';
import {
IMAGE_CATEGORIES,
imageSelected,
} from 'features/gallery/store/gallerySlice';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
import {
ImageCache,
getListImagesUrl,
@@ -17,7 +15,7 @@ export const addFirstListImagesListener = () => {
matcher: imagesApi.endpoints.listImages.matchFulfilled,
effect: async (
action,
{ getState, dispatch, unsubscribe, cancelActiveListeners }
{ dispatch, unsubscribe, cancelActiveListeners }
) => {
// Only run this listener on the first listImages request for no-board images
if (

View File

@@ -6,10 +6,7 @@ export const appStarted = createAction('app/appStarted');
export const addAppStartedListener = () => {
startAppListening({
actionCreator: appStarted,
effect: async (
action,
{ getState, dispatch, unsubscribe, cancelActiveListeners }
) => {
effect: async (action, { unsubscribe, cancelActiveListeners }) => {
// this should only run once
cancelActiveListeners();
unsubscribe();

View File

@@ -1,6 +1,6 @@
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
import { getImageUsage } from 'features/imageDeletion/store/imageDeletionSlice';
import { getImageUsage } from 'features/imageDeletion/store/imageDeletionSelectors';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { startAppListening } from '..';
@@ -9,8 +9,8 @@ import { boardsApi } from '../../../../../services/api/endpoints/boards';
export const addDeleteBoardAndImagesFulfilledListener = () => {
startAppListening({
matcher: boardsApi.endpoints.deleteBoardAndImages.matchFulfilled,
effect: async (action, { dispatch, getState, condition }) => {
const { board_id, deleted_board_images, deleted_images } = action.payload;
effect: async (action, { dispatch, getState }) => {
const { deleted_images } = action.payload;
// Remove all deleted images from the UI

View File

@@ -1,16 +1,15 @@
import { log } from 'app/logging/useLogger';
import { isAnyOf } from '@reduxjs/toolkit';
import {
ASSETS_CATEGORIES,
IMAGE_CATEGORIES,
boardIdSelected,
galleryViewChanged,
imageSelected,
} from 'features/gallery/store/gallerySlice';
import {
ASSETS_CATEGORIES,
IMAGE_CATEGORIES,
} from 'features/gallery/store/types';
import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..';
import { isAnyOf } from '@reduxjs/toolkit';
const moduleLog = log.child({ namespace: 'boards' });
export const addBoardIdSelectedListener = () => {
startAppListening({

View File

@@ -1,16 +1,17 @@
import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
import { $logger } from 'app/logging/logger';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice';
import { copyBlobToClipboard } from 'features/canvas/util/copyBlobToClipboard';
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
export const addCanvasCopiedToClipboardListener = () => {
startAppListening({
actionCreator: canvasCopiedToClipboard,
effect: async (action, { dispatch, getState }) => {
const moduleLog = $logger
.get()
.child({ namespace: 'canvasCopiedToClipboardListener' });
const state = getState();
const blob = await getBaseLayerBlob(state);

View File

@@ -1,16 +1,17 @@
import { canvasDownloadedAsImage } from 'features/canvas/store/actions';
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
import { $logger } from 'app/logging/logger';
import { downloadBlob } from 'features/canvas/util/downloadBlob';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice';
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
export const addCanvasDownloadedAsImageListener = () => {
startAppListening({
actionCreator: canvasDownloadedAsImage,
effect: async (action, { dispatch, getState }) => {
const moduleLog = $logger
.get()
.child({ namespace: 'canvasSavedToGalleryListener' });
const state = getState();
const blob = await getBaseLayerBlob(state);

View File

@@ -1,4 +1,4 @@
import { log } from 'app/logging/useLogger';
import { $logger } from 'app/logging/logger';
import { canvasMerged } from 'features/canvas/store/actions';
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
@@ -7,12 +7,13 @@ import { addToast } from 'features/system/store/systemSlice';
import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
export const addCanvasMergedListener = () => {
startAppListening({
actionCreator: canvasMerged,
effect: async (action, { dispatch, getState, take }) => {
effect: async (action, { dispatch }) => {
const moduleLog = $logger
.get()
.child({ namespace: 'canvasCopiedToClipboardListener' });
const blob = await getFullBaseLayerBlob();
if (!blob) {

View File

@@ -1,22 +1,21 @@
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { canvasSavedToGallery } from 'features/canvas/store/actions';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice';
import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
export const addCanvasSavedToGalleryListener = () => {
startAppListening({
actionCreator: canvasSavedToGallery,
effect: async (action, { dispatch, getState, take }) => {
effect: async (action, { dispatch, getState }) => {
const log = logger('canvas');
const state = getState();
const blob = await getBaseLayerBlob(state);
if (!blob) {
moduleLog.error('Problem getting base layer blob');
log.error('Problem getting base layer blob');
dispatch(
addToast({
title: 'Problem Saving Canvas',

View File

@@ -1,6 +1,6 @@
import { AnyListenerPredicate } from '@reduxjs/toolkit';
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { RootState } from 'app/store/store';
import { controlNetImageProcessed } from 'features/controlNet/store/actions';
import {
controlNetAutoConfigToggled,
@@ -9,9 +9,7 @@ import {
controlNetProcessorParamsChanged,
controlNetProcessorTypeChanged,
} from 'features/controlNet/store/controlNetSlice';
import { RootState } from 'app/store/store';
const moduleLog = log.child({ namespace: 'controlNet' });
import { startAppListening } from '..';
const predicate: AnyListenerPredicate<RootState> = (
action,
@@ -64,18 +62,13 @@ const predicate: AnyListenerPredicate<RootState> = (
export const addControlNetAutoProcessListener = () => {
startAppListening({
predicate,
effect: async (
action,
{ dispatch, getState, cancelActiveListeners, delay }
) => {
effect: async (action, { dispatch, cancelActiveListeners, delay }) => {
const log = logger('session');
const { controlNetId } = action.payload;
// Cancel any in-progress instances of this listener
cancelActiveListeners();
moduleLog.trace(
{ data: action.payload },
'ControlNet auto-process triggered'
);
log.trace('ControlNet auto-process triggered');
// Delay before starting actual work
await delay(300);

View File

@@ -1,4 +1,4 @@
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { controlNetImageProcessed } from 'features/controlNet/store/actions';
import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice';
import { sessionReadyToInvoke } from 'features/system/store/actions';
@@ -9,20 +9,16 @@ import { Graph, ImageDTO } from 'services/api/types';
import { socketInvocationComplete } from 'services/events/actions';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'controlNet' });
export const addControlNetImageProcessedListener = () => {
startAppListening({
actionCreator: controlNetImageProcessed,
effect: async (
action,
{ dispatch, getState, take, unsubscribe, subscribe }
) => {
effect: async (action, { dispatch, getState, take }) => {
const log = logger('session');
const { controlNetId } = action.payload;
const controlNet = getState().controlNet.controlNets[controlNetId];
if (!controlNet.controlImage) {
moduleLog.error('Unable to process ControlNet image');
log.error('Unable to process ControlNet image');
return;
}
@@ -70,8 +66,8 @@ export const addControlNetImageProcessedListener = () => {
const processedControlImage = payload as ImageDTO;
moduleLog.debug(
{ data: { arg: action.payload, processedControlImage } },
log.debug(
{ controlNetId: action.payload, processedControlImage },
'ControlNet image processed'
);

View File

@@ -1,18 +1,17 @@
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'boards' });
export const addImageAddedToBoardFulfilledListener = () => {
startAppListening({
matcher: imagesApi.endpoints.addImageToBoard.matchFulfilled,
effect: (action, { getState, dispatch }) => {
effect: (action) => {
const log = logger('images');
const { board_id, imageDTO } = action.meta.arg.originalArgs;
// TODO: update listImages cache for this board
moduleLog.debug({ data: { board_id, imageDTO } }, 'Image added to board');
log.debug({ board_id, imageDTO }, 'Image added to board');
},
});
};
@@ -20,13 +19,11 @@ export const addImageAddedToBoardFulfilledListener = () => {
export const addImageAddedToBoardRejectedListener = () => {
startAppListening({
matcher: imagesApi.endpoints.addImageToBoard.matchRejected,
effect: (action, { getState, dispatch }) => {
effect: (action) => {
const log = logger('images');
const { board_id, imageDTO } = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { board_id, imageDTO } },
'Problem adding image to board'
);
log.debug({ board_id, imageDTO }, 'Problem adding image to board');
},
});
};

View File

@@ -1,12 +1,10 @@
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlNetReset } from 'features/controlNet/store/controlNetSlice';
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import {
imageDeletionConfirmed,
isModalOpenChanged,
} from 'features/imageDeletion/store/imageDeletionSlice';
import { imageDeletionConfirmed } from 'features/imageDeletion/store/actions';
import { isModalOpenChanged } from 'features/imageDeletion/store/imageDeletionSlice';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { clamp } from 'lodash-es';
@@ -14,8 +12,6 @@ import { api } from 'services/api';
import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'image' });
/**
* Called when the user requests an image deletion
*/
@@ -107,7 +103,7 @@ export const addRequestedImageDeletionListener = () => {
export const addImageDeletedPendingListener = () => {
startAppListening({
matcher: imagesApi.endpoints.deleteImage.matchPending,
effect: (action, { dispatch, getState }) => {
effect: () => {
//
},
});
@@ -119,11 +115,9 @@ export const addImageDeletedPendingListener = () => {
export const addImageDeletedFulfilledListener = () => {
startAppListening({
matcher: imagesApi.endpoints.deleteImage.matchFulfilled,
effect: (action, { dispatch, getState }) => {
moduleLog.debug(
{ data: { image: action.meta.arg.originalArgs } },
'Image deleted'
);
effect: (action) => {
const log = logger('images');
log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Image deleted');
},
});
};
@@ -134,9 +128,10 @@ export const addImageDeletedFulfilledListener = () => {
export const addImageDeletedRejectedListener = () => {
startAppListening({
matcher: imagesApi.endpoints.deleteImage.matchRejected,
effect: (action, { dispatch, getState }) => {
moduleLog.debug(
{ data: { image: action.meta.arg.originalArgs } },
effect: (action) => {
const log = logger('images');
log.debug(
{ imageDTO: action.meta.arg.originalArgs },
'Unable to delete image'
);
},

View File

@@ -3,7 +3,7 @@ import {
TypesafeDraggableData,
TypesafeDroppableData,
} from 'app/components/ImageDnd/typesafeDnd';
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
import {
@@ -15,8 +15,6 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '../';
const moduleLog = log.child({ namespace: 'dnd' });
export const dndDropped = createAction<{
overData: TypesafeDroppableData;
activeData: TypesafeDraggableData;
@@ -25,14 +23,11 @@ export const dndDropped = createAction<{
export const addImageDroppedListener = () => {
startAppListening({
actionCreator: dndDropped,
effect: async (action, { dispatch, getState, take }) => {
effect: async (action, { dispatch }) => {
const log = logger('images');
const { activeData, overData } = action.payload;
const state = getState();
moduleLog.debug(
{ data: { activeData, overData } },
'Image or selection dropped'
);
log.debug({ activeData, overData }, 'Image or selection dropped');
// set current image
if (

View File

@@ -1,19 +1,15 @@
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'boards' });
export const addImageRemovedFromBoardFulfilledListener = () => {
startAppListening({
matcher: imagesApi.endpoints.removeImageFromBoard.matchFulfilled,
effect: (action, { getState, dispatch }) => {
const { board_id, image_name } = action.meta.arg.originalArgs;
effect: (action) => {
const log = logger('images');
const imageDTO = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { board_id, image_name } },
'Image added to board'
);
log.debug({ imageDTO }, 'Image removed from board');
},
});
};
@@ -21,13 +17,11 @@ export const addImageRemovedFromBoardFulfilledListener = () => {
export const addImageRemovedFromBoardRejectedListener = () => {
startAppListening({
matcher: imagesApi.endpoints.removeImageFromBoard.matchRejected,
effect: (action, { getState, dispatch }) => {
const { board_id, image_name } = action.meta.arg.originalArgs;
effect: (action) => {
const log = logger('images');
const imageDTO = action.meta.arg.originalArgs;
moduleLog.debug(
{ data: { board_id, image_name } },
'Problem adding image to board'
);
log.debug({ imageDTO }, 'Problem removing image from board');
},
});
};

View File

@@ -1,18 +1,15 @@
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
import { imageDeletionConfirmed } from 'features/imageDeletion/store/actions';
import { selectImageUsage } from 'features/imageDeletion/store/imageDeletionSelectors';
import {
imageDeletionConfirmed,
imageToDeleteSelected,
isModalOpenChanged,
selectImageUsage,
} from 'features/imageDeletion/store/imageDeletionSlice';
const moduleLog = log.child({ namespace: 'image' });
import { startAppListening } from '..';
export const addImageToDeleteSelectedListener = () => {
startAppListening({
actionCreator: imageToDeleteSelected,
effect: async (action, { dispatch, getState, condition }) => {
effect: async (action, { dispatch, getState }) => {
const imageDTO = action.payload;
const state = getState();
const { shouldConfirmOnDelete } = state.system;

View File

@@ -1,34 +0,0 @@
import { log } from 'app/logging/useLogger';
import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'image' });
export const addImageUpdatedFulfilledListener = () => {
// startAppListening({
// matcher: imagesApi.endpoints.updateImage.matchFulfilled,
// effect: (action, { dispatch, getState }) => {
// moduleLog.debug(
// {
// data: {
// oldImage: action.meta.arg.originalArgs,
// updatedImage: action.payload,
// },
// },
// 'Image updated'
// );
// },
// });
};
export const addImageUpdatedRejectedListener = () => {
// startAppListening({
// matcher: imagesApi.endpoints.updateImage.matchRejected,
// effect: (action, { dispatch }) => {
// moduleLog.debug(
// { data: action.meta.arg.originalArgs },
// 'Image update failed'
// );
// },
// });
};

View File

@@ -1,5 +1,5 @@
import { UseToastOptions } from '@chakra-ui/react';
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { controlNetImageChanged } from 'features/controlNet/store/controlNetSlice';
import { imagesAddedToBatch } from 'features/gallery/store/gallerySlice';
@@ -9,8 +9,7 @@ import { addToast } from 'features/system/store/systemSlice';
import { boardsApi } from 'services/api/endpoints/boards';
import { startAppListening } from '..';
import { imagesApi } from '../../../../../services/api/endpoints/images';
const moduleLog = log.child({ namespace: 'image' });
import { omit } from 'lodash-es';
const DEFAULT_UPLOADED_TOAST: UseToastOptions = {
title: 'Image Uploaded',
@@ -21,11 +20,12 @@ export const addImageUploadedFulfilledListener = () => {
startAppListening({
matcher: imagesApi.endpoints.uploadImage.matchFulfilled,
effect: (action, { dispatch, getState }) => {
const log = logger('images');
const imageDTO = action.payload;
const state = getState();
const { selectedBoardId, autoAddBoardId } = state.gallery;
const { autoAddBoardId } = state.gallery;
moduleLog.debug({ arg: '<Blob>', imageDTO }, 'Image uploaded');
log.debug({ imageDTO }, 'Image uploaded');
const { postUploadAction } = action.meta.arg.originalArgs;
@@ -140,9 +140,14 @@ export const addImageUploadedRejectedListener = () => {
startAppListening({
matcher: imagesApi.endpoints.uploadImage.matchRejected,
effect: (action, { dispatch }) => {
const { file, postUploadAction, ...rest } = action.meta.arg.originalArgs;
const sanitizedData = { arg: { ...rest, file: '<Blob>' } };
moduleLog.error({ data: sanitizedData }, 'Image upload failed');
const log = logger('images');
const sanitizedData = {
arg: {
...omit(action.meta.arg.originalArgs, ['file', 'postUploadAction']),
file: '<Blob>',
},
};
log.error({ ...sanitizedData }, 'Image upload failed');
dispatch(
addToast({
title: 'Image Upload Failed',

View File

@@ -1,14 +1,14 @@
import { makeToast } from 'app/components/Toaster';
import { initialImageSelected } from 'features/parameters/store/actions';
import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next';
import { startAppListening } from '..';
export const addInitialImageSelectedListener = () => {
startAppListening({
actionCreator: initialImageSelected,
effect: (action, { getState, dispatch }) => {
effect: (action, { dispatch }) => {
if (!action.payload) {
dispatch(
addToast(

View File

@@ -1,5 +1,5 @@
import { makeToast } from 'app/components/Toaster';
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { controlNetRemoved } from 'features/controlNet/store/controlNetSlice';
import { loraRemoved } from 'features/lora/store/loraSlice';
import { modelSelected } from 'features/parameters/store/actions';
import {
@@ -8,21 +8,21 @@ import {
} from 'features/parameters/store/generationSlice';
import { zMainModel } from 'features/parameters/types/parameterSchemas';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { forEach } from 'lodash-es';
import { startAppListening } from '..';
import { controlNetRemoved } from 'features/controlNet/store/controlNetSlice';
const moduleLog = log.child({ module: 'models' });
export const addModelSelectedListener = () => {
startAppListening({
actionCreator: modelSelected,
effect: (action, { getState, dispatch }) => {
const log = logger('models');
const state = getState();
const result = zMainModel.safeParse(action.payload);
if (!result.success) {
moduleLog.error(
log.error(
{ error: result.error.format() },
'Failed to parse main model'
);

View File

@@ -1,4 +1,5 @@
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { controlNetRemoved } from 'features/controlNet/store/controlNetSlice';
import { loraRemoved } from 'features/lora/store/loraSlice';
import {
modelChanged,
@@ -11,15 +12,17 @@ import {
import { forEach, some } from 'lodash-es';
import { modelsApi } from 'services/api/endpoints/models';
import { startAppListening } from '..';
import { controlNetRemoved } from 'features/controlNet/store/controlNetSlice';
const moduleLog = log.child({ module: 'models' });
export const addModelsLoadedListener = () => {
startAppListening({
matcher: modelsApi.endpoints.getMainModels.matchFulfilled,
effect: async (action, { getState, dispatch }) => {
// models loaded, we need to ensure the selected model is available and if not, select the first one
const log = logger('models');
log.info(
{ models: action.payload.entities },
`Main models loaded (${action.payload.ids.length})`
);
const currentModel = getState().generation.model;
@@ -46,7 +49,7 @@ export const addModelsLoadedListener = () => {
const result = zMainModel.safeParse(firstModel);
if (!result.success) {
moduleLog.error(
log.error(
{ error: result.error.format() },
'Failed to parse main model'
);
@@ -60,6 +63,11 @@ export const addModelsLoadedListener = () => {
matcher: modelsApi.endpoints.getVaeModels.matchFulfilled,
effect: async (action, { getState, dispatch }) => {
// VAEs loaded, need to reset the VAE is it's no longer available
const log = logger('models');
log.info(
{ models: action.payload.entities },
`VAEs loaded (${action.payload.ids.length})`
);
const currentVae = getState().generation.vae;
@@ -91,7 +99,7 @@ export const addModelsLoadedListener = () => {
const result = zVaeModel.safeParse(firstModel);
if (!result.success) {
moduleLog.error(
log.error(
{ error: result.error.format() },
'Failed to parse VAE model'
);
@@ -105,6 +113,11 @@ export const addModelsLoadedListener = () => {
matcher: modelsApi.endpoints.getLoRAModels.matchFulfilled,
effect: async (action, { getState, dispatch }) => {
// LoRA models loaded - need to remove missing LoRAs from state
const log = logger('models');
log.info(
{ models: action.payload.entities },
`LoRAs loaded (${action.payload.ids.length})`
);
const loras = getState().lora.loras;
@@ -128,6 +141,12 @@ export const addModelsLoadedListener = () => {
matcher: modelsApi.endpoints.getControlNetModels.matchFulfilled,
effect: async (action, { getState, dispatch }) => {
// ControlNet models loaded - need to remove missing ControlNets from state
const log = logger('models');
log.info(
{ models: action.payload.entities },
`ControlNet models loaded (${action.payload.ids.length})`
);
const controlNets = getState().controlNet.controlNets;
forEach(controlNets, (controlNet, controlNetId) => {
@@ -146,4 +165,14 @@ export const addModelsLoadedListener = () => {
});
},
});
startAppListening({
matcher: modelsApi.endpoints.getTextualInversionModels.matchFulfilled,
effect: async (action) => {
const log = logger('models');
log.info(
{ models: action.payload.entities },
`Embeddings loaded (${action.payload.ids.length})`
);
},
});
};

View File

@@ -1,24 +1,24 @@
import { logger } from 'app/logging/logger';
import { parseify } from 'common/util/serialize';
import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice';
import { parseSchema } from 'features/nodes/util/parseSchema';
import { size } from 'lodash-es';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
import { parseSchema } from 'features/nodes/util/parseSchema';
import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice';
import { size } from 'lodash-es';
const schemaLog = log.child({ namespace: 'schema' });
export const addReceivedOpenAPISchemaListener = () => {
startAppListening({
actionCreator: receivedOpenAPISchema.fulfilled,
effect: (action, { dispatch, getState }) => {
effect: (action, { dispatch }) => {
const log = logger('system');
const schemaJSON = action.payload;
schemaLog.info({ data: { schemaJSON } }, 'Dereferenced OpenAPI schema');
log.debug({ schemaJSON }, 'Dereferenced OpenAPI schema');
const nodeTemplates = parseSchema(schemaJSON);
schemaLog.info(
{ data: { nodeTemplates } },
log.debug(
{ nodeTemplates: parseify(nodeTemplates) },
`Built ${size(nodeTemplates)} node templates`
);
@@ -28,8 +28,9 @@ export const addReceivedOpenAPISchemaListener = () => {
startAppListening({
actionCreator: receivedOpenAPISchema.rejected,
effect: (action, { dispatch, getState }) => {
schemaLog.error('Problem dereferencing OpenAPI Schema');
effect: () => {
const log = logger('system');
log.error('Problem dereferencing OpenAPI Schema');
},
});
};

View File

@@ -1,14 +1,12 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { sessionCanceled } from 'services/api/thunks/session';
import { logger } from 'app/logging/logger';
import { serializeError } from 'serialize-error';
const moduleLog = log.child({ namespace: 'session' });
import { sessionCanceled } from 'services/api/thunks/session';
import { startAppListening } from '..';
export const addSessionCanceledPendingListener = () => {
startAppListening({
actionCreator: sessionCanceled.pending,
effect: (action, { getState, dispatch }) => {
effect: () => {
//
},
});
@@ -17,12 +15,10 @@ export const addSessionCanceledPendingListener = () => {
export const addSessionCanceledFulfilledListener = () => {
startAppListening({
actionCreator: sessionCanceled.fulfilled,
effect: (action, { getState, dispatch }) => {
effect: (action) => {
const log = logger('session');
const { session_id } = action.meta.arg;
moduleLog.debug(
{ data: { session_id } },
`Session canceled (${session_id})`
);
log.debug({ session_id }, `Session canceled (${session_id})`);
},
});
};
@@ -30,15 +26,15 @@ export const addSessionCanceledFulfilledListener = () => {
export const addSessionCanceledRejectedListener = () => {
startAppListening({
actionCreator: sessionCanceled.rejected,
effect: (action, { getState, dispatch }) => {
effect: (action) => {
const log = logger('session');
const { session_id } = action.meta.arg;
if (action.payload) {
const { arg, error } = action.payload;
moduleLog.error(
const { error } = action.payload;
log.error(
{
data: {
arg,
error: serializeError(error),
},
session_id,
error: serializeError(error),
},
`Problem canceling session`
);

View File

@@ -1,14 +1,13 @@
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { parseify } from 'common/util/serialize';
import { serializeError } from 'serialize-error';
import { sessionCreated } from 'services/api/thunks/session';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'session' });
export const addSessionCreatedPendingListener = () => {
startAppListening({
actionCreator: sessionCreated.pending,
effect: (action, { getState, dispatch }) => {
effect: () => {
//
},
});
@@ -17,9 +16,13 @@ export const addSessionCreatedPendingListener = () => {
export const addSessionCreatedFulfilledListener = () => {
startAppListening({
actionCreator: sessionCreated.fulfilled,
effect: (action, { getState, dispatch }) => {
effect: (action) => {
const log = logger('session');
const session = action.payload;
moduleLog.debug({ data: { session } }, `Session created (${session.id})`);
log.debug(
{ session: parseify(session) },
`Session created (${session.id})`
);
},
});
};
@@ -27,17 +30,14 @@ export const addSessionCreatedFulfilledListener = () => {
export const addSessionCreatedRejectedListener = () => {
startAppListening({
actionCreator: sessionCreated.rejected,
effect: (action, { getState, dispatch }) => {
effect: (action) => {
const log = logger('session');
if (action.payload) {
const { arg, error } = action.payload;
const { error } = action.payload;
const graph = parseify(action.meta.arg);
const stringifiedError = JSON.stringify(error);
moduleLog.error(
{
data: {
arg,
error: serializeError(error),
},
},
log.error(
{ graph, error: serializeError(error) },
`Problem creating session: ${stringifiedError}`
);
}

View File

@@ -1,14 +1,12 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { sessionInvoked } from 'services/api/thunks/session';
import { logger } from 'app/logging/logger';
import { serializeError } from 'serialize-error';
const moduleLog = log.child({ namespace: 'session' });
import { sessionInvoked } from 'services/api/thunks/session';
import { startAppListening } from '..';
export const addSessionInvokedPendingListener = () => {
startAppListening({
actionCreator: sessionInvoked.pending,
effect: (action, { getState, dispatch }) => {
effect: () => {
//
},
});
@@ -17,12 +15,10 @@ export const addSessionInvokedPendingListener = () => {
export const addSessionInvokedFulfilledListener = () => {
startAppListening({
actionCreator: sessionInvoked.fulfilled,
effect: (action, { getState, dispatch }) => {
effect: (action) => {
const log = logger('session');
const { session_id } = action.meta.arg;
moduleLog.debug(
{ data: { session_id } },
`Session invoked (${session_id})`
);
log.debug({ session_id }, `Session invoked (${session_id})`);
},
});
};
@@ -30,16 +26,16 @@ export const addSessionInvokedFulfilledListener = () => {
export const addSessionInvokedRejectedListener = () => {
startAppListening({
actionCreator: sessionInvoked.rejected,
effect: (action, { getState, dispatch }) => {
effect: (action) => {
const log = logger('session');
const { session_id } = action.meta.arg;
if (action.payload) {
const { arg, error } = action.payload;
const { error } = action.payload;
const stringifiedError = JSON.stringify(error);
moduleLog.error(
log.error(
{
data: {
arg,
error: serializeError(error),
},
session_id,
error: serializeError(error),
},
`Problem invoking session: ${stringifiedError}`
);

View File

@@ -1,20 +1,16 @@
import { startAppListening } from '..';
import { sessionInvoked } from 'services/api/thunks/session';
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { sessionReadyToInvoke } from 'features/system/store/actions';
const moduleLog = log.child({ namespace: 'session' });
import { sessionInvoked } from 'services/api/thunks/session';
import { startAppListening } from '..';
export const addSessionReadyToInvokeListener = () => {
startAppListening({
actionCreator: sessionReadyToInvoke,
effect: (action, { getState, dispatch }) => {
const log = logger('session');
const { sessionId: session_id } = getState().system;
if (session_id) {
moduleLog.debug(
{ session_id },
`Session ready to invoke (${session_id})})`
);
log.debug({ session_id }, `Session ready to invoke (${session_id})})`);
dispatch(sessionInvoked({ session_id }));
}
},

View File

@@ -1,18 +1,16 @@
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { modelsApi } from 'services/api/endpoints/models';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
import { appSocketConnected, socketConnected } from 'services/events/actions';
import { startAppListening } from '../..';
const moduleLog = log.child({ namespace: 'socketio' });
export const addSocketConnectedEventListener = () => {
startAppListening({
actionCreator: socketConnected,
effect: (action, { dispatch, getState }) => {
const { timestamp } = action.payload;
const log = logger('socketio');
moduleLog.debug({ timestamp }, 'Connected');
log.debug('Connected');
const { nodes, config } = getState();

View File

@@ -1,17 +1,16 @@
import { startAppListening } from '../..';
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import {
socketDisconnected,
appSocketDisconnected,
socketDisconnected,
} from 'services/events/actions';
const moduleLog = log.child({ namespace: 'socketio' });
import { startAppListening } from '../..';
export const addSocketDisconnectedEventListener = () => {
startAppListening({
actionCreator: socketDisconnected,
effect: (action, { dispatch, getState }) => {
moduleLog.debug(action.payload, 'Disconnected');
effect: (action, { dispatch }) => {
const log = logger('socketio');
log.debug('Disconnected');
// pass along the socket event as an application action
dispatch(appSocketDisconnected(action.payload));
},

View File

@@ -1,28 +1,27 @@
import { startAppListening } from '../..';
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import {
appSocketGeneratorProgress,
socketGeneratorProgress,
} from 'services/events/actions';
const moduleLog = log.child({ namespace: 'socketio' });
import { startAppListening } from '../..';
export const addGeneratorProgressEventListener = () => {
startAppListening({
actionCreator: socketGeneratorProgress,
effect: (action, { dispatch, getState }) => {
const log = logger('socketio');
if (
getState().system.canceledSession ===
action.payload.data.graph_execution_state_id
) {
moduleLog.trace(
log.trace(
action.payload,
'Ignored generator progress for canceled session'
);
return;
}
moduleLog.trace(
log.trace(
action.payload,
`Generator progress (${action.payload.data.node.type})`
);

View File

@@ -1,20 +1,16 @@
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import {
appSocketGraphExecutionStateComplete,
socketGraphExecutionStateComplete,
} from 'services/events/actions';
import { startAppListening } from '../..';
const moduleLog = log.child({ namespace: 'socketio' });
export const addGraphExecutionStateCompleteEventListener = () => {
startAppListening({
actionCreator: socketGraphExecutionStateComplete,
effect: (action, { dispatch, getState }) => {
moduleLog.debug(
action.payload,
`Session invocation complete (${action.payload.data.graph_execution_state_id})`
);
effect: (action, { dispatch }) => {
const log = logger('socketio');
log.debug(action.payload, 'Session complete');
// pass along the socket event as an application action
dispatch(appSocketGraphExecutionStateComplete(action.payload));
},

View File

@@ -1,11 +1,12 @@
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { parseify } from 'common/util/serialize';
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
import {
IMAGE_CATEGORIES,
boardIdSelected,
galleryViewChanged,
imageSelected,
} from 'features/gallery/store/gallerySlice';
import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
import { progressImageSet } from 'features/system/store/systemSlice';
import { imagesAdapter, imagesApi } from 'services/api/endpoints/images';
import { isImageOutput } from 'services/api/guards';
@@ -16,15 +17,16 @@ import {
} from 'services/events/actions';
import { startAppListening } from '../..';
const moduleLog = log.child({ namespace: 'socketio' });
const nodeDenylist = ['dataURL_image'];
export const addInvocationCompleteEventListener = () => {
startAppListening({
actionCreator: socketInvocationComplete,
effect: async (action, { dispatch, getState, take }) => {
moduleLog.debug(
{ data: action.payload },
effect: async (action, { dispatch, getState }) => {
const log = logger('socketio');
const { data } = action.payload;
log.debug(
{ data: parseify(data) },
`Invocation complete (${action.payload.data.node.type})`
);
const session_id = action.payload.data.graph_execution_state_id;
@@ -36,7 +38,6 @@ export const addInvocationCompleteEventListener = () => {
dispatch(sessionCanceled({ session_id }));
}
const { data } = action.payload;
const { result, node, graph_execution_state_id } = data;
// This complete event has an associated image output

View File

@@ -1,19 +1,18 @@
import { startAppListening } from '../..';
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import {
appSocketInvocationError,
socketInvocationError,
} from 'services/events/actions';
const moduleLog = log.child({ namespace: 'socketio' });
import { startAppListening } from '../..';
export const addInvocationErrorEventListener = () => {
startAppListening({
actionCreator: socketInvocationError,
effect: (action, { dispatch, getState }) => {
moduleLog.error(
effect: (action, { dispatch }) => {
const log = logger('socketio');
log.error(
action.payload,
`Invocation error (${action.payload.data.node.type}): ${action.payload.data.error}`
`Invocation error (${action.payload.data.node.type})`
);
dispatch(appSocketInvocationError(action.payload));
},

View File

@@ -1,31 +1,27 @@
import { startAppListening } from '../..';
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import {
appSocketInvocationStarted,
socketInvocationStarted,
} from 'services/events/actions';
const moduleLog = log.child({ namespace: 'socketio' });
import { startAppListening } from '../..';
export const addInvocationStartedEventListener = () => {
startAppListening({
actionCreator: socketInvocationStarted,
effect: (action, { dispatch, getState }) => {
const log = logger('socketio');
if (
getState().system.canceledSession ===
action.payload.data.graph_execution_state_id
) {
moduleLog.trace(
log.trace(
action.payload,
'Ignored invocation started for canceled session'
);
return;
}
moduleLog.debug(
action.payload,
`Invocation started (${action.payload.data.node.type})`
);
log.debug(action.payload, 'Invocation started');
dispatch(appSocketInvocationStarted(action.payload));
},
});

View File

@@ -0,0 +1,49 @@
import { logger } from 'app/logging/logger';
import {
appSocketModelLoadCompleted,
appSocketModelLoadStarted,
socketModelLoadCompleted,
socketModelLoadStarted,
} from 'services/events/actions';
import { startAppListening } from '../..';
export const addModelLoadEventListener = () => {
startAppListening({
actionCreator: socketModelLoadStarted,
effect: (action, { dispatch }) => {
const log = logger('socketio');
const { base_model, model_name, model_type, submodel } =
action.payload.data;
let message = `Model load started: ${base_model}/${model_type}/${model_name}`;
if (submodel) {
message = message.concat(`/${submodel}`);
}
log.debug(action.payload, message);
// pass along the socket event as an application action
dispatch(appSocketModelLoadStarted(action.payload));
},
});
startAppListening({
actionCreator: socketModelLoadCompleted,
effect: (action, { dispatch }) => {
const log = logger('socketio');
const { base_model, model_name, model_type, submodel } =
action.payload.data;
let message = `Model load complete: ${base_model}/${model_type}/${model_name}`;
if (submodel) {
message = message.concat(`/${submodel}`);
}
log.debug(action.payload, message);
// pass along the socket event as an application action
dispatch(appSocketModelLoadCompleted(action.payload));
},
});
};

View File

@@ -1,28 +0,0 @@
import { log } from 'app/logging/useLogger';
import {
appSocketModelLoadCompleted,
socketModelLoadCompleted,
} from 'services/events/actions';
import { startAppListening } from '../..';
const moduleLog = log.child({ namespace: 'socketio' });
export const addModelLoadCompletedEventListener = () => {
startAppListening({
actionCreator: socketModelLoadCompleted,
effect: (action, { dispatch, getState }) => {
const { model_name, model_type, submodel } = action.payload.data;
let modelString = `${model_type} model: ${model_name}`;
if (submodel) {
modelString = modelString.concat(`, submodel: ${submodel}`);
}
moduleLog.debug(action.payload, `Model load completed (${modelString})`);
// pass along the socket event as an application action
dispatch(appSocketModelLoadCompleted(action.payload));
},
});
};

View File

@@ -1,28 +0,0 @@
import { log } from 'app/logging/useLogger';
import {
appSocketModelLoadStarted,
socketModelLoadStarted,
} from 'services/events/actions';
import { startAppListening } from '../..';
const moduleLog = log.child({ namespace: 'socketio' });
export const addModelLoadStartedEventListener = () => {
startAppListening({
actionCreator: socketModelLoadStarted,
effect: (action, { dispatch, getState }) => {
const { model_name, model_type, submodel } = action.payload.data;
let modelString = `${model_type} model: ${model_name}`;
if (submodel) {
modelString = modelString.concat(`, submodel: ${submodel}`);
}
moduleLog.debug(action.payload, `Model load started (${modelString})`);
// pass along the socket event as an application action
dispatch(appSocketModelLoadStarted(action.payload));
},
});
};

View File

@@ -1,17 +1,13 @@
import { startAppListening } from '../..';
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { appSocketSubscribed, socketSubscribed } from 'services/events/actions';
const moduleLog = log.child({ namespace: 'socketio' });
import { startAppListening } from '../..';
export const addSocketSubscribedEventListener = () => {
startAppListening({
actionCreator: socketSubscribed,
effect: (action, { dispatch, getState }) => {
moduleLog.debug(
action.payload,
`Subscribed (${action.payload.sessionId}))`
);
effect: (action, { dispatch }) => {
const log = logger('socketio');
log.debug(action.payload, 'Subscribed');
dispatch(appSocketSubscribed(action.payload));
},
});

View File

@@ -1,20 +1,16 @@
import { startAppListening } from '../..';
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import {
appSocketUnsubscribed,
socketUnsubscribed,
} from 'services/events/actions';
const moduleLog = log.child({ namespace: 'socketio' });
import { startAppListening } from '../..';
export const addSocketUnsubscribedEventListener = () => {
startAppListening({
actionCreator: socketUnsubscribed,
effect: (action, { dispatch, getState }) => {
moduleLog.debug(
action.payload,
`Unsubscribed (${action.payload.sessionId})`
);
effect: (action, { dispatch }) => {
const log = logger('socketio');
log.debug(action.payload, 'Unsubscribed');
dispatch(appSocketUnsubscribed(action.payload));
},
});

View File

@@ -1,15 +1,12 @@
import { log } from 'app/logging/useLogger';
import { stagingAreaImageSaved } from 'features/canvas/store/actions';
import { addToast } from 'features/system/store/systemSlice';
import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'canvas' });
export const addStagingAreaImageSavedListener = () => {
startAppListening({
actionCreator: stagingAreaImageSaved,
effect: async (action, { dispatch, getState, take }) => {
effect: async (action, { dispatch, getState }) => {
const { imageDTO } = action.payload;
try {

View File

@@ -1,12 +1,9 @@
import { createAction } from '@reduxjs/toolkit';
import { log } from 'app/logging/useLogger';
import { buildAdHocUpscaleGraph } from 'features/nodes/util/graphBuilders/buildAdHocUpscaleGraph';
import { sessionReadyToInvoke } from 'features/system/store/actions';
import { sessionCreated } from 'services/api/thunks/session';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'upscale' });
export const upscaleRequested = createAction<{ image_name: string }>(
`upscale/upscaleRequested`
);
@@ -14,10 +11,7 @@ export const upscaleRequested = createAction<{ image_name: string }>(
export const addUpscaleRequestedListener = () => {
startAppListening({
actionCreator: upscaleRequested,
effect: async (
action,
{ dispatch, getState, take, unsubscribe, subscribe }
) => {
effect: async (action, { dispatch, getState, take }) => {
const { image_name } = action.payload;
const { esrganModelName } = getState().postprocessing;

View File

@@ -1,4 +1,4 @@
import { log } from 'app/logging/useLogger';
import { logger } from 'app/logging/logger';
import { userInvoked } from 'app/store/actions';
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
import {
@@ -15,8 +15,7 @@ import { imagesApi } from 'services/api/endpoints/images';
import { sessionCreated } from 'services/api/thunks/session';
import { ImageDTO } from 'services/api/types';
import { startAppListening } from '..';
const moduleLog = log.child({ namespace: 'invoke' });
import { parseify } from 'common/util/serialize';
/**
* This listener is responsible invoking the canvas. This involves a number of steps:
@@ -36,13 +35,15 @@ export const addUserInvokedCanvasListener = () => {
predicate: (action): action is ReturnType<typeof userInvoked> =>
userInvoked.match(action) && action.payload === 'unifiedCanvas',
effect: async (action, { getState, dispatch, take }) => {
const log = logger('session');
const state = getState();
// Build canvas blobs
const canvasBlobsAndImageData = await getCanvasData(state);
if (!canvasBlobsAndImageData) {
moduleLog.error('Unable to create canvas data');
log.error('Unable to create canvas data');
return;
}
@@ -64,7 +65,7 @@ export const addUserInvokedCanvasListener = () => {
]);
}
moduleLog.debug(`Generation mode: ${generationMode}`);
log.debug(`Generation mode: ${generationMode}`);
// Temp placeholders for the init and mask images
let canvasInitImage: ImageDTO | undefined;
@@ -105,7 +106,7 @@ export const addUserInvokedCanvasListener = () => {
canvasMaskImage
);
moduleLog.debug({ graph }, `Canvas graph built`);
log.debug({ graph: parseify(graph) }, `Canvas graph built`);
// currently this action is just listened to for logging
dispatch(canvasGraphBuilt(graph));

View File

@@ -1,23 +1,23 @@
import { startAppListening } from '..';
import { sessionCreated } from 'services/api/thunks/session';
import { log } from 'app/logging/useLogger';
import { imageToImageGraphBuilt } from 'features/nodes/store/actions';
import { logger } from 'app/logging/logger';
import { userInvoked } from 'app/store/actions';
import { sessionReadyToInvoke } from 'features/system/store/actions';
import { parseify } from 'common/util/serialize';
import { imageToImageGraphBuilt } from 'features/nodes/store/actions';
import { buildLinearImageToImageGraph } from 'features/nodes/util/graphBuilders/buildLinearImageToImageGraph';
const moduleLog = log.child({ namespace: 'invoke' });
import { sessionReadyToInvoke } from 'features/system/store/actions';
import { sessionCreated } from 'services/api/thunks/session';
import { startAppListening } from '..';
export const addUserInvokedImageToImageListener = () => {
startAppListening({
predicate: (action): action is ReturnType<typeof userInvoked> =>
userInvoked.match(action) && action.payload === 'img2img',
effect: async (action, { getState, dispatch, take }) => {
const log = logger('session');
const state = getState();
const graph = buildLinearImageToImageGraph(state);
dispatch(imageToImageGraphBuilt(graph));
moduleLog.debug({ data: graph }, 'Image to Image graph built');
log.debug({ graph: parseify(graph) }, 'Image to Image graph built');
dispatch(sessionCreated({ graph }));

View File

@@ -1,23 +1,23 @@
import { startAppListening } from '..';
import { sessionCreated } from 'services/api/thunks/session';
import { buildNodesGraph } from 'features/nodes/util/graphBuilders/buildNodesGraph';
import { log } from 'app/logging/useLogger';
import { nodesGraphBuilt } from 'features/nodes/store/actions';
import { logger } from 'app/logging/logger';
import { userInvoked } from 'app/store/actions';
import { parseify } from 'common/util/serialize';
import { nodesGraphBuilt } from 'features/nodes/store/actions';
import { buildNodesGraph } from 'features/nodes/util/graphBuilders/buildNodesGraph';
import { sessionReadyToInvoke } from 'features/system/store/actions';
const moduleLog = log.child({ namespace: 'invoke' });
import { sessionCreated } from 'services/api/thunks/session';
import { startAppListening } from '..';
export const addUserInvokedNodesListener = () => {
startAppListening({
predicate: (action): action is ReturnType<typeof userInvoked> =>
userInvoked.match(action) && action.payload === 'nodes',
effect: async (action, { getState, dispatch, take }) => {
const log = logger('session');
const state = getState();
const graph = buildNodesGraph(state);
dispatch(nodesGraphBuilt(graph));
moduleLog.debug({ data: graph }, 'Nodes graph built');
log.debug({ graph: parseify(graph) }, 'Nodes graph built');
dispatch(sessionCreated({ graph }));

View File

@@ -1,25 +1,25 @@
import { startAppListening } from '..';
import { sessionCreated } from 'services/api/thunks/session';
import { log } from 'app/logging/useLogger';
import { textToImageGraphBuilt } from 'features/nodes/store/actions';
import { logger } from 'app/logging/logger';
import { userInvoked } from 'app/store/actions';
import { sessionReadyToInvoke } from 'features/system/store/actions';
import { parseify } from 'common/util/serialize';
import { textToImageGraphBuilt } from 'features/nodes/store/actions';
import { buildLinearTextToImageGraph } from 'features/nodes/util/graphBuilders/buildLinearTextToImageGraph';
const moduleLog = log.child({ namespace: 'invoke' });
import { sessionReadyToInvoke } from 'features/system/store/actions';
import { sessionCreated } from 'services/api/thunks/session';
import { startAppListening } from '..';
export const addUserInvokedTextToImageListener = () => {
startAppListening({
predicate: (action): action is ReturnType<typeof userInvoked> =>
userInvoked.match(action) && action.payload === 'txt2img',
effect: async (action, { getState, dispatch, take }) => {
const log = logger('session');
const state = getState();
const graph = buildLinearTextToImageGraph(state);
dispatch(textToImageGraphBuilt(graph));
moduleLog.debug({ data: graph }, 'Text to Image graph built');
log.debug({ graph: parseify(graph) }, 'Text to Image graph built');
dispatch(sessionCreated({ graph }));

View File

@@ -5,13 +5,9 @@ import {
combineReducers,
configureStore,
} from '@reduxjs/toolkit';
import dynamicMiddlewares from 'redux-dynamic-middlewares';
import { rememberEnhancer, rememberReducer } from 'redux-remember';
import canvasReducer from 'features/canvas/store/canvasSlice';
import controlNetReducer from 'features/controlNet/store/controlNetSlice';
import dynamicPromptsReducer from 'features/dynamicPrompts/store/slice';
import dynamicPromptsReducer from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import boardsReducer from 'features/gallery/store/boardSlice';
import galleryReducer from 'features/gallery/store/gallerySlice';
import imageDeletionReducer from 'features/imageDeletion/store/imageDeletionSlice';
@@ -24,9 +20,8 @@ import systemReducer from 'features/system/store/systemSlice';
import modelmanagerReducer from 'features/ui/components/tabs/ModelManager/store/modelManagerSlice';
import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import uiReducer from 'features/ui/store/uiSlice';
import { listenerMiddleware } from './middleware/listenerMiddleware';
import dynamicMiddlewares from 'redux-dynamic-middlewares';
import { rememberEnhancer, rememberReducer } from 'redux-remember';
import { api } from 'services/api';
import { LOCALSTORAGE_PREFIX } from './constants';
import { serialize } from './enhancers/reduxRemember/serialize';
@@ -34,6 +29,7 @@ import { unserialize } from './enhancers/reduxRemember/unserialize';
import { actionSanitizer } from './middleware/devtools/actionSanitizer';
import { actionsDenylist } from './middleware/devtools/actionsDenylist';
import { stateSanitizer } from './middleware/devtools/stateSanitizer';
import { listenerMiddleware } from './middleware/listenerMiddleware';
const allReducers = {
canvas: canvasReducer,
@@ -121,6 +117,7 @@ export const store = configureStore({
export type AppGetState = typeof store.getState;
export type RootState = ReturnType<typeof store.getState>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AppThunkDispatch = ThunkDispatch<RootState, any, AnyAction>;
export type AppDispatch = typeof store.dispatch;
export const stateSelector = (state: RootState) => state;

View File

@@ -1,8 +0,0 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { AppDispatch, RootState } from 'app/store/store';
// https://redux-toolkit.js.org/usage/usage-with-typescript#defining-a-pre-typed-createasyncthunk
export const createAppAsyncThunk = createAsyncThunk.withTypes<{
state: RootState;
dispatch: AppDispatch;
}>();

View File

@@ -1,23 +0,0 @@
import { Box, forwardRef, Icon } from '@chakra-ui/react';
import { Feature } from 'app/features';
import { memo } from 'react';
import { IconType } from 'react-icons';
import { MdHelp } from 'react-icons/md';
import GuidePopover from './GuidePopover';
type GuideIconProps = {
feature: Feature;
icon?: IconType;
};
const GuideIcon = forwardRef(
({ feature, icon = MdHelp }: GuideIconProps, ref) => (
<GuidePopover feature={feature}>
<Box ref={ref}>
<Icon marginBottom="-.15rem" as={icon} />
</Box>
</GuidePopover>
)
);
export default memo(GuideIcon);

View File

@@ -1,49 +0,0 @@
import {
Box,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { Feature, useFeatureHelpInfo } from 'app/features';
import { useAppSelector } from 'app/store/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors';
import { SystemState } from 'features/system/store/systemSlice';
import { memo, ReactElement } from 'react';
type GuideProps = {
children: ReactElement;
feature: Feature;
};
const guidePopoverSelector = createSelector(
systemSelector,
(system: SystemState) => system.shouldDisplayGuides
);
const GuidePopover = ({ children, feature }: GuideProps) => {
const shouldDisplayGuides = useAppSelector(guidePopoverSelector);
const { text } = useFeatureHelpInfo(feature);
if (!shouldDisplayGuides) return null;
return (
<Popover trigger="hover" isLazy>
<PopoverTrigger>
<Box>{children}</Box>
</PopoverTrigger>
<PopoverContent
maxWidth="400px"
onClick={(e) => e.preventDefault()}
cursor="initial"
>
<PopoverArrow />
<PopoverBody>{text}</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default memo(GuidePopover);

View File

@@ -169,7 +169,9 @@ const IAIDndImage = (props: IAIDndImageProps) => {
...imageSx,
}}
/>
{withMetadataOverlay && <ImageMetadataOverlay image={imageDTO} />}
{withMetadataOverlay && (
<ImageMetadataOverlay imageDTO={imageDTO} />
)}
<SelectionOverlay
isSelected={isSelected}
isHovered={withHoverOverlay ? isHovered : false}

View File

@@ -13,11 +13,10 @@ type IAIDroppableProps = {
dropLabel?: ReactNode;
disabled?: boolean;
data?: TypesafeDroppableData;
hoverRef?: React.Ref<HTMLDivElement>;
};
const IAIDroppable = (props: IAIDroppableProps) => {
const { dropLabel, data, disabled, hoverRef } = props;
const { dropLabel, data, disabled } = props;
const dndId = useRef(uuidv4());
const { isOver, setNodeRef, active } = useDroppable({

View File

@@ -10,7 +10,10 @@ interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
}
const IAIMantineSelectItemWithTooltip = forwardRef<HTMLDivElement, ItemProps>(
({ label, tooltip, description, disabled, ...others }: ItemProps, ref) => (
(
{ label, tooltip, description, disabled: _disabled, ...others }: ItemProps,
ref
) => (
<Tooltip label={tooltip} placement="top" hasArrow openDelay={500}>
<Box ref={ref} {...others}>
<Box>

View File

@@ -1,5 +1,4 @@
import {
ChakraProps,
FormControl,
FormControlProps,
FormLabel,
@@ -24,16 +23,15 @@ import {
Tooltip,
TooltipProps,
} from '@chakra-ui/react';
import { clamp } from 'lodash-es';
import { useAppDispatch } from 'app/store/storeHooks';
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice';
import { clamp } from 'lodash-es';
import {
FocusEvent,
KeyboardEvent,
memo,
MouseEvent,
memo,
useCallback,
useEffect,
useMemo,

View File

@@ -1,21 +1,11 @@
import { Badge, Flex } from '@chakra-ui/react';
import { isString } from 'lodash-es';
import { useMemo } from 'react';
import { ImageDTO } from 'services/api/types';
type ImageMetadataOverlayProps = {
image: ImageDTO;
imageDTO: ImageDTO;
};
const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => {
const model = useMemo(() => {
if (!isString(image.metadata?.model)) {
return;
}
return image.metadata?.model;
}, [image.metadata]);
const ImageMetadataOverlay = ({ imageDTO }: ImageMetadataOverlayProps) => {
return (
<Flex
sx={{
@@ -30,13 +20,8 @@ const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => {
}}
>
<Badge variant="solid" colorScheme="base">
{image.width} × {image.height}
{imageDTO.width} × {imageDTO.height}
</Badge>
{model && (
<Badge variant="solid" colorScheme="base">
{model}
</Badge>
)}
</Flex>
);
};

View File

@@ -2,17 +2,16 @@ import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { validateSeedWeights } from 'common/util/seedWeightPairs';
// import { validateSeedWeights } from 'common/util/seedWeightPairs';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { modelsApi } from '../../services/api/endpoints/models';
import { forEach } from 'lodash-es';
import { modelsApi } from '../../services/api/endpoints/models';
const readinessSelector = createSelector(
[stateSelector, activeTabNameSelector],
(state, activeTabName) => {
const { generation, system } = state;
const { shouldGenerateVariations, seedWeights, initialImage, seed } =
generation;
const { initialImage } = generation;
const { isProcessing, isConnected } = system;
@@ -44,19 +43,19 @@ const readinessSelector = createSelector(
reasonsWhyNotReady.push('System Disconnected');
}
// Cannot generate variations without valid seed weights
if (
shouldGenerateVariations &&
(!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
) {
isReady = false;
reasonsWhyNotReady.push('Seed-Weights badly formatted.');
}
// // Cannot generate variations without valid seed weights
// if (
// shouldGenerateVariations &&
// (!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
// ) {
// isReady = false;
// reasonsWhyNotReady.push('Seed-Weights badly formatted.');
// }
forEach(state.controlNet.controlNets, (controlNet, id) => {
if (!controlNet.model) {
isReady = false;
reasonsWhyNotReady.push('ControlNet ${id} has no model selected.');
reasonsWhyNotReady.push(`ControlNet ${id} has no model selected.`);
}
});

View File

@@ -1,31 +0,0 @@
import * as InvokeAI from 'app/types/invokeai';
import promptToString from './promptToString';
export function getPromptAndNegative(inputPrompt: InvokeAI.Prompt) {
let prompt: string =
typeof inputPrompt === 'string' ? inputPrompt : promptToString(inputPrompt);
let negativePrompt = '';
// Matches all negative prompts, 1st capturing group is the prompt itself
const negativePromptRegExp = new RegExp(/\[([^\][]*)]/, 'gi');
// Grab the actual prompt matches (capturing group 1 is 1st index of match)
const negativePromptMatches = [...prompt.matchAll(negativePromptRegExp)].map(
(match) => match[1]
);
if (negativePromptMatches.length) {
// Build the negative prompt itself
negativePrompt = negativePromptMatches.join(' ');
// Replace each match, including its surrounding brackets
// Remove each pair of empty brackets
// Trim whitespace
negativePromptMatches.forEach((match) => {
prompt = prompt.replace(`[${match}]`, '').replaceAll('[]', '').trim();
});
}
return [prompt, negativePrompt];
}

View File

@@ -1,20 +0,0 @@
import * as InvokeAI from 'app/types/invokeai';
const promptToString = (prompt: InvokeAI.Prompt): string => {
if (typeof prompt === 'string') {
return prompt;
}
if (prompt.length === 1) {
return prompt[0].prompt;
}
return prompt
.map(
(promptItem: InvokeAI.PromptItem): string =>
`${promptItem.prompt}:${promptItem.weight}`
)
.join(' ');
};
export default promptToString;

View File

@@ -1,68 +1,71 @@
import * as InvokeAI from 'app/types/invokeai';
// TODO: Restore variations
// Support code from v2.3 in here.
export const stringToSeedWeights = (
string: string
): InvokeAI.SeedWeights | boolean => {
const stringPairs = string.split(',');
const arrPairs = stringPairs.map((p) => p.split(':'));
const pairs = arrPairs.map((p: Array<string>): InvokeAI.SeedWeightPair => {
return { seed: Number(p[0]), weight: Number(p[1]) };
});
// export const stringToSeedWeights = (
// string: string
// ): InvokeAI.SeedWeights | boolean => {
// const stringPairs = string.split(',');
// const arrPairs = stringPairs.map((p) => p.split(':'));
// const pairs = arrPairs.map((p: Array<string>): InvokeAI.SeedWeightPair => {
// return { seed: Number(p[0]), weight: Number(p[1]) };
// });
if (!validateSeedWeights(pairs)) {
return false;
}
// if (!validateSeedWeights(pairs)) {
// return false;
// }
return pairs;
};
// return pairs;
// };
export const validateSeedWeights = (
seedWeights: InvokeAI.SeedWeights | string
): boolean => {
return typeof seedWeights === 'string'
? Boolean(stringToSeedWeights(seedWeights))
: Boolean(
seedWeights.length &&
!seedWeights.some((pair: InvokeAI.SeedWeightPair) => {
const { seed, weight } = pair;
const isSeedValid = !isNaN(parseInt(seed.toString(), 10));
const isWeightValid =
!isNaN(parseInt(weight.toString(), 10)) &&
weight >= 0 &&
weight <= 1;
return !(isSeedValid && isWeightValid);
})
);
};
// export const validateSeedWeights = (
// seedWeights: InvokeAI.SeedWeights | string
// ): boolean => {
// return typeof seedWeights === 'string'
// ? Boolean(stringToSeedWeights(seedWeights))
// : Boolean(
// seedWeights.length &&
// !seedWeights.some((pair: InvokeAI.SeedWeightPair) => {
// const { seed, weight } = pair;
// const isSeedValid = !isNaN(parseInt(seed.toString(), 10));
// const isWeightValid =
// !isNaN(parseInt(weight.toString(), 10)) &&
// weight >= 0 &&
// weight <= 1;
// return !(isSeedValid && isWeightValid);
// })
// );
// };
export const seedWeightsToString = (
seedWeights: InvokeAI.SeedWeights
): string => {
return seedWeights.reduce((acc, pair, i, arr) => {
const { seed, weight } = pair;
acc += `${seed}:${weight}`;
if (i !== arr.length - 1) {
acc += ',';
}
return acc;
}, '');
};
// export const seedWeightsToString = (
// seedWeights: InvokeAI.SeedWeights
// ): string => {
// return seedWeights.reduce((acc, pair, i, arr) => {
// const { seed, weight } = pair;
// acc += `${seed}:${weight}`;
// if (i !== arr.length - 1) {
// acc += ',';
// }
// return acc;
// }, '');
// };
export const seedWeightsToArray = (
seedWeights: InvokeAI.SeedWeights
): Array<Array<number>> => {
return seedWeights.map((pair: InvokeAI.SeedWeightPair) => [
pair.seed,
pair.weight,
]);
};
// export const seedWeightsToArray = (
// seedWeights: InvokeAI.SeedWeights
// ): Array<Array<number>> => {
// return seedWeights.map((pair: InvokeAI.SeedWeightPair) => [
// pair.seed,
// pair.weight,
// ]);
// };
export const stringToSeedWeightsArray = (
string: string
): Array<Array<number>> => {
const stringPairs = string.split(',');
const arrPairs = stringPairs.map((p) => p.split(':'));
return arrPairs.map(
(p: Array<string>): Array<number> => [parseInt(p[0], 10), parseFloat(p[1])]
);
};
// export const stringToSeedWeightsArray = (
// string: string
// ): Array<Array<number>> => {
// const stringPairs = string.split(',');
// const arrPairs = stringPairs.map((p) => p.split(':'));
// return arrPairs.map(
// (p: Array<string>): Array<number> => [parseInt(p[0], 10), parseFloat(p[1])]
// );
// };
export default {};

View File

@@ -0,0 +1,4 @@
/**
* Serialize an object to JSON and back to a new object
*/
export const parseify = (obj: unknown) => JSON.parse(JSON.stringify(obj));

View File

@@ -1,6 +1,7 @@
import { Box, chakra, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import {
canvasSelector,
isStagingSelector,
@@ -8,8 +9,6 @@ import {
import Konva from 'konva';
import { KonvaEventObject } from 'konva/lib/Node';
import { Vector2d } from 'konva/lib/types';
import { isEqual } from 'lodash-es';
import { useCallback, useRef } from 'react';
import { Layer, Stage } from 'react-konva';
import useCanvasDragMove from '../hooks/useCanvasDragMove';
@@ -34,7 +33,6 @@ import IAICanvasStagingAreaToolbar from './IAICanvasStagingAreaToolbar';
import IAICanvasStatusText from './IAICanvasStatusText';
import IAICanvasBoundingBox from './IAICanvasToolbar/IAICanvasBoundingBox';
import IAICanvasToolPreview from './IAICanvasToolPreview';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
const selector = createSelector(
[canvasSelector, isStagingSelector],

View File

@@ -1,10 +1,10 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
roundDownToMultiple,
roundToMultiple,
} from 'common/util/roundDownToMultiple';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import {
setBoundingBoxCoordinates,
setBoundingBoxDimensions,
@@ -13,7 +13,6 @@ import {
setIsTransformingBoundingBox,
setShouldSnapToGrid,
} from 'features/canvas/store/canvasSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import Konva from 'konva';
import { GroupConfig } from 'konva/lib/Group';
import { KonvaEventObject } from 'konva/lib/Node';
@@ -25,8 +24,8 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { Group, Rect, Transformer } from 'react-konva';
const boundingBoxPreviewSelector = createSelector(
[canvasSelector, uiSelector],
(canvas, ui) => {
[stateSelector],
({ canvas, generation }) => {
const {
boundingBoxCoordinates,
boundingBoxDimensions,
@@ -38,7 +37,7 @@ const boundingBoxPreviewSelector = createSelector(
shouldSnapToGrid,
} = canvas;
const { aspectRatio } = ui;
const { aspectRatio } = generation;
return {
boundingBoxCoordinates,

View File

@@ -4,14 +4,13 @@ import {
roundDownToMultiple,
roundToMultiple,
} from 'common/util/roundDownToMultiple';
import { IRect, Vector2d } from 'konva/lib/types';
import { clamp, cloneDeep } from 'lodash-es';
//
import { setAspectRatio } from 'features/parameters/store/generationSlice';
import {
setActiveTab,
setAspectRatio,
setShouldUseCanvasBetaLayout,
} from 'features/ui/store/uiSlice';
import { IRect, Vector2d } from 'konva/lib/types';
import { clamp, cloneDeep } from 'lodash-es';
import { RgbaColor } from 'react-colorful';
import { sessionCanceled } from 'services/api/thunks/session';
import { ImageDTO } from 'services/api/types';
@@ -714,7 +713,7 @@ export const canvasSlice = createSlice({
},
commitStagingAreaImage: (
state,
action: PayloadAction<string | undefined>
_action: PayloadAction<string | undefined>
) => {
if (!state.layerState.stagingArea.images.length) {
return;
@@ -867,11 +866,11 @@ export const canvasSlice = createSlice({
}
});
builder.addCase(setShouldUseCanvasBetaLayout, (state, action) => {
builder.addCase(setShouldUseCanvasBetaLayout, (state) => {
state.doesCanvasNeedScaling = true;
});
builder.addCase(setActiveTab, (state, action) => {
builder.addCase(setActiveTab, (state) => {
state.doesCanvasNeedScaling = true;
});
builder.addCase(setAspectRatio, (state, action) => {
@@ -883,26 +882,6 @@ export const canvasSlice = createSlice({
);
}
});
// builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
// const { image_name, image_url, thumbnail_url } = action.payload;
// state.layerState.objects.forEach((object) => {
// if (object.kind === 'image') {
// if (object.image.image_name === image_name) {
// object.image.image_url = image_url;
// object.image.thumbnail_url = thumbnail_url;
// }
// }
// });
// state.layerState.stagingArea.images.forEach((stagedImage) => {
// if (stagedImage.image.image_name === image_name) {
// stagedImage.image.image_url = image_url;
// stagedImage.image.thumbnail_url = thumbnail_url;
// }
// });
// });
},
});

View File

@@ -1,7 +1,5 @@
import * as InvokeAI from 'app/types/invokeai';
import { IRect, Vector2d } from 'konva/lib/types';
import { RgbaColor } from 'react-colorful';
import { ImageDTO } from 'services/api/types';
export const LAYER_NAMES_DICT = [
{ label: 'Base', value: 'base' },
@@ -133,7 +131,6 @@ export interface CanvasState {
cursorPosition: Vector2d | null;
doesCanvasNeedScaling: boolean;
futureLayerStates: CanvasLayerState[];
intermediateImage?: InvokeAI.Image;
isCanvasInitialized: boolean;
isDrawing: boolean;
isMaskEnabled: boolean;

View File

@@ -1,22 +1,22 @@
import { logger } from 'app/logging/logger';
import { RootState } from 'app/store/store';
import { getCanvasBaseLayer, getCanvasStage } from './konvaInstanceProvider';
import { isCanvasMaskLine } from '../store/canvasTypes';
import { log } from 'app/logging/useLogger';
import createMaskStage from './createMaskStage';
import { konvaNodeToImageData } from './konvaNodeToImageData';
import { getCanvasBaseLayer, getCanvasStage } from './konvaInstanceProvider';
import { konvaNodeToBlob } from './konvaNodeToBlob';
const moduleLog = log.child({ namespace: 'getCanvasDataURLs' });
import { konvaNodeToImageData } from './konvaNodeToImageData';
/**
* Gets Blob and ImageData objects for the base and mask layers
*/
export const getCanvasData = async (state: RootState) => {
const log = logger('canvas');
const canvasBaseLayer = getCanvasBaseLayer();
const canvasStage = getCanvasStage();
if (!canvasBaseLayer || !canvasStage) {
moduleLog.error('Unable to find canvas / stage');
log.error('Unable to find canvas / stage');
return;
}

View File

@@ -11,8 +11,8 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDndImage from 'common/components/IAIDndImage';
import { memo, useCallback, useMemo, useState } from 'react';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { controlNetImageChanged } from '../store/controlNetSlice';
import { PostUploadAction } from 'services/api/types';
import { controlNetImageChanged } from '../store/controlNetSlice';
type Props = {
controlNetId: string;
@@ -59,19 +59,13 @@ const ControlNetImagePreview = (props: Props) => {
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
const {
currentData: controlImage,
isLoading: isLoadingControlImage,
isError: isErrorControlImage,
isSuccess: isSuccessControlImage,
} = useGetImageDTOQuery(controlImageName ?? skipToken);
const { currentData: controlImage } = useGetImageDTOQuery(
controlImageName ?? skipToken
);
const {
currentData: processedControlImage,
isLoading: isLoadingProcessedControlImage,
isError: isErrorProcessedControlImage,
isSuccess: isSuccessProcessedControlImage,
} = useGetImageDTOQuery(processedControlImageName ?? skipToken);
const { currentData: processedControlImage } = useGetImageDTOQuery(
processedControlImageName ?? skipToken
);
const handleResetControlImage = useCallback(() => {
dispatch(controlNetImageChanged({ controlNetId, controlImage: null }));

View File

@@ -55,11 +55,6 @@ const ParamControlNetBeginEnd = (props: Props) => {
[controlNetId, dispatch]
);
const handleStepPctReset = useCallback(() => {
dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 }));
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: 1 }));
}, [controlNetId, dispatch]);
return (
<FormControl isDisabled={!isEnabled}>
<FormLabel>Begin / End Step Percentage</FormLabel>

View File

@@ -8,7 +8,6 @@ import {
controlNetControlModeChanged,
} from 'features/controlNet/store/controlNetSlice';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type ParamControlNetControlModeProps = {
controlNetId: string;
@@ -42,8 +41,6 @@ export default function ParamControlNetControlMode(
const { controlMode, isEnabled } = useAppSelector(selector);
const { t } = useTranslation();
const handleControlModeChange = useCallback(
(controlMode: ControlModes) => {
dispatch(controlNetControlModeChanged({ controlNetId, controlMode }));

View File

@@ -13,7 +13,6 @@ import { memo, useCallback, useMemo } from 'react';
import { CONTROLNET_PROCESSORS } from '../../store/constants';
import { controlNetProcessorTypeChanged } from '../../store/controlNetSlice';
import { ControlNetProcessorType } from '../../store/types';
import { FormControl, FormLabel } from '@chakra-ui/react';
type ParamControlNetProcessorSelectProps = {
controlNetId: string;

View File

@@ -8,7 +8,6 @@ import {
controlNetResizeModeChanged,
} from 'features/controlNet/store/controlNetSlice';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type ParamControlNetResizeModeProps = {
controlNetId: string;
@@ -41,8 +40,6 @@ export default function ParamControlNetResizeMode(
const { resizeMode, isEnabled } = useAppSelector(selector);
const { t } = useTranslation();
const handleResizeModeChange = useCallback(
(resizeMode: ResizeModes) => {
dispatch(controlNetResizeModeChanged({ controlNetId, resizeMode }));

View File

@@ -7,7 +7,7 @@ type Props = {
isEnabled: boolean;
};
const ZoeDepthProcessor = (props: Props) => {
const ZoeDepthProcessor = (_props: Props) => {
// Has no parameters?
return null;
};

View File

@@ -1,5 +1,4 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { ControlNetModelParam } from 'features/parameters/types/parameterSchemas';
import { cloneDeep, forEach } from 'lodash-es';
import { imagesApi } from 'services/api/endpoints/images';
@@ -315,11 +314,11 @@ export const controlNetSlice = createSlice({
}
});
builder.addCase(appSocketInvocationError, (state, action) => {
builder.addCase(appSocketInvocationError, (state) => {
state.pendingControlImages = [];
});
builder.addMatcher(isAnySessionRejected, (state, action) => {
builder.addMatcher(isAnySessionRejected, (state) => {
state.pendingControlImages = [];
});
@@ -365,5 +364,3 @@ export const {
} = controlNetSlice.actions;
export default controlNetSlice.reducer;
export const controlNetSelector = (state: RootState) => state.controlNet;

View File

@@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISwitch from 'common/components/IAISwitch';
import { useCallback } from 'react';
import { combinatorialToggled } from '../store/slice';
import { combinatorialToggled } from '../store/dynamicPromptsSlice';
const selector = createSelector(
stateSelector,

View File

@@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISwitch from 'common/components/IAISwitch';
import { useCallback } from 'react';
import { isEnabledToggled } from '../store/slice';
import { isEnabledToggled } from '../store/dynamicPromptsSlice';
const selector = createSelector(
stateSelector,

View File

@@ -4,7 +4,10 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISlider from 'common/components/IAISlider';
import { useCallback } from 'react';
import { maxPromptsChanged, maxPromptsReset } from '../store/slice';
import {
maxPromptsChanged,
maxPromptsReset,
} from '../store/dynamicPromptsSlice';
const selector = createSelector(
stateSelector,

View File

@@ -1,5 +1,4 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
export interface DynamicPromptsState {
isEnabled: boolean;
@@ -32,9 +31,6 @@ export const dynamicPromptsSlice = createSlice({
state.isEnabled = !state.isEnabled;
},
},
extraReducers: (builder) => {
//
},
});
export const {
@@ -45,6 +41,3 @@ export const {
} = dynamicPromptsSlice.actions;
export default dynamicPromptsSlice.reducer;
export const dynamicPromptsSelector = (state: RootState) =>
state.dynamicPrompts;

View File

@@ -1,19 +1,16 @@
import { MenuGroup, MenuItem, MenuList } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
import {
autoAddBoardIdChanged,
boardIdSelected,
} from 'features/gallery/store/gallerySlice';
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
import { MouseEvent, memo, useCallback, useMemo } from 'react';
import { FaFolder, FaPlus } from 'react-icons/fa';
import { FaPlus } from 'react-icons/fa';
import { useBoardName } from 'services/api/hooks/useBoardName';
import { BoardDTO } from 'services/api/types';
import { menuListMotionProps } from 'theme/components/menu';
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
import NoBoardContextMenuItems from './NoBoardContextMenuItems';
import { useBoardName } from 'services/api/hooks/useBoardName';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
type Props = {
board?: BoardDTO;
@@ -29,20 +26,15 @@ const BoardContextMenu = memo(
const selector = useMemo(
() =>
createSelector(stateSelector, ({ gallery }) => {
const isSelected = gallery.selectedBoardId === board_id;
const isAutoAdd = gallery.autoAddBoardId === board_id;
return { isSelected, isAutoAdd };
return { isAutoAdd };
}),
[board_id]
);
const { isSelected, isAutoAdd } = useAppSelector(selector);
const { isAutoAdd } = useAppSelector(selector);
const boardName = useBoardName(board_id);
const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected(board_id));
}, [board_id, dispatch]);
const handleSetAutoAdd = useCallback(() => {
dispatch(autoAddBoardIdChanged(board_id));
}, [board_id, dispatch]);

View File

@@ -1,21 +1,16 @@
import { ButtonGroup, Collapse, Flex, Grid, GridItem } from '@chakra-ui/react';
import { Collapse, Flex, Grid, GridItem } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIIconButton from 'common/components/IAIIconButton';
import { AnimatePresence, motion } from 'framer-motion';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { memo, useCallback, useState } from 'react';
import { FaSearch } from 'react-icons/fa';
import { memo, useState } from 'react';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
import { BoardDTO } from 'services/api/types';
import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
import DeleteBoardModal from '../DeleteBoardModal';
import AddBoardButton from './AddBoardButton';
import BoardsSearch from './BoardsSearch';
import GalleryBoard from './GalleryBoard';
import SystemBoardButton from './SystemBoardButton';
import NoBoardBoard from './NoBoardBoard';
const selector = createSelector(
@@ -36,7 +31,6 @@ const BoardsList = (props: Props) => {
const { isOpen } = props;
const { selectedBoardId, searchText } = useAppSelector(selector);
const { data: boards } = useListAllBoardsQuery();
const isBatchEnabled = useFeatureStatus('batches').isFeatureEnabled;
const filteredBoards = searchText
? boards?.filter((board) =>
board.board_name.toLowerCase().includes(searchText.toLowerCase())

View File

@@ -1,7 +1,5 @@
import {
Badge,
Box,
ChakraProps,
Editable,
EditableInput,
EditablePreview,
@@ -17,21 +15,16 @@ import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDroppable from 'common/components/IAIDroppable';
import SelectionOverlay from 'common/components/SelectionOverlay';
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo, useState } from 'react';
import { FaUser } from 'react-icons/fa';
import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
import { BoardDTO } from 'services/api/types';
import AutoAddIcon from '../AutoAddIcon';
import BoardContextMenu from '../BoardContextMenu';
import SelectionOverlay from 'common/components/SelectionOverlay';
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
bg: 'base.500',
color: 'whiteAlpha.900',
};
interface GalleryBoardProps {
board: BoardDTO;
isSelected: boolean;
@@ -68,8 +61,6 @@ const GalleryBoard = memo(
board.cover_image_name ?? skipToken
);
const { totalImages, totalAssets } = useBoardTotal(board.board_id);
const { board_name, board_id } = board;
const [localBoardName, setLocalBoardName] = useState(board_name);

View File

@@ -2,7 +2,7 @@ import { As, Badge, Flex } from '@chakra-ui/react';
import { TypesafeDroppableData } from 'app/components/ImageDnd/typesafeDnd';
import IAIDroppable from 'common/components/IAIDroppable';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { BoardId } from 'features/gallery/store/gallerySlice';
import { BoardId } from 'features/gallery/store/types';
import { ReactNode } from 'react';
import BoardContextMenu from '../BoardContextMenu';

View File

@@ -1,4 +1,4 @@
import { Box, ChakraProps, Flex, Image, Text } from '@chakra-ui/react';
import { Box, Flex, Image, Text } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { MoveBoardDropData } from 'app/components/ImageDnd/typesafeDnd';
import { stateSelector } from 'app/store/store';
@@ -10,14 +10,8 @@ import SelectionOverlay from 'common/components/SelectionOverlay';
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo, useState } from 'react';
import { useBoardName } from 'services/api/hooks/useBoardName';
import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
import AutoAddIcon from '../AutoAddIcon';
import BoardContextMenu from '../BoardContextMenu';
const BASE_BADGE_STYLES: ChakraProps['sx'] = {
bg: 'base.500',
color: 'whiteAlpha.900',
};
interface Props {
isSelected: boolean;
}
@@ -33,7 +27,6 @@ const selector = createSelector(
const NoBoardBoard = memo(({ isSelected }: Props) => {
const dispatch = useAppDispatch();
const { totalImages, totalAssets } = useBoardTotal(undefined);
const { autoAddBoardId } = useAppSelector(selector);
const boardName = useBoardName(undefined);
const handleSelectBoard = useCallback(() => {

View File

@@ -11,14 +11,12 @@ import {
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { skipToken } from '@reduxjs/toolkit/dist/query';
import { ImageUsage } from 'app/contexts/AddImageToBoardContext';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import ImageUsageMessage from 'features/imageDeletion/components/ImageUsageMessage';
import {
ImageUsage,
getImageUsage,
} from 'features/imageDeletion/store/imageDeletionSlice';
import { getImageUsage } from 'features/imageDeletion/store/imageDeletionSelectors';
import { some } from 'lodash-es';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -1,11 +1,6 @@
import { MenuItem } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react';
import { FaPlus, FaTrash } from 'react-icons/fa';
import { memo, useCallback } from 'react';
import { FaTrash } from 'react-icons/fa';
import { BoardDTO } from 'services/api/types';
type Props = {
@@ -14,25 +9,6 @@ type Props = {
};
const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createSelector(
stateSelector,
({ gallery }) => {
const isSelectedForAutoAdd =
board.board_id === gallery.autoAddBoardId;
return { isSelectedForAutoAdd };
},
defaultSelectorOptions
),
[board.board_id]
);
const { isSelectedForAutoAdd } = useAppSelector(selector);
const handleDelete = useCallback(() => {
if (!setBoardToDelete) {
return;
@@ -40,12 +16,6 @@ const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
setBoardToDelete(board);
}, [board, setBoardToDelete]);
const handleToggleAutoAdd = useCallback(() => {
dispatch(
autoAddBoardIdChanged(isSelectedForAutoAdd ? undefined : board.board_id)
);
}, [board.board_id, dispatch, isSelectedForAutoAdd]);
return (
<>
{board.image_count > 0 && (
@@ -59,11 +29,6 @@ const GalleryBoardContextMenuItems = ({ board, setBoardToDelete }: Props) => {
</MenuItem> */}
</>
)}
{/* {!isSelectedForAutoAdd && (
<MenuItem icon={<FaPlus />} onClick={handleToggleAutoAdd}>
Auto-add to this Board
</MenuItem>
)} */}
<MenuItem
sx={{ color: 'error.600', _dark: { color: 'error.300' } }}
icon={<FaTrash />}

View File

@@ -1,19 +1,6 @@
import { MenuItem } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
import { memo, useCallback } from 'react';
import { FaPlus } from 'react-icons/fa';
import { memo } from 'react';
const NoBoardContextMenuItems = () => {
const dispatch = useAppDispatch();
const autoAddBoardId = useAppSelector(
(state) => state.gallery.autoAddBoardId
);
const handleDisableAutoAdd = useCallback(() => {
dispatch(autoAddBoardIdChanged(undefined));
}, [dispatch]);
return (
<>
{/* {autoAddBoardId && (

View File

@@ -108,7 +108,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
500
);
const { currentData: imageDTO, isFetching } = useGetImageDTOQuery(
const { currentData: imageDTO } = useGetImageDTOQuery(
lastSelectedImage ?? skipToken
);
@@ -210,6 +210,14 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
[imageDTO, shouldShowImageDetails, toaster]
);
useHotkeys(
'delete',
() => {
handleDelete();
},
[dispatch, imageDTO]
);
const handleClickProgressImagesToggle = useCallback(() => {
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
}, [dispatch, shouldShowProgressInViewer]);

View File

@@ -8,17 +8,17 @@ import {
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { useNextPrevImage } from 'features/gallery/hooks/useNextPrevImage';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import { AnimatePresence, motion } from 'framer-motion';
import { isEqual } from 'lodash-es';
import { memo, useCallback, useMemo, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { FaImage } from 'react-icons/fa';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import ImageMetadataViewer from '../ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from '../NextPrevImageButtons';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { FaImage } from 'react-icons/fa';
export const imagesSelector = createSelector(
[stateSelector, selectLastSelectedImage],
@@ -93,12 +93,7 @@ const CurrentImagePreview = () => {
]
);
const {
currentData: imageDTO,
isLoading,
isError,
isSuccess,
} = useGetImageDTOQuery(imageName ?? skipToken);
const { currentData: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken);
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
if (imageDTO) {

Some files were not shown because too many files have changed in this diff Show More