diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 6220e98ca2..0b7891e0f2 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -83,7 +83,7 @@ async def get_thumbnail( status_code=201, ) async def upload_image( - file: UploadFile, request: Request, response: Response + file: UploadFile, image_type: ImageType, request: Request, response: Response ) -> ImageResponse: if not file.content_type.startswith("image"): raise HTTPException(status_code=415, detail="Not an image") @@ -99,21 +99,21 @@ async def upload_image( filename = f"{uuid.uuid4()}_{str(int(datetime.now(timezone.utc).timestamp()))}.png" saved_image = ApiDependencies.invoker.services.images.save( - ImageType.UPLOAD, filename, img + image_type, filename, img ) invokeai_metadata = ApiDependencies.invoker.services.metadata.get_metadata(img) image_url = ApiDependencies.invoker.services.images.get_uri( - ImageType.UPLOAD, saved_image.image_name + image_type, saved_image.image_name ) thumbnail_url = ApiDependencies.invoker.services.images.get_uri( - ImageType.UPLOAD, saved_image.image_name, True + image_type, saved_image.image_name, True ) res = ImageResponse( - image_type=ImageType.UPLOAD, + image_type=image_type, image_name=saved_image.image_name, image_url=image_url, thumbnail_url=thumbnail_url, diff --git a/invokeai/app/invocations/collections.py b/invokeai/app/invocations/collections.py index 24a89c2cf4..93130bfaad 100644 --- a/invokeai/app/invocations/collections.py +++ b/invokeai/app/invocations/collections.py @@ -3,12 +3,12 @@ from typing import Literal, Optional import numpy as np -import numpy.random from pydantic import Field +from invokeai.app.util.misc import SEED_MAX, get_random_seed + from .baseinvocation import ( BaseInvocation, - InvocationConfig, InvocationContext, BaseInvocationOutput, ) @@ -50,11 +50,11 @@ class RandomRangeInvocation(BaseInvocation): default=np.iinfo(np.int32).max, description="The exclusive high value" ) size: int = Field(default=1, description="The number of values to generate") - seed: Optional[int] = Field( + seed: int = Field( ge=0, - le=np.iinfo(np.int32).max, - description="The seed for the RNG", - default_factory=lambda: numpy.random.randint(0, np.iinfo(np.int32).max), + le=SEED_MAX, + description="The seed for the RNG (omit for random)", + default_factory=get_random_seed, ) def invoke(self, context: InvocationContext) -> IntCollectionOutput: diff --git a/invokeai/app/invocations/generate.py b/invokeai/app/invocations/generate.py index 580df3987d..9a29502048 100644 --- a/invokeai/app/invocations/generate.py +++ b/invokeai/app/invocations/generate.py @@ -1,15 +1,17 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) from functools import partial -from typing import Literal, Optional, Union +from typing import Literal, Optional, Union, get_args import numpy as np from torch import Tensor from pydantic import BaseModel, Field -from invokeai.app.models.image import ImageField, ImageType +from invokeai.app.models.image import ColorField, ImageField, ImageType from invokeai.app.invocations.util.choose_model import choose_model +from invokeai.app.util.misc import SEED_MAX, get_random_seed +from invokeai.backend.generator.inpaint import infill_methods from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig from .image import ImageOutput, build_image_output from ...backend.generator import Txt2Img, Img2Img, Inpaint, InvokeAIGenerator @@ -17,7 +19,8 @@ from ...backend.stable_diffusion import PipelineIntermediateState from ..util.step_callback import stable_diffusion_step_callback SAMPLER_NAME_VALUES = Literal[tuple(InvokeAIGenerator.schedulers())] - +INFILL_METHODS = Literal[tuple(infill_methods())] +DEFAULT_INFILL_METHOD = 'patchmatch' if 'patchmatch' in get_args(INFILL_METHODS) else 'tile' class SDImageInvocation(BaseModel): """Helper class to provide all Stable Diffusion raster image invocations with additional config""" @@ -44,15 +47,13 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): # TODO: consider making prompt optional to enable providing prompt through a link # fmt: off prompt: Optional[str] = Field(description="The prompt to generate an image from") - seed: int = Field(default=-1,ge=-1, le=np.iinfo(np.uint32).max, description="The seed to use (-1 for a random seed)", ) - steps: int = Field(default=10, gt=0, description="The number of steps to use to generate the image") + seed: int = Field(ge=0, le=SEED_MAX, description="The seed to use (omit for random)", default_factory=get_random_seed) + steps: int = Field(default=30, gt=0, description="The number of steps to use to generate the image") width: int = Field(default=512, multiple_of=8, gt=0, description="The width of the resulting image", ) height: int = Field(default=512, multiple_of=8, gt=0, description="The height of the resulting image", ) - cfg_scale: float = Field(default=7.5, gt=0, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", ) + cfg_scale: float = Field(default=7.5, ge=1, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", ) scheduler: SAMPLER_NAME_VALUES = Field(default="k_lms", description="The scheduler to use" ) - seamless: bool = Field(default=False, description="Whether or not to generate an image that can tile without seams", ) model: str = Field(default="", description="The model to use (currently ignored)") - progress_images: bool = Field(default=False, description="Whether or not to produce progress images during generation", ) # fmt: on # TODO: pass this an emitter method or something? or a session for dispatching? @@ -148,7 +149,6 @@ class ImageToImageInvocation(TextToImageInvocation): self.image.image_type, self.image.image_name ) ) - mask = None if self.fit: image = image.resize((self.width, self.height)) @@ -165,7 +165,6 @@ class ImageToImageInvocation(TextToImageInvocation): outputs = Img2Img(model).generate( prompt=self.prompt, init_image=image, - init_mask=mask, step_callback=partial(self.dispatch_progress, context, source_node_id), **self.dict( exclude={"prompt", "image", "mask"} @@ -197,7 +196,6 @@ class ImageToImageInvocation(TextToImageInvocation): image=result_image, ) - class InpaintInvocation(ImageToImageInvocation): """Generates an image using inpaint.""" @@ -205,6 +203,17 @@ class InpaintInvocation(ImageToImageInvocation): # Inputs mask: Union[ImageField, None] = Field(description="The mask") + seam_size: int = Field(default=96, ge=1, description="The seam inpaint size (px)") + seam_blur: int = Field(default=16, ge=0, description="The seam inpaint blur radius (px)") + seam_strength: float = Field( + default=0.75, gt=0, le=1, description="The seam inpaint strength" + ) + seam_steps: int = Field(default=30, ge=1, description="The number of steps to use for seam inpaint") + tile_size: int = Field(default=32, ge=1, description="The tile infill method size (px)") + infill_method: INFILL_METHODS = Field(default=DEFAULT_INFILL_METHOD, description="The method used to infill empty regions (px)") + inpaint_width: Optional[int] = Field(default=None, multiple_of=8, gt=0, description="The width of the inpaint region (px)") + inpaint_height: Optional[int] = Field(default=None, multiple_of=8, gt=0, description="The height of the inpaint region (px)") + inpaint_fill: Optional[ColorField] = Field(default=ColorField(r=127, g=127, b=127, a=255), description="The solid infill method color") inpaint_replace: float = Field( default=0.0, ge=0.0, diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 883ef63f69..d32f96857d 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1,5 +1,6 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) +import io from typing import Literal, Optional import numpy @@ -37,9 +38,7 @@ class ImageOutput(BaseInvocationOutput): # fmt: on class Config: - schema_extra = { - "required": ["type", "image", "width", "height", "mode"] - } + schema_extra = {"required": ["type", "image", "width", "height"]} def build_image_output( @@ -54,7 +53,6 @@ def build_image_output( image=image_field, width=image.width, height=image.height, - mode=image.mode, ) @@ -151,7 +149,7 @@ class CropImageInvocation(BaseInvocation, PILInvocationConfig): metadata = context.services.metadata.build_metadata( session_id=context.graph_execution_state_id, node=self ) - + context.services.images.save(image_type, image_name, image_crop, metadata) return build_image_output( image_type=image_type, @@ -209,7 +207,7 @@ class PasteImageInvocation(BaseInvocation, PILInvocationConfig): metadata = context.services.metadata.build_metadata( session_id=context.graph_execution_state_id, node=self ) - + context.services.images.save(image_type, image_name, new_image, metadata) return build_image_output( image_type=image_type, diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py new file mode 100644 index 0000000000..ac055cef5b --- /dev/null +++ b/invokeai/app/invocations/infill.py @@ -0,0 +1,233 @@ +# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) + +from typing import Literal, Optional, Union, get_args + +import numpy as np +import math +from PIL import Image, ImageOps +from pydantic import Field + +from invokeai.app.invocations.image import ImageOutput, build_image_output +from invokeai.app.util.misc import SEED_MAX, get_random_seed +from invokeai.backend.image_util.patchmatch import PatchMatch + +from ..models.image import ColorField, ImageField, ImageType +from .baseinvocation import ( + BaseInvocation, + InvocationContext, +) + + +def infill_methods() -> list[str]: + methods = [ + "tile", + "solid", + ] + if PatchMatch.patchmatch_available(): + methods.insert(0, "patchmatch") + return methods + + +INFILL_METHODS = Literal[tuple(infill_methods())] +DEFAULT_INFILL_METHOD = ( + "patchmatch" if "patchmatch" in get_args(INFILL_METHODS) else "tile" +) + + +def infill_patchmatch(im: Image.Image) -> Image.Image: + if im.mode != "RGBA": + return im + + # Skip patchmatch if patchmatch isn't available + if not PatchMatch.patchmatch_available(): + return im + + # Patchmatch (note, we may want to expose patch_size? Increasing it significantly impacts performance though) + im_patched_np = PatchMatch.inpaint( + im.convert("RGB"), ImageOps.invert(im.split()[-1]), patch_size=3 + ) + im_patched = Image.fromarray(im_patched_np, mode="RGB") + return im_patched + + +def get_tile_images(image: np.ndarray, width=8, height=8): + _nrows, _ncols, depth = image.shape + _strides = image.strides + + nrows, _m = divmod(_nrows, height) + ncols, _n = divmod(_ncols, width) + if _m != 0 or _n != 0: + return None + + return np.lib.stride_tricks.as_strided( + np.ravel(image), + shape=(nrows, ncols, height, width, depth), + strides=(height * _strides[0], width * _strides[1], *_strides), + writeable=False, + ) + + +def tile_fill_missing( + im: Image.Image, tile_size: int = 16, seed: Union[int, None] = None +) -> Image.Image: + # Only fill if there's an alpha layer + if im.mode != "RGBA": + return im + + a = np.asarray(im, dtype=np.uint8) + + tile_size_tuple = (tile_size, tile_size) + + # Get the image as tiles of a specified size + tiles = get_tile_images(a, *tile_size_tuple).copy() + + # Get the mask as tiles + tiles_mask = tiles[:, :, :, :, 3] + + # Find any mask tiles with any fully transparent pixels (we will be replacing these later) + tmask_shape = tiles_mask.shape + tiles_mask = tiles_mask.reshape(math.prod(tiles_mask.shape)) + n, ny = (math.prod(tmask_shape[0:2])), math.prod(tmask_shape[2:]) + tiles_mask = tiles_mask > 0 + tiles_mask = tiles_mask.reshape((n, ny)).all(axis=1) + + # Get RGB tiles in single array and filter by the mask + tshape = tiles.shape + tiles_all = tiles.reshape((math.prod(tiles.shape[0:2]), *tiles.shape[2:])) + filtered_tiles = tiles_all[tiles_mask] + + if len(filtered_tiles) == 0: + return im + + # Find all invalid tiles and replace with a random valid tile + replace_count = (tiles_mask == False).sum() + rng = np.random.default_rng(seed=seed) + tiles_all[np.logical_not(tiles_mask)] = filtered_tiles[ + rng.choice(filtered_tiles.shape[0], replace_count), :, :, : + ] + + # Convert back to an image + tiles_all = tiles_all.reshape(tshape) + tiles_all = tiles_all.swapaxes(1, 2) + st = tiles_all.reshape( + ( + math.prod(tiles_all.shape[0:2]), + math.prod(tiles_all.shape[2:4]), + tiles_all.shape[4], + ) + ) + si = Image.fromarray(st, mode="RGBA") + + return si + + +class InfillColorInvocation(BaseInvocation): + """Infills transparent areas of an image with a solid color""" + + type: Literal["infill_rgba"] = "infill_rgba" + image: Optional[ImageField] = Field(default=None, description="The image to infill") + color: Optional[ColorField] = Field( + default=ColorField(r=127, g=127, b=127, a=255), + description="The color to use to infill", + ) + + def invoke(self, context: InvocationContext) -> ImageOutput: + image = context.services.images.get( + self.image.image_type, self.image.image_name + ) + + solid_bg = Image.new("RGBA", image.size, self.color.tuple()) + infilled = Image.alpha_composite(solid_bg, image) + + infilled.paste(image, (0, 0), image.split()[-1]) + + image_type = ImageType.RESULT + image_name = context.services.images.create_name( + context.graph_execution_state_id, self.id + ) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, infilled, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=image, + ) + + +class InfillTileInvocation(BaseInvocation): + """Infills transparent areas of an image with tiles of the image""" + + type: Literal["infill_tile"] = "infill_tile" + + image: Optional[ImageField] = Field(default=None, description="The image to infill") + tile_size: int = Field(default=32, ge=1, description="The tile size (px)") + seed: int = Field( + ge=0, + le=SEED_MAX, + description="The seed to use for tile generation (omit for random)", + default_factory=get_random_seed, + ) + + def invoke(self, context: InvocationContext) -> ImageOutput: + image = context.services.images.get( + self.image.image_type, self.image.image_name + ) + + infilled = tile_fill_missing( + image.copy(), seed=self.seed, tile_size=self.tile_size + ) + infilled.paste(image, (0, 0), image.split()[-1]) + + image_type = ImageType.RESULT + image_name = context.services.images.create_name( + context.graph_execution_state_id, self.id + ) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, infilled, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=image, + ) + + +class InfillPatchMatchInvocation(BaseInvocation): + """Infills transparent areas of an image using the PatchMatch algorithm""" + + type: Literal["infill_patchmatch"] = "infill_patchmatch" + + image: Optional[ImageField] = Field(default=None, description="The image to infill") + + def invoke(self, context: InvocationContext) -> ImageOutput: + image = context.services.images.get( + self.image.image_type, self.image.image_name + ) + + if PatchMatch.patchmatch_available(): + infilled = infill_patchmatch(image.copy()) + else: + raise ValueError("PatchMatch is not available on this system") + + image_type = ImageType.RESULT + image_name = context.services.images.create_name( + context.graph_execution_state_id, self.id + ) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, infilled, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=image, + ) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 0d3ef4a8cd..c6ddcb0396 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -1,11 +1,13 @@ # Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) import random -from typing import Literal, Optional +from typing import Literal, Optional, Union +import einops from pydantic import BaseModel, Field import torch from invokeai.app.invocations.util.choose_model import choose_model +from invokeai.app.util.misc import SEED_MAX, get_random_seed from invokeai.app.util.step_callback import stable_diffusion_step_callback @@ -13,7 +15,8 @@ from ...backend.model_management.model_manager import ModelManager from ...backend.util.devices import choose_torch_device, torch_dtype from ...backend.stable_diffusion.diffusion.shared_invokeai_diffusion import PostprocessingSettings from ...backend.image_util.seamless import configure_model_padding -from ...backend.stable_diffusion.diffusers_pipeline import ConditioningData, StableDiffusionGeneratorPipeline +from ...backend.prompting.conditioning import get_uc_and_c_and_ec +from ...backend.stable_diffusion.diffusers_pipeline import ConditioningData, StableDiffusionGeneratorPipeline, image_resized_to_grid_as_tensor from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext, InvocationConfig import numpy as np from ..services.image_storage import ImageType @@ -102,17 +105,13 @@ def get_noise(width:int, height:int, device:torch.device, seed:int = 0, latent_c return x -def random_seed(): - return random.randint(0, np.iinfo(np.uint32).max) - - class NoiseInvocation(BaseInvocation): """Generates latent noise.""" type: Literal["noise"] = "noise" # Inputs - seed: int = Field(ge=0, le=np.iinfo(np.uint32).max, description="The seed to use", default_factory=random_seed) + seed: int = Field(ge=0, le=SEED_MAX, description="The seed to use", default_factory=get_random_seed) width: int = Field(default=512, multiple_of=8, gt=0, description="The width of the resulting noise", ) height: int = Field(default=512, multiple_of=8, gt=0, description="The height of the resulting noise", ) @@ -150,10 +149,9 @@ class TextToLatentsInvocation(BaseInvocation): steps: int = Field(default=10, gt=0, description="The number of steps to use to generate the image") cfg_scale: float = Field(default=7.5, gt=0, description="The Classifier-Free Guidance, higher values may result in a result closer to the prompt", ) scheduler: SAMPLER_NAME_VALUES = Field(default="k_lms", description="The scheduler to use" ) + model: str = Field(default="", description="The model to use (currently ignored)") seamless: bool = Field(default=False, description="Whether or not to generate an image that can tile without seams", ) seamless_axes: str = Field(default="", description="The axes to tile the image on, 'x' and/or 'y'") - model: str = Field(default="", description="The model to use (currently ignored)") - progress_images: bool = Field(default=False, description="Whether or not to produce progress images during generation", ) # fmt: on # Schema customisation @@ -260,6 +258,10 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation): type: Literal["l2l"] = "l2l" + # Inputs + latents: Optional[LatentsField] = Field(description="The latents to use as a base image") + strength: float = Field(default=0.5, description="The strength of the latents to use") + # Schema customisation class Config(InvocationConfig): schema_extra = { @@ -271,10 +273,6 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation): }, } - # Inputs - latents: Optional[LatentsField] = Field(description="The latents to use as a base image") - strength: float = Field(default=0.5, description="The strength of the latents to use") - def invoke(self, context: InvocationContext) -> LatentsOutput: noise = context.services.latents.get(self.noise.latents_name) latent = context.services.latents.get(self.latents.latents_name) @@ -433,3 +431,47 @@ class ScaleLatentsInvocation(BaseInvocation): name = f"{context.graph_execution_state_id}__{self.id}" context.services.latents.set(name, resized_latents) return LatentsOutput(latents=LatentsField(latents_name=name)) + + +class ImageToLatentsInvocation(BaseInvocation): + """Encodes an image into latents.""" + + type: Literal["i2l"] = "i2l" + + # Inputs + image: Union[ImageField, None] = Field(description="The image to encode") + model: str = Field(default="", description="The model to use") + + # Schema customisation + class Config(InvocationConfig): + schema_extra = { + "ui": { + "tags": ["latents", "image"], + "type_hints": {"model": "model"}, + }, + } + + @torch.no_grad() + def invoke(self, context: InvocationContext) -> LatentsOutput: + image = context.services.images.get( + self.image.image_type, self.image.image_name + ) + + # TODO: this only really needs the vae + model_info = choose_model(context.services.model_manager, self.model) + model: StableDiffusionGeneratorPipeline = model_info["model"] + + image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB")) + + if image_tensor.dim() == 3: + image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w") + + latents = model.non_noised_latents_from_image( + image_tensor, + device=model._model_group.device_for(model.unet), + dtype=model.unet.dtype, + ) + + name = f"{context.graph_execution_state_id}__{self.id}" + context.services.latents.set(name, latents) + return LatentsOutput(latents=LatentsField(latents_name=name)) diff --git a/invokeai/app/invocations/util/choose_model.py b/invokeai/app/invocations/util/choose_model.py index cd03ce87a8..4c5ddca00d 100644 --- a/invokeai/app/invocations/util/choose_model.py +++ b/invokeai/app/invocations/util/choose_model.py @@ -4,10 +4,11 @@ from invokeai.backend.model_management.model_manager import ModelManager def choose_model(model_manager: ModelManager, model_name: str): """Returns the default model if the `model_name` not a valid model, else returns the selected model.""" logger = model_manager.logger - if model_manager.valid_model(model_name): - model = model_manager.get_model(model_name) - else: + if model_name and not model_manager.valid_model(model_name): + default_model_name = model_manager.default_model() + logger.warning(f"\'{model_name}\' is not a valid model name. Using default model \'{default_model_name}\' instead.") model = model_manager.get_model() - logger.warning(f"{model_name}' is not a valid model name. Using default model \'{model['model_name']}\' instead.") + else: + model = model_manager.get_model(model_name) return model diff --git a/invokeai/app/models/image.py b/invokeai/app/models/image.py index 5ef1ab0d35..f6813c6d96 100644 --- a/invokeai/app/models/image.py +++ b/invokeai/app/models/image.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional +from typing import Optional, Tuple from pydantic import BaseModel, Field @@ -27,3 +27,13 @@ class ImageField(BaseModel): class Config: schema_extra = {"required": ["image_type", "image_name"]} + + +class ColorField(BaseModel): + r: int = Field(ge=0, le=255, description="The red component") + g: int = Field(ge=0, le=255, description="The green component") + b: int = Field(ge=0, le=255, description="The blue component") + a: int = Field(ge=0, le=255, description="The alpha component") + + def tuple(self) -> Tuple[int, int, int, int]: + return (self.r, self.g, self.b, self.a) diff --git a/invokeai/app/services/default_graphs.py b/invokeai/app/services/default_graphs.py index c8347c043f..0ac6b08b4d 100644 --- a/invokeai/app/services/default_graphs.py +++ b/invokeai/app/services/default_graphs.py @@ -51,7 +51,7 @@ def create_system_graphs(graph_library: ItemStorageABC[LibraryGraph]) -> list[Li graphs: list[LibraryGraph] = list() - text_to_image = graph_library.get(default_text_to_image_graph_id) + # text_to_image = graph_library.get(default_text_to_image_graph_id) # TODO: Check if the graph is the same as the default one, and if not, update it #if text_to_image is None: diff --git a/invokeai/app/services/metadata.py b/invokeai/app/services/metadata.py index 2c8bb0d26b..a7f2378ab1 100644 --- a/invokeai/app/services/metadata.py +++ b/invokeai/app/services/metadata.py @@ -20,9 +20,18 @@ class MetadataLatentsField(TypedDict): latents_name: str +class MetadataColorField(TypedDict): + """Pydantic-less ColorField, used for metadata parsing""" + r: int + g: int + b: int + a: int + + + # TODO: This is a placeholder for `InvocationsUnion` pending resolution of circular imports NodeMetadata = Dict[ - str, str | int | float | bool | MetadataImageField | MetadataLatentsField + str, None | str | int | float | bool | MetadataImageField | MetadataLatentsField | MetadataColorField ] diff --git a/invokeai/app/util/misc.py b/invokeai/app/util/misc.py index b2b57bd086..c3d091b653 100644 --- a/invokeai/app/util/misc.py +++ b/invokeai/app/util/misc.py @@ -1,5 +1,13 @@ import datetime +import numpy as np def get_timestamp(): return int(datetime.datetime.now(datetime.timezone.utc).timestamp()) + + +SEED_MAX = np.iinfo(np.int32).max + + +def get_random_seed(): + return np.random.randint(0, SEED_MAX) diff --git a/invokeai/backend/generator/base.py b/invokeai/backend/generator/base.py index 8ad9dec026..9887434e90 100644 --- a/invokeai/backend/generator/base.py +++ b/invokeai/backend/generator/base.py @@ -226,10 +226,10 @@ class Inpaint(Img2Img): def generate(self, mask_image: Image.Image | torch.FloatTensor, # Seam settings - when 0, doesn't fill seam - seam_size: int = 0, - seam_blur: int = 0, + seam_size: int = 96, + seam_blur: int = 16, seam_strength: float = 0.7, - seam_steps: int = 10, + seam_steps: int = 30, tile_size: int = 32, inpaint_replace=False, infill_method=None, diff --git a/invokeai/backend/generator/inpaint.py b/invokeai/backend/generator/inpaint.py index 138779c864..8c471d025d 100644 --- a/invokeai/backend/generator/inpaint.py +++ b/invokeai/backend/generator/inpaint.py @@ -4,6 +4,7 @@ invokeai.backend.generator.inpaint descends from .generator from __future__ import annotations import math +from typing import Tuple, Union import cv2 import numpy as np @@ -59,7 +60,7 @@ class Inpaint(Img2Img): writeable=False, ) - def infill_patchmatch(self, im: Image.Image) -> Image: + def infill_patchmatch(self, im: Image.Image) -> Image.Image: if im.mode != "RGBA": return im @@ -75,18 +76,18 @@ class Inpaint(Img2Img): return im_patched def tile_fill_missing( - self, im: Image.Image, tile_size: int = 16, seed: int = None - ) -> Image: + self, im: Image.Image, tile_size: int = 16, seed: Union[int, None] = None + ) -> Image.Image: # Only fill if there's an alpha layer if im.mode != "RGBA": return im a = np.asarray(im, dtype=np.uint8) - tile_size = (tile_size, tile_size) + tile_size_tuple = (tile_size, tile_size) # Get the image as tiles of a specified size - tiles = self.get_tile_images(a, *tile_size).copy() + tiles = self.get_tile_images(a, *tile_size_tuple).copy() # Get the mask as tiles tiles_mask = tiles[:, :, :, :, 3] @@ -127,7 +128,9 @@ class Inpaint(Img2Img): return si - def mask_edge(self, mask: Image, edge_size: int, edge_blur: int) -> Image: + def mask_edge( + self, mask: Image.Image, edge_size: int, edge_blur: int + ) -> Image.Image: npimg = np.asarray(mask, dtype=np.uint8) # Detect any partially transparent regions @@ -206,15 +209,15 @@ class Inpaint(Img2Img): cfg_scale, ddim_eta, conditioning, - init_image: PIL.Image.Image | torch.FloatTensor, - mask_image: PIL.Image.Image | torch.FloatTensor, + init_image: Image.Image | torch.FloatTensor, + mask_image: Image.Image | torch.FloatTensor, strength: float, mask_blur_radius: int = 8, # Seam settings - when 0, doesn't fill seam - seam_size: int = 0, - seam_blur: int = 0, + seam_size: int = 96, + seam_blur: int = 16, seam_strength: float = 0.7, - seam_steps: int = 10, + seam_steps: int = 30, tile_size: int = 32, step_callback=None, inpaint_replace=False, @@ -222,7 +225,7 @@ class Inpaint(Img2Img): infill_method=None, inpaint_width=None, inpaint_height=None, - inpaint_fill: tuple(int) = (0x7F, 0x7F, 0x7F, 0xFF), + inpaint_fill: Tuple[int, int, int, int] = (0x7F, 0x7F, 0x7F, 0xFF), attention_maps_callback=None, **kwargs, ): @@ -239,7 +242,7 @@ class Inpaint(Img2Img): self.inpaint_width = inpaint_width self.inpaint_height = inpaint_height - if isinstance(init_image, PIL.Image.Image): + if isinstance(init_image, Image.Image): self.pil_image = init_image.copy() # Do infill @@ -250,8 +253,8 @@ class Inpaint(Img2Img): self.pil_image.copy(), seed=self.seed, tile_size=tile_size ) elif infill_method == "solid": - solid_bg = PIL.Image.new("RGBA", init_image.size, inpaint_fill) - init_filled = PIL.Image.alpha_composite(solid_bg, init_image) + solid_bg = Image.new("RGBA", init_image.size, inpaint_fill) + init_filled = Image.alpha_composite(solid_bg, init_image) else: raise ValueError( f"Non-supported infill type {infill_method}", infill_method @@ -269,7 +272,7 @@ class Inpaint(Img2Img): # Create init tensor init_image = image_resized_to_grid_as_tensor(init_filled.convert("RGB")) - if isinstance(mask_image, PIL.Image.Image): + if isinstance(mask_image, Image.Image): self.pil_mask = mask_image.copy() debug_image( mask_image, diff --git a/invokeai/backend/prompting/conditioning.py b/invokeai/backend/prompting/conditioning.py index f94f82ef72..71e51f1103 100644 --- a/invokeai/backend/prompting/conditioning.py +++ b/invokeai/backend/prompting/conditioning.py @@ -64,6 +64,7 @@ def get_uc_and_c_and_ec(prompt_string, step_count=-1): c, options = compel.build_conditioning_tensor_for_prompt_object(positive_prompt) uc, _ = compel.build_conditioning_tensor_for_prompt_object(negative_prompt) + [c, uc] = compel.pad_conditioning_tensors_to_same_length([c, uc]) # now build the "real" ec ec = InvokeAIDiffuserComponent.ExtraConditioningInfo(tokens_count_including_eos_bos=tokens_count, diff --git a/invokeai/frontend/web/.babelrc b/invokeai/frontend/web/.babelrc deleted file mode 100644 index 809872138a..0000000000 --- a/invokeai/frontend/web/.babelrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "plugins": [ - [ - "transform-imports", - { - "lodash": { - "transform": "lodash/${member}", - "preventFullImport": true - } - } - ] - ] -} diff --git a/invokeai/frontend/web/.gitignore b/invokeai/frontend/web/.gitignore index 83c5fcf2d7..cacf107e1b 100644 --- a/invokeai/frontend/web/.gitignore +++ b/invokeai/frontend/web/.gitignore @@ -34,4 +34,8 @@ stats.html !.yarn/plugins !.yarn/releases !.yarn/sdks -!.yarn/versions \ No newline at end of file +!.yarn/versions + +# Yalc +.yalc +yalc.lock \ No newline at end of file diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index ef61f65cf4..561efdd8e9 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -21,7 +21,6 @@ "scripts": { "prepare": "cd ../../../ && husky install invokeai/frontend/web/.husky", "dev": "concurrently \"vite dev\" \"yarn run theme:watch\"", - "dev:nodes": "concurrently \"vite dev --mode nodes\" \"yarn run theme:watch\"", "dev:host": "concurrently \"vite dev --host\" \"yarn run theme:watch\"", "build": "yarn run lint && vite build", "api:web": "openapi -i http://localhost:9090/openapi.json -o src/services/api --client axios --useOptions --useUnionTypes --exportSchemas true --indent 2 --request src/services/fixtures/request.ts", @@ -90,6 +89,7 @@ "react-konva": "^18.2.7", "react-konva-utils": "^1.0.4", "react-redux": "^8.0.5", + "react-resizable-panels": "^0.0.42", "react-rnd": "^10.4.1", "react-transition-group": "^4.4.5", "react-use": "^17.4.0", @@ -99,6 +99,7 @@ "redux-deep-persist": "^1.0.7", "redux-dynamic-middlewares": "^2.2.0", "redux-persist": "^6.0.0", + "redux-remember": "^3.3.1", "roarr": "^7.15.0", "serialize-error": "^11.0.0", "socket.io-client": "^4.6.0", @@ -118,6 +119,7 @@ "@types/node": "^18.16.2", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.1", + "@types/react-redux": "^7.1.25", "@types/react-transition-group": "^4.4.5", "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^5.59.1", diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 876cd96b39..dccb77c267 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -54,7 +54,7 @@ "img2img": "Image To Image", "unifiedCanvas": "Unified Canvas", "linear": "Linear", - "nodes": "Nodes", + "nodes": "Node Editor", "postprocessing": "Post Processing", "nodesDesc": "A node based system for the generation of images is under development currently. Stay tuned for updates about this amazing feature.", "postProcessing": "Post Processing", @@ -102,7 +102,8 @@ "generate": "Generate", "openInNewTab": "Open in New Tab", "dontAskMeAgain": "Don't ask me again", - "areYouSure": "Are you sure?" + "areYouSure": "Are you sure?", + "imagePrompt": "Image Prompt" }, "gallery": { "generations": "Generations", @@ -453,9 +454,10 @@ "seed": "Seed", "imageToImage": "Image to Image", "randomizeSeed": "Randomize Seed", - "shuffle": "Shuffle", + "shuffle": "Shuffle Seed", "noiseThreshold": "Noise Threshold", "perlinNoise": "Perlin Noise", + "noiseSettings": "Noise", "variations": "Variations", "variationAmount": "Variation Amount", "seedWeights": "Seed Weights", @@ -470,6 +472,8 @@ "scale": "Scale", "otherOptions": "Other Options", "seamlessTiling": "Seamless Tiling", + "seamlessXAxis": "X Axis", + "seamlessYAxis": "Y Axis", "hiresOptim": "High Res Optimization", "hiresStrength": "High Res Strength", "imageFit": "Fit Initial Image To Output Size", @@ -527,7 +531,8 @@ "useCanvasBeta": "Use Canvas Beta Layout", "enableImageDebugging": "Enable Image Debugging", "useSlidersForAll": "Use Sliders For All Options", - "autoShowProgress": "Auto Show Progress Images", + "showProgressInViewer": "Show Progress Images in Viewer", + "antialiasProgressImages": "Antialias Progress Images", "resetWebUI": "Reset Web UI", "resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images and remembered settings. It does not delete any images from disk.", "resetWebUIDesc2": "If images aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.", @@ -549,8 +554,9 @@ "downloadImageStarted": "Image Download Started", "imageCopied": "Image Copied", "imageLinkCopied": "Image Link Copied", + "problemCopyingImageLink": "Unable to Copy Image Link", "imageNotLoaded": "No Image Loaded", - "imageNotLoadedDesc": "No image found to send to image to image module", + "imageNotLoadedDesc": "Could not find image", "imageSavedToGallery": "Image Saved to Gallery", "canvasMerged": "Canvas Merged", "sentToImageToImage": "Sent To Image To Image", @@ -645,7 +651,8 @@ "betaClear": "Clear", "betaDarkenOutside": "Darken Outside", "betaLimitToBox": "Limit To Box", - "betaPreserveMasked": "Preserve Masked" + "betaPreserveMasked": "Preserve Masked", + "antialiasing": "Antialiasing" }, "ui": { "showProgressImages": "Show Progress Images", diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 3aebfa4097..b49e44e554 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -1,6 +1,6 @@ import ImageUploader from 'common/components/ImageUploader'; -import ProgressBar from 'features/system/components/ProgressBar'; import SiteHeader from 'features/system/components/SiteHeader'; +import ProgressBar from 'features/system/components/ProgressBar'; import InvokeTabs from 'features/ui/components/InvokeTabs'; import useToastWatcher from 'features/system/hooks/useToastWatcher'; @@ -9,7 +9,7 @@ import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton' import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons'; import { Box, Flex, Grid, Portal, useColorMode } from '@chakra-ui/react'; import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants'; -import ImageGalleryPanel from 'features/gallery/components/ImageGalleryPanel'; +import GalleryDrawer from 'features/gallery/components/ImageGalleryPanel'; import Lightbox from 'features/lightbox/components/Lightbox'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { @@ -27,7 +27,8 @@ import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys'; import { configChanged } from 'features/system/store/configSlice'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useLogger } from 'app/logging/useLogger'; -import ProgressImagePreview from 'features/parameters/components/ProgressImagePreview'; +import ProgressImagePreview from 'features/parameters/components/_ProgressImagePreview'; +import ParametersDrawer from 'features/ui/components/ParametersDrawer'; const DEFAULT_CONFIG = {}; @@ -84,11 +85,13 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => { flexDir={{ base: 'column', xl: 'row' }} > - + + + {!isApplicationReady && !loadingOverridden && ( { - ); }; diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index 97a8be6fc1..d9d99de6cf 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -1,8 +1,6 @@ import React, { lazy, memo, PropsWithChildren, useEffect } from 'react'; import { Provider } from 'react-redux'; -import { PersistGate } from 'redux-persist/integration/react'; import { store } from 'app/store/store'; -import { persistor } from '../store/persistor'; import { OpenAPI } from 'services/api'; import '@fontsource/inter/100.css'; import '@fontsource/inter/200.css'; @@ -57,13 +55,11 @@ const InvokeAIUI = ({ apiUrl, token, config, children }: Props) => { return ( - } persistor={persistor}> - }> - - {children} - - - + }> + + {children} + + ); diff --git a/invokeai/frontend/web/src/app/selectors/readinessSelector.ts b/invokeai/frontend/web/src/app/selectors/readinessSelector.ts index d70043e545..6fd212494f 100644 --- a/invokeai/frontend/web/src/app/selectors/readinessSelector.ts +++ b/invokeai/frontend/web/src/app/selectors/readinessSelector.ts @@ -1,26 +1,20 @@ import { createSelector } from '@reduxjs/toolkit'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { validateSeedWeights } from 'common/util/seedWeightPairs'; -import { initialCanvasImageSelector } from 'features/canvas/store/canvasSelectors'; import { generationSelector } from 'features/parameters/store/generationSelectors'; import { systemSelector } from 'features/system/store/systemSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { isEqual } from 'lodash-es'; export const readinessSelector = createSelector( - [ - generationSelector, - systemSelector, - initialCanvasImageSelector, - activeTabNameSelector, - ], - (generation, system, initialCanvasImage, activeTabName) => { + [generationSelector, systemSelector, activeTabNameSelector], + (generation, system, activeTabName) => { const { prompt, shouldGenerateVariations, seedWeights, initialImage, seed, - isImageToImageEnabled, } = generation; const { isProcessing, isConnected } = system; @@ -34,7 +28,7 @@ export const readinessSelector = createSelector( reasonsWhyNotReady.push('Missing prompt'); } - if (isImageToImageEnabled && !initialImage) { + if (activeTabName === 'img2img' && !initialImage) { isReady = false; reasonsWhyNotReady.push('No initial image selected'); } @@ -64,10 +58,5 @@ export const readinessSelector = createSelector( // All good return { isReady, reasonsWhyNotReady }; }, - { - memoizeOptions: { - equalityCheck: isEqual, - resultEqualityCheck: isEqual, - }, - } + defaultSelectorOptions ); diff --git a/invokeai/frontend/web/src/app/socketio/emitters.ts b/invokeai/frontend/web/src/app/socketio/emitters.ts index ad7979503f..8ed46cbc82 100644 --- a/invokeai/frontend/web/src/app/socketio/emitters.ts +++ b/invokeai/frontend/web/src/app/socketio/emitters.ts @@ -1,209 +1,209 @@ -// import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit'; -// import * as InvokeAI from 'app/types/invokeai'; -// import type { RootState } from 'app/store/store'; -// import { -// frontendToBackendParameters, -// FrontendToBackendParametersConfig, -// } from 'common/util/parameterTranslation'; -// import dateFormat from 'dateformat'; -// import { -// GalleryCategory, -// GalleryState, -// removeImage, -// } from 'features/gallery/store/gallerySlice'; -// import { -// generationRequested, -// modelChangeRequested, -// modelConvertRequested, -// modelMergingRequested, -// setIsProcessing, -// } from 'features/system/store/systemSlice'; -// import { InvokeTabName } from 'features/ui/store/tabMap'; -// import { Socket } from 'socket.io-client'; +import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit'; +import * as InvokeAI from 'app/types/invokeai'; +import type { RootState } from 'app/store/store'; +import { + frontendToBackendParameters, + FrontendToBackendParametersConfig, +} from 'common/util/parameterTranslation'; +import dateFormat from 'dateformat'; +import { + GalleryCategory, + GalleryState, + removeImage, +} from 'features/gallery/store/gallerySlice'; +import { + generationRequested, + modelChangeRequested, + modelConvertRequested, + modelMergingRequested, + setIsProcessing, +} from 'features/system/store/systemSlice'; +import { InvokeTabName } from 'features/ui/store/tabMap'; +import { Socket } from 'socket.io-client'; -// /** -// * Returns an object containing all functions which use `socketio.emit()`. -// * i.e. those which make server requests. -// */ -// const makeSocketIOEmitters = ( -// store: MiddlewareAPI, RootState>, -// socketio: Socket -// ) => { -// // We need to dispatch actions to redux and get pieces of state from the store. -// const { dispatch, getState } = store; +/** + * Returns an object containing all functions which use `socketio.emit()`. + * i.e. those which make server requests. + */ +const makeSocketIOEmitters = ( + store: MiddlewareAPI, RootState>, + socketio: Socket +) => { + // We need to dispatch actions to redux and get pieces of state from the store. + const { dispatch, getState } = store; -// return { -// emitGenerateImage: (generationMode: InvokeTabName) => { -// dispatch(setIsProcessing(true)); + return { + emitGenerateImage: (generationMode: InvokeTabName) => { + dispatch(setIsProcessing(true)); -// const state: RootState = getState(); + const state: RootState = getState(); -// const { -// generation: generationState, -// postprocessing: postprocessingState, -// system: systemState, -// canvas: canvasState, -// } = state; + const { + generation: generationState, + postprocessing: postprocessingState, + system: systemState, + canvas: canvasState, + } = state; -// const frontendToBackendParametersConfig: FrontendToBackendParametersConfig = -// { -// generationMode, -// generationState, -// postprocessingState, -// canvasState, -// systemState, -// }; + const frontendToBackendParametersConfig: FrontendToBackendParametersConfig = + { + generationMode, + generationState, + postprocessingState, + canvasState, + systemState, + }; -// dispatch(generationRequested()); + dispatch(generationRequested()); -// const { generationParameters, esrganParameters, facetoolParameters } = -// frontendToBackendParameters(frontendToBackendParametersConfig); + const { generationParameters, esrganParameters, facetoolParameters } = + frontendToBackendParameters(frontendToBackendParametersConfig); -// socketio.emit( -// 'generateImage', -// generationParameters, -// esrganParameters, -// facetoolParameters -// ); + socketio.emit( + 'generateImage', + generationParameters, + esrganParameters, + facetoolParameters + ); -// // we need to truncate the init_mask base64 else it takes up the whole log -// // TODO: handle maintaining masks for reproducibility in future -// if (generationParameters.init_mask) { -// generationParameters.init_mask = generationParameters.init_mask -// .substr(0, 64) -// .concat('...'); -// } -// if (generationParameters.init_img) { -// generationParameters.init_img = generationParameters.init_img -// .substr(0, 64) -// .concat('...'); -// } + // we need to truncate the init_mask base64 else it takes up the whole log + // TODO: handle maintaining masks for reproducibility in future + if (generationParameters.init_mask) { + generationParameters.init_mask = generationParameters.init_mask + .substr(0, 64) + .concat('...'); + } + if (generationParameters.init_img) { + generationParameters.init_img = generationParameters.init_img + .substr(0, 64) + .concat('...'); + } -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Image generation requested: ${JSON.stringify({ -// ...generationParameters, -// ...esrganParameters, -// ...facetoolParameters, -// })}`, -// }) -// ); -// }, -// emitRunESRGAN: (imageToProcess: InvokeAI._Image) => { -// dispatch(setIsProcessing(true)); + dispatch( + addLogEntry({ + timestamp: dateFormat(new Date(), 'isoDateTime'), + message: `Image generation requested: ${JSON.stringify({ + ...generationParameters, + ...esrganParameters, + ...facetoolParameters, + })}`, + }) + ); + }, + emitRunESRGAN: (imageToProcess: InvokeAI._Image) => { + dispatch(setIsProcessing(true)); -// const { -// postprocessing: { -// upscalingLevel, -// upscalingDenoising, -// upscalingStrength, -// }, -// } = getState(); + const { + postprocessing: { + upscalingLevel, + upscalingDenoising, + upscalingStrength, + }, + } = getState(); -// const esrganParameters = { -// upscale: [upscalingLevel, upscalingDenoising, upscalingStrength], -// }; -// socketio.emit('runPostprocessing', imageToProcess, { -// type: 'esrgan', -// ...esrganParameters, -// }); -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `ESRGAN upscale requested: ${JSON.stringify({ -// file: imageToProcess.url, -// ...esrganParameters, -// })}`, -// }) -// ); -// }, -// emitRunFacetool: (imageToProcess: InvokeAI._Image) => { -// dispatch(setIsProcessing(true)); + const esrganParameters = { + upscale: [upscalingLevel, upscalingDenoising, upscalingStrength], + }; + socketio.emit('runPostprocessing', imageToProcess, { + type: 'esrgan', + ...esrganParameters, + }); + dispatch( + addLogEntry({ + timestamp: dateFormat(new Date(), 'isoDateTime'), + message: `ESRGAN upscale requested: ${JSON.stringify({ + file: imageToProcess.url, + ...esrganParameters, + })}`, + }) + ); + }, + emitRunFacetool: (imageToProcess: InvokeAI._Image) => { + dispatch(setIsProcessing(true)); -// const { -// postprocessing: { facetoolType, facetoolStrength, codeformerFidelity }, -// } = getState(); + const { + postprocessing: { facetoolType, facetoolStrength, codeformerFidelity }, + } = getState(); -// const facetoolParameters: Record = { -// facetool_strength: facetoolStrength, -// }; + const facetoolParameters: Record = { + facetool_strength: facetoolStrength, + }; -// if (facetoolType === 'codeformer') { -// facetoolParameters.codeformer_fidelity = codeformerFidelity; -// } + if (facetoolType === 'codeformer') { + facetoolParameters.codeformer_fidelity = codeformerFidelity; + } -// socketio.emit('runPostprocessing', imageToProcess, { -// type: facetoolType, -// ...facetoolParameters, -// }); -// dispatch( -// addLogEntry({ -// timestamp: dateFormat(new Date(), 'isoDateTime'), -// message: `Face restoration (${facetoolType}) requested: ${JSON.stringify( -// { -// file: imageToProcess.url, -// ...facetoolParameters, -// } -// )}`, -// }) -// ); -// }, -// emitDeleteImage: (imageToDelete: InvokeAI._Image) => { -// const { url, uuid, category, thumbnail } = imageToDelete; -// dispatch(removeImage(imageToDelete)); -// socketio.emit('deleteImage', url, thumbnail, uuid, category); -// }, -// emitRequestImages: (category: GalleryCategory) => { -// const gallery: GalleryState = getState().gallery; -// const { earliest_mtime } = gallery.categories[category]; -// socketio.emit('requestImages', category, earliest_mtime); -// }, -// emitRequestNewImages: (category: GalleryCategory) => { -// const gallery: GalleryState = getState().gallery; -// const { latest_mtime } = gallery.categories[category]; -// socketio.emit('requestLatestImages', category, latest_mtime); -// }, -// emitCancelProcessing: () => { -// socketio.emit('cancel'); -// }, -// emitRequestSystemConfig: () => { -// socketio.emit('requestSystemConfig'); -// }, -// emitSearchForModels: (modelFolder: string) => { -// socketio.emit('searchForModels', modelFolder); -// }, -// emitAddNewModel: (modelConfig: InvokeAI.InvokeModelConfigProps) => { -// socketio.emit('addNewModel', modelConfig); -// }, -// emitDeleteModel: (modelName: string) => { -// socketio.emit('deleteModel', modelName); -// }, -// emitConvertToDiffusers: ( -// modelToConvert: InvokeAI.InvokeModelConversionProps -// ) => { -// dispatch(modelConvertRequested()); -// socketio.emit('convertToDiffusers', modelToConvert); -// }, -// emitMergeDiffusersModels: ( -// modelMergeInfo: InvokeAI.InvokeModelMergingProps -// ) => { -// dispatch(modelMergingRequested()); -// socketio.emit('mergeDiffusersModels', modelMergeInfo); -// }, -// emitRequestModelChange: (modelName: string) => { -// dispatch(modelChangeRequested()); -// socketio.emit('requestModelChange', modelName); -// }, -// emitSaveStagingAreaImageToGallery: (url: string) => { -// socketio.emit('requestSaveStagingAreaImageToGallery', url); -// }, -// emitRequestEmptyTempFolder: () => { -// socketio.emit('requestEmptyTempFolder'); -// }, -// }; -// }; + socketio.emit('runPostprocessing', imageToProcess, { + type: facetoolType, + ...facetoolParameters, + }); + dispatch( + addLogEntry({ + timestamp: dateFormat(new Date(), 'isoDateTime'), + message: `Face restoration (${facetoolType}) requested: ${JSON.stringify( + { + file: imageToProcess.url, + ...facetoolParameters, + } + )}`, + }) + ); + }, + emitDeleteImage: (imageToDelete: InvokeAI._Image) => { + const { url, uuid, category, thumbnail } = imageToDelete; + dispatch(removeImage(imageToDelete)); + socketio.emit('deleteImage', url, thumbnail, uuid, category); + }, + emitRequestImages: (category: GalleryCategory) => { + const gallery: GalleryState = getState().gallery; + const { earliest_mtime } = gallery.categories[category]; + socketio.emit('requestImages', category, earliest_mtime); + }, + emitRequestNewImages: (category: GalleryCategory) => { + const gallery: GalleryState = getState().gallery; + const { latest_mtime } = gallery.categories[category]; + socketio.emit('requestLatestImages', category, latest_mtime); + }, + emitCancelProcessing: () => { + socketio.emit('cancel'); + }, + emitRequestSystemConfig: () => { + socketio.emit('requestSystemConfig'); + }, + emitSearchForModels: (modelFolder: string) => { + socketio.emit('searchForModels', modelFolder); + }, + emitAddNewModel: (modelConfig: InvokeAI.InvokeModelConfigProps) => { + socketio.emit('addNewModel', modelConfig); + }, + emitDeleteModel: (modelName: string) => { + socketio.emit('deleteModel', modelName); + }, + emitConvertToDiffusers: ( + modelToConvert: InvokeAI.InvokeModelConversionProps + ) => { + dispatch(modelConvertRequested()); + socketio.emit('convertToDiffusers', modelToConvert); + }, + emitMergeDiffusersModels: ( + modelMergeInfo: InvokeAI.InvokeModelMergingProps + ) => { + dispatch(modelMergingRequested()); + socketio.emit('mergeDiffusersModels', modelMergeInfo); + }, + emitRequestModelChange: (modelName: string) => { + dispatch(modelChangeRequested()); + socketio.emit('requestModelChange', modelName); + }, + emitSaveStagingAreaImageToGallery: (url: string) => { + socketio.emit('requestSaveStagingAreaImageToGallery', url); + }, + emitRequestEmptyTempFolder: () => { + socketio.emit('requestEmptyTempFolder'); + }, + }; +}; -// export default makeSocketIOEmitters; +export default makeSocketIOEmitters; export default {}; diff --git a/invokeai/frontend/web/src/app/store/actions.ts b/invokeai/frontend/web/src/app/store/actions.ts new file mode 100644 index 0000000000..e07920ce0c --- /dev/null +++ b/invokeai/frontend/web/src/app/store/actions.ts @@ -0,0 +1,4 @@ +import { createAction } from '@reduxjs/toolkit'; +import { InvokeTabName } from 'features/ui/store/tabMap'; + +export const userInvoked = createAction('app/userInvoked'); diff --git a/invokeai/frontend/web/src/app/store/constants.ts b/invokeai/frontend/web/src/app/store/constants.ts new file mode 100644 index 0000000000..6d48762bef --- /dev/null +++ b/invokeai/frontend/web/src/app/store/constants.ts @@ -0,0 +1,8 @@ +export const LOCALSTORAGE_KEYS = [ + 'chakra-ui-color-mode', + 'i18nextLng', + 'ROARR_FILTER', + 'ROARR_LOG', +]; + +export const LOCALSTORAGE_PREFIX = '@@invokeai-'; diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts new file mode 100644 index 0000000000..52995e0da3 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts @@ -0,0 +1,36 @@ +import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist'; +import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist'; +import { resultsPersistDenylist } from 'features/gallery/store/resultsPersistDenylist'; +import { uploadsPersistDenylist } from 'features/gallery/store/uploadsPersistDenylist'; +import { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersistDenylist'; +import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist'; +import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist'; +import { postprocessingPersistDenylist } from 'features/parameters/store/postprocessingPersistDenylist'; +import { modelsPersistDenylist } from 'features/system/store/modelsPersistDenylist'; +import { systemPersistDenylist } from 'features/system/store/systemPersistDenylist'; +import { uiPersistDenylist } from 'features/ui/store/uiPersistDenylist'; +import { omit } from 'lodash-es'; +import { SerializeFunction } from 'redux-remember'; + +const serializationDenylist: { + [key: string]: string[]; +} = { + canvas: canvasPersistDenylist, + gallery: galleryPersistDenylist, + generation: generationPersistDenylist, + lightbox: lightboxPersistDenylist, + models: modelsPersistDenylist, + nodes: nodesPersistDenylist, + postprocessing: postprocessingPersistDenylist, + results: resultsPersistDenylist, + system: systemPersistDenylist, + // config: configPersistDenyList, + ui: uiPersistDenylist, + uploads: uploadsPersistDenylist, + // hotkeys: hotkeysPersistDenylist, +}; + +export const serialize: SerializeFunction = (data, key) => { + const result = omit(data, serializationDenylist[key]); + return JSON.stringify(result); +}; diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts new file mode 100644 index 0000000000..155a7786b3 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts @@ -0,0 +1,38 @@ +import { initialCanvasState } from 'features/canvas/store/canvasSlice'; +import { initialGalleryState } from 'features/gallery/store/gallerySlice'; +import { initialResultsState } from 'features/gallery/store/resultsSlice'; +import { initialUploadsState } from 'features/gallery/store/uploadsSlice'; +import { initialLightboxState } from 'features/lightbox/store/lightboxSlice'; +import { initialNodesState } from 'features/nodes/store/nodesSlice'; +import { initialGenerationState } from 'features/parameters/store/generationSlice'; +import { initialPostprocessingState } from 'features/parameters/store/postprocessingSlice'; +import { initialConfigState } from 'features/system/store/configSlice'; +import { initialModelsState } from 'features/system/store/modelSlice'; +import { initialSystemState } from 'features/system/store/systemSlice'; +import { initialHotkeysState } from 'features/ui/store/hotkeysSlice'; +import { initialUIState } from 'features/ui/store/uiSlice'; +import { defaultsDeep } from 'lodash-es'; +import { UnserializeFunction } from 'redux-remember'; + +const initialStates: { + [key: string]: any; +} = { + canvas: initialCanvasState, + gallery: initialGalleryState, + generation: initialGenerationState, + lightbox: initialLightboxState, + models: initialModelsState, + nodes: initialNodesState, + postprocessing: initialPostprocessingState, + results: initialResultsState, + system: initialSystemState, + config: initialConfigState, + ui: initialUIState, + uploads: initialUploadsState, + hotkeys: initialHotkeysState, +}; + +export const unserialize: UnserializeFunction = (data, key) => { + const result = defaultsDeep(JSON.parse(data), initialStates[key]); + return result; +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts new file mode 100644 index 0000000000..24b85e0f83 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts @@ -0,0 +1,30 @@ +import { AnyAction } from '@reduxjs/toolkit'; +import { isAnyGraphBuilt } from 'features/nodes/store/actions'; +import { forEach } from 'lodash-es'; +import { Graph } from 'services/api'; + +export const actionSanitizer = (action: A): A => { + if (isAnyGraphBuilt(action)) { + if (action.payload.nodes) { + const sanitizedNodes: Graph['nodes'] = {}; + + // Sanitize nodes as needed + forEach(action.payload.nodes, (node, key) => { + // Don't log the whole freaking dataURL + if (node.type === 'dataURL_image') { + const { dataURL, ...rest } = node; + sanitizedNodes[key] = { ...rest, dataURL: '' }; + } else { + sanitizedNodes[key] = { ...node }; + } + }); + + return { + ...action, + payload: { ...action.payload, nodes: sanitizedNodes }, + }; + } + } + + return action; +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts new file mode 100644 index 0000000000..743537d7ea --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts @@ -0,0 +1,11 @@ +export const actionsDenylist = [ + 'canvas/setCursorPosition', + 'canvas/setStageCoordinates', + 'canvas/setStageScale', + 'canvas/setIsDrawing', + 'canvas/setBoundingBoxCoordinates', + 'canvas/setBoundingBoxDimensions', + 'canvas/setIsDrawing', + 'canvas/addPointToCurrentLine', + 'socket/generatorProgress', +]; diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/stateSanitizer.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/stateSanitizer.ts new file mode 100644 index 0000000000..312b4db189 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/stateSanitizer.ts @@ -0,0 +1,3 @@ +export const stateSanitizer = (state: S): S => { + return state; +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts new file mode 100644 index 0000000000..36bf6adfe7 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -0,0 +1,45 @@ +import { + createListenerMiddleware, + addListener, + ListenerEffect, + AnyAction, +} from '@reduxjs/toolkit'; +import type { TypedStartListening, TypedAddListener } from '@reduxjs/toolkit'; + +import type { RootState, AppDispatch } from '../../store'; +import { addInitialImageSelectedListener } from './listeners/initialImageSelected'; +import { addImageResultReceivedListener } from './listeners/invocationComplete'; +import { addImageUploadedListener } from './listeners/imageUploaded'; +import { addRequestedImageDeletionListener } from './listeners/imageDeleted'; +import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas'; +import { addUserInvokedNodesListener } from './listeners/userInvokedNodes'; +import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage'; +import { addUserInvokedImageToImageListener } from './listeners/userInvokedImageToImage'; + +export const listenerMiddleware = createListenerMiddleware(); + +export type AppStartListening = TypedStartListening; + +export const startAppListening = + listenerMiddleware.startListening as AppStartListening; + +export const addAppListener = addListener as TypedAddListener< + RootState, + AppDispatch +>; + +export type AppListenerEffect = ListenerEffect< + AnyAction, + RootState, + AppDispatch +>; + +addImageUploadedListener(); +addInitialImageSelectedListener(); +addImageResultReceivedListener(); +addRequestedImageDeletionListener(); + +addUserInvokedCanvasListener(); +addUserInvokedNodesListener(); +addUserInvokedTextToImageListener(); +addUserInvokedImageToImageListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasGraphBuilt.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasGraphBuilt.ts new file mode 100644 index 0000000000..532bac3eee --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasGraphBuilt.ts @@ -0,0 +1,31 @@ +import { canvasGraphBuilt } from 'features/nodes/store/actions'; +import { startAppListening } from '..'; +import { + canvasSessionIdChanged, + stagingAreaInitialized, +} from 'features/canvas/store/canvasSlice'; +import { sessionInvoked } from 'services/thunks/session'; + +export const addCanvasGraphBuiltListener = () => + startAppListening({ + actionCreator: canvasGraphBuilt, + effect: async (action, { dispatch, getState, take }) => { + const [{ meta }] = await take(sessionInvoked.fulfilled.match); + const { sessionId } = meta.arg; + const state = getState(); + + if (!state.canvas.layerState.stagingArea.boundingBox) { + dispatch( + stagingAreaInitialized({ + sessionId, + boundingBox: { + ...state.canvas.boundingBoxCoordinates, + ...state.canvas.boundingBoxDimensions, + }, + }) + ); + } + + dispatch(canvasSessionIdChanged(sessionId)); + }, + }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts new file mode 100644 index 0000000000..00cbf86527 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts @@ -0,0 +1,59 @@ +import { requestedImageDeletion } from 'features/gallery/store/actions'; +import { startAppListening } from '..'; +import { imageDeleted } from 'services/thunks/image'; +import { log } from 'app/logging/useLogger'; +import { clamp } from 'lodash-es'; +import { imageSelected } from 'features/gallery/store/gallerySlice'; + +const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' }); + +export const addRequestedImageDeletionListener = () => { + startAppListening({ + actionCreator: requestedImageDeletion, + effect: (action, { dispatch, getState }) => { + const image = action.payload; + if (!image) { + moduleLog.warn('No image provided'); + return; + } + + const { name, type } = image; + + if (type !== 'uploads' && type !== 'results') { + moduleLog.warn({ data: image }, `Invalid image type ${type}`); + return; + } + + const selectedImageName = getState().gallery.selectedImage?.name; + + if (selectedImageName === name) { + const allIds = getState()[type].ids; + const allEntities = getState()[type].entities; + + const deletedImageIndex = allIds.findIndex( + (result) => result.toString() === name + ); + + const filteredIds = allIds.filter((id) => id.toString() !== name); + + const newSelectedImageIndex = clamp( + deletedImageIndex, + 0, + filteredIds.length - 1 + ); + + const newSelectedImageId = filteredIds[newSelectedImageIndex]; + + const newSelectedImage = allEntities[newSelectedImageId]; + + if (newSelectedImageId) { + dispatch(imageSelected(newSelectedImage)); + } else { + dispatch(imageSelected()); + } + } + + dispatch(imageDeleted({ imageName: name, imageType: type })); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts new file mode 100644 index 0000000000..7d578356f4 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -0,0 +1,22 @@ +import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; +import { startAppListening } from '..'; +import { uploadAdded } from 'features/gallery/store/uploadsSlice'; +import { imageSelected } from 'features/gallery/store/gallerySlice'; +import { imageUploaded } from 'services/thunks/image'; + +export const addImageUploadedListener = () => { + startAppListening({ + actionCreator: imageUploaded.fulfilled, + effect: (action, { dispatch, getState }) => { + const { response } = action.payload; + const state = getState(); + const image = deserializeImageResponse(response); + + dispatch(uploadAdded(image)); + + if (state.gallery.shouldAutoSwitchToNewImages) { + dispatch(imageSelected(image)); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts new file mode 100644 index 0000000000..6bc2f9e9bc --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts @@ -0,0 +1,54 @@ +import { initialImageChanged } from 'features/parameters/store/generationSlice'; +import { Image, isInvokeAIImage } from 'app/types/invokeai'; +import { selectResultsById } from 'features/gallery/store/resultsSlice'; +import { selectUploadsById } from 'features/gallery/store/uploadsSlice'; +import { makeToast } from 'features/system/hooks/useToastWatcher'; +import { t } from 'i18next'; +import { addToast } from 'features/system/store/systemSlice'; +import { startAppListening } from '..'; +import { initialImageSelected } from 'features/parameters/store/actions'; + +export const addInitialImageSelectedListener = () => { + startAppListening({ + actionCreator: initialImageSelected, + effect: (action, { getState, dispatch }) => { + if (!action.payload) { + dispatch( + addToast( + makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' }) + ) + ); + return; + } + + if (isInvokeAIImage(action.payload)) { + dispatch(initialImageChanged(action.payload)); + dispatch(addToast(makeToast(t('toast.sentToImageToImage')))); + return; + } + + const { name, type } = action.payload; + + let image: Image | undefined; + const state = getState(); + + if (type === 'results') { + image = selectResultsById(state, name); + } else if (type === 'uploads') { + image = selectUploadsById(state, name); + } + + if (!image) { + dispatch( + addToast( + makeToast({ title: t('toast.imageNotLoadedDesc'), status: 'error' }) + ) + ); + return; + } + + dispatch(initialImageChanged(image)); + dispatch(addToast(makeToast(t('toast.sentToImageToImage')))); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts new file mode 100644 index 0000000000..9d84b2cbf0 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts @@ -0,0 +1,88 @@ +import { invocationComplete } from 'services/events/actions'; +import { isImageOutput } from 'services/types/guards'; +import { + buildImageUrls, + extractTimestampFromImageName, +} from 'services/util/deserializeImageField'; +import { Image } from 'app/types/invokeai'; +import { resultAdded } from 'features/gallery/store/resultsSlice'; +import { imageReceived, thumbnailReceived } from 'services/thunks/image'; +import { startAppListening } from '..'; +import { imageSelected } from 'features/gallery/store/gallerySlice'; +import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; + +const nodeDenylist = ['dataURL_image']; + +export const addImageResultReceivedListener = () => { + startAppListening({ + predicate: (action) => { + if ( + invocationComplete.match(action) && + isImageOutput(action.payload.data.result) + ) { + return true; + } + return false; + }, + effect: (action, { getState, dispatch }) => { + if (!invocationComplete.match(action)) { + return; + } + + const { data, shouldFetchImages } = action.payload; + const { result, node, graph_execution_state_id } = data; + + if (isImageOutput(result) && !nodeDenylist.includes(node.type)) { + const name = result.image.image_name; + const type = result.image.image_type; + const state = getState(); + + // if we need to refetch, set URLs to placeholder for now + const { url, thumbnail } = shouldFetchImages + ? { url: '', thumbnail: '' } + : buildImageUrls(type, name); + + const timestamp = extractTimestampFromImageName(name); + + const image: Image = { + name, + type, + url, + thumbnail, + metadata: { + created: timestamp, + width: result.width, + height: result.height, + invokeai: { + session_id: graph_execution_state_id, + ...(node ? { node } : {}), + }, + }, + }; + + dispatch(resultAdded(image)); + + if (state.gallery.shouldAutoSwitchToNewImages) { + dispatch(imageSelected(image)); + } + + if (state.config.shouldFetchImages) { + dispatch(imageReceived({ imageName: name, imageType: type })); + dispatch( + thumbnailReceived({ + thumbnailName: name, + thumbnailType: type, + }) + ); + } + + if ( + graph_execution_state_id === + state.canvas.layerState.stagingArea.sessionId + ) { + dispatch(addImageToStagingArea(image)); + } + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts new file mode 100644 index 0000000000..cdb2c83e12 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts @@ -0,0 +1,126 @@ +import { startAppListening } from '..'; +import { sessionCreated, sessionInvoked } from 'services/thunks/session'; +import { buildCanvasGraphAndBlobs } from 'features/nodes/util/graphBuilders/buildCanvasGraph'; +import { log } from 'app/logging/useLogger'; +import { canvasGraphBuilt } from 'features/nodes/store/actions'; +import { imageUploaded } from 'services/thunks/image'; +import { v4 as uuidv4 } from 'uuid'; +import { Graph } from 'services/api'; +import { + canvasSessionIdChanged, + stagingAreaInitialized, +} from 'features/canvas/store/canvasSlice'; +import { userInvoked } from 'app/store/actions'; + +const moduleLog = log.child({ namespace: 'invoke' }); + +export const addUserInvokedCanvasListener = () => { + startAppListening({ + predicate: (action): action is ReturnType => + userInvoked.match(action) && action.payload === 'unifiedCanvas', + effect: async (action, { getState, dispatch, take }) => { + const state = getState(); + + const data = await buildCanvasGraphAndBlobs(state); + + if (!data) { + moduleLog.error('Problem building graph'); + return; + } + + const { + rangeNode, + iterateNode, + baseNode, + edges, + baseBlob, + maskBlob, + generationMode, + } = data; + + const baseFilename = `${uuidv4()}.png`; + const maskFilename = `${uuidv4()}.png`; + + dispatch( + imageUploaded({ + imageType: 'intermediates', + formData: { + file: new File([baseBlob], baseFilename, { type: 'image/png' }), + }, + }) + ); + + if (baseNode.type === 'img2img' || baseNode.type === 'inpaint') { + const [{ payload: basePayload }] = await take( + (action): action is ReturnType => + imageUploaded.fulfilled.match(action) && + action.meta.arg.formData.file.name === baseFilename + ); + + const { image_name: baseName, image_type: baseType } = + basePayload.response; + + baseNode.image = { + image_name: baseName, + image_type: baseType, + }; + } + + if (baseNode.type === 'inpaint') { + dispatch( + imageUploaded({ + imageType: 'intermediates', + formData: { + file: new File([maskBlob], maskFilename, { type: 'image/png' }), + }, + }) + ); + + const [{ payload: maskPayload }] = await take( + (action): action is ReturnType => + imageUploaded.fulfilled.match(action) && + action.meta.arg.formData.file.name === maskFilename + ); + + const { image_name: maskName, image_type: maskType } = + maskPayload.response; + + baseNode.mask = { + image_name: maskName, + image_type: maskType, + }; + } + + // Assemble! + const nodes: Graph['nodes'] = { + [rangeNode.id]: rangeNode, + [iterateNode.id]: iterateNode, + [baseNode.id]: baseNode, + }; + + const graph = { nodes, edges }; + + dispatch(canvasGraphBuilt(graph)); + moduleLog({ data: graph }, 'Canvas graph built'); + + dispatch(sessionCreated({ graph })); + + const [{ meta }] = await take(sessionInvoked.fulfilled.match); + const { sessionId } = meta.arg; + + if (!state.canvas.layerState.stagingArea.boundingBox) { + dispatch( + stagingAreaInitialized({ + sessionId, + boundingBox: { + ...state.canvas.boundingBoxCoordinates, + ...state.canvas.boundingBoxDimensions, + }, + }) + ); + } + + dispatch(canvasSessionIdChanged(sessionId)); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedImageToImage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedImageToImage.ts new file mode 100644 index 0000000000..e747aefa08 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedImageToImage.ts @@ -0,0 +1,24 @@ +import { startAppListening } from '..'; +import { buildImageToImageGraph } from 'features/nodes/util/graphBuilders/buildImageToImageGraph'; +import { sessionCreated } from 'services/thunks/session'; +import { log } from 'app/logging/useLogger'; +import { imageToImageGraphBuilt } from 'features/nodes/store/actions'; +import { userInvoked } from 'app/store/actions'; + +const moduleLog = log.child({ namespace: 'invoke' }); + +export const addUserInvokedImageToImageListener = () => { + startAppListening({ + predicate: (action): action is ReturnType => + userInvoked.match(action) && action.payload === 'img2img', + effect: (action, { getState, dispatch }) => { + const state = getState(); + + const graph = buildImageToImageGraph(state); + dispatch(imageToImageGraphBuilt(graph)); + moduleLog({ data: graph }, 'Image to Image graph built'); + + dispatch(sessionCreated({ graph })); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedNodes.ts new file mode 100644 index 0000000000..01e532d5ff --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedNodes.ts @@ -0,0 +1,24 @@ +import { startAppListening } from '..'; +import { sessionCreated } from 'services/thunks/session'; +import { buildNodesGraph } from 'features/nodes/util/graphBuilders/buildNodesGraph'; +import { log } from 'app/logging/useLogger'; +import { nodesGraphBuilt } from 'features/nodes/store/actions'; +import { userInvoked } from 'app/store/actions'; + +const moduleLog = log.child({ namespace: 'invoke' }); + +export const addUserInvokedNodesListener = () => { + startAppListening({ + predicate: (action): action is ReturnType => + userInvoked.match(action) && action.payload === 'nodes', + effect: (action, { getState, dispatch }) => { + const state = getState(); + + const graph = buildNodesGraph(state); + dispatch(nodesGraphBuilt(graph)); + moduleLog({ data: graph }, 'Nodes graph built'); + + dispatch(sessionCreated({ graph })); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedTextToImage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedTextToImage.ts new file mode 100644 index 0000000000..e3eb5d0b38 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedTextToImage.ts @@ -0,0 +1,24 @@ +import { startAppListening } from '..'; +import { buildTextToImageGraph } from 'features/nodes/util/graphBuilders/buildTextToImageGraph'; +import { sessionCreated } from 'services/thunks/session'; +import { log } from 'app/logging/useLogger'; +import { textToImageGraphBuilt } from 'features/nodes/store/actions'; +import { userInvoked } from 'app/store/actions'; + +const moduleLog = log.child({ namespace: 'invoke' }); + +export const addUserInvokedTextToImageListener = () => { + startAppListening({ + predicate: (action): action is ReturnType => + userInvoked.match(action) && action.payload === 'txt2img', + effect: (action, { getState, dispatch }) => { + const state = getState(); + + const graph = buildTextToImageGraph(state); + dispatch(textToImageGraphBuilt(graph)); + moduleLog({ data: graph }, 'Text to Image graph built'); + + dispatch(sessionCreated({ graph })); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/persistor.ts b/invokeai/frontend/web/src/app/store/persistor.ts deleted file mode 100644 index 85dc934943..0000000000 --- a/invokeai/frontend/web/src/app/store/persistor.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { store } from 'app/store/store'; -import { persistStore } from 'redux-persist'; - -export const persistor = persistStore(store); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index b0f73a759e..b89615b2c0 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -1,9 +1,12 @@ -import { combineReducers, configureStore } from '@reduxjs/toolkit'; +import { + AnyAction, + ThunkDispatch, + combineReducers, + configureStore, +} from '@reduxjs/toolkit'; -import { persistReducer } from 'redux-persist'; -import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web +import { rememberReducer, rememberEnhancer } from 'redux-remember'; import dynamicMiddlewares from 'redux-dynamic-middlewares'; -import { getPersistConfig } from 'redux-deep-persist'; import canvasReducer from 'features/canvas/store/canvasSlice'; import galleryReducer from 'features/gallery/store/gallerySlice'; @@ -19,33 +22,17 @@ import hotkeysReducer from 'features/ui/store/hotkeysSlice'; import modelsReducer from 'features/system/store/modelSlice'; import nodesReducer from 'features/nodes/store/nodesSlice'; -import { canvasDenylist } from 'features/canvas/store/canvasPersistDenylist'; -import { galleryDenylist } from 'features/gallery/store/galleryPersistDenylist'; -import { generationDenylist } from 'features/parameters/store/generationPersistDenylist'; -import { lightboxDenylist } from 'features/lightbox/store/lightboxPersistDenylist'; -import { modelsDenylist } from 'features/system/store/modelsPersistDenylist'; -import { nodesDenylist } from 'features/nodes/store/nodesPersistDenylist'; -import { postprocessingDenylist } from 'features/parameters/store/postprocessingPersistDenylist'; -import { systemDenylist } from 'features/system/store/systemPersistDenylist'; -import { uiDenylist } from 'features/ui/store/uiPersistDenylist'; -import { resultsDenylist } from 'features/gallery/store/resultsPersistDenylist'; -import { uploadsDenylist } from 'features/gallery/store/uploadsPersistDenylist'; +import { listenerMiddleware } from './middleware/listenerMiddleware'; -/** - * redux-persist provides an easy and reliable way to persist state across reloads. - * - * While we definitely want generation parameters to be persisted, there are a number - * of things we do *not* want to be persisted across reloads: - * - Gallery/selected image (user may add/delete images from disk between page loads) - * - Connection/processing status - * - Availability of external libraries like ESRGAN/GFPGAN - * - * These can be denylisted in redux-persist. - * - * The necesssary nested persistors with denylists are configured below. - */ +import { actionSanitizer } from './middleware/devtools/actionSanitizer'; +import { stateSanitizer } from './middleware/devtools/stateSanitizer'; +import { actionsDenylist } from './middleware/devtools/actionsDenylist'; -const rootReducer = combineReducers({ +import { serialize } from './enhancers/reduxRemember/serialize'; +import { unserialize } from './enhancers/reduxRemember/unserialize'; +import { LOCALSTORAGE_PREFIX } from './constants'; + +const allReducers = { canvas: canvasReducer, gallery: galleryReducer, generation: generationReducer, @@ -59,65 +46,54 @@ const rootReducer = combineReducers({ ui: uiReducer, uploads: uploadsReducer, hotkeys: hotkeysReducer, -}); +}; -const rootPersistConfig = getPersistConfig({ - key: 'root', - storage, - rootReducer, - blacklist: [ - ...canvasDenylist, - ...galleryDenylist, - ...generationDenylist, - ...lightboxDenylist, - ...modelsDenylist, - ...nodesDenylist, - ...postprocessingDenylist, - // ...resultsDenylist, - 'results', - ...systemDenylist, - ...uiDenylist, - // ...uploadsDenylist, - 'uploads', - 'hotkeys', - 'config', - ], -}); +const rootReducer = combineReducers(allReducers); -const persistedReducer = persistReducer(rootPersistConfig, rootReducer); +const rememberedRootReducer = rememberReducer(rootReducer); -// TODO: rip the old middleware out when nodes is complete -// export function buildMiddleware() { -// if (import.meta.env.MODE === 'nodes' || import.meta.env.MODE === 'package') { -// return socketMiddleware(); -// } else { -// return socketioMiddleware(); -// } -// } +const rememberedKeys: (keyof typeof allReducers)[] = [ + 'canvas', + 'gallery', + 'generation', + 'lightbox', + // 'models', + 'nodes', + 'postprocessing', + 'system', + 'ui', + // 'hotkeys', + // 'results', + // 'uploads', + // 'config', +]; export const store = configureStore({ - reducer: persistedReducer, + reducer: rememberedRootReducer, + enhancers: [ + rememberEnhancer(window.localStorage, rememberedKeys, { + persistDebounce: 300, + serialize, + unserialize, + prefix: LOCALSTORAGE_PREFIX, + }), + ], middleware: (getDefaultMiddleware) => getDefaultMiddleware({ immutableCheck: false, serializableCheck: false, - }).concat(dynamicMiddlewares), + }) + .concat(dynamicMiddlewares) + .prepend(listenerMiddleware.middleware), devTools: { - // Uncommenting these very rapidly called actions makes the redux dev tools output much more readable - actionsDenylist: [ - 'canvas/setCursorPosition', - 'canvas/setStageCoordinates', - 'canvas/setStageScale', - 'canvas/setIsDrawing', - 'canvas/setBoundingBoxCoordinates', - 'canvas/setBoundingBoxDimensions', - 'canvas/setIsDrawing', - 'canvas/addPointToCurrentLine', - 'socket/generatorProgress', - ], + actionsDenylist, + actionSanitizer, + stateSanitizer, + trace: true, }, }); export type AppGetState = typeof store.getState; export type RootState = ReturnType; +export type AppThunkDispatch = ThunkDispatch; export type AppDispatch = typeof store.dispatch; diff --git a/invokeai/frontend/web/src/app/store/storeHooks.ts b/invokeai/frontend/web/src/app/store/storeHooks.ts index 387bc7ea68..f0400c3a3c 100644 --- a/invokeai/frontend/web/src/app/store/storeHooks.ts +++ b/invokeai/frontend/web/src/app/store/storeHooks.ts @@ -1,6 +1,6 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; -import { AppDispatch, RootState } from 'app/store/store'; +import { AppThunkDispatch, RootState } from 'app/store/store'; // Use throughout your app instead of plain `useDispatch` and `useSelector` -export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppDispatch = () => useDispatch(); export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/invokeai/frontend/web/src/app/store/util/defaultMemoizeOptions.ts b/invokeai/frontend/web/src/app/store/util/defaultMemoizeOptions.ts new file mode 100644 index 0000000000..fd2abd228d --- /dev/null +++ b/invokeai/frontend/web/src/app/store/util/defaultMemoizeOptions.ts @@ -0,0 +1,7 @@ +import { isEqual } from 'lodash-es'; + +export const defaultSelectorOptions = { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, +}; diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 05e6e088d6..4023f7665d 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -12,12 +12,10 @@ * 'gfpgan'. */ -import { GalleryCategory } from 'features/gallery/store/gallerySlice'; -import { FacetoolType } from 'features/parameters/store/postprocessingSlice'; +import { SelectedImage } from 'features/parameters/store/actions'; import { InvokeTabName } from 'features/ui/store/tabMap'; import { IRect } from 'konva/lib/types'; import { ImageResponseMetadata, ImageType } from 'services/api'; -import { AnyInvocation } from 'services/events/types'; import { O } from 'ts-toolbelt'; /** @@ -126,6 +124,14 @@ export type Image = { metadata: ImageResponseMetadata; }; +export const isInvokeAIImage = (obj: Image | SelectedImage): obj is Image => { + if ('url' in obj && 'thumbnail' in obj) { + return true; + } + + return false; +}; + /** * Types related to the system status. */ @@ -270,7 +276,7 @@ export type FoundModelResponse = { // export type SystemConfigResponse = SystemConfig; -export type ImageResultResponse = Omit<_Image, 'uuid'> & { +export type ImageResultResponse = Omit & { boundingBox?: IRect; generationMode: InvokeTabName; }; diff --git a/invokeai/frontend/web/src/common/components/IAICollapse.tsx b/invokeai/frontend/web/src/common/components/IAICollapse.tsx new file mode 100644 index 0000000000..161caca24d --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAICollapse.tsx @@ -0,0 +1,61 @@ +import { ChevronUpIcon } from '@chakra-ui/icons'; +import { Box, Collapse, Flex, Spacer, Switch } from '@chakra-ui/react'; +import { PropsWithChildren, memo } from 'react'; + +export type IAIToggleCollapseProps = PropsWithChildren & { + label: string; + isOpen: boolean; + onToggle: () => void; + withSwitch?: boolean; +}; + +const IAICollapse = (props: IAIToggleCollapseProps) => { + const { label, isOpen, onToggle, children, withSwitch = false } = props; + return ( + + + {label} + + {withSwitch && } + {!withSwitch && ( + + )} + + + + {children} + + + + ); +}; + +export default memo(IAICollapse); diff --git a/invokeai/frontend/web/src/common/components/IAIPopover.tsx b/invokeai/frontend/web/src/common/components/IAIPopover.tsx index ba3fbdd109..51562b969c 100644 --- a/invokeai/frontend/web/src/common/components/IAIPopover.tsx +++ b/invokeai/frontend/web/src/common/components/IAIPopover.tsx @@ -27,7 +27,7 @@ const IAIPopover = (props: IAIPopoverProps) => { return ( {triggerComponent} - + {hasArrow && } {children} diff --git a/invokeai/frontend/web/src/common/components/ImageToImageSettingsHeader.tsx b/invokeai/frontend/web/src/common/components/ImageToImageButtons.tsx similarity index 78% rename from invokeai/frontend/web/src/common/components/ImageToImageSettingsHeader.tsx rename to invokeai/frontend/web/src/common/components/ImageToImageButtons.tsx index a7cf1cb172..315571b6e7 100644 --- a/invokeai/frontend/web/src/common/components/ImageToImageSettingsHeader.tsx +++ b/invokeai/frontend/web/src/common/components/ImageToImageButtons.tsx @@ -7,7 +7,7 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { useCallback } from 'react'; import { clearInitialImage } from 'features/parameters/store/generationSlice'; -const ImageToImageSettingsHeader = () => { +const InitialImageButtons = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -18,24 +18,19 @@ const ImageToImageSettingsHeader = () => { return ( - Image to Image + {t('parameters.initialImage')} } aria-label={t('accessibility.reset')} onClick={handleResetInitialImage} /> - } - aria-label={t('common.upload')} - /> + } aria-label={t('common.upload')} /> ); }; -export default ImageToImageSettingsHeader; +export default InitialImageButtons; diff --git a/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx index 45a45e37d3..9d864f5c9c 100644 --- a/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx @@ -14,6 +14,7 @@ const ImageToImageOverlay = ({ image }: ImageToImageOverlayProps) => { w: 'full', h: 'full', position: 'absolute', + pointerEvents: 'none', }} > { const fileAcceptedCallback = useCallback( async (file: File) => { - dispatch(imageUploaded({ formData: { file } })); + dispatch(imageUploaded({ imageType: 'uploads', formData: { file } })); }, [dispatch] ); @@ -124,7 +124,7 @@ const ImageUploader = (props: ImageUploaderProps) => { return; } - dispatch(imageUploaded({ formData: { file } })); + dispatch(imageUploaded({ imageType: 'uploads', formData: { file } })); }; document.addEventListener('paste', pasteImageListener); return () => { diff --git a/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx b/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx index 2c0d71ca69..a19d447755 100644 --- a/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx +++ b/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx @@ -7,7 +7,7 @@ const SelectImagePlaceholder = () => { sx={{ w: 'full', h: 'full', - bg: 'base.800', + // bg: 'base.800', borderRadius: 'base', alignItems: 'center', justifyContent: 'center', diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts index 5078c8d358..3935a390fb 100644 --- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts +++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts @@ -2,6 +2,13 @@ import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice'; +import { + setActiveTab, + toggleGalleryPanel, + toggleParametersPanel, + togglePinGalleryPanel, + togglePinParametersPanel, +} from 'features/ui/store/uiSlice'; import { isEqual } from 'lodash-es'; import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook'; @@ -36,4 +43,36 @@ export const useGlobalHotkeys = () => { { keyup: true, keydown: true }, [shift] ); + + useHotkeys('o', () => { + dispatch(toggleParametersPanel()); + }); + + useHotkeys(['shift+o'], () => { + dispatch(togglePinParametersPanel()); + }); + + useHotkeys('g', () => { + dispatch(toggleGalleryPanel()); + }); + + useHotkeys(['shift+g'], () => { + dispatch(togglePinGalleryPanel()); + }); + + useHotkeys('1', () => { + dispatch(setActiveTab('txt2img')); + }); + + useHotkeys('2', () => { + dispatch(setActiveTab('img2img')); + }); + + useHotkeys('3', () => { + dispatch(setActiveTab('unifiedCanvas')); + }); + + useHotkeys('4', () => { + dispatch(setActiveTab('nodes')); + }); }; diff --git a/invokeai/frontend/web/src/common/util/arrayBuffer.ts b/invokeai/frontend/web/src/common/util/arrayBuffer.ts new file mode 100644 index 0000000000..cdfe84fccc --- /dev/null +++ b/invokeai/frontend/web/src/common/util/arrayBuffer.ts @@ -0,0 +1,33 @@ +export const getImageDataTransparency = (pixels: Uint8ClampedArray) => { + let isFullyTransparent = true; + let isPartiallyTransparent = false; + const len = pixels.length; + let i = 3; + for (i; i < len; i += 4) { + if (pixels[i] === 255) { + isFullyTransparent = false; + } else { + isPartiallyTransparent = true; + } + if (!isFullyTransparent && isPartiallyTransparent) { + return { isFullyTransparent, isPartiallyTransparent }; + } + } + return { isFullyTransparent, isPartiallyTransparent }; +}; + +export const areAnyPixelsBlack = (pixels: Uint8ClampedArray) => { + const len = pixels.length; + let i = 0; + for (i; i < len; ) { + if ( + pixels[i++] === 0 && + pixels[i++] === 0 && + pixels[i++] === 0 && + pixels[i++] === 255 + ) { + return true; + } + } + return false; +}; diff --git a/invokeai/frontend/web/src/common/util/parameterTranslation.ts b/invokeai/frontend/web/src/common/util/parameterTranslation.ts index 07b8ac8ea1..83df66aab2 100644 --- a/invokeai/frontend/web/src/common/util/parameterTranslation.ts +++ b/invokeai/frontend/web/src/common/util/parameterTranslation.ts @@ -19,6 +19,7 @@ import { InvokeTabName } from 'features/ui/store/tabMap'; import openBase64ImageInTab from './openBase64ImageInTab'; import randomInt from './randomInt'; import { stringToSeedWeightsArray } from './seedWeightPairs'; +import { getIsImageDataTransparent, getIsImageDataWhite } from './arrayBuffer'; export type FrontendToBackendParametersConfig = { generationMode: InvokeTabName; @@ -256,7 +257,7 @@ export const frontendToBackendParameters = ( ...boundingBoxDimensions, }; - const maskDataURL = generateMask( + const { dataURL: maskDataURL, imageData: maskImageData } = generateMask( isMaskEnabled ? objects.filter(isCanvasMaskLine) : [], boundingBox ); @@ -287,6 +288,17 @@ export const frontendToBackendParameters = ( height: boundingBox.height, }); + const ctx = canvasBaseLayer.getContext(); + const imageData = ctx.getImageData( + boundingBox.x + absPos.x, + boundingBox.y + absPos.y, + boundingBox.width, + boundingBox.height + ); + + const doesBaseHaveTransparency = getIsImageDataTransparent(imageData); + const doesMaskHaveTransparency = getIsImageDataWhite(maskImageData); + if (enableImageDebugging) { openBase64ImageInTab([ { base64: maskDataURL, caption: 'mask sent as init_mask' }, diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx index 1850b4bfe4..aa785c379d 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvas.tsx @@ -34,6 +34,7 @@ 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], @@ -52,6 +53,7 @@ const selector = createSelector( shouldShowIntermediates, shouldShowGrid, shouldRestrictStrokesToBox, + shouldAntialias, } = canvas; let stageCursor: string | undefined = 'none'; @@ -80,13 +82,10 @@ const selector = createSelector( tool, isStaging, shouldShowIntermediates, + shouldAntialias, }; }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } + defaultSelectorOptions ); const ChakraStage = chakra(Stage, { @@ -106,6 +105,7 @@ const IAICanvas = () => { tool, isStaging, shouldShowIntermediates, + shouldAntialias, } = useAppSelector(selector); useCanvasHotkeys(); @@ -190,7 +190,7 @@ const IAICanvas = () => { id="base" ref={canvasBaseLayerRefCallback} listening={false} - imageSmoothingEnabled={false} + imageSmoothingEnabled={shouldAntialias} > @@ -201,7 +201,7 @@ const IAICanvas = () => { - + {!isStaging && ( { const { - layerState: { - stagingArea: { images, selectedImageIndex }, - }, + layerState, shouldShowStagingImage, shouldShowStagingOutline, boundingBoxCoordinates: { x, y }, boundingBoxDimensions: { width, height }, } = canvas; + const { selectedImageIndex, images } = layerState.stagingArea; + return { currentStagingAreaImage: - images.length > 0 ? images[selectedImageIndex] : undefined, + images.length > 0 && selectedImageIndex !== undefined + ? images[selectedImageIndex] + : undefined, isOnFirstImage: selectedImageIndex === 0, isOnLastImage: selectedImageIndex === images.length - 1, shouldShowStagingImage, diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx index 87e3435127..94a990bb4c 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasSettingsButtonPopover.tsx @@ -6,6 +6,7 @@ import IAIIconButton from 'common/components/IAIIconButton'; import IAIPopover from 'common/components/IAIPopover'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { + setShouldAntialias, setShouldAutoSave, setShouldCropToBoundingBoxOnSave, setShouldDarkenOutsideBoundingBox, @@ -36,6 +37,7 @@ export const canvasControlsSelector = createSelector( shouldShowIntermediates, shouldSnapToGrid, shouldRestrictStrokesToBox, + shouldAntialias, } = canvas; return { @@ -47,6 +49,7 @@ export const canvasControlsSelector = createSelector( shouldShowIntermediates, shouldSnapToGrid, shouldRestrictStrokesToBox, + shouldAntialias, }; }, { @@ -69,6 +72,7 @@ const IAICanvasSettingsButtonPopover = () => { shouldShowIntermediates, shouldSnapToGrid, shouldRestrictStrokesToBox, + shouldAntialias, } = useAppSelector(canvasControlsSelector); useHotkeys( @@ -148,6 +152,12 @@ const IAICanvasSettingsButtonPopover = () => { dispatch(setShouldShowCanvasDebugInfo(e.target.checked)) } /> + + dispatch(setShouldAntialias(e.target.checked))} + /> diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasPersistDenylist.ts b/invokeai/frontend/web/src/features/canvas/store/canvasPersistDenylist.ts index abaefab8b0..1f44b43021 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasPersistDenylist.ts @@ -9,6 +9,12 @@ const itemsToDenylist: (keyof CanvasState)[] = [ 'doesCanvasNeedScaling', ]; +export const canvasPersistDenylist: (keyof CanvasState)[] = [ + 'cursorPosition', + 'isCanvasInitialized', + 'doesCanvasNeedScaling', +]; + export const canvasDenylist = itemsToDenylist.map( (denylistItem) => `canvas.${denylistItem}` ); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index ab3ab0c4e9..037d353f42 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -38,7 +38,7 @@ export const initialLayerState: CanvasLayerState = { }, }; -const initialCanvasState: CanvasState = { +export const initialCanvasState: CanvasState = { boundingBoxCoordinates: { x: 0, y: 0 }, boundingBoxDimensions: { width: 512, height: 512 }, boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.5 }, @@ -66,6 +66,7 @@ const initialCanvasState: CanvasState = { minimumStageScale: 1, pastLayerStates: [], scaledBoundingBoxDimensions: { width: 512, height: 512 }, + shouldAntialias: true, shouldAutoSave: false, shouldCropToBoundingBoxOnSave: false, shouldDarkenOutsideBoundingBox: false, @@ -156,22 +157,20 @@ export const canvasSlice = createSlice({ setCursorPosition: (state, action: PayloadAction) => { state.cursorPosition = action.payload; }, - setInitialCanvasImage: (state, action: PayloadAction) => { + setInitialCanvasImage: (state, action: PayloadAction) => { const image = action.payload; + const { width, height } = image.metadata; const { stageDimensions } = state; const newBoundingBoxDimensions = { - width: roundDownToMultiple(clamp(image.width, 64, 512), 64), - height: roundDownToMultiple(clamp(image.height, 64, 512), 64), + width: roundDownToMultiple(clamp(width, 64, 512), 64), + height: roundDownToMultiple(clamp(height, 64, 512), 64), }; const newBoundingBoxCoordinates = { - x: roundToMultiple( - image.width / 2 - newBoundingBoxDimensions.width / 2, - 64 - ), + x: roundToMultiple(width / 2 - newBoundingBoxDimensions.width / 2, 64), y: roundToMultiple( - image.height / 2 - newBoundingBoxDimensions.height / 2, + height / 2 - newBoundingBoxDimensions.height / 2, 64 ), }; @@ -196,8 +195,8 @@ export const canvasSlice = createSlice({ layer: 'base', x: 0, y: 0, - width: image.width, - height: image.height, + width: width, + height: height, image: image, }, ], @@ -208,8 +207,8 @@ export const canvasSlice = createSlice({ const newScale = calculateScale( stageDimensions.width, stageDimensions.height, - image.width, - image.height, + width, + height, STAGE_PADDING_PERCENTAGE ); @@ -218,8 +217,8 @@ export const canvasSlice = createSlice({ stageDimensions.height, 0, 0, - image.width, - image.height, + width, + height, newScale ); state.stageScale = newScale; @@ -287,16 +286,28 @@ export const canvasSlice = createSlice({ setIsMoveStageKeyHeld: (state, action: PayloadAction) => { state.isMoveStageKeyHeld = action.payload; }, - addImageToStagingArea: ( + canvasSessionIdChanged: (state, action: PayloadAction) => { + state.layerState.stagingArea.sessionId = action.payload; + }, + stagingAreaInitialized: ( state, - action: PayloadAction<{ - boundingBox: IRect; - image: InvokeAI._Image; - }> + action: PayloadAction<{ sessionId: string; boundingBox: IRect }> ) => { - const { boundingBox, image } = action.payload; + const { sessionId, boundingBox } = action.payload; - if (!boundingBox || !image) return; + state.layerState.stagingArea = { + boundingBox, + sessionId, + images: [], + selectedImageIndex: -1, + }; + }, + addImageToStagingArea: (state, action: PayloadAction) => { + const image = action.payload; + + if (!image || !state.layerState.stagingArea.boundingBox) { + return; + } state.pastLayerStates.push(cloneDeep(state.layerState)); @@ -307,7 +318,7 @@ export const canvasSlice = createSlice({ state.layerState.stagingArea.images.push({ kind: 'image', layer: 'base', - ...boundingBox, + ...state.layerState.stagingArea.boundingBox, image, }); @@ -323,9 +334,7 @@ export const canvasSlice = createSlice({ state.pastLayerStates.shift(); } - state.layerState.stagingArea = { - ...initialLayerState.stagingArea, - }; + state.layerState.stagingArea = { ...initialLayerState.stagingArea }; state.futureLayerStates = []; state.shouldShowStagingOutline = true; @@ -663,6 +672,10 @@ export const canvasSlice = createSlice({ } }, nextStagingAreaImage: (state) => { + if (!state.layerState.stagingArea.images.length) { + return; + } + const currentIndex = state.layerState.stagingArea.selectedImageIndex; const length = state.layerState.stagingArea.images.length; @@ -672,6 +685,10 @@ export const canvasSlice = createSlice({ ); }, prevStagingAreaImage: (state) => { + if (!state.layerState.stagingArea.images.length) { + return; + } + const currentIndex = state.layerState.stagingArea.selectedImageIndex; state.layerState.stagingArea.selectedImageIndex = Math.max( @@ -680,6 +697,10 @@ export const canvasSlice = createSlice({ ); }, commitStagingAreaImage: (state) => { + if (!state.layerState.stagingArea.images.length) { + return; + } + const { images, selectedImageIndex } = state.layerState.stagingArea; state.pastLayerStates.push(cloneDeep(state.layerState)); @@ -776,6 +797,9 @@ export const canvasSlice = createSlice({ setShouldRestrictStrokesToBox: (state, action: PayloadAction) => { state.shouldRestrictStrokesToBox = action.payload; }, + setShouldAntialias: (state, action: PayloadAction) => { + state.shouldAntialias = action.payload; + }, setShouldCropToBoundingBoxOnSave: ( state, action: PayloadAction @@ -885,6 +909,9 @@ export const { undo, setScaledBoundingBoxDimensions, setShouldRestrictStrokesToBox, + stagingAreaInitialized, + canvasSessionIdChanged, + setShouldAntialias, } = canvasSlice.actions; export default canvasSlice.reducer; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts index 2eec0e9bed..2a6461aaf6 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts @@ -37,7 +37,7 @@ export type CanvasImage = { y: number; width: number; height: number; - image: InvokeAI._Image; + image: InvokeAI.Image; }; export type CanvasMaskLine = { @@ -90,9 +90,16 @@ export type CanvasLayerState = { stagingArea: { images: CanvasImage[]; selectedImageIndex: number; + sessionId?: string; + boundingBox?: IRect; }; }; +export type CanvasSession = { + sessionId: string; + boundingBox: IRect; +}; + // type guards export const isCanvasMaskLine = (obj: CanvasObject): obj is CanvasMaskLine => obj.kind === 'line' && obj.layer === 'mask'; @@ -125,7 +132,7 @@ export interface CanvasState { cursorPosition: Vector2d | null; doesCanvasNeedScaling: boolean; futureLayerStates: CanvasLayerState[]; - intermediateImage?: InvokeAI._Image; + intermediateImage?: InvokeAI.Image; isCanvasInitialized: boolean; isDrawing: boolean; isMaskEnabled: boolean; @@ -142,6 +149,7 @@ export interface CanvasState { minimumStageScale: number; pastLayerStates: CanvasLayerState[]; scaledBoundingBoxDimensions: Dimensions; + shouldAntialias: boolean; shouldAutoSave: boolean; shouldCropToBoundingBoxOnSave: boolean; shouldDarkenOutsideBoundingBox: boolean; diff --git a/invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts b/invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts new file mode 100644 index 0000000000..44220c8ba4 --- /dev/null +++ b/invokeai/frontend/web/src/features/canvas/util/canvasToBlob.ts @@ -0,0 +1,13 @@ +/** + * Gets a Blob from a canvas. + */ +export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise => + new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + return; + } + reject('Unable to create Blob'); + }); + }); diff --git a/invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts b/invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts new file mode 100644 index 0000000000..739240a79d --- /dev/null +++ b/invokeai/frontend/web/src/features/canvas/util/dataURLToImageData.ts @@ -0,0 +1,29 @@ +/** + * Gets an ImageData object from an image dataURL by drawing it to a canvas. + */ +export const dataURLToImageData = async ( + dataURL: string, + width: number, + height: number +): Promise => + new Promise((resolve, reject) => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + const image = new Image(); + + if (!ctx) { + canvas.remove(); + reject('Unable to get context'); + return; + } + + image.onload = function () { + ctx.drawImage(image, 0, 0); + canvas.remove(); + resolve(ctx.getImageData(0, 0, width, height)); + }; + + image.src = dataURL; + }); diff --git a/invokeai/frontend/web/src/features/canvas/util/generateMask.ts b/invokeai/frontend/web/src/features/canvas/util/generateMask.ts index 9187ac2ac7..a5cd41ad10 100644 --- a/invokeai/frontend/web/src/features/canvas/util/generateMask.ts +++ b/invokeai/frontend/web/src/features/canvas/util/generateMask.ts @@ -1,6 +1,110 @@ +// import { CanvasMaskLine } from 'features/canvas/store/canvasTypes'; +// import Konva from 'konva'; +// import { Stage } from 'konva/lib/Stage'; +// import { IRect } from 'konva/lib/types'; + +// /** +// * Generating a mask image from InpaintingCanvas.tsx is not as simple +// * as calling toDataURL() on the canvas, because the mask may be represented +// * by colored lines or transparency, or the user may have inverted the mask +// * display. +// * +// * So we need to regenerate the mask image by creating an offscreen canvas, +// * drawing the mask and compositing everything correctly to output a valid +// * mask image. +// */ +// export const getStageDataURL = (stage: Stage, boundingBox: IRect): string => { +// // create an offscreen canvas and add the mask to it +// // const { stage, offscreenContainer } = buildMaskStage(lines, boundingBox); + +// const dataURL = stage.toDataURL({ ...boundingBox }); + +// // const imageData = stage +// // .toCanvas() +// // .getContext('2d') +// // ?.getImageData( +// // boundingBox.x, +// // boundingBox.y, +// // boundingBox.width, +// // boundingBox.height +// // ); + +// // offscreenContainer.remove(); + +// // return { dataURL, imageData }; + +// return dataURL; +// }; + +// export const getStageImageData = ( +// stage: Stage, +// boundingBox: IRect +// ): ImageData | undefined => { +// const imageData = stage +// .toCanvas() +// .getContext('2d') +// ?.getImageData( +// boundingBox.x, +// boundingBox.y, +// boundingBox.width, +// boundingBox.height +// ); + +// return imageData; +// }; + +// export const buildMaskStage = ( +// lines: CanvasMaskLine[], +// boundingBox: IRect +// ): { stage: Stage; offscreenContainer: HTMLDivElement } => { +// // create an offscreen canvas and add the mask to it +// const { width, height } = boundingBox; + +// const offscreenContainer = document.createElement('div'); + +// const stage = new Konva.Stage({ +// container: offscreenContainer, +// width: width, +// height: height, +// }); + +// const baseLayer = new Konva.Layer(); +// const maskLayer = new Konva.Layer(); + +// // composite the image onto the mask layer +// baseLayer.add( +// new Konva.Rect({ +// ...boundingBox, +// fill: 'white', +// }) +// ); + +// lines.forEach((line) => +// maskLayer.add( +// new Konva.Line({ +// points: line.points, +// stroke: 'black', +// strokeWidth: line.strokeWidth * 2, +// tension: 0, +// lineCap: 'round', +// lineJoin: 'round', +// shadowForStrokeEnabled: false, +// globalCompositeOperation: +// line.tool === 'brush' ? 'source-over' : 'destination-out', +// }) +// ) +// ); + +// stage.add(baseLayer); +// stage.add(maskLayer); + +// return { stage, offscreenContainer }; +// }; + import { CanvasMaskLine } from 'features/canvas/store/canvasTypes'; import Konva from 'konva'; import { IRect } from 'konva/lib/types'; +import { canvasToBlob } from './canvasToBlob'; /** * Generating a mask image from InpaintingCanvas.tsx is not as simple @@ -12,7 +116,7 @@ import { IRect } from 'konva/lib/types'; * drawing the mask and compositing everything correctly to output a valid * mask image. */ -const generateMask = (lines: CanvasMaskLine[], boundingBox: IRect): string => { +const generateMask = async (lines: CanvasMaskLine[], boundingBox: IRect) => { // create an offscreen canvas and add the mask to it const { width, height } = boundingBox; @@ -54,11 +158,13 @@ const generateMask = (lines: CanvasMaskLine[], boundingBox: IRect): string => { stage.add(baseLayer); stage.add(maskLayer); - const dataURL = stage.toDataURL({ ...boundingBox }); + const maskDataURL = stage.toDataURL(boundingBox); + + const maskBlob = await canvasToBlob(stage.toCanvas(boundingBox)); offscreenContainer.remove(); - return dataURL; + return { maskDataURL, maskBlob }; }; export default generateMask; diff --git a/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts b/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts new file mode 100644 index 0000000000..131b109f55 --- /dev/null +++ b/invokeai/frontend/web/src/features/canvas/util/getCanvasData.ts @@ -0,0 +1,128 @@ +import { RootState } from 'app/store/store'; +import { getCanvasBaseLayer, getCanvasStage } from './konvaInstanceProvider'; +import { isCanvasMaskLine } from '../store/canvasTypes'; +import { log } from 'app/logging/useLogger'; +import { + areAnyPixelsBlack, + getImageDataTransparency, +} from 'common/util/arrayBuffer'; +import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; +import generateMask from './generateMask'; +import { dataURLToImageData } from './dataURLToImageData'; +import { canvasToBlob } from './canvasToBlob'; + +const moduleLog = log.child({ namespace: 'getCanvasDataURLs' }); + +export const getCanvasData = async (state: RootState) => { + const canvasBaseLayer = getCanvasBaseLayer(); + const canvasStage = getCanvasStage(); + + if (!canvasBaseLayer || !canvasStage) { + moduleLog.error('Unable to find canvas / stage'); + return; + } + + const { + layerState: { objects }, + boundingBoxCoordinates, + boundingBoxDimensions, + stageScale, + isMaskEnabled, + shouldPreserveMaskedArea, + boundingBoxScaleMethod: boundingBoxScale, + scaledBoundingBoxDimensions, + } = state.canvas; + + const boundingBox = { + ...boundingBoxCoordinates, + ...boundingBoxDimensions, + }; + + // generationParameters.fit = false; + + // generationParameters.strength = img2imgStrength; + + // generationParameters.invert_mask = shouldPreserveMaskedArea; + + // generationParameters.bounding_box = boundingBox; + + const tempScale = canvasBaseLayer.scale(); + + canvasBaseLayer.scale({ + x: 1 / stageScale, + y: 1 / stageScale, + }); + + const absPos = canvasBaseLayer.getAbsolutePosition(); + + const offsetBoundingBox = { + x: boundingBox.x + absPos.x, + y: boundingBox.y + absPos.y, + width: boundingBox.width, + height: boundingBox.height, + }; + + const baseDataURL = canvasBaseLayer.toDataURL(offsetBoundingBox); + const baseBlob = await canvasToBlob( + canvasBaseLayer.toCanvas(offsetBoundingBox) + ); + + canvasBaseLayer.scale(tempScale); + + const { maskDataURL, maskBlob } = await generateMask( + isMaskEnabled ? objects.filter(isCanvasMaskLine) : [], + boundingBox + ); + + const baseImageData = await dataURLToImageData( + baseDataURL, + boundingBox.width, + boundingBox.height + ); + + const maskImageData = await dataURLToImageData( + maskDataURL, + boundingBox.width, + boundingBox.height + ); + + const { + isPartiallyTransparent: baseIsPartiallyTransparent, + isFullyTransparent: baseIsFullyTransparent, + } = getImageDataTransparency(baseImageData.data); + + const doesMaskHaveBlackPixels = areAnyPixelsBlack(maskImageData.data); + + if (state.system.enableImageDebugging) { + openBase64ImageInTab([ + { base64: maskDataURL, caption: 'mask b64' }, + { base64: baseDataURL, caption: 'image b64' }, + ]); + } + + // generationParameters.init_img = imageDataURL; + // generationParameters.progress_images = false; + + // if (boundingBoxScale !== 'none') { + // generationParameters.inpaint_width = scaledBoundingBoxDimensions.width; + // generationParameters.inpaint_height = scaledBoundingBoxDimensions.height; + // } + + // generationParameters.seam_size = seamSize; + // generationParameters.seam_blur = seamBlur; + // generationParameters.seam_strength = seamStrength; + // generationParameters.seam_steps = seamSteps; + // generationParameters.tile_size = tileSize; + // generationParameters.infill_method = infillMethod; + // generationParameters.force_outpaint = false; + + return { + baseDataURL, + baseBlob, + maskDataURL, + maskBlob, + baseIsPartiallyTransparent, + baseIsFullyTransparent, + doesMaskHaveBlackPixels, + }; +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index 4fef811d46..2a3d12ce91 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -1,12 +1,17 @@ import { createSelector } from '@reduxjs/toolkit'; -import { get, isEqual, isNumber, isString } from 'lodash-es'; +import { isEqual, isString } from 'lodash-es'; import { ButtonGroup, Flex, FlexProps, - FormControl, + IconButton, Link, + Menu, + MenuButton, + MenuItemOption, + MenuList, + MenuOptionGroup, useDisclosure, useToast, } from '@chakra-ui/react'; @@ -15,21 +20,12 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; import IAIIconButton from 'common/components/IAIIconButton'; import IAIPopover from 'common/components/IAIPopover'; -import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; -import { GalleryState } from 'features/gallery/store/gallerySlice'; + import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; -import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings'; -import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings'; -import { - initialImageSelected, - setAllParameters, - // setInitialImage, - setSeed, -} from 'features/parameters/store/generationSlice'; import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors'; import { systemSelector } from 'features/system/store/systemSelectors'; -import { SystemState } from 'features/system/store/systemSlice'; + import { activeTabNameSelector, uiSelector, @@ -56,6 +52,7 @@ import { FaShare, FaShareAlt, FaTrash, + FaWrench, } from 'react-icons/fa'; import { gallerySelector, @@ -66,8 +63,13 @@ import { useCallback } from 'react'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { useGetUrl } from 'common/util/getUrl'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { imageDeleted } from 'services/thunks/image'; import { useParameters } from 'features/parameters/hooks/useParameters'; +import { initialImageSelected } from 'features/parameters/store/actions'; +import { requestedImageDeletion } from '../store/actions'; +import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings'; +import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings'; +import { allParametersSet } from 'features/parameters/store/generationSlice'; +import DeleteImageButton from './ImageActionButtons/DeleteImageButton'; const currentImageButtonsSelector = createSelector( [ @@ -164,40 +166,59 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { const toast = useToast(); const { t } = useTranslation(); - const { recallPrompt, recallSeed, sendToImageToImage } = useParameters(); + const { recallPrompt, recallSeed, recallAllParameters } = useParameters(); - const handleCopyImage = useCallback(async () => { - if (!image?.url) { - return; - } + // const handleCopyImage = useCallback(async () => { + // if (!image?.url) { + // return; + // } - const url = getUrl(image.url); + // const url = getUrl(image.url); - if (!url) { - return; - } + // if (!url) { + // return; + // } - const blob = await fetch(url).then((res) => res.blob()); - const data = [new ClipboardItem({ [blob.type]: blob })]; + // const blob = await fetch(url).then((res) => res.blob()); + // const data = [new ClipboardItem({ [blob.type]: blob })]; - await navigator.clipboard.write(data); + // await navigator.clipboard.write(data); - toast({ - title: t('toast.imageCopied'), - status: 'success', - duration: 2500, - isClosable: true, - }); - }, [getUrl, t, image?.url, toast]); + // toast({ + // title: t('toast.imageCopied'), + // status: 'success', + // duration: 2500, + // isClosable: true, + // }); + // }, [getUrl, t, image?.url, toast]); const handleCopyImageLink = useCallback(() => { - const url = image - ? shouldTransformUrls - ? getUrl(image.url) - : window.location.toString() + image.url - : ''; + const getImageUrl = () => { + if (!image) { + return; + } + + if (shouldTransformUrls) { + return getUrl(image.url); + } + + if (image.url.startsWith('http')) { + return image.url; + } + + return window.location.toString() + image.url; + }; + + const url = getImageUrl(); if (!url) { + toast({ + title: t('toast.problemCopyingImageLink'), + status: 'error', + duration: 2500, + isClosable: true, + }); + return; } @@ -216,39 +237,15 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }, [dispatch, shouldHidePreview]); const handleClickUseAllParameters = useCallback(() => { - if (!image) return; - // selectedImage.metadata && - // dispatch(setAllParameters(selectedImage.metadata)); - // if (selectedImage.metadata?.image.type === 'img2img') { - // dispatch(setActiveTab('img2img')); - // } else if (selectedImage.metadata?.image.type === 'txt2img') { - // dispatch(setActiveTab('txt2img')); - // } - }, [image]); + recallAllParameters(image); + }, [image, recallAllParameters]); useHotkeys( 'a', () => { - const type = image?.metadata?.invokeai?.node?.types; - if (isString(type) && ['txt2img', 'img2img'].includes(type)) { - handleClickUseAllParameters(); - toast({ - title: t('toast.parametersSet'), - status: 'success', - duration: 2500, - isClosable: true, - }); - } else { - toast({ - title: t('toast.parametersNotSet'), - description: t('toast.parametersNotSetDesc'), - status: 'error', - duration: 2500, - isClosable: true, - }); - } + handleClickUseAllParameters; }, - [image] + [image, recallAllParameters] ); const handleUseSeed = useCallback(() => { @@ -264,8 +261,8 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { useHotkeys('p', handleUsePrompt, [image]); const handleSendToImageToImage = useCallback(() => { - sendToImageToImage(image); - }, [image, sendToImageToImage]); + dispatch(initialImageSelected(image)); + }, [dispatch, image]); useHotkeys('shift+i', handleSendToImageToImage, [image]); @@ -375,7 +372,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { const handleDelete = useCallback(() => { if (canDeleteImage && image) { - dispatch(imageDeleted({ imageType: image.type, imageName: image.name })); + dispatch(requestedImageDeletion(image)); } }, [image, canDeleteImage, dispatch]); @@ -440,13 +437,13 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { {t('parameters.sendToUnifiedCanvas')} - } > {t('parameters.copyImage')} - + */} { - : } tooltip={ !shouldHidePreview @@ -476,7 +473,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { } isChecked={shouldHidePreview} onClick={handlePreviewVisibility} - /> + /> */} {isLightboxEnabled && ( } @@ -518,8 +515,8 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { tooltip={`${t('parameters.useAll')} (A)`} aria-label={`${t('parameters.useAll')} (A)`} isDisabled={ - !['txt2img', 'img2img'].includes( - image?.metadata?.sd_metadata?.type + !['txt2img', 'img2img', 'inpaint'].includes( + String(image?.metadata?.invokeai?.node?.type) ) } onClick={handleClickUseAllParameters} @@ -602,22 +599,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { /> - } - tooltip={`${t('gallery.deleteImage')} (Del)`} - aria-label={`${t('gallery.deleteImage')} (Del)`} - isDisabled={!image || !isConnected} - colorScheme="error" - /> + + + - {image && ( - - )} > ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index f6cfa99237..a0fbd7c5d1 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -1,27 +1,35 @@ -import { Box, Flex, Image } from '@chakra-ui/react'; +import { Box, Flex, Image, Skeleton, useBoolean } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useGetUrl } from 'common/util/getUrl'; -import { systemSelector } from 'features/system/store/systemSelectors'; import { uiSelector } from 'features/ui/store/uiSelectors'; import { isEqual } from 'lodash-es'; -import { selectedImageSelector } from '../store/gallerySelectors'; -import CurrentImageFallback from './CurrentImageFallback'; +import { gallerySelector } from '../store/gallerySelectors'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import NextPrevImageButtons from './NextPrevImageButtons'; import CurrentImageHidden from './CurrentImageHidden'; -import { memo } from 'react'; +import { DragEvent, memo, useCallback } from 'react'; +import { systemSelector } from 'features/system/store/systemSelectors'; +import CurrentImageFallback from './CurrentImageFallback'; export const imagesSelector = createSelector( - [uiSelector, selectedImageSelector, systemSelector], - (ui, selectedImage, system) => { - const { shouldShowImageDetails, shouldHidePreview } = ui; - + [uiSelector, gallerySelector, systemSelector], + (ui, gallery, system) => { + const { + shouldShowImageDetails, + shouldHidePreview, + shouldShowProgressInViewer, + } = ui; + const { selectedImage } = gallery; + const { progressImage, shouldAntialiasProgressImage } = system; return { shouldShowImageDetails, shouldHidePreview, image: selectedImage, + progressImage, + shouldShowProgressInViewer, + shouldAntialiasProgressImage, }; }, { @@ -32,10 +40,30 @@ export const imagesSelector = createSelector( ); const CurrentImagePreview = () => { - const { shouldShowImageDetails, image, shouldHidePreview } = - useAppSelector(imagesSelector); + const { + shouldShowImageDetails, + image, + shouldHidePreview, + progressImage, + shouldShowProgressInViewer, + shouldAntialiasProgressImage, + } = useAppSelector(imagesSelector); const { getUrl } = useGetUrl(); + const [isLoaded, { on, off }] = useBoolean(); + + const handleDragStart = useCallback( + (e: DragEvent) => { + if (!image) { + return; + } + e.dataTransfer.setData('invokeai/imageName', image.name); + e.dataTransfer.setData('invokeai/imageType', image.type); + e.dataTransfer.effectAllowed = 'move'; + }, + [image] + ); + return ( { height: '100%', }} > - {image && ( + {progressImage && shouldShowProgressInViewer ? ( : undefined} + src={progressImage.dataURL} + width={progressImage.width} + height={progressImage.height} sx={{ objectFit: 'contain', maxWidth: '100%', @@ -59,8 +86,34 @@ const CurrentImagePreview = () => { height: 'auto', position: 'absolute', borderRadius: 'base', + imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated', }} /> + ) : ( + image && ( + + ) : ( + + ) + } + sx={{ + objectFit: 'contain', + maxWidth: '100%', + maxHeight: '100%', + height: 'auto', + position: 'absolute', + borderRadius: 'base', + }} + /> + ) )} {shouldShowImageDetails && image && 'metadata' in image && ( { + const { shouldUseSingleGalleryColumn, galleryImageObjectFit } = gallery; + const { progressImage, shouldAntialiasProgressImage } = system; + + return { + progressImage, + shouldUseSingleGalleryColumn, + galleryImageObjectFit, + shouldAntialiasProgressImage, + }; + }, + defaultSelectorOptions +); + +const GalleryProgressImage = () => { + const { + progressImage, + shouldUseSingleGalleryColumn, + galleryImageObjectFit, + shouldAntialiasProgressImage, + } = useAppSelector(selector); + + if (!progressImage) { + return null; + } + + return ( + + + + ); +}; + +export default memo(GalleryProgressImage); diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index 032784fbf9..2e5f166025 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -5,19 +5,20 @@ import { Image, MenuItem, MenuList, - Skeleton, useDisclosure, - useTheme, useToast, } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { imageSelected } from 'features/gallery/store/gallerySlice'; -import { DragEvent, memo, useCallback, useState } from 'react'; +import { DragEvent, MouseEvent, memo, useCallback, useState } from 'react'; import { FaCheck, FaExpand, FaImage, FaShare, FaTrash } from 'react-icons/fa'; import DeleteImageModal from './DeleteImageModal'; import { ContextMenu } from 'chakra-ui-contextmenu'; import * as InvokeAI from 'app/types/invokeai'; -import { resizeAndScaleCanvas } from 'features/canvas/store/canvasSlice'; +import { + resizeAndScaleCanvas, + setInitialCanvasImage, +} from 'features/canvas/store/canvasSlice'; import { gallerySelector } from 'features/gallery/store/gallerySelectors'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useTranslation } from 'react-i18next'; @@ -25,7 +26,6 @@ import IAIIconButton from 'common/components/IAIIconButton'; import { useGetUrl } from 'common/util/getUrl'; import { ExternalLinkIcon } from '@chakra-ui/icons'; import { IoArrowUndoCircleOutline } from 'react-icons/io5'; -import { imageDeleted } from 'services/thunks/image'; import { createSelector } from '@reduxjs/toolkit'; import { systemSelector } from 'features/system/store/systemSelectors'; import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; @@ -33,6 +33,8 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { isEqual } from 'lodash-es'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useParameters } from 'features/parameters/hooks/useParameters'; +import { initialImageSelected } from 'features/parameters/store/actions'; +import { requestedImageDeletion } from '../store/actions'; export const selector = createSelector( [gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector], @@ -94,16 +96,16 @@ const HoverableImage = memo((props: HoverableImageProps) => { } = useDisclosure(); const { image, isSelected } = props; - const { url, thumbnail, name, metadata } = image; + const { url, thumbnail, name } = image; const { getUrl } = useGetUrl(); const [isHovered, setIsHovered] = useState(false); const toast = useToast(); - const { direction } = useTheme(); + const { t } = useTranslation(); const { isFeatureEnabled: isLightboxEnabled } = useFeatureStatus('lightbox'); - const { recallSeed, recallPrompt, sendToImageToImage, recallInitialImage } = + const { recallSeed, recallPrompt, recallInitialImage, recallAllParameters } = useParameters(); const handleMouseOver = () => setIsHovered(true); @@ -112,18 +114,22 @@ const HoverableImage = memo((props: HoverableImageProps) => { // Immediately deletes an image const handleDelete = useCallback(() => { if (canDeleteImage && image) { - dispatch(imageDeleted({ imageType: image.type, imageName: image.name })); + dispatch(requestedImageDeletion(image)); } }, [dispatch, image, canDeleteImage]); // Opens the alert dialog to check if user is sure they want to delete - const handleInitiateDelete = useCallback(() => { - if (shouldConfirmOnDelete) { - onDeleteDialogOpen(); - } else { - handleDelete(); - } - }, [handleDelete, onDeleteDialogOpen, shouldConfirmOnDelete]); + const handleInitiateDelete = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + if (shouldConfirmOnDelete) { + onDeleteDialogOpen(); + } else { + handleDelete(); + } + }, + [handleDelete, onDeleteDialogOpen, shouldConfirmOnDelete] + ); const handleSelectImage = useCallback(() => { dispatch(imageSelected(image)); @@ -148,8 +154,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { }, [image, recallSeed]); const handleSendToImageToImage = useCallback(() => { - sendToImageToImage(image); - }, [image, sendToImageToImage]); + dispatch(initialImageSelected(image)); + }, [dispatch, image]); const handleRecallInitialImage = useCallback(() => { recallInitialImage(image.metadata.invokeai?.node?.image); @@ -159,7 +165,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { * TODO: the rest of these */ const handleSendToCanvas = () => { - // dispatch(setInitialCanvasImage(image)); + dispatch(setInitialCanvasImage(image)); dispatch(resizeAndScaleCanvas()); @@ -175,16 +181,9 @@ const HoverableImage = memo((props: HoverableImageProps) => { }); }; - const handleUseAllParameters = () => { - // metadata.invokeai?.node && - // dispatch(setAllParameters(metadata.invokeai?.node)); - // toast({ - // title: t('toast.parametersSet'), - // status: 'success', - // duration: 2500, - // isClosable: true, - // }); - }; + const handleUseAllParameters = useCallback(() => { + recallAllParameters(image); + }, [image, recallAllParameters]); const handleLightBox = () => { // dispatch(setCurrentImage(image)); @@ -238,7 +237,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { icon={} onClickCapture={handleUseAllParameters} isDisabled={ - !['txt2img', 'img2img'].includes( + !['txt2img', 'img2img', 'inpaint'].includes( String(image?.metadata?.invokeai?.node?.type) ) } @@ -315,6 +314,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { sx={{ width: '50%', height: '50%', + maxWidth: '4rem', + maxHeight: '4rem', fill: 'ok.500', }} /> diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx new file mode 100644 index 0000000000..6e35ccd63b --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx @@ -0,0 +1,92 @@ +import { createSelector } from '@reduxjs/toolkit'; + +import { useDisclosure } from '@chakra-ui/react'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { systemSelector } from 'features/system/store/systemSelectors'; + +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { FaTrash } from 'react-icons/fa'; +import { memo, useCallback } from 'react'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import DeleteImageModal from '../DeleteImageModal'; +import { requestedImageDeletion } from 'features/gallery/store/actions'; +import { Image } from 'app/types/invokeai'; + +const selector = createSelector( + [systemSelector], + (system) => { + const { isProcessing, isConnected, shouldConfirmOnDelete } = system; + + return { + canDeleteImage: isConnected && !isProcessing, + shouldConfirmOnDelete, + isProcessing, + isConnected, + }; + }, + defaultSelectorOptions +); + +type DeleteImageButtonProps = { + image: Image | undefined; +}; + +const DeleteImageButton = (props: DeleteImageButtonProps) => { + const { image } = props; + const dispatch = useAppDispatch(); + const { isProcessing, isConnected, canDeleteImage, shouldConfirmOnDelete } = + useAppSelector(selector); + + const { + isOpen: isDeleteDialogOpen, + onOpen: onDeleteDialogOpen, + onClose: onDeleteDialogClose, + } = useDisclosure(); + + const { t } = useTranslation(); + + const handleDelete = useCallback(() => { + if (canDeleteImage && image) { + dispatch(requestedImageDeletion(image)); + } + }, [image, canDeleteImage, dispatch]); + + const handleInitiateDelete = useCallback(() => { + if (shouldConfirmOnDelete) { + onDeleteDialogOpen(); + } else { + handleDelete(); + } + }, [shouldConfirmOnDelete, onDeleteDialogOpen, handleDelete]); + + useHotkeys('delete', handleInitiateDelete, [ + image, + shouldConfirmOnDelete, + isConnected, + isProcessing, + ]); + + return ( + <> + } + tooltip={`${t('gallery.deleteImage')} (Del)`} + aria-label={`${t('gallery.deleteImage')} (Del)`} + isDisabled={!image || !isConnected} + colorScheme="error" + /> + {image && ( + + )} + > + ); +}; + +export default memo(DeleteImageButton); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index 1e1e73812f..1426aff43d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -5,6 +5,7 @@ import { FlexProps, Grid, Icon, + Image, Text, forwardRef, } from '@chakra-ui/react'; @@ -14,7 +15,10 @@ import IAICheckbox from 'common/components/IAICheckbox'; import IAIIconButton from 'common/components/IAIIconButton'; import IAIPopover from 'common/components/IAIPopover'; import IAISlider from 'common/components/IAISlider'; -import { imageGallerySelector } from 'features/gallery/store/gallerySelectors'; +import { + gallerySelector, + imageGallerySelector, +} from 'features/gallery/store/gallerySelectors'; import { setCurrentCategory, setGalleryImageMinimumWidth, @@ -50,30 +54,48 @@ import { uploadsAdapter } from '../store/uploadsSlice'; import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; import { Virtuoso, VirtuosoGrid } from 'react-virtuoso'; +import ProgressImagePreview from 'features/parameters/components/_ProgressImagePreview'; +import ProgressImage from 'features/parameters/components/ProgressImage'; +import { systemSelector } from 'features/system/store/systemSelectors'; +import { Image as ImageType } from 'app/types/invokeai'; +import { ProgressImage as ProgressImageType } from 'services/events/types'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import GalleryProgressImage from './GalleryProgressImage'; const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290; +const PROGRESS_IMAGE_PLACEHOLDER = 'PROGRESS_IMAGE_PLACEHOLDER'; -const gallerySelector = createSelector( - [ - (state: RootState) => state.uploads, - (state: RootState) => state.results, - (state: RootState) => state.gallery, - ], - (uploads, results, gallery) => { +const selector = createSelector( + [(state: RootState) => state], + (state) => { + const { results, uploads, system, gallery } = state; const { currentCategory } = gallery; - return currentCategory === 'results' - ? { - images: resultsAdapter.getSelectors().selectAll(results), - isLoading: results.isLoading, - areMoreImagesAvailable: results.page < results.pages - 1, - } - : { - images: uploadsAdapter.getSelectors().selectAll(uploads), - isLoading: uploads.isLoading, - areMoreImagesAvailable: uploads.page < uploads.pages - 1, - }; - } + const tempImages: (ImageType | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = []; + + if (system.progressImage) { + tempImages.push(PROGRESS_IMAGE_PLACEHOLDER); + } + + if (currentCategory === 'results') { + return { + images: tempImages.concat( + resultsAdapter.getSelectors().selectAll(results) + ), + isLoading: results.isLoading, + areMoreImagesAvailable: results.page < results.pages - 1, + }; + } + + return { + images: tempImages.concat( + uploadsAdapter.getSelectors().selectAll(uploads) + ), + isLoading: uploads.isLoading, + areMoreImagesAvailable: uploads.page < uploads.pages - 1, + }; + }, + defaultSelectorOptions ); const ImageGalleryContent = () => { @@ -108,7 +130,7 @@ const ImageGalleryContent = () => { } = useAppSelector(imageGallerySelector); const { images, areMoreImagesAvailable, isLoading } = - useAppSelector(gallerySelector); + useAppSelector(selector); const handleClickLoadMore = () => { if (currentCategory === 'results') { @@ -170,8 +192,24 @@ const ImageGalleryContent = () => { } }, []); + const handleEndReached = useCallback(() => { + if (currentCategory === 'results') { + dispatch(receivedResultImagesPage()); + } else if (currentCategory === 'uploads') { + dispatch(receivedUploadImagesPage()); + } + }, [dispatch, currentCategory]); + return ( - + { setScrollerRef(ref)} itemContent={(index, image) => { - const { name } = image; - const isSelected = selectedImage?.name === name; + const isSelected = + image === PROGRESS_IMAGE_PLACEHOLDER + ? false + : selectedImage?.name === image?.name; return ( - + {image === PROGRESS_IMAGE_PLACEHOLDER ? ( + + ) : ( + + )} ); }} @@ -310,18 +357,23 @@ const ImageGalleryContent = () => { { - const { name } = image; - const isSelected = selectedImage?.name === name; + const isSelected = + image === PROGRESS_IMAGE_PLACEHOLDER + ? false + : selectedImage?.name === image?.name; - return ( + return image === PROGRESS_IMAGE_PLACEHOLDER ? ( + + ) : ( @@ -334,6 +386,7 @@ const ImageGalleryContent = () => { onClick={handleClickLoadMore} isDisabled={!areMoreImagesAvailable} isLoading={isLoading} + loadingText="Loading" flexShrink={0} > {areMoreImagesAvailable diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx index 76442d340b..cfb6ba0914 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx @@ -5,7 +5,6 @@ import { // selectPrevImage, setGalleryImageMinimumWidth, } from 'features/gallery/store/gallerySlice'; -import { InvokeTabName } from 'features/ui/store/tabMap'; import { clamp, isEqual } from 'lodash-es'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -13,11 +12,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import './ImageGallery.css'; import ImageGalleryContent from './ImageGalleryContent'; import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer'; -import { - setShouldShowGallery, - toggleGalleryPanel, - togglePinGalleryPanel, -} from 'features/ui/store/uiSlice'; +import { setShouldShowGallery } from 'features/ui/store/uiSlice'; import { createSelector } from '@reduxjs/toolkit'; import { activeTabNameSelector, @@ -26,22 +21,20 @@ import { import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; -import useResolution from 'common/hooks/useResolution'; -import { Flex } from '@chakra-ui/react'; import { memo } from 'react'; -const GALLERY_TAB_WIDTHS: Record< - InvokeTabName, - { galleryMinWidth: number; galleryMaxWidth: number } -> = { - // txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, - // img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, - generate: { galleryMinWidth: 200, galleryMaxWidth: 500 }, - unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 }, - nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 }, - // postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 }, - // training: { galleryMinWidth: 200, galleryMaxWidth: 500 }, -}; +// const GALLERY_TAB_WIDTHS: Record< +// InvokeTabName, +// { galleryMinWidth: number; galleryMaxWidth: number } +// > = { +// txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, +// img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, +// generate: { galleryMinWidth: 200, galleryMaxWidth: 500 }, +// unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 }, +// nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 }, +// postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 }, +// training: { galleryMinWidth: 200, galleryMaxWidth: 500 }, +// }; const galleryPanelSelector = createSelector( [ @@ -73,50 +66,50 @@ const galleryPanelSelector = createSelector( } ); -export const ImageGalleryPanel = () => { +const GalleryDrawer = () => { const dispatch = useAppDispatch(); const { shouldPinGallery, shouldShowGallery, galleryImageMinimumWidth, - activeTabName, - isStaging, - isResizable, - isLightboxOpen, + // activeTabName, + // isStaging, + // isResizable, + // isLightboxOpen, } = useAppSelector(galleryPanelSelector); - const handleSetShouldPinGallery = () => { - dispatch(togglePinGalleryPanel()); - dispatch(requestCanvasRescale()); - }; + // const handleSetShouldPinGallery = () => { + // dispatch(togglePinGalleryPanel()); + // dispatch(requestCanvasRescale()); + // }; - const handleToggleGallery = () => { - dispatch(toggleGalleryPanel()); - shouldPinGallery && dispatch(requestCanvasRescale()); - }; + // const handleToggleGallery = () => { + // dispatch(toggleGalleryPanel()); + // shouldPinGallery && dispatch(requestCanvasRescale()); + // }; const handleCloseGallery = () => { dispatch(setShouldShowGallery(false)); shouldPinGallery && dispatch(requestCanvasRescale()); }; - const resolution = useResolution(); + // const resolution = useResolution(); - useHotkeys( - 'g', - () => { - handleToggleGallery(); - }, - [shouldPinGallery] - ); + // useHotkeys( + // 'g', + // () => { + // handleToggleGallery(); + // }, + // [shouldPinGallery] + // ); - useHotkeys( - 'shift+g', - () => { - handleSetShouldPinGallery(); - }, - [shouldPinGallery] - ); + // useHotkeys( + // 'shift+g', + // () => { + // handleSetShouldPinGallery(); + // }, + // [shouldPinGallery] + // ); useHotkeys( 'esc', @@ -162,55 +155,71 @@ export const ImageGalleryPanel = () => { [galleryImageMinimumWidth] ); - const calcGalleryMinHeight = () => { - if (resolution === 'desktop') return; - return 300; - }; + // const calcGalleryMinHeight = () => { + // if (resolution === 'desktop') return; + // return 300; + // }; - const imageGalleryContent = () => { - return ( - - - - ); - }; + // const imageGalleryContent = () => { + // return ( + // + // + // + // ); + // }; - const resizableImageGalleryContent = () => { - return ( - - - - ); - }; + // const resizableImageGalleryContent = () => { + // return ( + // + // + // + // ); + // }; - const renderImageGallery = () => { - if (['mobile', 'tablet'].includes(resolution)) return imageGalleryContent(); - return resizableImageGalleryContent(); - }; + // const renderImageGallery = () => { + // if (['mobile', 'tablet'].includes(resolution)) return imageGalleryContent(); + // return resizableImageGalleryContent(); + // }; - return renderImageGallery(); + if (shouldPinGallery) { + return null; + } + + return ( + + + + ); + + // return renderImageGallery(); }; -export default memo(ImageGalleryPanel); +export default memo(GalleryDrawer); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx index eedbf63081..4f34100fd6 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -3,7 +3,6 @@ import { Box, Center, Flex, - Heading, IconButton, Link, Text, @@ -19,8 +18,6 @@ import { setCfgScale, setHeight, setImg2imgStrength, - // setInitialImage, - setMaskPath, setPerlin, setSampler, setSeamless, @@ -31,21 +28,14 @@ import { setThreshold, setWidth, } from 'features/parameters/store/generationSlice'; -import { - setCodeformerFidelity, - setFacetoolStrength, - setFacetoolType, - setHiresFix, - setUpscalingDenoising, - setUpscalingLevel, - setUpscalingStrength, -} from 'features/parameters/store/postprocessingSlice'; +import { setHiresFix } from 'features/parameters/store/postprocessingSlice'; import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; import { memo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { FaCopy } from 'react-icons/fa'; import { IoArrowUndoCircleOutline } from 'react-icons/io5'; +import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; type MetadataItemProps = { isLink?: boolean; @@ -300,7 +290,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { )} - + { Metadata JSON: - - {metadataJSON} - + + + {metadataJSON} + + ); diff --git a/invokeai/frontend/web/src/features/gallery/store/actions.ts b/invokeai/frontend/web/src/features/gallery/store/actions.ts new file mode 100644 index 0000000000..55c974b169 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/store/actions.ts @@ -0,0 +1,7 @@ +import { createAction } from '@reduxjs/toolkit'; +import { Image } from 'app/types/invokeai'; +import { SelectedImage } from 'features/parameters/store/actions'; + +export const requestedImageDeletion = createAction< + Image | SelectedImage | undefined +>('gallery/requestedImageDeletion'); diff --git a/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts index 243fe26dd4..dcec4fa373 100644 --- a/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts @@ -4,12 +4,13 @@ import { GalleryState } from './gallerySlice'; * Gallery slice persist denylist */ const itemsToDenylist: (keyof GalleryState)[] = [ - 'categories', 'currentCategory', - 'currentImage', - 'currentImageUuid', 'shouldAutoSwitchToNewImages', - 'intermediateImage', +]; + +export const galleryPersistDenylist: (keyof GalleryState)[] = [ + 'currentCategory', + 'shouldAutoSwitchToNewImages', ]; export const galleryDenylist = itemsToDenylist.map( diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts index 0fc8d300e9..3eeb2aa933 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts @@ -1,23 +1,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; -import { configSelector } from 'features/system/store/configSelectors'; -import { systemSelector } from 'features/system/store/systemSelectors'; + import { activeTabNameSelector, uiSelector, } from 'features/ui/store/uiSelectors'; import { isEqual } from 'lodash-es'; -import { - selectResultsAll, - selectResultsById, - selectResultsEntities, -} from './resultsSlice'; -import { - selectUploadsAll, - selectUploadsById, - selectUploadsEntities, -} from './uploadsSlice'; +import { selectResultsById, selectResultsEntities } from './resultsSlice'; +import { selectUploadsAll, selectUploadsById } from './uploadsSlice'; export const gallerySelector = (state: RootState) => state.gallery; @@ -44,6 +35,11 @@ export const imageGallerySelector = createSelector( const { isLightboxOpen } = lightbox; + const images = + currentCategory === 'results' + ? selectResultsEntities(state) + : selectUploadsAll(state); + return { shouldPinGallery, galleryImageMinimumWidth, @@ -53,7 +49,7 @@ export const imageGallerySelector = createSelector( : `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`, shouldAutoSwitchToNewImages, currentCategory, - images: state[currentCategory].entities, + images, galleryWidth, shouldEnableResize: isLightboxOpen || diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 47c2c4e0fd..2326295451 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,10 +1,6 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import { invocationComplete } from 'services/events/actions'; -import { isImageOutput } from 'services/types/guards'; -import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; -import { imageUploaded } from 'services/thunks/image'; -import { SelectedImage } from 'features/parameters/store/generationSlice'; +import { Image } from 'app/types/invokeai'; type GalleryImageObjectFitType = 'contain' | 'cover'; @@ -12,7 +8,7 @@ export interface GalleryState { /** * The selected image */ - selectedImage?: SelectedImage; + selectedImage?: Image; galleryImageMinimumWidth: number; galleryImageObjectFit: GalleryImageObjectFitType; shouldAutoSwitchToNewImages: boolean; @@ -21,8 +17,7 @@ export interface GalleryState { currentCategory: 'results' | 'uploads'; } -const initialState: GalleryState = { - selectedImage: undefined, +export const initialGalleryState: GalleryState = { galleryImageMinimumWidth: 64, galleryImageObjectFit: 'cover', shouldAutoSwitchToNewImages: true, @@ -33,12 +28,9 @@ const initialState: GalleryState = { export const gallerySlice = createSlice({ name: 'gallery', - initialState, + initialState: initialGalleryState, reducers: { - imageSelected: ( - state, - action: PayloadAction - ) => { + imageSelected: (state, action: PayloadAction) => { state.selectedImage = action.payload; // TODO: if the user selects an image, disable the auto switch? // state.shouldAutoSwitchToNewImages = false; @@ -71,30 +63,6 @@ export const gallerySlice = createSlice({ state.shouldUseSingleGalleryColumn = action.payload; }, }, - extraReducers(builder) { - /** - * Invocation Complete - */ - builder.addCase(invocationComplete, (state, action) => { - const { data } = action.payload; - if (isImageOutput(data.result) && state.shouldAutoSwitchToNewImages) { - state.selectedImage = { - name: data.result.image.image_name, - type: 'results', - }; - } - }); - - /** - * Upload Image - FULFILLED - */ - builder.addCase(imageUploaded.fulfilled, (state, action) => { - const { response } = action.payload; - - const uploadedImage = deserializeImageResponse(response); - state.selectedImage = { name: uploadedImage.name, type: 'uploads' }; - }); - }, }); export const { diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts index b62a199b33..4b8ccac6a7 100644 --- a/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts @@ -5,7 +5,9 @@ import { ResultsState } from './resultsSlice'; * * Currently denylisting results slice entirely, see persist config in store.ts */ -const itemsToDenylist: (keyof ResultsState)[] = ['isLoading']; +const itemsToDenylist: (keyof ResultsState)[] = []; + +export const resultsPersistDenylist: (keyof ResultsState)[] = []; export const resultsDenylist = itemsToDenylist.map( (denylistItem) => `results.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts index 26af366e03..f1286137a9 100644 --- a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts @@ -1,17 +1,11 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; import { Image } from 'app/types/invokeai'; -import { invocationComplete } from 'services/events/actions'; import { RootState } from 'app/store/store'; import { receivedResultImagesPage, IMAGES_PER_PAGE, } from 'services/thunks/gallery'; -import { isImageOutput } from 'services/types/guards'; -import { - buildImageUrls, - extractTimestampFromImageName, -} from 'services/util/deserializeImageField'; import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; import { imageDeleted, @@ -73,44 +67,6 @@ const resultsSlice = createSlice({ state.isLoading = false; }); - /** - * Invocation Complete - */ - builder.addCase(invocationComplete, (state, action) => { - const { data, shouldFetchImages } = action.payload; - const { result, node, graph_execution_state_id } = data; - - if (isImageOutput(result)) { - const name = result.image.image_name; - const type = result.image.image_type; - - // if we need to refetch, set URLs to placeholder for now - const { url, thumbnail } = shouldFetchImages - ? { url: '', thumbnail: '' } - : buildImageUrls(type, name); - - const timestamp = extractTimestampFromImageName(name); - - const image: Image = { - name, - type, - url, - thumbnail, - metadata: { - created: timestamp, - width: result.width, - height: result.height, - invokeai: { - session_id: graph_execution_state_id, - ...(node ? { node } : {}), - }, - }, - }; - - resultsAdapter.setOne(state, image); - } - }); - /** * Image Received - FULFILLED */ @@ -142,9 +98,10 @@ const resultsSlice = createSlice({ }); /** - * Delete Image - FULFILLED + * Delete Image - PENDING + * Pre-emptively remove the image from the gallery */ - builder.addCase(imageDeleted.fulfilled, (state, action) => { + builder.addCase(imageDeleted.pending, (state, action) => { const { imageType, imageName } = action.meta.arg; if (imageType === 'results') { diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts index 6e2ac1c3aa..97e23660a9 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts @@ -5,7 +5,8 @@ import { UploadsState } from './uploadsSlice'; * * Currently denylisting uploads slice entirely, see persist config in store.ts */ -const itemsToDenylist: (keyof UploadsState)[] = ['isLoading']; +const itemsToDenylist: (keyof UploadsState)[] = []; +export const uploadsPersistDenylist: (keyof UploadsState)[] = []; export const uploadsDenylist = itemsToDenylist.map( (denylistItem) => `uploads.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts index bb77844f42..d0a7821d9d 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts @@ -6,7 +6,7 @@ import { receivedUploadImagesPage, IMAGES_PER_PAGE, } from 'services/thunks/gallery'; -import { imageDeleted, imageUploaded } from 'services/thunks/image'; +import { imageDeleted } from 'services/thunks/image'; import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; export const uploadsAdapter = createEntityAdapter({ @@ -21,7 +21,7 @@ type AdditionalUploadsState = { nextPage: number; }; -const initialUploadsState = +export const initialUploadsState = uploadsAdapter.getInitialState({ page: 0, pages: 0, @@ -35,7 +35,7 @@ const uploadsSlice = createSlice({ name: 'uploads', initialState: initialUploadsState, reducers: { - uploadAdded: uploadsAdapter.addOne, + uploadAdded: uploadsAdapter.upsertOne, }, extraReducers: (builder) => { /** @@ -62,20 +62,10 @@ const uploadsSlice = createSlice({ }); /** - * Upload Image - FULFILLED + * Delete Image - pending + * Pre-emptively remove the image from the gallery */ - builder.addCase(imageUploaded.fulfilled, (state, action) => { - const { location, response } = action.payload; - - const uploadedImage = deserializeImageResponse(response); - - uploadsAdapter.setOne(state, uploadedImage); - }); - - /** - * Delete Image - FULFILLED - */ - builder.addCase(imageDeleted.fulfilled, (state, action) => { + builder.addCase(imageDeleted.pending, (state, action) => { const { imageType, imageName } = action.meta.arg; if (imageType === 'uploads') { diff --git a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx index 1eea014dfd..9781625949 100644 --- a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx +++ b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx @@ -4,7 +4,7 @@ import * as InvokeAI from 'app/types/invokeai'; import { useGetUrl } from 'common/util/getUrl'; type ReactPanZoomProps = { - image: InvokeAI._Image; + image: InvokeAI.Image; styleClass?: string; alt?: string; ref?: React.Ref; diff --git a/invokeai/frontend/web/src/features/lightbox/store/lightboxPersistDenylist.ts b/invokeai/frontend/web/src/features/lightbox/store/lightboxPersistDenylist.ts index 96cf69f373..194fe50ca3 100644 --- a/invokeai/frontend/web/src/features/lightbox/store/lightboxPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/lightbox/store/lightboxPersistDenylist.ts @@ -4,6 +4,9 @@ import { LightboxState } from './lightboxSlice'; * Lightbox slice persist denylist */ const itemsToDenylist: (keyof LightboxState)[] = ['isLightboxOpen']; +export const lightboxPersistDenylist: (keyof LightboxState)[] = [ + 'isLightboxOpen', +]; export const lightboxDenylist = itemsToDenylist.map( (denylistItem) => `lightbox.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/lightbox/store/lightboxSlice.ts b/invokeai/frontend/web/src/features/lightbox/store/lightboxSlice.ts index f7f6507d93..ea73e5bb13 100644 --- a/invokeai/frontend/web/src/features/lightbox/store/lightboxSlice.ts +++ b/invokeai/frontend/web/src/features/lightbox/store/lightboxSlice.ts @@ -5,7 +5,7 @@ export interface LightboxState { isLightboxOpen: boolean; } -const initialLightboxState: LightboxState = { +export const initialLightboxState: LightboxState = { isLightboxOpen: false, }; diff --git a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx index 3265a2620f..a4ce2f55f6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx @@ -1,5 +1,3 @@ -import { v4 as uuidv4 } from 'uuid'; - import 'reactflow/dist/style.css'; import { memo, useCallback } from 'react'; import { @@ -8,12 +6,11 @@ import { MenuButton, MenuList, MenuItem, - IconButton, } from '@chakra-ui/react'; -import { FaEllipsisV, FaPlus } from 'react-icons/fa'; +import { FaEllipsisV } from 'react-icons/fa'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { nodeAdded } from '../store/nodesSlice'; -import { cloneDeep, map } from 'lodash-es'; +import { map } from 'lodash-es'; import { RootState } from 'app/store/store'; import { useBuildInvocation } from '../hooks/useBuildInvocation'; import { addToast } from 'features/system/store/systemSlice'; diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx index f21ab09be3..86099a7315 100644 --- a/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx @@ -1,12 +1,6 @@ import { Tooltip } from '@chakra-ui/react'; -import { CSSProperties, memo, useMemo } from 'react'; -import { - Handle, - Position, - Connection, - HandleType, - useReactFlow, -} from 'reactflow'; +import { CSSProperties, memo } from 'react'; +import { Handle, Position, Connection, HandleType } from 'reactflow'; import { FIELDS, HANDLE_TOOLTIP_OPEN_DELAY } from '../types/constants'; // import { useConnectionEventStyles } from '../hooks/useConnectionEventStyles'; import { InputFieldTemplate, OutputFieldTemplate } from '../types/types'; @@ -26,9 +20,9 @@ const outputHandleStyles: CSSProperties = { right: '-0.5rem', }; -const requiredConnectionStyles: CSSProperties = { - boxShadow: '0 0 0.5rem 0.5rem var(--invokeai-colors-error-400)', -}; +// const requiredConnectionStyles: CSSProperties = { +// boxShadow: '0 0 0.5rem 0.5rem var(--invokeai-colors-error-400)', +// }; type FieldHandleProps = { nodeId: string; @@ -39,8 +33,8 @@ type FieldHandleProps = { }; const FieldHandle = (props: FieldHandleProps) => { - const { nodeId, field, isValidConnection, handleType, styles } = props; - const { name, title, type, description } = field; + const { field, isValidConnection, handleType, styles } = props; + const { name, type } = field; return ( { style: { strokeWidth: 2 }, }} > - - {/* */} + diff --git a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx index 2a61d4cc2b..f944c6e463 100644 --- a/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/IAINode/IAINodeHeader.tsx @@ -1,6 +1,6 @@ import { Flex, Heading, Tooltip, Icon } from '@chakra-ui/react'; import { InvocationTemplate } from 'features/nodes/types/types'; -import { memo, MutableRefObject } from 'react'; +import { memo } from 'react'; import { FaInfoCircle } from 'react-icons/fa'; interface IAINodeHeaderProps { diff --git a/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx index b4502d1ff3..9527708c40 100644 --- a/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx @@ -10,6 +10,7 @@ import ConditioningInputFieldComponent from './fields/ConditioningInputFieldComp import ModelInputFieldComponent from './fields/ModelInputFieldComponent'; import NumberInputFieldComponent from './fields/NumberInputFieldComponent'; import StringInputFieldComponent from './fields/StringInputFieldComponent'; +import ColorInputFieldComponent from './fields/ColorInputFieldComponent'; import ItemInputFieldComponent from './fields/ItemInputFieldComponent'; type InputFieldComponentProps = { @@ -21,7 +22,7 @@ type InputFieldComponentProps = { // build an individual input element based on the schema const InputFieldComponent = (props: InputFieldComponentProps) => { const { nodeId, field, template } = props; - const { type, value } = field; + const { type } = field; if (type === 'string' && template.type === 'string') { return ( @@ -126,6 +127,26 @@ const InputFieldComponent = (props: InputFieldComponentProps) => { ); } + if (type === 'color' && template.type === 'color') { + return ( + + ); + } + + if (type === 'item' && template.type === 'item') { + return ( + + ); + } + return Unknown field type: {type}; }; diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx index 90c8e1396f..e66f75792b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeGraphOverlay.tsx @@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react'; import { RootState } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { memo } from 'react'; -import { buildNodesGraph } from '../util/nodesGraphBuilder/buildNodesGraph'; +import { buildNodesGraph } from '../util/graphBuilders/buildNodesGraph'; const NodeGraphOverlay = () => { const state = useAppSelector((state: RootState) => state); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ColorInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ColorInputFieldComponent.tsx new file mode 100644 index 0000000000..c4884dcffc --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ColorInputFieldComponent.tsx @@ -0,0 +1,31 @@ +import { + ColorInputFieldTemplate, + ColorInputFieldValue, +} from 'features/nodes/types/types'; +import { memo } from 'react'; +import { FieldComponentProps } from './types'; +import { RgbaColor, RgbaColorPicker } from 'react-colorful'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { useAppDispatch } from 'app/store/storeHooks'; + +const ColorInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + + const dispatch = useAppDispatch(); + + const handleValueChanged = (value: RgbaColor) => { + dispatch(fieldValueChanged({ nodeId, fieldName: field.name, value })); + }; + + return ( + + ); +}; + +export default memo(ColorInputFieldComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx index 74967c20d8..b43338f930 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx @@ -1,16 +1,16 @@ -import { Box, Image, Icon, Flex } from '@chakra-ui/react'; +import { Box, Image } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder'; import { useGetUrl } from 'common/util/getUrl'; import useGetImageByNameAndType from 'features/gallery/hooks/useGetImageByName'; -import useGetImageByUuid from 'features/gallery/hooks/useGetImageByUuid'; + import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { ImageInputFieldTemplate, ImageInputFieldValue, } from 'features/nodes/types/types'; import { DragEvent, memo, useCallback, useState } from 'react'; -import { FaImage } from 'react-icons/fa'; + import { ImageType } from 'services/api'; import { FieldComponentProps } from './types'; @@ -18,7 +18,6 @@ const ImageInputFieldComponent = ( props: FieldComponentProps ) => { const { nodeId, field } = props; - const { value } = field; const getImageByNameAndType = useGetImageByNameAndType(); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ItemInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ItemInputFieldComponent.tsx index 85ce887e50..fa8eb5a26d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ItemInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ItemInputFieldComponent.tsx @@ -3,7 +3,7 @@ import { ItemInputFieldValue, } from 'features/nodes/types/types'; import { memo } from 'react'; -import { FaAddressCard, FaList } from 'react-icons/fa'; +import { FaAddressCard } from 'react-icons/fa'; import { FieldComponentProps } from './types'; const ItemInputFieldComponent = ( diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx index 3ce790171a..a1ef69de01 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx @@ -1,17 +1,13 @@ import { Select } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { ModelInputFieldTemplate, ModelInputFieldValue, } from 'features/nodes/types/types'; -import { - selectModelsById, - selectModelsIds, -} from 'features/system/store/modelSlice'; -import { isEqual, map } from 'lodash-es'; +import { selectModelsIds } from 'features/system/store/modelSlice'; +import { isEqual } from 'lodash-es'; import { ChangeEvent, memo } from 'react'; import { FieldComponentProps } from './types'; @@ -48,7 +44,10 @@ const ModelInputFieldComponent = ( }; return ( - + {allModelNames.map((option) => ( {option} ))} diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx index 4bb7abf982..b97bf423e1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx @@ -1,16 +1,16 @@ import { HStack } from '@chakra-ui/react'; +import { userInvoked } from 'app/store/actions'; import { useAppDispatch } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; import { memo, useCallback } from 'react'; import { Panel } from 'reactflow'; import { receivedOpenAPISchema } from 'services/thunks/schema'; -import { nodesGraphBuilt } from 'services/thunks/session'; const TopCenterPanel = () => { const dispatch = useAppDispatch(); const handleInvoke = useCallback(() => { - dispatch(nodesGraphBuilt()); + dispatch(userInvoked('nodes')); }, [dispatch]); const handleReloadSchema = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx index 2b89db000a..3fe72225eb 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopLeftPanel.tsx @@ -1,10 +1,10 @@ import { memo } from 'react'; import { Panel } from 'reactflow'; -import AddNodeMenu from '../AddNodeMenu'; +import NodeSearch from '../search/NodeSearch'; const TopLeftPanel = () => ( - + ); diff --git a/invokeai/frontend/web/src/features/nodes/components/search/NodeSearch.tsx b/invokeai/frontend/web/src/features/nodes/components/search/NodeSearch.tsx index 66b1d72014..b06619e76f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/search/NodeSearch.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/search/NodeSearch.tsx @@ -2,7 +2,6 @@ import { Box, Flex } from '@chakra-ui/layout'; import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIInput from 'common/components/IAIInput'; -import { Panel } from 'reactflow'; import { map } from 'lodash-es'; import { ChangeEvent, @@ -192,19 +191,17 @@ const NodeSearch = () => { }; return ( - - setShowNodeList(true)} - onBlur={searchInputBlurHandler} - ref={nodeSearchRef} - > - - {showNodeList && renderNodeList()} - - + setShowNodeList(true)} + onBlur={searchInputBlurHandler} + ref={nodeSearchRef} + > + + {showNodeList && renderNodeList()} + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/store/actions.ts b/invokeai/frontend/web/src/features/nodes/store/actions.ts new file mode 100644 index 0000000000..eda753b9dc --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/actions.ts @@ -0,0 +1,18 @@ +import { createAction, isAnyOf } from '@reduxjs/toolkit'; +import { Graph } from 'services/api'; + +export const textToImageGraphBuilt = createAction( + 'nodes/textToImageGraphBuilt' +); +export const imageToImageGraphBuilt = createAction( + 'nodes/imageToImageGraphBuilt' +); +export const canvasGraphBuilt = createAction('nodes/canvasGraphBuilt'); +export const nodesGraphBuilt = createAction('nodes/nodesGraphBuilt'); + +export const isAnyGraphBuilt = isAnyOf( + textToImageGraphBuilt, + imageToImageGraphBuilt, + canvasGraphBuilt, + nodesGraphBuilt +); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts b/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts index 31d859ba8b..36da41f56c 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts @@ -4,6 +4,10 @@ import { NodesState } from './nodesSlice'; * Nodes slice persist denylist */ const itemsToDenylist: (keyof NodesState)[] = ['schema', 'invocationTemplates']; +export const nodesPersistDenylist: (keyof NodesState)[] = [ + 'schema', + 'invocationTemplates', +]; export const nodesDenylist = itemsToDenylist.map( (denylistItem) => `nodes.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index d0202a5932..4ce0120c21 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -11,13 +11,14 @@ import { NodeChange, OnConnectStartParams, } from 'reactflow'; -import { Graph, ImageField } from 'services/api'; +import { ImageField } from 'services/api'; import { receivedOpenAPISchema } from 'services/thunks/schema'; -import { isFulfilledAnyGraphBuilt } from 'services/thunks/session'; import { InvocationTemplate, InvocationValue } from '../types/types'; import { parseSchema } from '../util/parseSchema'; import { log } from 'app/logging/useLogger'; import { size } from 'lodash-es'; +import { isAnyGraphBuilt } from './actions'; +import { RgbaColor } from 'react-colorful'; export type NodesState = { nodes: Node[]; @@ -25,7 +26,6 @@ export type NodesState = { schema: OpenAPIV3.Document | null; invocationTemplates: Record; connectionStartParams: OnConnectStartParams | null; - lastGraph: Graph | null; shouldShowGraphOverlay: boolean; }; @@ -35,7 +35,6 @@ export const initialNodesState: NodesState = { schema: null, invocationTemplates: {}, connectionStartParams: null, - lastGraph: null, shouldShowGraphOverlay: false, }; @@ -71,6 +70,7 @@ const nodesSlice = createSlice({ | number | boolean | Pick + | RgbaColor | undefined; }> ) => { @@ -104,8 +104,9 @@ const nodesSlice = createSlice({ state.schema = action.payload; }); - builder.addMatcher(isFulfilledAnyGraphBuilt, (state, action) => { - state.lastGraph = action.payload; + builder.addMatcher(isAnyGraphBuilt, (state, action) => { + // TODO: Achtung! Side effect in a reducer! + log.info({ namespace: 'nodes', data: action.payload }, 'Graph built'); }); }, }); diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts index e3b8d0563d..7e4dadc21d 100644 --- a/invokeai/frontend/web/src/features/nodes/types/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -1,4 +1,3 @@ -import { getCSSVar } from '@chakra-ui/utils'; import { FieldType, FieldUIConfig } from './types'; export const HANDLE_TOOLTIP_OPEN_DELAY = 500; @@ -15,6 +14,7 @@ export const FIELD_TYPE_MAP: Record = { model: 'model', array: 'array', item: 'item', + ColorField: 'color', }; const COLOR_TOKEN_VALUE = 500; @@ -89,4 +89,10 @@ export const FIELDS: Record = { title: 'Collection Item', description: 'TODO: Collection Item type description.', }, + color: { + color: 'gray', + colorCssVar: getColorTokenCssVariable('gray'), + title: 'Color', + description: 'A RGBA color.', + }, }; diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index f637fc965f..876ba95cac 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -1,4 +1,5 @@ import { OpenAPIV3 } from 'openapi-types'; +import { RgbaColor } from 'react-colorful'; import { ImageField } from 'services/api'; import { AnyInvocationType } from 'services/events/types'; @@ -59,7 +60,8 @@ export type FieldType = | 'conditioning' | 'model' | 'array' - | 'item'; + | 'item' + | 'color'; /** * An input field is persisted across reloads as part of the user's local state. @@ -80,7 +82,8 @@ export type InputFieldValue = | EnumInputFieldValue | ModelInputFieldValue | ArrayInputFieldValue - | ItemInputFieldValue; + | ItemInputFieldValue + | ColorInputFieldValue; /** * An input field template is generated on each page load from the OpenAPI schema. @@ -99,7 +102,8 @@ export type InputFieldTemplate = | EnumInputFieldTemplate | ModelInputFieldTemplate | ArrayInputFieldTemplate - | ItemInputFieldTemplate; + | ItemInputFieldTemplate + | ColorInputFieldTemplate; /** * An output field is persisted across as part of the user's local state. @@ -193,6 +197,11 @@ export type ItemInputFieldValue = FieldValueBase & { value?: undefined; }; +export type ColorInputFieldValue = FieldValueBase & { + type: 'color'; + value?: RgbaColor; +}; + export type InputFieldTemplateBase = { name: string; title: string; @@ -241,7 +250,7 @@ export type ImageInputFieldTemplate = InputFieldTemplateBase & { }; export type LatentsInputFieldTemplate = InputFieldTemplateBase & { - default: undefined; + default: string; type: 'latents'; }; @@ -272,6 +281,11 @@ export type ItemInputFieldTemplate = InputFieldTemplateBase & { type: 'item'; }; +export type ColorInputFieldTemplate = InputFieldTemplateBase & { + default: RgbaColor; + type: 'color'; +}; + /** * JANKY CUSTOMISATION OF OpenAPI SCHEMA TYPES */ diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildEdges.ts b/invokeai/frontend/web/src/features/nodes/util/edgeBuilders/buildEdges.ts similarity index 87% rename from invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildEdges.ts rename to invokeai/frontend/web/src/features/nodes/util/edgeBuilders/buildEdges.ts index 873dba3ac3..a1e9837647 100644 --- a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildEdges.ts +++ b/invokeai/frontend/web/src/features/nodes/util/edgeBuilders/buildEdges.ts @@ -1,6 +1,7 @@ import { Edge, ImageToImageInvocation, + InpaintInvocation, IterateInvocation, RandomRangeInvocation, RangeInvocation, @@ -8,7 +9,7 @@ import { } from 'services/api'; export const buildEdges = ( - baseNode: TextToImageInvocation | ImageToImageInvocation, + baseNode: TextToImageInvocation | ImageToImageInvocation | InpaintInvocation, rangeNode: RangeInvocation | RandomRangeInvocation, iterateNode: IterateInvocation ): Edge[] => { diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts index 1912cb483e..11f0087488 100644 --- a/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts +++ b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts @@ -12,12 +12,13 @@ import { ConditioningInputFieldTemplate, StringInputFieldTemplate, ModelInputFieldTemplate, + ArrayInputFieldTemplate, + ItemInputFieldTemplate, + ColorInputFieldTemplate, InputFieldTemplateBase, OutputFieldTemplate, TypeHints, FieldType, - ArrayInputFieldTemplate, - ItemInputFieldTemplate, } from '../types/types'; export type BaseFieldProperties = 'name' | 'title' | 'description'; @@ -233,7 +234,6 @@ const buildEnumInputFieldTemplate = ({ }; const buildArrayInputFieldTemplate = ({ - schemaObject, baseField, }: BuildInputFieldArg): ArrayInputFieldTemplate => { const template: ArrayInputFieldTemplate = { @@ -248,7 +248,6 @@ const buildArrayInputFieldTemplate = ({ }; const buildItemInputFieldTemplate = ({ - schemaObject, baseField, }: BuildInputFieldArg): ItemInputFieldTemplate => { const template: ItemInputFieldTemplate = { @@ -262,6 +261,21 @@ const buildItemInputFieldTemplate = ({ return template; }; +const buildColorInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): ColorInputFieldTemplate => { + const template: ColorInputFieldTemplate = { + ...baseField, + type: 'color', + inputRequirement: 'always', + inputKind: 'direct', + default: schemaObject.default ?? { r: 127, g: 127, b: 127, a: 255 }, + }; + + return template; +}; + export const getFieldType = ( schemaObject: OpenAPIV3.SchemaObject, name: string, @@ -341,6 +355,15 @@ export const buildInputFieldTemplate = ( if (['item'].includes(fieldType)) { return buildItemInputFieldTemplate({ schemaObject, baseField }); } + if (['color'].includes(fieldType)) { + return buildColorInputFieldTemplate({ schemaObject, baseField }); + } + if (['array'].includes(fieldType)) { + return buildArrayInputFieldTemplate({ schemaObject, baseField }); + } + if (['item'].includes(fieldType)) { + return buildItemInputFieldTemplate({ schemaObject, baseField }); + } return; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/getGenerationMode.ts b/invokeai/frontend/web/src/features/nodes/util/getGenerationMode.ts new file mode 100644 index 0000000000..28b316be40 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/getGenerationMode.ts @@ -0,0 +1,19 @@ +export const getGenerationMode = ( + baseIsPartiallyTransparent: boolean, + baseIsFullyTransparent: boolean, + doesMaskHaveBlackPixels: boolean +): 'txt2img' | `img2img` | 'inpaint' | 'outpaint' => { + if (baseIsPartiallyTransparent) { + if (baseIsFullyTransparent) { + return 'txt2img'; + } + + return 'outpaint'; + } else { + if (doesMaskHaveBlackPixels) { + return 'inpaint'; + } + + return 'img2img'; + } +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts new file mode 100644 index 0000000000..45deed7070 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts @@ -0,0 +1,141 @@ +import { RootState } from 'app/store/store'; +import { + Edge, + ImageToImageInvocation, + InpaintInvocation, + IterateInvocation, + RandomRangeInvocation, + RangeInvocation, + TextToImageInvocation, +} from 'services/api'; +import { buildImg2ImgNode } from '../nodeBuilders/buildImageToImageNode'; +import { buildTxt2ImgNode } from '../nodeBuilders/buildTextToImageNode'; +import { buildRangeNode } from '../nodeBuilders/buildRangeNode'; +import { buildIterateNode } from '../nodeBuilders/buildIterateNode'; +import { buildEdges } from '../edgeBuilders/buildEdges'; +import { getCanvasData } from 'features/canvas/util/getCanvasData'; +import { getGenerationMode } from '../getGenerationMode'; +import { log } from 'app/logging/useLogger'; +import { buildInpaintNode } from '../nodeBuilders/buildInpaintNode'; + +const moduleLog = log.child({ namespace: 'buildCanvasGraph' }); + +const buildBaseNode = ( + nodeType: 'txt2img' | 'img2img' | 'inpaint' | 'outpaint', + state: RootState +): + | TextToImageInvocation + | ImageToImageInvocation + | InpaintInvocation + | undefined => { + if (nodeType === 'txt2img') { + return buildTxt2ImgNode(state, state.canvas.boundingBoxDimensions); + } + + if (nodeType === 'img2img') { + return buildImg2ImgNode(state, state.canvas.boundingBoxDimensions); + } + + if (nodeType === 'inpaint' || nodeType === 'outpaint') { + return buildInpaintNode(state, state.canvas.boundingBoxDimensions); + } +}; + +/** + * Builds the Canvas workflow graph and image blobs. + */ +export const buildCanvasGraphAndBlobs = async ( + state: RootState +): Promise< + | { + rangeNode: RangeInvocation | RandomRangeInvocation; + iterateNode: IterateInvocation; + baseNode: + | TextToImageInvocation + | ImageToImageInvocation + | InpaintInvocation; + edges: Edge[]; + baseBlob: Blob; + maskBlob: Blob; + generationMode: 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; + } + | undefined +> => { + const c = await getCanvasData(state); + + if (!c) { + moduleLog.error('Unable to create canvas graph'); + return; + } + + const { + baseBlob, + maskBlob, + baseIsPartiallyTransparent, + baseIsFullyTransparent, + doesMaskHaveBlackPixels, + } = c; + + moduleLog.debug( + { + data: { + baseIsPartiallyTransparent, + baseIsFullyTransparent, + doesMaskHaveBlackPixels, + }, + }, + 'Built canvas data' + ); + + const generationMode = getGenerationMode( + baseIsPartiallyTransparent, + baseIsFullyTransparent, + doesMaskHaveBlackPixels + ); + + moduleLog.debug(`Generation mode: ${generationMode}`); + + // The base node is a txt2img, img2img or inpaint node + const baseNode = buildBaseNode(generationMode, state); + + if (!baseNode) { + moduleLog.error('Problem building base node'); + return; + } + + if (baseNode.type === 'inpaint') { + const { seamSize, seamBlur, seamSteps, seamStrength, tileSize } = + state.generation; + + // generationParameters.invert_mask = shouldPreserveMaskedArea; + // if (boundingBoxScale !== 'none') { + // generationParameters.inpaint_width = scaledBoundingBoxDimensions.width; + // generationParameters.inpaint_height = scaledBoundingBoxDimensions.height; + // } + baseNode.seam_size = seamSize; + baseNode.seam_blur = seamBlur; + baseNode.seam_strength = seamStrength; + baseNode.seam_steps = seamSteps; + baseNode.tile_size = tileSize; + // baseNode.infill_method = infillMethod; + // baseNode.force_outpaint = false; + } + + // We always range and iterate nodes, no matter the iteration count + // This is required to provide the correct seeds to the backend engine + const rangeNode = buildRangeNode(state); + const iterateNode = buildIterateNode(); + + // Build the edges for the nodes selected. + const edges = buildEdges(baseNode, rangeNode, iterateNode); + + return { + rangeNode, + iterateNode, + baseNode, + edges, + baseBlob, + maskBlob, + generationMode, + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts similarity index 59% rename from invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildLinearGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts index 3e638c8239..d7a0fc66d3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts @@ -1,19 +1,15 @@ import { RootState } from 'app/store/store'; import { Graph } from 'services/api'; -import { buildImg2ImgNode } from './buildImageToImageNode'; -import { buildTxt2ImgNode } from './buildTextToImageNode'; -import { buildRangeNode } from './buildRangeNode'; -import { buildIterateNode } from './buildIterateNode'; -import { buildEdges } from './buildEdges'; +import { buildImg2ImgNode } from '../nodeBuilders/buildImageToImageNode'; +import { buildRangeNode } from '../nodeBuilders/buildRangeNode'; +import { buildIterateNode } from '../nodeBuilders/buildIterateNode'; +import { buildEdges } from '../edgeBuilders/buildEdges'; /** * Builds the Linear workflow graph. */ -export const buildLinearGraph = (state: RootState): Graph => { - // The base node is either a txt2img or img2img node - const baseNode = state.generation.isImageToImageEnabled - ? buildImg2ImgNode(state) - : buildTxt2ImgNode(state); +export const buildImageToImageGraph = (state: RootState): Graph => { + const baseNode = buildImg2ImgNode(state); // We always range and iterate nodes, no matter the iteration count // This is required to provide the correct seeds to the backend engine diff --git a/invokeai/frontend/web/src/features/nodes/util/nodesGraphBuilder/buildNodesGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts similarity index 69% rename from invokeai/frontend/web/src/features/nodes/util/nodesGraphBuilder/buildNodesGraph.ts rename to invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts index f12b141e09..eef7379624 100644 --- a/invokeai/frontend/web/src/features/nodes/util/nodesGraphBuilder/buildNodesGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildNodesGraph.ts @@ -1,8 +1,30 @@ import { Graph } from 'services/api'; import { v4 as uuidv4 } from 'uuid'; -import { reduce } from 'lodash-es'; +import { cloneDeep, reduce } from 'lodash-es'; import { RootState } from 'app/store/store'; -import { AnyInvocation } from 'services/events/types'; +import { InputFieldValue } from 'features/nodes/types/types'; + +/** + * We need to do special handling for some fields + */ +export const parseFieldValue = (field: InputFieldValue) => { + if (field.type === 'color') { + if (field.value) { + const clonedValue = cloneDeep(field.value); + + const { r, g, b, a } = field.value; + + // scale alpha value to PIL's desired range 0-255 + const scaledAlpha = Math.max(0, Math.min(a * 255, 255)); + const transformedColor = { r, g, b, a: scaledAlpha }; + + Object.assign(clonedValue, transformedColor); + return clonedValue; + } + } + + return field.value; +}; /** * Builds a graph from the node editor state. @@ -20,7 +42,8 @@ export const buildNodesGraph = (state: RootState): Graph => { const transformedInputs = reduce( inputs, (inputsAccumulator, input, name) => { - inputsAccumulator[name] = input.value; + const parsedValue = parseFieldValue(input); + inputsAccumulator[name] = parsedValue; return inputsAccumulator; }, diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts new file mode 100644 index 0000000000..8b1d8edcc9 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -0,0 +1,35 @@ +import { RootState } from 'app/store/store'; +import { Graph } from 'services/api'; +import { buildTxt2ImgNode } from '../nodeBuilders/buildTextToImageNode'; +import { buildRangeNode } from '../nodeBuilders/buildRangeNode'; +import { buildIterateNode } from '../nodeBuilders/buildIterateNode'; +import { buildEdges } from '../edgeBuilders/buildEdges'; + +/** + * Builds the Linear workflow graph. + */ +export const buildTextToImageGraph = (state: RootState): Graph => { + const baseNode = buildTxt2ImgNode(state); + + // We always range and iterate nodes, no matter the iteration count + // This is required to provide the correct seeds to the backend engine + const rangeNode = buildRangeNode(state); + const iterateNode = buildIterateNode(); + + // Build the edges for the nodes selected. + const edges = buildEdges(baseNode, rangeNode, iterateNode); + + // Assemble! + const graph = { + nodes: { + [rangeNode.id]: rangeNode, + [iterateNode.id]: iterateNode, + [baseNode.id]: baseNode, + }, + edges, + }; + + // TODO: hires fix requires latent space upscaling; we don't have nodes for this yet + + return graph; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildImageToImageNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts similarity index 68% rename from invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildImageToImageNode.ts rename to invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts index f9213dfeae..c8b328370a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildImageToImageNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts @@ -5,14 +5,17 @@ import { ImageToImageInvocation, TextToImageInvocation, } from 'services/api'; -import { _Image } from 'app/types/invokeai'; -import { initialImageSelector } from 'features/parameters/store/generationSelectors'; +import { O } from 'ts-toolbelt'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -export const buildImg2ImgNode = (state: RootState): ImageToImageInvocation => { +export const buildImg2ImgNode = ( + state: RootState, + overrides: O.Partial = {} +): ImageToImageInvocation => { const nodeId = uuidv4(); - const { generation, system, models } = state; + const { generation } = state; - const { selectedModelName } = models; + const activeTabName = activeTabNameSelector(state); const { prompt, @@ -23,18 +26,14 @@ export const buildImg2ImgNode = (state: RootState): ImageToImageInvocation => { height, cfgScale, sampler, - seamless, + model, img2imgStrength: strength, shouldFitToWidthHeight: fit, shouldRandomizeSeed, + initialImage, } = generation; - const initialImage = initialImageSelector(state); - - if (!initialImage) { - // TODO: handle this - throw 'no initial image'; - } + // const initialImage = initialImageSelector(state); const imageToImageNode: ImageToImageInvocation = { id: nodeId, @@ -45,21 +44,30 @@ export const buildImg2ImgNode = (state: RootState): ImageToImageInvocation => { height, cfg_scale: cfgScale, scheduler: sampler as ImageToImageInvocation['scheduler'], - seamless, - model: selectedModelName, - progress_images: true, - image: { - image_name: initialImage.name, - image_type: initialImage.type, - }, + model, strength, fit, }; + // on Canvas tab, we do not manually specific init image + if (activeTabName === 'img2img') { + if (!initialImage) { + // TODO: handle this more better + throw 'no initial image'; + } + + imageToImageNode.image = { + image_name: initialImage.name, + image_type: initialImage.type, + }; + } + if (!shouldRandomizeSeed) { imageToImageNode.seed = seed; } + Object.assign(imageToImageNode, overrides); + return imageToImageNode; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts new file mode 100644 index 0000000000..9cd124ba9e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts @@ -0,0 +1,67 @@ +import { v4 as uuidv4 } from 'uuid'; +import { RootState } from 'app/store/store'; +import { InpaintInvocation } from 'services/api'; +import { initialImageSelector } from 'features/parameters/store/generationSelectors'; +import { O } from 'ts-toolbelt'; + +export const buildInpaintNode = ( + state: RootState, + overrides: O.Partial = {} +): InpaintInvocation => { + const nodeId = uuidv4(); + const { generation, models } = state; + + const { selectedModelName } = models; + + const { + prompt, + negativePrompt, + seed, + steps, + width, + height, + cfgScale, + sampler, + seamless, + img2imgStrength: strength, + shouldFitToWidthHeight: fit, + shouldRandomizeSeed, + } = generation; + + const initialImage = initialImageSelector(state); + + if (!initialImage) { + // TODO: handle this + // throw 'no initial image'; + } + + const imageToImageNode: InpaintInvocation = { + id: nodeId, + type: 'inpaint', + prompt: `${prompt} [${negativePrompt}]`, + steps, + width, + height, + cfg_scale: cfgScale, + scheduler: sampler as InpaintInvocation['scheduler'], + seamless, + model: selectedModelName, + progress_images: true, + image: initialImage + ? { + image_name: initialImage.name, + image_type: initialImage.type, + } + : undefined, + strength, + fit, + }; + + if (!shouldRandomizeSeed) { + imageToImageNode.seed = seed; + } + + Object.assign(imageToImageNode, overrides); + + return imageToImageNode; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildIterateNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildIterateNode.ts similarity index 100% rename from invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildIterateNode.ts rename to invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildIterateNode.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildRangeNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildRangeNode.ts similarity index 100% rename from invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildRangeNode.ts rename to invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildRangeNode.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildTextToImageNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildTextToImageNode.ts similarity index 71% rename from invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildTextToImageNode.ts rename to invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildTextToImageNode.ts index 08952bcfb1..fe76531c59 100644 --- a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildTextToImageNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildTextToImageNode.ts @@ -1,12 +1,14 @@ import { v4 as uuidv4 } from 'uuid'; import { RootState } from 'app/store/store'; import { TextToImageInvocation } from 'services/api'; +import { O } from 'ts-toolbelt'; -export const buildTxt2ImgNode = (state: RootState): TextToImageInvocation => { +export const buildTxt2ImgNode = ( + state: RootState, + overrides: O.Partial = {} +): TextToImageInvocation => { const nodeId = uuidv4(); - const { generation, models } = state; - - const { selectedModelName } = models; + const { generation } = state; const { prompt, @@ -17,8 +19,8 @@ export const buildTxt2ImgNode = (state: RootState): TextToImageInvocation => { height, cfgScale: cfg_scale, sampler, - seamless, shouldRandomizeSeed, + model, } = generation; const textToImageNode: NonNullable = { @@ -30,14 +32,14 @@ export const buildTxt2ImgNode = (state: RootState): TextToImageInvocation => { height, cfg_scale, scheduler: sampler as TextToImageInvocation['scheduler'], - seamless, - model: selectedModelName, - progress_images: true, + model, }; if (!shouldRandomizeSeed) { textToImageNode.seed = seed; } + Object.assign(textToImageNode, overrides); + return textToImageNode; }; diff --git a/invokeai/frontend/web/src/features/parameters/components/AccordionItems/InvokeAccordionItem.tsx b/invokeai/frontend/web/src/features/parameters/components/AccordionItems/InvokeAccordionItem.tsx deleted file mode 100644 index ccf0a8ed26..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AccordionItems/InvokeAccordionItem.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { - AccordionButton, - AccordionIcon, - AccordionItem, - AccordionPanel, - Box, - Flex, -} from '@chakra-ui/react'; -import GuideIcon from 'common/components/GuideIcon'; -import { ParametersAccordionItem } from '../ParametersAccordion'; - -type InvokeAccordionItemProps = { - accordionItem: ParametersAccordionItem; -}; - -export default function InvokeAccordionItem({ - accordionItem, -}: InvokeAccordionItemProps) { - const { header, feature, content, additionalHeaderComponents } = - accordionItem; - - return ( - - - - - {header} - - {additionalHeaderComponents} - {/* {feature && } */} - - - - {content} - - ); -} diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/BoundingBox/BoundingBoxSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/BoundingBox/BoundingBoxSettings.tsx deleted file mode 100644 index 35a325e74e..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/BoundingBox/BoundingBoxSettings.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Box, VStack } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAISlider from 'common/components/IAISlider'; -import { canvasSelector } from 'features/canvas/store/canvasSelectors'; -import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; -import { isEqual } from 'lodash-es'; - -import { useTranslation } from 'react-i18next'; - -const selector = createSelector( - canvasSelector, - (canvas) => { - const { boundingBoxDimensions, boundingBoxScaleMethod: boundingBoxScale } = - canvas; - return { - boundingBoxDimensions, - boundingBoxScale, - }; - }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } -); - -const BoundingBoxSettings = () => { - const dispatch = useAppDispatch(); - const { boundingBoxDimensions } = useAppSelector(selector); - - const { t } = useTranslation(); - - const handleChangeWidth = (v: number) => { - dispatch( - setBoundingBoxDimensions({ - ...boundingBoxDimensions, - width: Math.floor(v), - }) - ); - }; - - const handleChangeHeight = (v: number) => { - dispatch( - setBoundingBoxDimensions({ - ...boundingBoxDimensions, - height: Math.floor(v), - }) - ); - }; - - const handleResetWidth = () => { - dispatch( - setBoundingBoxDimensions({ - ...boundingBoxDimensions, - width: Math.floor(512), - }) - ); - }; - - const handleResetHeight = () => { - dispatch( - setBoundingBoxDimensions({ - ...boundingBoxDimensions, - height: Math.floor(512), - }) - ); - }; - - return ( - - - - - ); -}; - -export default BoundingBoxSettings; - -export const BoundingBoxSettingsHeader = () => { - const { t } = useTranslation(); - return ( - - {t('parameters.boundingBoxHeader')} - - ); -}; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/InfillAndScalingSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/InfillAndScalingSettings.tsx deleted file mode 100644 index c18934e22b..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/InfillAndScalingSettings.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { VStack } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAISelect from 'common/components/IAISelect'; -import IAISlider from 'common/components/IAISlider'; -import { canvasSelector } from 'features/canvas/store/canvasSelectors'; -import { - setBoundingBoxScaleMethod, - setScaledBoundingBoxDimensions, -} from 'features/canvas/store/canvasSlice'; -import { - BoundingBoxScale, - BOUNDING_BOX_SCALES_DICT, -} from 'features/canvas/store/canvasTypes'; -import { generationSelector } from 'features/parameters/store/generationSelectors'; -import { - setInfillMethod, - setTileSize, -} from 'features/parameters/store/generationSlice'; -import { systemSelector } from 'features/system/store/systemSelectors'; -import { isEqual } from 'lodash-es'; - -import { ChangeEvent } from 'react'; -import { useTranslation } from 'react-i18next'; - -const selector = createSelector( - [generationSelector, systemSelector, canvasSelector], - (parameters, system, canvas) => { - const { tileSize, infillMethod } = parameters; - - const { infill_methods: availableInfillMethods } = system; - - const { - boundingBoxScaleMethod: boundingBoxScale, - scaledBoundingBoxDimensions, - } = canvas; - - return { - boundingBoxScale, - scaledBoundingBoxDimensions, - tileSize, - infillMethod, - availableInfillMethods, - isManual: boundingBoxScale === 'manual', - }; - }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } -); - -const InfillAndScalingSettings = () => { - const dispatch = useAppDispatch(); - const { - tileSize, - infillMethod, - availableInfillMethods, - boundingBoxScale, - isManual, - scaledBoundingBoxDimensions, - } = useAppSelector(selector); - - const { t } = useTranslation(); - - const handleChangeScaledWidth = (v: number) => { - dispatch( - setScaledBoundingBoxDimensions({ - ...scaledBoundingBoxDimensions, - width: Math.floor(v), - }) - ); - }; - - const handleChangeScaledHeight = (v: number) => { - dispatch( - setScaledBoundingBoxDimensions({ - ...scaledBoundingBoxDimensions, - height: Math.floor(v), - }) - ); - }; - - const handleResetScaledWidth = () => { - dispatch( - setScaledBoundingBoxDimensions({ - ...scaledBoundingBoxDimensions, - width: Math.floor(512), - }) - ); - }; - - const handleResetScaledHeight = () => { - dispatch( - setScaledBoundingBoxDimensions({ - ...scaledBoundingBoxDimensions, - height: Math.floor(512), - }) - ); - }; - - const handleChangeBoundingBoxScaleMethod = ( - e: ChangeEvent - ) => { - dispatch(setBoundingBoxScaleMethod(e.target.value as BoundingBoxScale)); - }; - - return ( - - - - - dispatch(setInfillMethod(e.target.value))} - /> - { - dispatch(setTileSize(v)); - }} - withInput - withSliderMarks - withReset - handleReset={() => { - dispatch(setTileSize(32)); - }} - /> - - ); -}; - -export default InfillAndScalingSettings; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamCorrectionSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamCorrectionSettings.tsx deleted file mode 100644 index 176dfe1590..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamCorrectionSettings.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { VStack } from '@chakra-ui/react'; -import SeamBlur from './SeamBlur'; -import SeamSize from './SeamSize'; -import SeamSteps from './SeamSteps'; -import SeamStrength from './SeamStrength'; - -const SeamCorrectionSettings = () => { - return ( - - - - - - - ); -}; - -export default SeamCorrectionSettings; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings.tsx deleted file mode 100644 index 71b853f162..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { - Box, - ButtonGroup, - Collapse, - Flex, - Heading, - HStack, - Image, - Spacer, - useDisclosure, - VStack, -} from '@chakra-ui/react'; -import { motion } from 'framer-motion'; - -import IAIButton from 'common/components/IAIButton'; -import ImageFit from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageFit'; -import ImageToImageStrength from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength'; -import IAIIconButton from 'common/components/IAIIconButton'; - -import { useTranslation } from 'react-i18next'; -import InitialImagePreview from './InitialImagePreview'; -import { useState } from 'react'; -import { FaUndo, FaUpload } from 'react-icons/fa'; -import ImageToImageSettingsHeader from 'common/components/ImageToImageSettingsHeader'; - -export default function ImageToImageSettings() { - const { t } = useTranslation(); - return ( - - - - - - - ); -} diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle.tsx deleted file mode 100644 index 89da0ae8b0..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Flex } from '@chakra-ui/react'; -import { RootState } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAISwitch from 'common/components/IAISwitch'; -import { isImageToImageEnabledChanged } from 'features/parameters/store/generationSlice'; -import { ChangeEvent } from 'react'; -import { useTranslation } from 'react-i18next'; - -export default function ImageToImageToggle() { - const isImageToImageEnabled = useAppSelector( - (state: RootState) => state.generation.isImageToImageEnabled - ); - - const { t } = useTranslation(); - - const dispatch = useAppDispatch(); - - const handleChange = (e: ChangeEvent) => - dispatch(isImageToImageEnabledChanged(e.target.checked)); - - return ( - - - - ); -} diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx deleted file mode 100644 index 9682d2eb0b..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { Box, Flex, Image, Spinner, Text } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder'; -import { useGetUrl } from 'common/util/getUrl'; -import useGetImageByNameAndType from 'features/gallery/hooks/useGetImageByName'; -import { - clearInitialImage, - initialImageSelected, -} from 'features/parameters/store/generationSlice'; -import { addToast } from 'features/system/store/systemSlice'; -import { isEqual } from 'lodash-es'; -import { DragEvent, useCallback, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ImageType } from 'services/api'; -import ImageToImageOverlay from 'common/components/ImageToImageOverlay'; -import { initialImageSelector } from 'features/parameters/store/generationSelectors'; - -const selector = createSelector( - [initialImageSelector], - (initialImage) => { - return { - initialImage, - }; - }, - { memoizeOptions: { resultEqualityCheck: isEqual } } -); - -const InitialImagePreview = () => { - const isImageToImageEnabled = useAppSelector( - (state: RootState) => state.generation.isImageToImageEnabled - ); - const { initialImage } = useAppSelector(selector); - const { getUrl } = useGetUrl(); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const [isLoaded, setIsLoaded] = useState(false); - const getImageByNameAndType = useGetImageByNameAndType(); - - const onError = () => { - dispatch( - addToast({ - title: t('toast.parametersFailed'), - description: t('toast.parametersFailedDesc'), - status: 'error', - isClosable: true, - }) - ); - dispatch(clearInitialImage()); - setIsLoaded(false); - }; - - const handleDrop = useCallback( - (e: DragEvent) => { - setIsLoaded(false); - const name = e.dataTransfer.getData('invokeai/imageName'); - const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; - - if (!name || !type) { - return; - } - - const image = getImageByNameAndType(name, type); - - if (!image) { - return; - } - - dispatch(initialImageSelected({ name, type })); - }, - [getImageByNameAndType, dispatch] - ); - - return ( - - {initialImage?.url && ( - - { - setIsLoaded(true); - }} - fallback={ - - - - } - /> - {isLoaded && } - - )} - {!initialImage?.url && } - {!isImageToImageEnabled && ( - - - Image to Image is Disabled - - - )} - - ); -}; - -export default InitialImagePreview; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/ImageToImageOutputSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/ImageToImageOutputSettings.tsx deleted file mode 100644 index c2dea1cbf8..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/ImageToImageOutputSettings.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { VStack } from '@chakra-ui/react'; -import SeamlessSettings from './SeamlessSettings'; - -const ImageToImageOutputSettings = () => { - return ( - - - - ); -}; - -export default ImageToImageOutputSettings; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/OutputSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/OutputSettings.tsx deleted file mode 100644 index 93ba63d065..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/OutputSettings.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { VStack } from '@chakra-ui/react'; -import { HiresStrength, HiresToggle } from './HiresSettings'; -import SeamlessSettings from './SeamlessSettings'; - -const OutputSettings = () => { - return ( - - - - - - ); -}; - -export default OutputSettings; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/SymmetrySettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/SymmetrySettings.tsx deleted file mode 100644 index 21e014b715..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/SymmetrySettings.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { VStack } from '@chakra-ui/react'; -import { RootState } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAISlider from 'common/components/IAISlider'; -import { - setHorizontalSymmetrySteps, - setVerticalSymmetrySteps, -} from 'features/parameters/store/generationSlice'; -import { useTranslation } from 'react-i18next'; - -export default function SymmetrySettings() { - const horizontalSymmetrySteps = useAppSelector( - (state: RootState) => state.generation.horizontalSymmetrySteps - ); - - const verticalSymmetrySteps = useAppSelector( - (state: RootState) => state.generation.verticalSymmetrySteps - ); - - const steps = useAppSelector((state: RootState) => state.generation.steps); - - const dispatch = useAppDispatch(); - - const { t } = useTranslation(); - - return ( - - dispatch(setHorizontalSymmetrySteps(v))} - min={0} - max={steps} - step={1} - withInput - withSliderMarks - withReset - handleReset={() => dispatch(setHorizontalSymmetrySteps(0))} - /> - dispatch(setVerticalSymmetrySteps(v))} - min={0} - max={steps} - step={1} - withInput - withSliderMarks - withReset - handleReset={() => dispatch(setVerticalSymmetrySteps(0))} - /> - - ); -} diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/SeedSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/SeedSettings.tsx deleted file mode 100644 index 576358d2e1..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/SeedSettings.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { VStack } from '@chakra-ui/react'; -import Perlin from './Perlin'; -import RandomizeSeed from './RandomizeSeed'; -import Seed from './Seed'; -import Threshold from './Threshold'; - -/** - * Seed & variation options. Includes iteration, seed, seed randomization, variation options. - */ -const SeedSettings = () => { - return ( - - - - - - ); -}; - -export default SeedSettings; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/GenerateVariations.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/GenerateVariations.tsx deleted file mode 100644 index ec9a8ae276..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/GenerateVariations.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { RootState } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAISwitch from 'common/components/IAISwitch'; -import { setShouldGenerateVariations } from 'features/parameters/store/generationSlice'; -import { ChangeEvent } from 'react'; - -export default function GenerateVariationsToggle() { - const shouldGenerateVariations = useAppSelector( - (state: RootState) => state.generation.shouldGenerateVariations - ); - - const dispatch = useAppDispatch(); - - const handleChangeShouldGenerateVariations = ( - e: ChangeEvent - ) => dispatch(setShouldGenerateVariations(e.target.checked)); - - return ( - - ); -} diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationsSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationsSettings.tsx deleted file mode 100644 index d3bc43f7ae..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationsSettings.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { VStack } from '@chakra-ui/react'; -import SeedWeights from './SeedWeights'; -import VariationAmount from './VariationAmount'; - -/** - * Seed & variation options. Includes iteration, seed, seed randomization, variation options. - */ -const VariationsSettings = () => { - return ( - - - - - ); -}; - -export default VariationsSettings; diff --git a/invokeai/frontend/web/src/features/parameters/components/AnimatedImageToImagePanel.tsx b/invokeai/frontend/web/src/features/parameters/components/AnimatedImageToImagePanel.tsx deleted file mode 100644 index da9262ac0f..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/AnimatedImageToImagePanel.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { memo, useState } from 'react'; -import { AnimatePresence, motion } from 'framer-motion'; - -import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings'; -import { useAppSelector } from 'app/store/storeHooks'; -import { RootState } from 'app/store/store'; -import { Box } from '@chakra-ui/react'; - -const AnimatedImageToImagePanel = () => { - const isImageToImageEnabled = useAppSelector( - (state: RootState) => state.generation.isImageToImageEnabled - ); - - return ( - - {isImageToImageEnabled && ( - - - - - - )} - - ); -}; - -export default memo(AnimatedImageToImagePanel); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx deleted file mode 100644 index e3a312f706..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { HEIGHTS } from 'app/constants'; -import { RootState } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAISelect from 'common/components/IAISelect'; -import IAISlider from 'common/components/IAISlider'; -import { setHeight } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; - -import { useTranslation } from 'react-i18next'; - -export default function MainHeight() { - const height = useAppSelector((state: RootState) => state.generation.height); - const shouldUseSliders = useAppSelector( - (state: RootState) => state.ui.shouldUseSliders - ); - const activeTabName = useAppSelector(activeTabNameSelector); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - return shouldUseSliders ? ( - dispatch(setHeight(v))} - handleReset={() => dispatch(setHeight(512))} - withInput - withReset - withSliderMarks - sliderNumberInputProps={{ max: 15360 }} - /> - ) : ( - dispatch(setHeight(Number(e.target.value)))} - validValues={HEIGHTS} - /> - ); -} diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSettings.tsx deleted file mode 100644 index db2701e0c9..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSettings.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Box, Flex, VStack } from '@chakra-ui/react'; -import { RootState } from 'app/store/store'; -import { useAppSelector } from 'app/store/storeHooks'; -import ModelSelect from 'features/system/components/ModelSelect'; -import { memo } from 'react'; -import HeightSlider from './HeightSlider'; -import MainCFGScale from './MainCFGScale'; -import MainIterations from './MainIterations'; -import MainSampler from './MainSampler'; -import MainSteps from './MainSteps'; -import WidthSlider from './WidthSlider'; - -const MainSettings = () => { - const shouldUseSliders = useAppSelector( - (state: RootState) => state.ui.shouldUseSliders - ); - - return shouldUseSliders ? ( - - - - - - - - - - - - - - - - ) : ( - - - - - - - - - - - - - - - - - - ); -}; - -export default memo(MainSettings); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainWidth.tsx deleted file mode 100644 index 7a8534147c..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainWidth.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { WIDTHS } from 'app/constants'; -import { RootState } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAISelect from 'common/components/IAISelect'; -import IAISlider from 'common/components/IAISlider'; -import { setWidth } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { useTranslation } from 'react-i18next'; - -export default function MainWidth() { - const width = useAppSelector((state: RootState) => state.generation.width); - const shouldUseSliders = useAppSelector( - (state: RootState) => state.ui.shouldUseSliders - ); - const activeTabName = useAppSelector(activeTabNameSelector); - const { t } = useTranslation(); - - const dispatch = useAppDispatch(); - - return shouldUseSliders ? ( - dispatch(setWidth(v))} - handleReset={() => dispatch(setWidth(512))} - withInput - withReset - withSliderMarks - inputReadOnly - sliderNumberInputProps={{ max: 15360 }} - /> - ) : ( - dispatch(setWidth(Number(e.target.value)))} - validValues={WIDTHS} - /> - ); -} diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxCollapse.tsx new file mode 100644 index 0000000000..fea0d8330a --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxCollapse.tsx @@ -0,0 +1,26 @@ +import { Flex, useDisclosure } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import IAICollapse from 'common/components/IAICollapse'; +import { memo } from 'react'; +import ParamBoundingBoxWidth from './ParamBoundingBoxWidth'; +import ParamBoundingBoxHeight from './ParamBoundingBoxHeight'; + +const ParamBoundingBoxCollapse = () => { + const { t } = useTranslation(); + const { isOpen, onToggle } = useDisclosure(); + + return ( + + + + + + + ); +}; + +export default memo(ParamBoundingBoxCollapse); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx new file mode 100644 index 0000000000..75ec70f257 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxHeight.tsx @@ -0,0 +1,64 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAISlider from 'common/components/IAISlider'; +import { canvasSelector } from 'features/canvas/store/canvasSelectors'; +import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { memo } from 'react'; + +import { useTranslation } from 'react-i18next'; + +const selector = createSelector( + canvasSelector, + (canvas) => { + const { boundingBoxDimensions } = canvas; + return { + boundingBoxDimensions, + }; + }, + defaultSelectorOptions +); + +const ParamBoundingBoxWidth = () => { + const dispatch = useAppDispatch(); + const { boundingBoxDimensions } = useAppSelector(selector); + + const { t } = useTranslation(); + + const handleChangeHeight = (v: number) => { + dispatch( + setBoundingBoxDimensions({ + ...boundingBoxDimensions, + height: Math.floor(v), + }) + ); + }; + + const handleResetHeight = () => { + dispatch( + setBoundingBoxDimensions({ + ...boundingBoxDimensions, + height: Math.floor(512), + }) + ); + }; + + return ( + + ); +}; + +export default memo(ParamBoundingBoxWidth); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx new file mode 100644 index 0000000000..cf6ccff852 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxWidth.tsx @@ -0,0 +1,64 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAISlider from 'common/components/IAISlider'; +import { canvasSelector } from 'features/canvas/store/canvasSelectors'; +import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { memo } from 'react'; + +import { useTranslation } from 'react-i18next'; + +const selector = createSelector( + canvasSelector, + (canvas) => { + const { boundingBoxDimensions } = canvas; + return { + boundingBoxDimensions, + }; + }, + defaultSelectorOptions +); + +const ParamBoundingBoxWidth = () => { + const dispatch = useAppDispatch(); + const { boundingBoxDimensions } = useAppSelector(selector); + + const { t } = useTranslation(); + + const handleChangeWidth = (v: number) => { + dispatch( + setBoundingBoxDimensions({ + ...boundingBoxDimensions, + width: Math.floor(v), + }) + ); + }; + + const handleResetWidth = () => { + dispatch( + setBoundingBoxDimensions({ + ...boundingBoxDimensions, + width: Math.floor(512), + }) + ); + }; + + return ( + + ); +}; + +export default memo(ParamBoundingBoxWidth); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillAndScalingCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillAndScalingCollapse.tsx new file mode 100644 index 0000000000..78a8995bee --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillAndScalingCollapse.tsx @@ -0,0 +1,33 @@ +import { Flex, useDisclosure } from '@chakra-ui/react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import IAICollapse from 'common/components/IAICollapse'; +import ParamInfillMethod from './ParamInfillMethod'; +import ParamInfillTilesize from './ParamInfillTilesize'; +import ParamScaleBeforeProcessing from './ParamScaleBeforeProcessing'; +import ParamScaledWidth from './ParamScaledWidth'; +import ParamScaledHeight from './ParamScaledHeight'; + +const ParamInfillCollapse = () => { + const { t } = useTranslation(); + const { isOpen, onToggle } = useDisclosure(); + + return ( + + + + + + + + + + ); +}; + +export default memo(ParamInfillCollapse); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillMethod.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillMethod.tsx new file mode 100644 index 0000000000..00812f458a --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillMethod.tsx @@ -0,0 +1,49 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAISelect from 'common/components/IAISelect'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; +import { setInfillMethod } from 'features/parameters/store/generationSlice'; +import { systemSelector } from 'features/system/store/systemSelectors'; + +import { ChangeEvent, memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +const selector = createSelector( + [generationSelector, systemSelector], + (parameters, system) => { + const { infillMethod } = parameters; + const { infillMethods } = system; + + return { + infillMethod, + infillMethods, + }; + }, + defaultSelectorOptions +); + +const ParamInfillMethod = () => { + const dispatch = useAppDispatch(); + const { infillMethod, infillMethods } = useAppSelector(selector); + + const { t } = useTranslation(); + + const handleChange = useCallback( + (e: ChangeEvent) => { + dispatch(setInfillMethod(e.target.value)); + }, + [dispatch] + ); + + return ( + + ); +}; + +export default memo(ParamInfillMethod); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillTilesize.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillTilesize.tsx new file mode 100644 index 0000000000..fc6f02184c --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillTilesize.tsx @@ -0,0 +1,58 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAISlider from 'common/components/IAISlider'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; +import { setTileSize } from 'features/parameters/store/generationSlice'; +import { memo, useCallback } from 'react'; + +import { useTranslation } from 'react-i18next'; + +const selector = createSelector( + [generationSelector], + (parameters) => { + const { tileSize, infillMethod } = parameters; + + return { + tileSize, + infillMethod, + }; + }, + defaultSelectorOptions +); + +const ParamInfillTileSize = () => { + const dispatch = useAppDispatch(); + const { tileSize, infillMethod } = useAppSelector(selector); + + const { t } = useTranslation(); + + const handleChange = useCallback( + (v: number) => { + dispatch(setTileSize(v)); + }, + [dispatch] + ); + + const handleReset = useCallback(() => { + dispatch(setTileSize(32)); + }, [dispatch]); + + return ( + + ); +}; + +export default memo(ParamInfillTileSize); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx new file mode 100644 index 0000000000..8164371b56 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamScaleBeforeProcessing.tsx @@ -0,0 +1,49 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAISelect from 'common/components/IAISelect'; +import { canvasSelector } from 'features/canvas/store/canvasSelectors'; +import { setBoundingBoxScaleMethod } from 'features/canvas/store/canvasSlice'; +import { + BoundingBoxScale, + BOUNDING_BOX_SCALES_DICT, +} from 'features/canvas/store/canvasTypes'; + +import { ChangeEvent, memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const selector = createSelector( + [canvasSelector], + (canvas) => { + const { boundingBoxScaleMethod: boundingBoxScale } = canvas; + + return { + boundingBoxScale, + }; + }, + defaultSelectorOptions +); + +const ParamScaleBeforeProcessing = () => { + const dispatch = useAppDispatch(); + const { boundingBoxScale } = useAppSelector(selector); + + const { t } = useTranslation(); + + const handleChangeBoundingBoxScaleMethod = ( + e: ChangeEvent + ) => { + dispatch(setBoundingBoxScaleMethod(e.target.value as BoundingBoxScale)); + }; + + return ( + + ); +}; + +export default memo(ParamScaleBeforeProcessing); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamScaledHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamScaledHeight.tsx new file mode 100644 index 0000000000..a7e4a926b8 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamScaledHeight.tsx @@ -0,0 +1,68 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAISlider from 'common/components/IAISlider'; +import { canvasSelector } from 'features/canvas/store/canvasSelectors'; +import { setScaledBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; +import { systemSelector } from 'features/system/store/systemSelectors'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const selector = createSelector( + [generationSelector, systemSelector, canvasSelector], + (parameters, system, canvas) => { + const { scaledBoundingBoxDimensions, boundingBoxScaleMethod } = canvas; + + return { + scaledBoundingBoxDimensions, + isManual: boundingBoxScaleMethod === 'manual', + }; + }, + defaultSelectorOptions +); + +const ParamScaledHeight = () => { + const dispatch = useAppDispatch(); + const { isManual, scaledBoundingBoxDimensions } = useAppSelector(selector); + + const { t } = useTranslation(); + + const handleChangeScaledHeight = (v: number) => { + dispatch( + setScaledBoundingBoxDimensions({ + ...scaledBoundingBoxDimensions, + height: Math.floor(v), + }) + ); + }; + + const handleResetScaledHeight = () => { + dispatch( + setScaledBoundingBoxDimensions({ + ...scaledBoundingBoxDimensions, + height: Math.floor(512), + }) + ); + }; + + return ( + + ); +}; + +export default memo(ParamScaledHeight); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamScaledWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamScaledWidth.tsx new file mode 100644 index 0000000000..8104140808 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamScaledWidth.tsx @@ -0,0 +1,66 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAISlider from 'common/components/IAISlider'; +import { canvasSelector } from 'features/canvas/store/canvasSelectors'; +import { setScaledBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const selector = createSelector( + [canvasSelector], + (canvas) => { + const { boundingBoxScaleMethod, scaledBoundingBoxDimensions } = canvas; + + return { + scaledBoundingBoxDimensions, + isManual: boundingBoxScaleMethod === 'manual', + }; + }, + defaultSelectorOptions +); + +const ParamScaledWidth = () => { + const dispatch = useAppDispatch(); + const { isManual, scaledBoundingBoxDimensions } = useAppSelector(selector); + + const { t } = useTranslation(); + + const handleChangeScaledWidth = (v: number) => { + dispatch( + setScaledBoundingBoxDimensions({ + ...scaledBoundingBoxDimensions, + width: Math.floor(v), + }) + ); + }; + + const handleResetScaledWidth = () => { + dispatch( + setScaledBoundingBoxDimensions({ + ...scaledBoundingBoxDimensions, + width: Math.floor(512), + }) + ); + }; + + return ( + + ); +}; + +export default memo(ParamScaledWidth); diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamBlur.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamBlur.tsx similarity index 95% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamBlur.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamBlur.tsx index 693313e606..5c20ba7a13 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamBlur.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamBlur.tsx @@ -4,7 +4,7 @@ import IAISlider from 'common/components/IAISlider'; import { setSeamBlur } from 'features/parameters/store/generationSlice'; import { useTranslation } from 'react-i18next'; -export default function SeamBlur() { +export default function ParamSeamBlur() { const dispatch = useAppDispatch(); const seamBlur = useAppSelector( (state: RootState) => state.generation.seamBlur diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamCorrectionCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamCorrectionCollapse.tsx new file mode 100644 index 0000000000..992e8b6d02 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamCorrectionCollapse.tsx @@ -0,0 +1,28 @@ +import ParamSeamBlur from './ParamSeamBlur'; +import ParamSeamSize from './ParamSeamSize'; +import ParamSeamSteps from './ParamSeamSteps'; +import ParamSeamStrength from './ParamSeamStrength'; +import { useDisclosure } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import IAICollapse from 'common/components/IAICollapse'; +import { memo } from 'react'; + +const ParamSeamCorrectionCollapse = () => { + const { t } = useTranslation(); + const { isOpen, onToggle } = useDisclosure(); + + return ( + + + + + + + ); +}; + +export default memo(ParamSeamCorrectionCollapse); diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamSize.tsx similarity index 95% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamSize.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamSize.tsx index 02403ac5ec..8e56cded7b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamSize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamSize.tsx @@ -4,7 +4,7 @@ import IAISlider from 'common/components/IAISlider'; import { setSeamSize } from 'features/parameters/store/generationSlice'; import { useTranslation } from 'react-i18next'; -export default function SeamSize() { +export default function ParamSeamSize() { const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamSteps.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamSteps.tsx similarity index 95% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamSteps.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamSteps.tsx index 0319b26820..8ca5226621 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamSteps.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamSteps.tsx @@ -4,7 +4,7 @@ import IAISlider from 'common/components/IAISlider'; import { setSeamSteps } from 'features/parameters/store/generationSlice'; import { useTranslation } from 'react-i18next'; -export default function SeamSteps() { +export default function ParamSeamSteps() { const { t } = useTranslation(); const seamSteps = useAppSelector( (state: RootState) => state.generation.seamSteps diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamStrength.tsx similarity index 94% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamStrength.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamStrength.tsx index 7d447cfda1..de74156cd3 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamStrength.tsx @@ -4,7 +4,7 @@ import IAISlider from 'common/components/IAISlider'; import { setSeamStrength } from 'features/parameters/store/generationSlice'; import { useTranslation } from 'react-i18next'; -export default function SeamStrength() { +export default function ParamSeamStrength() { const dispatch = useAppDispatch(); const { t } = useTranslation(); const seamStrength = useAppSelector( diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainCFGScale.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamCFGScale.tsx similarity index 97% rename from invokeai/frontend/web/src/features/parameters/components/MainParameters/MainCFGScale.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamCFGScale.tsx index 928cccafd1..111e3d3ae8 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainCFGScale.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamCFGScale.tsx @@ -30,7 +30,7 @@ const selector = createSelector( } ); -const GuidanceScale = () => { +const ParamCFGScale = () => { const { cfgScale, initial, @@ -82,4 +82,4 @@ const GuidanceScale = () => { ); }; -export default memo(GuidanceScale); +export default memo(ParamCFGScale); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/HeightSlider.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamHeight.tsx similarity index 75% rename from invokeai/frontend/web/src/features/parameters/components/MainParameters/HeightSlider.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamHeight.tsx index 35e97fb266..9501c8b475 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/HeightSlider.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamHeight.tsx @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAISlider from 'common/components/IAISlider'; +import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider'; import { generationSelector } from 'features/parameters/store/generationSelectors'; import { setHeight } from 'features/parameters/store/generationSlice'; import { configSelector } from 'features/system/store/configSelectors'; @@ -13,8 +13,7 @@ const selector = createSelector( (generation, hotkeys, config) => { const { initial, min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.height; - const { height, shouldFitToWidthHeight, isImageToImageEnabled } = - generation; + const { height } = generation; const step = hotkeys.shift ? fineStep : coarseStep; @@ -25,23 +24,18 @@ const selector = createSelector( sliderMax, inputMax, step, - shouldFitToWidthHeight, - isImageToImageEnabled, }; } ); -const HeightSlider = () => { - const { - height, - initial, - min, - sliderMax, - inputMax, - step, - shouldFitToWidthHeight, - isImageToImageEnabled, - } = useAppSelector(selector); +type ParamHeightProps = Omit< + IAIFullSliderProps, + 'label' | 'value' | 'onChange' +>; + +const ParamHeight = (props: ParamHeightProps) => { + const { height, initial, min, sliderMax, inputMax, step } = + useAppSelector(selector); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -58,7 +52,6 @@ const HeightSlider = () => { return ( { withReset withSliderMarks sliderNumberInputProps={{ max: inputMax }} + {...props} /> ); }; -export default memo(HeightSlider); +export default memo(ParamHeight); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainIterations.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamIterations.tsx similarity index 96% rename from invokeai/frontend/web/src/features/parameters/components/MainParameters/MainIterations.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamIterations.tsx index d1d142d7ff..5a5b782c04 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainIterations.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamIterations.tsx @@ -32,7 +32,7 @@ const selector = createSelector( } ); -const MainIterations = () => { +const ParamIterations = () => { const { iterations, initial, @@ -83,4 +83,4 @@ const MainIterations = () => { ); }; -export default memo(MainIterations); +export default memo(ParamIterations); diff --git a/invokeai/frontend/web/src/features/parameters/components/PromptInput/NegativePromptInput.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx similarity index 91% rename from invokeai/frontend/web/src/features/parameters/components/PromptInput/NegativePromptInput.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx index ea3f12db42..d3790d4c24 100644 --- a/invokeai/frontend/web/src/features/parameters/components/PromptInput/NegativePromptInput.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx @@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setNegativePrompt } from 'features/parameters/store/generationSlice'; import { useTranslation } from 'react-i18next'; -const NegativePromptInput = () => { +const ParamNegativeConditioning = () => { const negativePrompt = useAppSelector( (state: RootState) => state.generation.negativePrompt ); @@ -29,4 +29,4 @@ const NegativePromptInput = () => { ); }; -export default NegativePromptInput; +export default ParamNegativeConditioning; diff --git a/invokeai/frontend/web/src/features/parameters/components/PromptInput/PromptInput.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx similarity index 75% rename from invokeai/frontend/web/src/features/parameters/components/PromptInput/PromptInput.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx index b54106733c..70e9b81957 100644 --- a/invokeai/frontend/web/src/features/parameters/components/PromptInput/PromptInput.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx @@ -1,7 +1,7 @@ import { Box, FormControl, Textarea } from '@chakra-ui/react'; import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { ChangeEvent, KeyboardEvent, useRef } from 'react'; +import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react'; import { createSelector } from '@reduxjs/toolkit'; import { readinessSelector } from 'app/selectors/readinessSelector'; @@ -15,7 +15,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { isEqual } from 'lodash-es'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; -import { generateGraphBuilt } from 'services/thunks/session'; +import { userInvoked } from 'app/store/actions'; const promptInputSelector = createSelector( [(state: RootState) => state.generation, activeTabNameSelector], @@ -35,9 +35,9 @@ const promptInputSelector = createSelector( /** * Prompt input text area. */ -const PromptInput = () => { +const ParamPositiveConditioning = () => { const dispatch = useAppDispatch(); - const { prompt } = useAppSelector(promptInputSelector); + const { prompt, activeTabName } = useAppSelector(promptInputSelector); const { isReady } = useAppSelector(readinessSelector); const promptRef = useRef(null); @@ -56,13 +56,16 @@ const PromptInput = () => { [] ); - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter' && e.shiftKey === false && isReady) { - e.preventDefault(); - dispatch(clampSymmetrySteps()); - dispatch(generateGraphBuilt()); - } - }; + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter' && e.shiftKey === false && isReady) { + e.preventDefault(); + dispatch(clampSymmetrySteps()); + dispatch(userInvoked(activeTabName)); + } + }, + [dispatch, activeTabName, isReady] + ); return ( @@ -85,4 +88,4 @@ const PromptInput = () => { ); }; -export default PromptInput; +export default ParamPositiveConditioning; diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSampler.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamSampler.tsx similarity index 93% rename from invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSampler.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamSampler.tsx index b71ff20e01..5a20f54438 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSampler.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamSampler.tsx @@ -6,7 +6,7 @@ import { setSampler } from 'features/parameters/store/generationSlice'; import { ChangeEvent, memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -const Scheduler = () => { +const ParamSampler = () => { const sampler = useAppSelector( (state: RootState) => state.generation.sampler ); @@ -29,4 +29,4 @@ const Scheduler = () => { ); }; -export default memo(Scheduler); +export default memo(ParamSampler); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSteps.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamSteps.tsx similarity index 97% rename from invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSteps.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamSteps.tsx index 43e399848e..f43cdd425b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSteps.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamSteps.tsx @@ -36,7 +36,7 @@ const selector = createSelector( } ); -const MainSteps = () => { +const ParamSteps = () => { const { steps, initial, min, sliderMax, inputMax, step, shouldUseSliders } = useAppSelector(selector); const dispatch = useAppDispatch(); @@ -84,4 +84,4 @@ const MainSteps = () => { ); }; -export default memo(MainSteps); +export default memo(ParamSteps); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/WidthSlider.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamWidth.tsx similarity index 78% rename from invokeai/frontend/web/src/features/parameters/components/MainParameters/WidthSlider.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamWidth.tsx index 0b871245c7..b7d63038d1 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/WidthSlider.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamWidth.tsx @@ -1,6 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAISlider from 'common/components/IAISlider'; +import { IAIFullSliderProps } from 'common/components/IAISlider'; import { generationSelector } from 'features/parameters/store/generationSelectors'; import { setWidth } from 'features/parameters/store/generationSlice'; import { configSelector } from 'features/system/store/configSelectors'; @@ -13,7 +14,7 @@ const selector = createSelector( (generation, hotkeys, config) => { const { initial, min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.width; - const { width, shouldFitToWidthHeight, isImageToImageEnabled } = generation; + const { width } = generation; const step = hotkeys.shift ? fineStep : coarseStep; @@ -24,23 +25,15 @@ const selector = createSelector( sliderMax, inputMax, step, - shouldFitToWidthHeight, - isImageToImageEnabled, }; } ); -const WidthSlider = () => { - const { - width, - initial, - min, - sliderMax, - inputMax, - step, - shouldFitToWidthHeight, - isImageToImageEnabled, - } = useAppSelector(selector); +type ParamWidthProps = Omit; + +const ParamWidth = (props: ParamWidthProps) => { + const { width, initial, min, sliderMax, inputMax, step } = + useAppSelector(selector); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -57,7 +50,6 @@ const WidthSlider = () => { return ( { withReset withSliderMarks sliderNumberInputProps={{ max: inputMax }} + {...props} /> ); }; -export default memo(WidthSlider); +export default memo(ParamWidth); diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/FaceRestore/CodeformerFidelity.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/FaceRestore/CodeformerFidelity.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/FaceRestore/CodeformerFidelity.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/FaceRestore/CodeformerFidelity.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/FaceRestore/FaceRestoreStrength.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreStrength.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/FaceRestore/FaceRestoreStrength.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreToggle.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/FaceRestore/FaceRestoreToggle.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreToggle.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/FaceRestore/FaceRestoreToggle.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreType.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/FaceRestore/FaceRestoreType.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreType.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/FaceRestore/FaceRestoreType.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresCollapse.tsx new file mode 100644 index 0000000000..9c1f3a3f14 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresCollapse.tsx @@ -0,0 +1,34 @@ +import { Flex } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { RootState } from 'app/store/store'; +import IAICollapse from 'common/components/IAICollapse'; +import { memo } from 'react'; +import { ParamHiresStrength } from './ParamHiresStrength'; +import { setHiresFix } from 'features/parameters/store/postprocessingSlice'; + +const ParamHiresCollapse = () => { + const { t } = useTranslation(); + const hiresFix = useAppSelector( + (state: RootState) => state.postprocessing.hiresFix + ); + + const dispatch = useAppDispatch(); + + const handleToggle = () => dispatch(setHiresFix(!hiresFix)); + + return ( + + + + + + ); +}; + +export default memo(ParamHiresCollapse); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresHeight.tsx new file mode 100644 index 0000000000..80a15f591b --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresHeight.tsx @@ -0,0 +1,3 @@ +// TODO + +export default {}; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresSteps.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresSteps.tsx new file mode 100644 index 0000000000..80a15f591b --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresSteps.tsx @@ -0,0 +1,3 @@ +// TODO + +export default {}; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/HiresSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresStrength.tsx similarity index 61% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/HiresSettings.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresStrength.tsx index 7f20a1d6c3..2655841590 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/HiresSettings.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresStrength.tsx @@ -1,15 +1,9 @@ import { createSelector } from '@reduxjs/toolkit'; -import type { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAISlider from 'common/components/IAISlider'; -import IAISwitch from 'common/components/IAISwitch'; import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors'; -import { - setHiresFix, - setHiresStrength, -} from 'features/parameters/store/postprocessingSlice'; +import { setHiresStrength } from 'features/parameters/store/postprocessingSlice'; import { isEqual } from 'lodash-es'; -import { ChangeEvent } from 'react'; import { useTranslation } from 'react-i18next'; const hiresStrengthSelector = createSelector( @@ -22,7 +16,7 @@ const hiresStrengthSelector = createSelector( } ); -export const HiresStrength = () => { +export const ParamHiresStrength = () => { const { hiresFix, hiresStrength } = useAppSelector(hiresStrengthSelector); const dispatch = useAppDispatch(); @@ -55,28 +49,3 @@ export const HiresStrength = () => { /> ); }; - -/** - * Hires Fix Toggle - */ -export const HiresToggle = () => { - const dispatch = useAppDispatch(); - - const hiresFix = useAppSelector( - (state: RootState) => state.postprocessing.hiresFix - ); - - const { t } = useTranslation(); - - const handleChangeHiresFix = (e: ChangeEvent) => - dispatch(setHiresFix(e.target.checked)); - - return ( - - ); -}; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresToggle.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresToggle.tsx new file mode 100644 index 0000000000..0fc600e9e8 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresToggle.tsx @@ -0,0 +1,31 @@ +import type { RootState } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAISwitch from 'common/components/IAISwitch'; +import { setHiresFix } from 'features/parameters/store/postprocessingSlice'; +import { ChangeEvent } from 'react'; +import { useTranslation } from 'react-i18next'; + +/** + * Hires Fix Toggle + */ +export const ParamHiresToggle = () => { + const dispatch = useAppDispatch(); + + const hiresFix = useAppSelector( + (state: RootState) => state.postprocessing.hiresFix + ); + + const { t } = useTranslation(); + + const handleChangeHiresFix = (e: ChangeEvent) => + dispatch(setHiresFix(e.target.checked)); + + return ( + + ); +}; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresWidth.tsx new file mode 100644 index 0000000000..80a15f591b --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Hires/ParamHiresWidth.tsx @@ -0,0 +1,3 @@ +// TODO + +export default {}; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageFit.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/ImageToImageFit.tsx similarity index 94% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageFit.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/ImageToImageFit.tsx index f479def1ab..03f502846c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageFit.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/ImageToImageFit.tsx @@ -5,7 +5,7 @@ import { setShouldFitToWidthHeight } from 'features/parameters/store/generationS import { ChangeEvent } from 'react'; import { useTranslation } from 'react-i18next'; -export default function ImageFit() { +export default function ImageToImageFit() { const dispatch = useAppDispatch(); const shouldFitToWidthHeight = useAppSelector( diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/ImageToImageSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/ImageToImageSettings.tsx new file mode 100644 index 0000000000..e8198c75ad --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/ImageToImageSettings.tsx @@ -0,0 +1,20 @@ +import { VStack } from '@chakra-ui/react'; + +import ImageToImageFit from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageFit'; +import ImageToImageStrength from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength'; + +import { useTranslation } from 'react-i18next'; +import InitialImagePreview from './InitialImagePreview'; +import InitialImageButtons from 'common/components/ImageToImageButtons'; + +export default function ImageToImageSettings() { + const { t } = useTranslation(); + return ( + + + + + + + ); +} diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/ImageToImageStrength.tsx similarity index 85% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/ImageToImageStrength.tsx index 284aa9a5c0..b467b15091 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/ImageToImageStrength.tsx @@ -1,5 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISlider from 'common/components/IAISlider'; import { generationSelector } from 'features/parameters/store/generationSelectors'; import { setImg2imgStrength } from 'features/parameters/store/generationSlice'; @@ -13,32 +14,25 @@ const selector = createSelector( (generation, hotkeys, config) => { const { initial, min, sliderMax, inputMax, fineStep, coarseStep } = config.sd.img2imgStrength; - const { img2imgStrength, isImageToImageEnabled } = generation; + const { img2imgStrength } = generation; const step = hotkeys.shift ? fineStep : coarseStep; return { img2imgStrength, - isImageToImageEnabled, initial, min, sliderMax, inputMax, step, }; - } + }, + defaultSelectorOptions ); const ImageToImageStrength = () => { - const { - img2imgStrength, - isImageToImageEnabled, - initial, - min, - sliderMax, - inputMax, - step, - } = useAppSelector(selector); + const { img2imgStrength, initial, min, sliderMax, inputMax, step } = + useAppSelector(selector); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -64,7 +58,6 @@ const ImageToImageStrength = () => { withInput withSliderMarks withReset - isDisabled={!isImageToImageEnabled} sliderNumberInputProps={{ max: inputMax }} /> ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx new file mode 100644 index 0000000000..c9c6e525b4 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImageDisplay.tsx @@ -0,0 +1,38 @@ +import { Flex } from '@chakra-ui/react'; +import InitialImagePreview from './InitialImagePreview'; +import InitialImageButtons from 'common/components/ImageToImageButtons'; + +const InitialImageDisplay = () => { + return ( + + + + + + + ); +}; + +export default InitialImageDisplay; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx new file mode 100644 index 0000000000..fbb833a14a --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx @@ -0,0 +1,111 @@ +import { Flex, Image, Spinner } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder'; +import { useGetUrl } from 'common/util/getUrl'; +import { clearInitialImage } from 'features/parameters/store/generationSlice'; +import { addToast } from 'features/system/store/systemSlice'; +import { DragEvent, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ImageType } from 'services/api'; +import ImageToImageOverlay from 'common/components/ImageToImageOverlay'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; +import { initialImageSelected } from 'features/parameters/store/actions'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; + +const selector = createSelector( + [generationSelector], + (generation) => { + const { initialImage } = generation; + return { + initialImage, + }; + }, + defaultSelectorOptions +); + +const InitialImagePreview = () => { + const { initialImage } = useAppSelector(selector); + const { getUrl } = useGetUrl(); + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + const [isLoaded, setIsLoaded] = useState(false); + + const onError = () => { + dispatch( + addToast({ + title: t('toast.parametersFailed'), + description: t('toast.parametersFailedDesc'), + status: 'error', + isClosable: true, + }) + ); + dispatch(clearInitialImage()); + setIsLoaded(false); + }; + + const handleDrop = useCallback( + (e: DragEvent) => { + setIsLoaded(false); + + const name = e.dataTransfer.getData('invokeai/imageName'); + const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; + + dispatch(initialImageSelected({ name, type })); + }, + [dispatch] + ); + + return ( + + + {initialImage?.url && ( + <> + { + setIsLoaded(true); + }} + fallback={ + + + + } + /> + {isLoaded && } + > + )} + {!initialImage?.url && } + + + ); +}; + +export default InitialImagePreview; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamNoiseCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamNoiseCollapse.tsx new file mode 100644 index 0000000000..30947e9709 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamNoiseCollapse.tsx @@ -0,0 +1,37 @@ +import { useTranslation } from 'react-i18next'; +import { Flex } from '@chakra-ui/react'; +import IAICollapse from 'common/components/IAICollapse'; +import ParamPerlinNoise from './ParamPerlinNoise'; +import ParamNoiseThreshold from './ParamNoiseThreshold'; +import { RootState } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { setShouldUseNoiseSettings } from 'features/parameters/store/generationSlice'; +import { memo } from 'react'; + +const ParamNoiseCollapse = () => { + const { t } = useTranslation(); + const shouldUseNoiseSettings = useAppSelector( + (state: RootState) => state.generation.shouldUseNoiseSettings + ); + + const dispatch = useAppDispatch(); + + const handleToggle = () => + dispatch(setShouldUseNoiseSettings(!shouldUseNoiseSettings)); + + return ( + + + + + + + ); +}; + +export default memo(ParamNoiseCollapse); diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/Threshold.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamNoiseThreshold.tsx similarity index 94% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/Threshold.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamNoiseThreshold.tsx index 14ca46b53c..e339734992 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/Threshold.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamNoiseThreshold.tsx @@ -4,7 +4,7 @@ import IAISlider from 'common/components/IAISlider'; import { setThreshold } from 'features/parameters/store/generationSlice'; import { useTranslation } from 'react-i18next'; -export default function Threshold() { +export default function ParamNoiseThreshold() { const dispatch = useAppDispatch(); const threshold = useAppSelector( (state: RootState) => state.generation.threshold diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/Perlin.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamPerlinNoise.tsx similarity index 94% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/Perlin.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamPerlinNoise.tsx index d2f4ea4249..ad710eae54 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/Perlin.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Noise/ParamPerlinNoise.tsx @@ -4,7 +4,7 @@ import IAISlider from 'common/components/IAISlider'; import { setPerlin } from 'features/parameters/store/generationSlice'; import { useTranslation } from 'react-i18next'; -export default function Perlin() { +export default function ParamPerlinNoise() { const dispatch = useAppDispatch(); const perlin = useAppSelector((state: RootState) => state.generation.perlin); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Seamless/ParamSeamlessCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seamless/ParamSeamlessCollapse.tsx new file mode 100644 index 0000000000..e5b88c7a60 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seamless/ParamSeamlessCollapse.tsx @@ -0,0 +1,50 @@ +import { useTranslation } from 'react-i18next'; +import { Box, Flex } from '@chakra-ui/react'; +import IAICollapse from 'common/components/IAICollapse'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { setSeamless } from 'features/parameters/store/generationSlice'; +import { memo } from 'react'; +import { createSelector } from '@reduxjs/toolkit'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import ParamSeamlessXAxis from './ParamSeamlessXAxis'; +import ParamSeamlessYAxis from './ParamSeamlessYAxis'; + +const selector = createSelector( + generationSelector, + (generation) => { + const { shouldUseSeamless, seamlessXAxis, seamlessYAxis } = generation; + + return { shouldUseSeamless, seamlessXAxis, seamlessYAxis }; + }, + defaultSelectorOptions +); + +const ParamSeamlessCollapse = () => { + const { t } = useTranslation(); + const { shouldUseSeamless } = useAppSelector(selector); + + const dispatch = useAppDispatch(); + + const handleToggle = () => dispatch(setSeamless(!shouldUseSeamless)); + + return ( + + + + + + + + + + + ); +}; + +export default memo(ParamSeamlessCollapse); diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/SeamlessSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seamless/ParamSeamlessToggle.tsx similarity index 91% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/SeamlessSettings.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Seamless/ParamSeamlessToggle.tsx index fb333c6f00..1a3b046bcf 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/SeamlessSettings.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seamless/ParamSeamlessToggle.tsx @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'; /** * Seamless tiling toggle */ -const SeamlessSettings = () => { +const ParamSeamlessToggle = () => { const dispatch = useAppDispatch(); const seamless = useAppSelector( @@ -30,4 +30,4 @@ const SeamlessSettings = () => { ); }; -export default SeamlessSettings; +export default ParamSeamlessToggle; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Seamless/ParamSeamlessXAxis.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seamless/ParamSeamlessXAxis.tsx new file mode 100644 index 0000000000..31e2aced9c --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seamless/ParamSeamlessXAxis.tsx @@ -0,0 +1,43 @@ +import { useTranslation } from 'react-i18next'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { ChangeEvent, memo, useCallback } from 'react'; +import { createSelector } from '@reduxjs/toolkit'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAISwitch from 'common/components/IAISwitch'; +import { setSeamlessXAxis } from 'features/parameters/store/generationSlice'; + +const selector = createSelector( + generationSelector, + (generation) => { + const { seamlessXAxis } = generation; + + return { seamlessXAxis }; + }, + defaultSelectorOptions +); + +const ParamSeamlessXAxis = () => { + const { t } = useTranslation(); + const { seamlessXAxis } = useAppSelector(selector); + + const dispatch = useAppDispatch(); + + const handleChange = useCallback( + (e: ChangeEvent) => { + dispatch(setSeamlessXAxis(e.target.checked)); + }, + [dispatch] + ); + + return ( + + ); +}; + +export default memo(ParamSeamlessXAxis); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Seamless/ParamSeamlessYAxis.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seamless/ParamSeamlessYAxis.tsx new file mode 100644 index 0000000000..edd78443c7 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seamless/ParamSeamlessYAxis.tsx @@ -0,0 +1,43 @@ +import { useTranslation } from 'react-i18next'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { ChangeEvent, memo, useCallback } from 'react'; +import { createSelector } from '@reduxjs/toolkit'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import IAISwitch from 'common/components/IAISwitch'; +import { setSeamlessYAxis } from 'features/parameters/store/generationSlice'; + +const selector = createSelector( + generationSelector, + (generation) => { + const { seamlessYAxis } = generation; + + return { seamlessYAxis }; + }, + defaultSelectorOptions +); + +const ParamSeamlessYAxis = () => { + const { t } = useTranslation(); + const { seamlessYAxis } = useAppSelector(selector); + + const dispatch = useAppDispatch(); + + const handleChange = useCallback( + (e: ChangeEvent) => { + dispatch(setSeamlessYAxis(e.target.checked)); + }, + [dispatch] + ); + + return ( + + ); +}; + +export default memo(ParamSeamlessYAxis); diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/Seed.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeed.tsx similarity index 54% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/Seed.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeed.tsx index 96c929a462..d5ced67d0e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/Seed.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeed.tsx @@ -1,13 +1,11 @@ -import { HStack } from '@chakra-ui/react'; import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAINumberInput from 'common/components/IAINumberInput'; import { setSeed } from 'features/parameters/store/generationSlice'; import { useTranslation } from 'react-i18next'; -import ShuffleSeed from './ShuffleSeed'; -export default function Seed() { +export default function ParamSeed() { const seed = useAppSelector((state: RootState) => state.generation.seed); const shouldRandomizeSeed = useAppSelector( (state: RootState) => state.generation.shouldRandomizeSeed @@ -23,25 +21,22 @@ export default function Seed() { const handleChangeSeed = (v: number) => dispatch(setSeed(v)); return ( - - - - + ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedCollapse.tsx new file mode 100644 index 0000000000..2867029f7e --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedCollapse.tsx @@ -0,0 +1,48 @@ +import { Flex } from '@chakra-ui/react'; +import ParamSeed from './ParamSeed'; +import { memo, useCallback } from 'react'; +import ParamSeedShuffle from './ParamSeedShuffle'; +import { useTranslation } from 'react-i18next'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { createSelector } from '@reduxjs/toolkit'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { setShouldRandomizeSeed } from 'features/parameters/store/generationSlice'; +import IAICollapse from 'common/components/IAICollapse'; + +const selector = createSelector( + generationSelector, + (generation) => { + const { shouldRandomizeSeed } = generation; + + return { shouldRandomizeSeed }; + }, + defaultSelectorOptions +); + +const ParamSeedSettings = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { shouldRandomizeSeed } = useAppSelector(selector); + + const handleToggle = useCallback( + () => dispatch(setShouldRandomizeSeed(!shouldRandomizeSeed)), + [dispatch, shouldRandomizeSeed] + ); + + return ( + + + + + + + ); +}; + +export default memo(ParamSeedSettings); diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/RandomizeSeed.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedRandomize.tsx similarity index 64% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/RandomizeSeed.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedRandomize.tsx index ea60124f74..13380f3660 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/RandomizeSeed.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedRandomize.tsx @@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAISwitch from 'common/components/IAISwitch'; import { setShouldRandomizeSeed } from 'features/parameters/store/generationSlice'; import { useTranslation } from 'react-i18next'; -import { Switch } from '@chakra-ui/react'; +import { FormControl, FormLabel, Switch } from '@chakra-ui/react'; // export default function RandomizeSeed() { // const dispatch = useAppDispatch(); @@ -27,7 +27,7 @@ import { Switch } from '@chakra-ui/react'; // ); // } -const SeedToggle = () => { +const ParamSeedRandomize = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -36,15 +36,33 @@ const SeedToggle = () => { ); const handleChangeShouldRandomizeSeed = (e: ChangeEvent) => - dispatch(setShouldRandomizeSeed(!e.target.checked)); + dispatch(setShouldRandomizeSeed(e.target.checked)); return ( - + + + {t('parameters.randomizeSeed')} + + + ); }; -export default memo(SeedToggle); +export default memo(ParamSeedRandomize); diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/ShuffleSeed.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedShuffle.tsx similarity index 66% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/ShuffleSeed.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedShuffle.tsx index f2d222de7c..dd1f05e2f9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/ShuffleSeed.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Seed/ParamSeedShuffle.tsx @@ -1,14 +1,13 @@ -import { Button } from '@chakra-ui/react'; +import { Box } from '@chakra-ui/react'; import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIIconButton from 'common/components/IAIIconButton'; +import IAIButton from 'common/components/IAIButton'; import randomInt from 'common/util/randomInt'; import { setSeed } from 'features/parameters/store/generationSlice'; import { useTranslation } from 'react-i18next'; -import { FaRandom } from 'react-icons/fa'; -export default function ShuffleSeed() { +export default function ParamSeedShuffle() { const dispatch = useAppDispatch(); const shouldRandomizeSeed = useAppSelector( (state: RootState) => state.generation.shouldRandomizeSeed @@ -19,20 +18,14 @@ export default function ShuffleSeed() { dispatch(setSeed(randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX))); return ( - } onClick={handleClickRandomizeSeed} - /> - // - // {t('parameters.shuffle')} - // + > + {t('parameters.shuffle')} + ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Symmetry/ParamSymmetryCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Symmetry/ParamSymmetryCollapse.tsx new file mode 100644 index 0000000000..97c51d4461 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Symmetry/ParamSymmetryCollapse.tsx @@ -0,0 +1,37 @@ +import { memo } from 'react'; +import { Flex } from '@chakra-ui/react'; +import ParamSymmetryHorizontal from './ParamSymmetryHorizontal'; +import ParamSymmetryVertical from './ParamSymmetryVertical'; + +import { useTranslation } from 'react-i18next'; +import IAICollapse from 'common/components/IAICollapse'; +import { RootState } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { setShouldUseSymmetry } from 'features/parameters/store/generationSlice'; + +const ParamSymmetryCollapse = () => { + const { t } = useTranslation(); + const shouldUseSymmetry = useAppSelector( + (state: RootState) => state.generation.shouldUseSymmetry + ); + + const dispatch = useAppDispatch(); + + const handleToggle = () => dispatch(setShouldUseSymmetry(!shouldUseSymmetry)); + + return ( + + + + + + + ); +}; + +export default memo(ParamSymmetryCollapse); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Symmetry/ParamSymmetryHorizontal.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Symmetry/ParamSymmetryHorizontal.tsx new file mode 100644 index 0000000000..99af147f2c --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Symmetry/ParamSymmetryHorizontal.tsx @@ -0,0 +1,32 @@ +import { RootState } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAISlider from 'common/components/IAISlider'; +import { setHorizontalSymmetrySteps } from 'features/parameters/store/generationSlice'; +import { useTranslation } from 'react-i18next'; + +export default function ParamSymmetryHorizontal() { + const horizontalSymmetrySteps = useAppSelector( + (state: RootState) => state.generation.horizontalSymmetrySteps + ); + + const steps = useAppSelector((state: RootState) => state.generation.steps); + + const dispatch = useAppDispatch(); + + const { t } = useTranslation(); + + return ( + dispatch(setHorizontalSymmetrySteps(v))} + min={0} + max={steps} + step={1} + withInput + withSliderMarks + withReset + handleReset={() => dispatch(setHorizontalSymmetrySteps(0))} + /> + ); +} diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/SymmetryToggle.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Symmetry/ParamSymmetryToggle.tsx similarity index 91% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/SymmetryToggle.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Symmetry/ParamSymmetryToggle.tsx index c155336c1e..7cc17c045e 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/SymmetryToggle.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Symmetry/ParamSymmetryToggle.tsx @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAISwitch from 'common/components/IAISwitch'; import { setShouldUseSymmetry } from 'features/parameters/store/generationSlice'; -export default function SymmetryToggle() { +export default function ParamSymmetryToggle() { const shouldUseSymmetry = useAppSelector( (state: RootState) => state.generation.shouldUseSymmetry ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Symmetry/ParamSymmetryVertical.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Symmetry/ParamSymmetryVertical.tsx new file mode 100644 index 0000000000..c8ddb46a3a --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Symmetry/ParamSymmetryVertical.tsx @@ -0,0 +1,32 @@ +import { RootState } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAISlider from 'common/components/IAISlider'; +import { setVerticalSymmetrySteps } from 'features/parameters/store/generationSlice'; +import { useTranslation } from 'react-i18next'; + +export default function ParamSymmetryVertical() { + const verticalSymmetrySteps = useAppSelector( + (state: RootState) => state.generation.verticalSymmetrySteps + ); + + const steps = useAppSelector((state: RootState) => state.generation.steps); + + const dispatch = useAppDispatch(); + + const { t } = useTranslation(); + + return ( + dispatch(setVerticalSymmetrySteps(v))} + min={0} + max={steps} + step={1} + withInput + withSliderMarks + withReset + handleReset={() => dispatch(setVerticalSymmetrySteps(0))} + /> + ); +} diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleDenoisingStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/UpscaleDenoisingStrength.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleDenoisingStrength.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/UpscaleDenoisingStrength.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleScale.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/UpscaleScale.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleScale.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/UpscaleScale.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/UpscaleSettings.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/UpscaleSettings.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/UpscaleStrength.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleStrength.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/UpscaleStrength.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleToggle.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/UpscaleToggle.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleToggle.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Upscale/UpscaleToggle.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationAmount.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Variations/ParamVariationAmount.tsx similarity index 95% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationAmount.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Variations/ParamVariationAmount.tsx index 21b5001d6a..9bc84a0bf4 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationAmount.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Variations/ParamVariationAmount.tsx @@ -4,7 +4,7 @@ import IAISlider from 'common/components/IAISlider'; import { setVariationAmount } from 'features/parameters/store/generationSlice'; import { useTranslation } from 'react-i18next'; -export default function VariationAmount() { +export default function ParamVariationAmount() { const variationAmount = useAppSelector( (state: RootState) => state.generation.variationAmount ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Variations/ParamVariationCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Variations/ParamVariationCollapse.tsx new file mode 100644 index 0000000000..0e1134e9f0 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Variations/ParamVariationCollapse.tsx @@ -0,0 +1,37 @@ +import ParamVariationWeights from './ParamVariationWeights'; +import ParamVariationAmount from './ParamVariationAmount'; +import { useTranslation } from 'react-i18next'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { RootState } from 'app/store/store'; +import { setShouldGenerateVariations } from 'features/parameters/store/generationSlice'; +import { Flex } from '@chakra-ui/react'; +import IAICollapse from 'common/components/IAICollapse'; +import { memo } from 'react'; + +const ParamVariationCollapse = () => { + const { t } = useTranslation(); + const shouldGenerateVariations = useAppSelector( + (state: RootState) => state.generation.shouldGenerateVariations + ); + + const dispatch = useAppDispatch(); + + const handleToggle = () => + dispatch(setShouldGenerateVariations(!shouldGenerateVariations)); + + return ( + + + + + + + ); +}; + +export default memo(ParamVariationCollapse); diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/SeedWeights.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Variations/ParamVariationWeights.tsx similarity index 95% rename from invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/SeedWeights.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/Variations/ParamVariationWeights.tsx index 7f8b096757..30876597a8 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/SeedWeights.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Variations/ParamVariationWeights.tsx @@ -6,7 +6,7 @@ import { setSeedWeights } from 'features/parameters/store/generationSlice'; import { ChangeEvent } from 'react'; import { useTranslation } from 'react-i18next'; -export default function SeedWeights() { +export default function ParamVariationWeights() { const seedWeights = useAppSelector( (state: RootState) => state.generation.seedWeights ); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageDimensions/AspectRatioPreview.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/_ImageDimensions/AspectRatioPreview.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/ImageDimensions/AspectRatioPreview.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/_ImageDimensions/AspectRatioPreview.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageDimensions/DimensionsSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/_ImageDimensions/DimensionsSettings.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/ImageDimensions/DimensionsSettings.tsx rename to invokeai/frontend/web/src/features/parameters/components/Parameters/_ImageDimensions/DimensionsSettings.tsx diff --git a/invokeai/frontend/web/src/features/parameters/components/ParametersAccordion.tsx b/invokeai/frontend/web/src/features/parameters/components/ParametersAccordion.tsx deleted file mode 100644 index 22d7a6228e..0000000000 --- a/invokeai/frontend/web/src/features/parameters/components/ParametersAccordion.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Accordion } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { Feature } from 'app/features'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { tabMap } from 'features/ui/store/tabMap'; -import { uiSelector } from 'features/ui/store/uiSelectors'; -import { openAccordionItemsChanged } from 'features/ui/store/uiSlice'; -import { map } from 'lodash-es'; -import { ReactNode, useCallback } from 'react'; -import InvokeAccordionItem from './AccordionItems/InvokeAccordionItem'; - -const parametersAccordionSelector = createSelector([uiSelector], (uiSlice) => { - const { - activeTab, - openLinearAccordionItems, - openUnifiedCanvasAccordionItems, - } = uiSlice; - - let openAccordions: number[] = []; - - if (tabMap[activeTab] === 'generate') { - openAccordions = openLinearAccordionItems; - } - - if (tabMap[activeTab] === 'unifiedCanvas') { - openAccordions = openUnifiedCanvasAccordionItems; - } - - return { - openAccordions, - }; -}); - -export type ParametersAccordionItem = { - name: string; - header: string; - content: ReactNode; - feature?: Feature; - additionalHeaderComponents?: ReactNode; -}; - -export type ParametersAccordionItems = { - [parametersAccordionKey: string]: ParametersAccordionItem; -}; - -type ParametersAccordionProps = { - accordionItems: ParametersAccordionItems; -}; - -/** - * Main container for generation and processing parameters. - */ -const ParametersAccordion = ({ accordionItems }: ParametersAccordionProps) => { - const { openAccordions } = useAppSelector(parametersAccordionSelector); - - const dispatch = useAppDispatch(); - - const handleChangeAccordionState = (openAccordions: number | number[]) => { - dispatch( - openAccordionItemsChanged( - Array.isArray(openAccordions) ? openAccordions : [openAccordions] - ) - ); - }; - - // Render function for accordion items - const renderAccordionItems = useCallback( - () => - map(accordionItems, (accordionItem) => ( - - )), - [accordionItems] - ); - - return ( - - {renderAccordionItems()} - - ); -}; - -export default ParametersAccordion; diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx index 4f6c2ecc1c..23f2ae409a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx @@ -11,7 +11,7 @@ import { CancelStrategy, } from 'features/system/store/systemSlice'; import { isEqual } from 'lodash-es'; -import { useCallback, memo } from 'react'; +import { useCallback, memo, useMemo } from 'react'; import { ButtonSpinner, ButtonGroup, @@ -20,7 +20,6 @@ import { MenuList, MenuOptionGroup, MenuItemOption, - IconButton, } from '@chakra-ui/react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -28,7 +27,7 @@ import { useTranslation } from 'react-i18next'; import { MdCancel, MdCancelScheduleSend } from 'react-icons/md'; import { sessionCanceled } from 'services/thunks/session'; -import { BiChevronDown } from 'react-icons/bi'; +import { ChevronDownIcon } from '@chakra-ui/icons'; const cancelButtonSelector = createSelector( systemSelector, @@ -102,46 +101,45 @@ const CancelButton = ( [isConnected, isProcessing, isCancelable] ); + const cancelLabel = useMemo(() => { + if (isCancelScheduled) { + return t('parameters.cancel.isScheduled'); + } + if (cancelType === 'immediate') { + return t('parameters.cancel.immediate'); + } + + return t('parameters.cancel.schedule'); + }, [t, cancelType, isCancelScheduled]); + + const cancelIcon = useMemo(() => { + if (isCancelScheduled) { + return ; + } + if (cancelType === 'immediate') { + return ; + } + + return ; + }, [cancelType, isCancelScheduled]); + return ( - {cancelType === 'immediate' ? ( - } - tooltip={t('parameters.cancel.immediate')} - aria-label={t('parameters.cancel.immediate')} - isDisabled={!isConnected || !isProcessing || !isCancelable} - onClick={handleClickCancel} - colorScheme="error" - {...rest} - /> - ) : ( - : - } - tooltip={ - isCancelScheduled - ? t('parameters.cancel.isScheduled') - : t('parameters.cancel.schedule') - } - aria-label={ - isCancelScheduled - ? t('parameters.cancel.isScheduled') - : t('parameters.cancel.schedule') - } - isDisabled={!isConnected || !isProcessing || !isCancelable} - onClick={handleClickCancel} - colorScheme="error" - {...rest} - /> - )} - + } + icon={} paddingX={0} paddingY={0} colorScheme="error" diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx index d2002eb04f..68d607c0fa 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx @@ -1,5 +1,6 @@ import { Box } from '@chakra-ui/react'; import { readinessSelector } from 'app/selectors/readinessSelector'; +import { userInvoked } from 'app/store/actions'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton, { IAIButtonProps } from 'common/components/IAIButton'; import IAIIconButton, { @@ -11,7 +12,6 @@ import { useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { FaPlay } from 'react-icons/fa'; -import { generateGraphBuilt } from 'services/thunks/session'; interface InvokeButton extends Omit { @@ -26,8 +26,8 @@ export default function InvokeButton(props: InvokeButton) { const handleInvoke = useCallback(() => { dispatch(clampSymmetrySteps()); - dispatch(generateGraphBuilt()); - }, [dispatch]); + dispatch(userInvoked(activeTabName)); + }, [dispatch, activeTabName]); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/ProcessButtons.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/ProcessButtons.tsx index ba8522f0bf..4449866ef2 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/ProcessButtons.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/ProcessButtons.tsx @@ -1,16 +1,11 @@ import { Flex } from '@chakra-ui/react'; -import { useAppSelector } from 'app/store/storeHooks'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import CancelButton from './CancelButton'; import InvokeButton from './InvokeButton'; -import LoopbackButton from './Loopback'; /** * Buttons to start and cancel image generation. */ const ProcessButtons = () => { - const activeTabName = useAppSelector(activeTabNameSelector); - return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/ProgressImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/_ProgressImagePreview.tsx similarity index 98% rename from invokeai/frontend/web/src/features/parameters/components/ProgressImagePreview.tsx rename to invokeai/frontend/web/src/features/parameters/components/_ProgressImagePreview.tsx index c31215a13e..09fad6acda 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProgressImagePreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/_ProgressImagePreview.tsx @@ -15,7 +15,7 @@ import { } from 'features/ui/store/uiSlice'; import { Rnd } from 'react-rnd'; import { Rect } from 'features/ui/store/uiTypes'; -import { isEqual } from 'lodash'; +import { isEqual } from 'lodash-es'; import ProgressImage from './ProgressImage'; const selector = createSelector( @@ -55,7 +55,6 @@ const ProgressImagePreview = () => { return ( <> - {' '} dispatch(setShouldShowProgressImages(!showProgressWindow)) diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts index 7c45f159b2..a093010343 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts @@ -1,12 +1,15 @@ -import { UseToastOptions, useToast } from '@chakra-ui/react'; +import { useToast } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; import { isFinite, isString } from 'lodash-es'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import useSetBothPrompts from './usePrompt'; -import { initialImageSelected, setSeed } from '../store/generationSlice'; -import { isImage, isImageField } from 'services/types/guards'; +import { allParametersSet, setSeed } from '../store/generationSlice'; +import { isImageField } from 'services/types/guards'; import { NUMPY_RAND_MAX } from 'app/constants'; +import { initialImageSelected } from '../store/actions'; +import { Image } from 'app/types/invokeai'; +import { setActiveTab } from 'features/ui/store/uiSlice'; export const useParameters = () => { const dispatch = useAppDispatch(); @@ -102,28 +105,48 @@ export const useParameters = () => { * Sets image as initial image with toast */ const sendToImageToImage = useCallback( - (image: unknown) => { - if (!isImage(image)) { + (image: Image) => { + dispatch(initialImageSelected({ name: image.name, type: image.type })); + }, + [dispatch] + ); + + const recallAllParameters = useCallback( + (image: Image | undefined) => { + const type = image?.metadata?.invokeai?.node?.type; + if (['txt2img', 'img2img', 'inpaint'].includes(String(type))) { + dispatch(allParametersSet(image)); + + if (image?.metadata?.invokeai?.node?.type === 'img2img') { + dispatch(setActiveTab('img2img')); + } else if (image?.metadata?.invokeai?.node?.type === 'txt2img') { + dispatch(setActiveTab('txt2img')); + } + toast({ - title: t('toast.imageNotLoaded'), - description: t('toast.imageNotLoadedDesc'), - status: 'warning', + title: t('toast.parametersSet'), + status: 'success', + duration: 2500, + isClosable: true, + }); + } else { + toast({ + title: t('toast.parametersNotSet'), + description: t('toast.parametersNotSetDesc'), + status: 'error', duration: 2500, isClosable: true, }); - return; } - - dispatch(initialImageSelected({ name: image.name, type: image.type })); - toast({ - title: t('toast.sentToImageToImage'), - status: 'info', - duration: 2500, - isClosable: true, - }); }, [t, toast, dispatch] ); - return { recallPrompt, recallSeed, recallInitialImage, sendToImageToImage }; + return { + recallPrompt, + recallSeed, + recallInitialImage, + sendToImageToImage, + recallAllParameters, + }; }; diff --git a/invokeai/frontend/web/src/features/parameters/store/actions.ts b/invokeai/frontend/web/src/features/parameters/store/actions.ts new file mode 100644 index 0000000000..4b261d7783 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/store/actions.ts @@ -0,0 +1,12 @@ +import { createAction } from '@reduxjs/toolkit'; +import { Image } from 'app/types/invokeai'; +import { ImageType } from 'services/api'; + +export type SelectedImage = { + name: string; + type: ImageType; +}; + +export const initialImageSelected = createAction< + Image | SelectedImage | undefined +>('generation/initialImageSelected'); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationPersistDenylist.ts b/invokeai/frontend/web/src/features/parameters/store/generationPersistDenylist.ts index 70f35aa564..ab3e77801e 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationPersistDenylist.ts @@ -4,6 +4,7 @@ import { GenerationState } from './generationSlice'; * Generation slice persist denylist */ const itemsToDenylist: (keyof GenerationState)[] = []; +export const generationPersistDenylist: (keyof GenerationState)[] = []; export const generationDenylist = itemsToDenylist.map( (denylistItem) => `generation.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 9d9d689cb0..b0adc578a0 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -1,23 +1,16 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import * as InvokeAI from 'app/types/invokeai'; -import { getPromptAndNegative } from 'common/util/getPromptAndNegative'; import promptToString from 'common/util/promptToString'; -import { seedWeightsToString } from 'common/util/seedWeightPairs'; import { clamp } from 'lodash-es'; -import { ImageField, ImageType } from 'services/api'; - -export type SelectedImage = { - name: string; - type: ImageType; -}; +import { setAllParametersReducer } from './setAllParametersReducer'; export interface GenerationState { cfgScale: number; height: number; img2imgStrength: number; infillMethod: string; - initialImage?: SelectedImage; // can be an Image or url + initialImage?: InvokeAI.Image; // can be an Image or url iterations: number; maskPath: string; perlin: number; @@ -25,7 +18,6 @@ export interface GenerationState { negativePrompt: string; sampler: string; seamBlur: number; - seamless: boolean; seamSize: number; seamSteps: number; seamStrength: number; @@ -34,6 +26,7 @@ export interface GenerationState { shouldFitToWidthHeight: boolean; shouldGenerateVariations: boolean; shouldRandomizeSeed: boolean; + shouldUseNoiseSettings: boolean; steps: number; threshold: number; tileSize: number; @@ -42,10 +35,13 @@ export interface GenerationState { shouldUseSymmetry: boolean; horizontalSymmetrySteps: number; verticalSymmetrySteps: number; - isImageToImageEnabled: boolean; + model: string; + shouldUseSeamless: boolean; + seamlessXAxis: boolean; + seamlessYAxis: boolean; } -const initialGenerationState: GenerationState = { +export const initialGenerationState: GenerationState = { cfgScale: 7.5, height: 512, img2imgStrength: 0.75, @@ -57,7 +53,6 @@ const initialGenerationState: GenerationState = { negativePrompt: '', sampler: 'k_lms', seamBlur: 16, - seamless: false, seamSize: 96, seamSteps: 30, seamStrength: 0.7, @@ -66,6 +61,7 @@ const initialGenerationState: GenerationState = { shouldFitToWidthHeight: true, shouldGenerateVariations: false, shouldRandomizeSeed: true, + shouldUseNoiseSettings: false, steps: 50, threshold: 0, tileSize: 32, @@ -74,7 +70,10 @@ const initialGenerationState: GenerationState = { shouldUseSymmetry: false, horizontalSymmetrySteps: 0, verticalSymmetrySteps: 0, - isImageToImageEnabled: false, + model: '', + shouldUseSeamless: false, + seamlessXAxis: true, + seamlessYAxis: true, }; const initialState: GenerationState = initialGenerationState; @@ -149,7 +148,13 @@ export const generationSlice = createSlice({ state.maskPath = action.payload; }, setSeamless: (state, action: PayloadAction) => { - state.seamless = action.payload; + state.shouldUseSeamless = action.payload; + }, + setSeamlessXAxis: (state, action: PayloadAction) => { + state.seamlessXAxis = action.payload; + }, + setSeamlessYAxis: (state, action: PayloadAction) => { + state.seamlessYAxis = action.payload; }, setShouldFitToWidthHeight: (state, action: PayloadAction) => { state.shouldFitToWidthHeight = action.payload; @@ -181,131 +186,7 @@ export const generationSlice = createSlice({ state.shouldGenerateVariations = true; state.variationAmount = 0; }, - setAllTextToImageParameters: ( - state, - action: PayloadAction - ) => { - // const { - // sampler, - // prompt, - // seed, - // variations, - // steps, - // cfg_scale, - // threshold, - // perlin, - // seamless, - // _hires_fix, - // width, - // height, - // } = action.payload.image; - // if (variations && variations.length > 0) { - // state.seedWeights = seedWeightsToString(variations); - // state.shouldGenerateVariations = true; - // state.variationAmount = 0; - // } else { - // state.shouldGenerateVariations = false; - // } - // if (seed) { - // state.seed = seed; - // state.shouldRandomizeSeed = false; - // } - // if (prompt) state.prompt = promptToString(prompt); - // if (sampler) state.sampler = sampler; - // if (steps) state.steps = steps; - // if (cfg_scale) state.cfgScale = cfg_scale; - // if (typeof threshold === 'undefined') { - // state.threshold = 0; - // } else { - // state.threshold = threshold; - // } - // if (typeof perlin === 'undefined') { - // state.perlin = 0; - // } else { - // state.perlin = perlin; - // } - // if (typeof seamless === 'boolean') state.seamless = seamless; - // // if (typeof hires_fix === 'boolean') state.hiresFix = hires_fix; // TODO: Needs to be fixed after reorg - // if (width) state.width = width; - // if (height) state.height = height; - }, - setAllImageToImageParameters: ( - state, - action: PayloadAction - ) => { - // const { type, strength, fit, init_image_path, mask_image_path } = - // action.payload.image; - // if (type === 'img2img') { - // if (init_image_path) state.initialImage = init_image_path; - // if (mask_image_path) state.maskPath = mask_image_path; - // if (strength) state.img2imgStrength = strength; - // if (typeof fit === 'boolean') state.shouldFitToWidthHeight = fit; - // } - }, - setAllParameters: (state, action: PayloadAction) => { - // const { - // type, - // sampler, - // prompt, - // seed, - // variations, - // steps, - // cfg_scale, - // threshold, - // perlin, - // seamless, - // _hires_fix, - // width, - // height, - // strength, - // fit, - // init_image_path, - // mask_image_path, - // } = action.payload.image; - // if (type === 'img2img') { - // if (init_image_path) state.initialImage = init_image_path; - // if (mask_image_path) state.maskPath = mask_image_path; - // if (strength) state.img2imgStrength = strength; - // if (typeof fit === 'boolean') state.shouldFitToWidthHeight = fit; - // } - // if (variations && variations.length > 0) { - // state.seedWeights = seedWeightsToString(variations); - // state.shouldGenerateVariations = true; - // state.variationAmount = 0; - // } else { - // state.shouldGenerateVariations = false; - // } - // if (seed) { - // state.seed = seed; - // state.shouldRandomizeSeed = false; - // } - // if (prompt) { - // const [promptOnly, negativePrompt] = getPromptAndNegative(prompt); - // if (promptOnly) state.prompt = promptOnly; - // negativePrompt - // ? (state.negativePrompt = negativePrompt) - // : (state.negativePrompt = ''); - // } - // if (sampler) state.sampler = sampler; - // if (steps) state.steps = steps; - // if (cfg_scale) state.cfgScale = cfg_scale; - // if (typeof threshold === 'undefined') { - // state.threshold = 0; - // } else { - // state.threshold = threshold; - // } - // if (typeof perlin === 'undefined') { - // state.perlin = 0; - // } else { - // state.perlin = perlin; - // } - // if (typeof seamless === 'boolean') state.seamless = seamless; - // // if (typeof hires_fix === 'boolean') state.hiresFix = hires_fix; // TODO: Needs to be fixed after reorg - // if (width) state.width = width; - // if (height) state.height = height; - // // state.shouldRunESRGAN = false; // TODO: Needs to be fixed after reorg - // // state.shouldRunFacetool = false; // TODO: Needs to be fixed after reorg - }, + allParametersSet: setAllParametersReducer, resetParametersState: (state) => { return { ...state, @@ -315,12 +196,6 @@ export const generationSlice = createSlice({ setShouldRandomizeSeed: (state, action: PayloadAction) => { state.shouldRandomizeSeed = action.payload; }, - // setInitialImage: ( - // state, - // action: PayloadAction - // ) => { - // state.initialImage = action.payload; - // }, clearInitialImage: (state) => { state.initialImage = undefined; }, @@ -351,12 +226,14 @@ export const generationSlice = createSlice({ setVerticalSymmetrySteps: (state, action: PayloadAction) => { state.verticalSymmetrySteps = action.payload; }, - initialImageSelected: (state, action: PayloadAction) => { - state.initialImage = action.payload; - state.isImageToImageEnabled = true; + setShouldUseNoiseSettings: (state, action: PayloadAction) => { + state.shouldUseNoiseSettings = action.payload; }, - isImageToImageEnabledChanged: (state, action: PayloadAction) => { - state.isImageToImageEnabled = action.payload; + initialImageChanged: (state, action: PayloadAction) => { + state.initialImage = action.payload; + }, + modelSelected: (state, action: PayloadAction) => { + state.model = action.payload; }, }, }); @@ -366,9 +243,6 @@ export const { clearInitialImage, resetParametersState, resetSeed, - setAllImageToImageParameters, - setAllParameters, - setAllTextToImageParameters, setCfgScale, setHeight, setImg2imgStrength, @@ -382,7 +256,6 @@ export const { setNegativePrompt, setSampler, setSeamBlur, - setSeamless, setSeamSize, setSeamSteps, setSeamStrength, @@ -399,8 +272,13 @@ export const { setShouldUseSymmetry, setHorizontalSymmetrySteps, setVerticalSymmetrySteps, - initialImageSelected, - isImageToImageEnabledChanged, + initialImageChanged, + modelSelected, + setShouldUseNoiseSettings, + setSeamless, + setSeamlessXAxis, + setSeamlessYAxis, + allParametersSet, } = generationSlice.actions; export default generationSlice.reducer; diff --git a/invokeai/frontend/web/src/features/parameters/store/hiresSlice.ts b/invokeai/frontend/web/src/features/parameters/store/hiresSlice.ts new file mode 100644 index 0000000000..89827270d1 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/store/hiresSlice.ts @@ -0,0 +1,97 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +export interface HiresState { + codeformerFidelity: number; + facetoolStrength: number; + facetoolType: FacetoolType; + hiresFix: boolean; + hiresStrength: number; + shouldLoopback: boolean; + shouldRunESRGAN: boolean; + shouldRunFacetool: boolean; + upscalingLevel: UpscalingLevel; + upscalingDenoising: number; + upscalingStrength: number; +} + +export const initialHiresState: HiresState = { + codeformerFidelity: 0.75, + facetoolStrength: 0.75, + facetoolType: 'gfpgan', + hiresFix: false, + hiresStrength: 0.75, + hiresSteps: 30, + hiresWidth: 512, + hiresHeight: 512, + hiresModel: '', + shouldLoopback: false, + shouldRunESRGAN: false, + shouldRunFacetool: false, + upscalingLevel: 4, + upscalingDenoising: 0.75, + upscalingStrength: 0.75, +}; + +export const postprocessingSlice = createSlice({ + name: 'postprocessing', + initialState: initialPostprocessingState, + reducers: { + setFacetoolStrength: (state, action: PayloadAction) => { + state.facetoolStrength = action.payload; + }, + setCodeformerFidelity: (state, action: PayloadAction) => { + state.codeformerFidelity = action.payload; + }, + setUpscalingLevel: (state, action: PayloadAction) => { + state.upscalingLevel = action.payload; + }, + setUpscalingDenoising: (state, action: PayloadAction) => { + state.upscalingDenoising = action.payload; + }, + setUpscalingStrength: (state, action: PayloadAction) => { + state.upscalingStrength = action.payload; + }, + setHiresFix: (state, action: PayloadAction) => { + state.hiresFix = action.payload; + }, + setHiresStrength: (state, action: PayloadAction) => { + state.hiresStrength = action.payload; + }, + resetPostprocessingState: (state) => { + return { + ...state, + ...initialPostprocessingState, + }; + }, + setShouldRunFacetool: (state, action: PayloadAction) => { + state.shouldRunFacetool = action.payload; + }, + setFacetoolType: (state, action: PayloadAction) => { + state.facetoolType = action.payload; + }, + setShouldRunESRGAN: (state, action: PayloadAction) => { + state.shouldRunESRGAN = action.payload; + }, + setShouldLoopback: (state, action: PayloadAction) => { + state.shouldLoopback = action.payload; + }, + }, +}); + +export const { + resetPostprocessingState, + setCodeformerFidelity, + setFacetoolStrength, + setFacetoolType, + setHiresFix, + setHiresStrength, + setShouldLoopback, + setShouldRunESRGAN, + setShouldRunFacetool, + setUpscalingLevel, + setUpscalingDenoising, + setUpscalingStrength, +} = postprocessingSlice.actions; + +export default postprocessingSlice.reducer; diff --git a/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistDenylist.ts b/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistDenylist.ts index 947a136964..a6ba084c2e 100644 --- a/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistDenylist.ts @@ -4,6 +4,7 @@ import { PostprocessingState } from './postprocessingSlice'; * Postprocessing slice persist denylist */ const itemsToDenylist: (keyof PostprocessingState)[] = []; +export const postprocessingPersistDenylist: (keyof PostprocessingState)[] = []; export const postprocessingDenylist = itemsToDenylist.map( (denylistItem) => `postprocessing.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/parameters/store/postprocessingSlice.ts b/invokeai/frontend/web/src/features/parameters/store/postprocessingSlice.ts index 60991d3673..399a474008 100644 --- a/invokeai/frontend/web/src/features/parameters/store/postprocessingSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/postprocessingSlice.ts @@ -20,7 +20,7 @@ export interface PostprocessingState { upscalingStrength: number; } -const initialPostprocessingState: PostprocessingState = { +export const initialPostprocessingState: PostprocessingState = { codeformerFidelity: 0.75, facetoolStrength: 0.75, facetoolType: 'gfpgan', @@ -34,11 +34,9 @@ const initialPostprocessingState: PostprocessingState = { upscalingStrength: 0.75, }; -const initialState: PostprocessingState = initialPostprocessingState; - export const postprocessingSlice = createSlice({ name: 'postprocessing', - initialState, + initialState: initialPostprocessingState, reducers: { setFacetoolStrength: (state, action: PayloadAction) => { state.facetoolStrength = action.payload; diff --git a/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts b/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts new file mode 100644 index 0000000000..7b02647ebc --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/store/setAllParametersReducer.ts @@ -0,0 +1,61 @@ +import { Draft, PayloadAction } from '@reduxjs/toolkit'; +import { Image } from 'app/types/invokeai'; +import { GenerationState } from './generationSlice'; +import { ImageToImageInvocation } from 'services/api'; + +export const setAllParametersReducer = ( + state: Draft, + action: PayloadAction +) => { + const node = action.payload?.metadata.invokeai?.node; + + if (!node) { + return; + } + + if ( + node.type === 'txt2img' || + node.type === 'img2img' || + node.type === 'inpaint' + ) { + const { cfg_scale, height, model, prompt, scheduler, seed, steps, width } = + node; + + if (cfg_scale !== undefined) { + state.cfgScale = Number(cfg_scale); + } + if (height !== undefined) { + state.height = Number(height); + } + if (model !== undefined) { + state.model = String(model); + } + if (prompt !== undefined) { + state.prompt = String(prompt); + } + if (scheduler !== undefined) { + state.sampler = String(scheduler); + } + if (seed !== undefined) { + state.seed = Number(seed); + state.shouldRandomizeSeed = false; + } + if (steps !== undefined) { + state.steps = Number(steps); + } + if (width !== undefined) { + state.width = Number(width); + } + } + + if (node.type === 'img2img') { + const { fit, image } = node as ImageToImageInvocation; + + if (fit !== undefined) { + state.shouldFitToWidthHeight = Boolean(fit); + } + // if (image !== undefined) { + // state.initialImage = image; + // } + } +}; diff --git a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx index d0ad89ba36..e38fda2676 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx @@ -5,17 +5,16 @@ import { useTranslation } from 'react-i18next'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAISelect from 'common/components/IAISelect'; -import { - modelSelected, - selectedModelSelector, - selectModelsIds, -} from '../store/modelSlice'; +import { selectModelsById, selectModelsIds } from '../store/modelSlice'; import { RootState } from 'app/store/store'; +import { modelSelected } from 'features/parameters/store/generationSlice'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; const selector = createSelector( - [(state: RootState) => state], - (state) => { - const selectedModel = selectedModelSelector(state); + [(state: RootState) => state, generationSelector], + (state, generation) => { + // const selectedModel = selectedModelSelector(state); + const selectedModel = selectModelsById(state, generation.model); const allModelNames = selectModelsIds(state); return { allModelNames, diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index 0ca0b496fc..58e6684b04 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -23,22 +23,23 @@ import { setEnableImageDebugging, setShouldConfirmOnDelete, setShouldDisplayGuides, + shouldAntialiasProgressImageChanged, shouldLogToConsoleChanged, SystemState, } from 'features/system/store/systemSlice'; import { uiSelector } from 'features/ui/store/uiSelectors'; import { - setShouldAutoShowProgressImages, + setShouldShowProgressInViewer, setShouldUseCanvasBetaLayout, setShouldUseSliders, } from 'features/ui/store/uiSlice'; import { UIState } from 'features/ui/store/uiTypes'; import { isEqual } from 'lodash-es'; -import { persistor } from 'app/store/persistor'; import { ChangeEvent, cloneElement, ReactElement, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { VALID_LOG_LEVELS } from 'app/logging/useLogger'; import { LogLevelName } from 'roarr'; +import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants'; const selector = createSelector( [systemSelector, uiSelector], @@ -49,12 +50,13 @@ const selector = createSelector( enableImageDebugging, consoleLogLevel, shouldLogToConsole, + shouldAntialiasProgressImage, } = system; const { shouldUseCanvasBetaLayout, shouldUseSliders, - shouldAutoShowProgressImages, + shouldShowProgressInViewer, } = ui; return { @@ -63,9 +65,10 @@ const selector = createSelector( enableImageDebugging, shouldUseCanvasBetaLayout, shouldUseSliders, - shouldAutoShowProgressImages, + shouldShowProgressInViewer, consoleLogLevel, shouldLogToConsole, + shouldAntialiasProgressImage, }; }, { @@ -114,20 +117,24 @@ const SettingsModal = ({ children }: SettingsModalProps) => { enableImageDebugging, shouldUseCanvasBetaLayout, shouldUseSliders, - shouldAutoShowProgressImages, + shouldShowProgressInViewer, consoleLogLevel, shouldLogToConsole, + shouldAntialiasProgressImage, } = useAppSelector(selector); - /** - * Resets localstorage, then opens a secondary modal informing user to - * refresh their browser. - * */ const handleClickResetWebUI = useCallback(() => { - persistor.purge().then(() => { - onSettingsModalClose(); - onRefreshModalOpen(); + // Only remove our keys + Object.keys(window.localStorage).forEach((key) => { + if ( + LOCALSTORAGE_KEYS.includes(key) || + key.startsWith(LOCALSTORAGE_PREFIX) + ) { + localStorage.removeItem(key); + } }); + onSettingsModalClose(); + onRefreshModalOpen(); }, [onSettingsModalClose, onRefreshModalOpen]); const handleLogLevelChanged = useCallback( @@ -194,10 +201,19 @@ const SettingsModal = ({ children }: SettingsModalProps) => { } /> ) => - dispatch(setShouldAutoShowProgressImages(e.target.checked)) + dispatch(setShouldShowProgressInViewer(e.target.checked)) + } + /> + ) => + dispatch( + shouldAntialiasProgressImageChanged(e.target.checked) + ) } /> diff --git a/invokeai/frontend/web/src/features/system/components/SiteHeader.tsx b/invokeai/frontend/web/src/features/system/components/SiteHeader.tsx index 350e1291aa..9b4159ecb6 100644 --- a/invokeai/frontend/web/src/features/system/components/SiteHeader.tsx +++ b/invokeai/frontend/web/src/features/system/components/SiteHeader.tsx @@ -1,6 +1,5 @@ import { Flex, Grid } from '@chakra-ui/react'; import { memo, useState } from 'react'; -import ModelSelect from './ModelSelect'; import StatusIndicator from './StatusIndicator'; import InvokeAILogoComponent from './InvokeAILogoComponent'; diff --git a/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx b/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx index 48418c9f19..cd0a4eacc3 100644 --- a/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx +++ b/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx @@ -9,6 +9,7 @@ import { AnimatePresence, motion } from 'framer-motion'; import { useMemo, useRef } from 'react'; import { FaCircle } from 'react-icons/fa'; import { useHoverDirty } from 'react-use'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; const statusIndicatorSelector = createSelector( systemSelector, @@ -31,9 +32,7 @@ const statusIndicatorSelector = createSelector( currentStatusHasSteps, }; }, - { - memoizeOptions: { resultEqualityCheck: isEqual }, - } + defaultSelectorOptions ); const StatusIndicator = () => { @@ -43,7 +42,6 @@ const StatusIndicator = () => { currentIteration, totalIterations, statusTranslationKey, - currentStatusHasSteps, } = useAppSelector(statusIndicatorSelector); const { t } = useTranslation(); const ref = useRef(null); diff --git a/invokeai/frontend/web/src/features/system/hooks/useIsApplicationReady.ts b/invokeai/frontend/web/src/features/system/hooks/useIsApplicationReady.ts index cecd739278..6e62c3642b 100644 --- a/invokeai/frontend/web/src/features/system/hooks/useIsApplicationReady.ts +++ b/invokeai/frontend/web/src/features/system/hooks/useIsApplicationReady.ts @@ -1,5 +1,4 @@ import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { useMemo } from 'react'; import { configSelector } from '../store/configSelectors'; diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index d668a59574..b773692908 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -3,7 +3,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { AppConfig, PartialAppConfig } from 'app/types/invokeai'; import { merge } from 'lodash-es'; -const initialConfigState: AppConfig = { +export const initialConfigState: AppConfig = { shouldTransformUrls: false, shouldFetchImages: false, disabledTabs: [], diff --git a/invokeai/frontend/web/src/features/system/store/modelSelectors.ts b/invokeai/frontend/web/src/features/system/store/modelSelectors.ts index 8b502fb3b6..f857bc85bc 100644 --- a/invokeai/frontend/web/src/features/system/store/modelSelectors.ts +++ b/invokeai/frontend/web/src/features/system/store/modelSelectors.ts @@ -1,5 +1,3 @@ -import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; -import { reduce } from 'lodash-es'; export const modelSelector = (state: RootState) => state.models; diff --git a/invokeai/frontend/web/src/features/system/store/modelSlice.ts b/invokeai/frontend/web/src/features/system/store/modelSlice.ts index cb1cf05328..9c7194672d 100644 --- a/invokeai/frontend/web/src/features/system/store/modelSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/modelSlice.ts @@ -1,7 +1,6 @@ -import { createEntityAdapter, PayloadAction } from '@reduxjs/toolkit'; +import { createEntityAdapter } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; -import { keys, sample } from 'lodash-es'; import { CkptModelInfo, DiffusersModelInfo } from 'services/api'; import { receivedModels } from 'services/thunks/model'; @@ -27,12 +26,13 @@ export type ModelsState = typeof initialModelsState; export const modelsSlice = createSlice({ name: 'models', - initialState: initialModelsState, + initialState: modelsAdapter.getInitialState(), + // initialState: initialModelsState, reducers: { modelAdded: modelsAdapter.upsertOne, - modelSelected: (state, action: PayloadAction) => { - state.selectedModelName = action.payload; - }, + // modelSelected: (state, action: PayloadAction) => { + // state.selectedModelName = action.payload; + // }, }, extraReducers(builder) { /** @@ -44,28 +44,28 @@ export const modelsSlice = createSlice({ // If the current selected model is `''` or isn't actually in the list of models, // choose a random model - if ( - !state.selectedModelName || - !keys(models).includes(state.selectedModelName) - ) { - const randomModel = sample(models); + // if ( + // !state.selectedModelName || + // !keys(models).includes(state.selectedModelName) + // ) { + // const randomModel = sample(models); - if (randomModel) { - state.selectedModelName = randomModel.name; - } else { - state.selectedModelName = ''; - } - } + // if (randomModel) { + // state.selectedModelName = randomModel.name; + // } else { + // state.selectedModelName = ''; + // } + // } }); }, }); -export const selectedModelSelector = (state: RootState) => { - const { selectedModelName } = state.models; - const selectedModel = selectModelsById(state, selectedModelName); +// export const selectedModelSelector = (state: RootState) => { +// const { selectedModelName } = state.models; +// const selectedModel = selectModelsById(state, selectedModelName); - return selectedModel ?? null; -}; +// return selectedModel ?? null; +// }; export const { selectAll: selectModelsAll, @@ -75,6 +75,9 @@ export const { selectTotal: selectModelsTotal, } = modelsAdapter.getSelectors((state) => state.models); -export const { modelAdded, modelSelected } = modelsSlice.actions; +export const { + modelAdded, + // modelSelected +} = modelsSlice.actions; export default modelsSlice.reducer; diff --git a/invokeai/frontend/web/src/features/system/store/modelsPersistDenylist.ts b/invokeai/frontend/web/src/features/system/store/modelsPersistDenylist.ts index f374948e39..5b36a3f196 100644 --- a/invokeai/frontend/web/src/features/system/store/modelsPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/system/store/modelsPersistDenylist.ts @@ -4,6 +4,7 @@ import { ModelsState } from './modelSlice'; * Models slice persist denylist */ const itemsToDenylist: (keyof ModelsState)[] = ['entities', 'ids']; +export const modelsPersistDenylist: (keyof ModelsState)[] = ['entities', 'ids']; export const modelsDenylist = itemsToDenylist.map( (denylistItem) => `models.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts b/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts index 8a4d381775..70284e831a 100644 --- a/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts @@ -21,6 +21,25 @@ const itemsToDenylist: (keyof SystemState)[] = [ 'wereModelsReceived', 'wasSchemaParsed', ]; +export const systemPersistDenylist: (keyof SystemState)[] = [ + 'currentIteration', + 'currentStatus', + 'currentStep', + 'isCancelable', + 'isConnected', + 'isESRGANAvailable', + 'isGFPGANAvailable', + 'isProcessing', + 'socketId', + 'totalIterations', + 'totalSteps', + 'openModel', + 'isCancelScheduled', + 'progressImage', + 'wereModelsReceived', + 'wasSchemaParsed', + 'isPersisted', +]; export const systemDenylist = itemsToDenylist.map( (denylistItem) => `system.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index a8c8da9bfb..1aeb2a1939 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -15,7 +15,6 @@ import { } from 'services/events/actions'; import { ProgressImage } from 'services/events/types'; -import { initialImageSelected } from 'features/parameters/store/generationSlice'; import { makeToast } from '../hooks/useToastWatcher'; import { sessionCanceled, sessionInvoked } from 'services/thunks/session'; import { receivedModels } from 'services/thunks/model'; @@ -24,9 +23,12 @@ import { LogLevelName } from 'roarr'; import { InvokeLogLevel } from 'app/logging/useLogger'; import { TFuncKey } from 'i18next'; import { t } from 'i18next'; +import { userInvoked } from 'app/store/actions'; export type CancelStrategy = 'immediate' | 'scheduled'; +export type InfillMethod = 'tile' | 'patchmatch'; + export interface SystemState { isGFPGANAvailable: boolean; isESRGANAvailable: boolean; @@ -79,10 +81,19 @@ export interface SystemState { consoleLogLevel: InvokeLogLevel; shouldLogToConsole: boolean; statusTranslationKey: TFuncKey; + /** + * When a session is canceled, its ID is stored here until a new session is created. + */ canceledSession: string; + /** + * TODO: get this from backend + */ + infillMethods: InfillMethod[]; + isPersisted: boolean; + shouldAntialiasProgressImage: boolean; } -const initialSystemState: SystemState = { +export const initialSystemState: SystemState = { isConnected: false, isProcessing: false, shouldDisplayGuides: true, @@ -101,6 +112,7 @@ const initialSystemState: SystemState = { foundModels: null, openModel: null, progressImage: null, + shouldAntialiasProgressImage: false, sessionId: null, cancelType: 'immediate', isCancelScheduled: false, @@ -111,6 +123,8 @@ const initialSystemState: SystemState = { shouldLogToConsole: true, statusTranslationKey: 'common.statusDisconnected', canceledSession: '', + infillMethods: ['tile', 'patchmatch'], + isPersisted: false, }; export const systemSlice = createSlice({ @@ -249,6 +263,15 @@ export const systemSlice = createSlice({ shouldLogToConsoleChanged: (state, action: PayloadAction) => { state.shouldLogToConsole = action.payload; }, + shouldAntialiasProgressImageChanged: ( + state, + action: PayloadAction + ) => { + state.shouldAntialiasProgressImage = action.payload; + }, + isPersistedChanged: (state, action: PayloadAction) => { + state.isPersisted = action.payload; + }, }, extraReducers(builder) { /** @@ -269,8 +292,7 @@ export const systemSlice = createSlice({ /** * Socket Connected */ - builder.addCase(socketConnected, (state, action) => { - const { timestamp } = action.payload; + builder.addCase(socketConnected, (state) => { state.isConnected = true; state.isCancelable = true; state.isProcessing = false; @@ -285,9 +307,7 @@ export const systemSlice = createSlice({ /** * Socket Disconnected */ - builder.addCase(socketDisconnected, (state, action) => { - const { timestamp } = action.payload; - + builder.addCase(socketDisconnected, (state) => { state.isConnected = false; state.isProcessing = false; state.isCancelable = true; @@ -302,7 +322,7 @@ export const systemSlice = createSlice({ /** * Invocation Started */ - builder.addCase(invocationStarted, (state, action) => { + builder.addCase(invocationStarted, (state) => { state.isCancelable = true; state.isProcessing = true; state.currentStatusHasSteps = false; @@ -317,14 +337,7 @@ export const systemSlice = createSlice({ * Generator Progress */ builder.addCase(generatorProgress, (state, action) => { - const { - step, - total_steps, - progress_image, - node, - source_node_id, - graph_execution_state_id, - } = action.payload.data; + const { step, total_steps, progress_image } = action.payload.data; state.isProcessing = true; state.isCancelable = true; @@ -341,7 +354,7 @@ export const systemSlice = createSlice({ * Invocation Complete */ builder.addCase(invocationComplete, (state, action) => { - const { data, timestamp } = action.payload; + const { data } = action.payload; // state.currentIteration = 0; // state.totalIterations = 0; @@ -349,6 +362,7 @@ export const systemSlice = createSlice({ state.currentStep = 0; state.totalSteps = 0; state.statusTranslationKey = 'common.statusProcessingComplete'; + state.progressImage = null; if (state.canceledSession === data.graph_execution_state_id) { state.isProcessing = false; @@ -359,9 +373,7 @@ export const systemSlice = createSlice({ /** * Invocation Error */ - builder.addCase(invocationError, (state, action) => { - const { data, timestamp } = action.payload; - + builder.addCase(invocationError, (state) => { state.isProcessing = false; state.isCancelable = true; // state.currentIteration = 0; @@ -370,6 +382,7 @@ export const systemSlice = createSlice({ state.currentStep = 0; state.totalSteps = 0; state.statusTranslationKey = 'common.statusError'; + state.progressImage = null; state.toastQueue.push( makeToast({ title: t('toast.serverError'), status: 'error' }) @@ -380,7 +393,10 @@ export const systemSlice = createSlice({ * Session Invoked - PENDING */ - builder.addCase(sessionInvoked.pending, (state) => { + builder.addCase(userInvoked, (state) => { + state.isProcessing = true; + state.isCancelable = true; + state.currentStatusHasSteps = false; state.statusTranslationKey = 'common.statusPreparing'; }); @@ -395,8 +411,6 @@ export const systemSlice = createSlice({ * Session Canceled */ builder.addCase(sessionCanceled.fulfilled, (state, action) => { - const { timestamp } = action.payload; - state.canceledSession = action.meta.arg.sessionId; state.isProcessing = false; state.isCancelable = false; @@ -413,9 +427,7 @@ export const systemSlice = createSlice({ /** * Session Canceled */ - builder.addCase(graphExecutionStateComplete, (state, action) => { - const { timestamp } = action.payload; - + builder.addCase(graphExecutionStateComplete, (state) => { state.isProcessing = false; state.isCancelable = false; state.isCancelScheduled = false; @@ -424,13 +436,6 @@ export const systemSlice = createSlice({ state.statusTranslationKey = 'common.statusConnected'; }); - /** - * Initial Image Selected - */ - builder.addCase(initialImageSelected, (state) => { - state.toastQueue.push(makeToast(t('toast.sentToImageToImage'))); - }); - /** * Received available models from the backend */ @@ -473,6 +478,8 @@ export const { subscribedNodeIdsSet, consoleLogLevelChanged, shouldLogToConsoleChanged, + isPersistedChanged, + shouldAntialiasProgressImageChanged, } = systemSlice.actions; export default systemSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx index 83a699aca0..bcbcc1cefc 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx @@ -16,7 +16,7 @@ const floatingGalleryButtonSelector = createSelector( return { shouldPinGallery, - shouldShowGalleryButton: !shouldPinGallery || !shouldShowGallery, + shouldShowGalleryButton: !shouldShowGallery, }; }, { memoizeOptions: { resultEqualityCheck: isEqual } } diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx index 3055216b66..95ac1257c0 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx @@ -39,8 +39,8 @@ export const floatingParametersPanelButtonSelector = createSelector( const shouldShowParametersPanelButton = !canvasBetaLayoutCheck && - (!shouldPinParametersPanel || !shouldShowParametersPanel) && - ['generate', 'unifiedCanvas'].includes(activeTabName); + !shouldShowParametersPanel && + ['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName); return { shouldPinParametersPanel, @@ -65,7 +65,11 @@ const FloatingParametersPanelButtons = () => { shouldPinParametersPanel && dispatch(requestCanvasRescale()); }; - return shouldShowParametersPanelButton ? ( + if (!shouldShowParametersPanelButton) { + return null; + } + + return ( { > )} - ) : null; + ); }; export default memo(FloatingParametersPanelButtons); diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 0c65a99293..d2bd4f6d51 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -1,5 +1,4 @@ import { - ChakraProps, Icon, Tab, TabList, @@ -14,42 +13,55 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import { InvokeTabName } from 'features/ui/store/tabMap'; import { setActiveTab, togglePanels } from 'features/ui/store/uiSlice'; -import { memo, ReactNode, useMemo } from 'react'; +import { memo, ReactNode, useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { MdDeviceHub, MdGridOn } from 'react-icons/md'; -import { activeTabIndexSelector } from '../store/uiSelectors'; -import UnifiedCanvasWorkarea from 'features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasWorkarea'; +import { GoTextSize } from 'react-icons/go'; +import { + activeTabIndexSelector, + activeTabNameSelector, +} from '../store/uiSelectors'; import { useTranslation } from 'react-i18next'; import { ResourceKey } from 'i18next'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; -import NodeEditor from 'features/nodes/components/NodeEditor'; -import GenerateWorkspace from './tabs/Generate/GenerateWorkspace'; import { createSelector } from '@reduxjs/toolkit'; -import { BsLightningChargeFill } from 'react-icons/bs'; import { configSelector } from 'features/system/store/configSelectors'; -import { isEqual } from 'lodash'; +import { isEqual } from 'lodash-es'; +import { Panel, PanelGroup } from 'react-resizable-panels'; +import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent'; +import TextToImageTab from './tabs/TextToImage/TextToImageTab'; +import UnifiedCanvasTab from './tabs/UnifiedCanvas/UnifiedCanvasTab'; +import NodesTab from './tabs/Nodes/NodesTab'; +import { FaImage } from 'react-icons/fa'; +import ResizeHandle from './tabs/ResizeHandle'; +import ImageTab from './tabs/ImageToImage/ImageToImageTab'; export interface InvokeTabInfo { id: InvokeTabName; icon: ReactNode; - workarea: ReactNode; + content: ReactNode; } const tabs: InvokeTabInfo[] = [ { - id: 'generate', - icon: , - workarea: , + id: 'txt2img', + icon: , + content: , + }, + { + id: 'img2img', + icon: , + content: , }, { id: 'unifiedCanvas', icon: , - workarea: , + content: , }, { id: 'nodes', icon: , - workarea: , + content: , }, ]; @@ -67,31 +79,19 @@ const enabledTabsSelector = createSelector( const InvokeTabs = () => { const activeTab = useAppSelector(activeTabIndexSelector); + const activeTabName = useAppSelector(activeTabNameSelector); const enabledTabs = useAppSelector(enabledTabsSelector); const isLightBoxOpen = useAppSelector( (state: RootState) => state.lightbox.isLightboxOpen ); - const { shouldPinGallery, shouldPinParametersPanel } = useAppSelector( - (state: RootState) => state.ui - ); + const { shouldPinGallery, shouldPinParametersPanel, shouldShowGallery } = + useAppSelector((state: RootState) => state.ui); const { t } = useTranslation(); const dispatch = useAppDispatch(); - useHotkeys('1', () => { - dispatch(setActiveTab('generate')); - }); - - useHotkeys('2', () => { - dispatch(setActiveTab('unifiedCanvas')); - }); - - useHotkeys('3', () => { - dispatch(setActiveTab('nodes')); - }); - // Lightbox Hotkey useHotkeys( 'z', @@ -111,6 +111,12 @@ const InvokeTabs = () => { [shouldPinGallery, shouldPinParametersPanel] ); + const handleResizeGallery = useCallback(() => { + if (activeTabName === 'unifiedCanvas') { + dispatch(requestCanvasRescale()); + } + }, [dispatch, activeTabName]); + const tabs = useMemo( () => enabledTabs.map((tab) => ( @@ -133,9 +139,7 @@ const InvokeTabs = () => { const tabPanels = useMemo( () => - enabledTabs.map((tab) => ( - {tab.workarea} - )), + enabledTabs.map((tab) => {tab.content}), [enabledTabs] ); @@ -159,7 +163,32 @@ const InvokeTabs = () => { > {tabs} - {tabPanels} + + + + {tabPanels} + + + {shouldPinGallery && shouldShowGallery && ( + <> + + + + + > + )} + ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx deleted file mode 100644 index f59028c8ca..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Box, BoxProps, Grid, GridItem } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { initialImageSelected } from 'features/parameters/store/generationSlice'; -import { - activeTabNameSelector, - uiSelector, -} from 'features/ui/store/uiSelectors'; -import { DragEvent, ReactNode } from 'react'; - -import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; -import useGetImageByUuid from 'features/gallery/hooks/useGetImageByUuid'; -import { isEqual } from 'lodash-es'; -import { APP_CONTENT_HEIGHT } from 'theme/util/constants'; -import ParametersPanel from './ParametersPanel'; - -const workareaSelector = createSelector( - [uiSelector, activeTabNameSelector], - (ui, activeTabName) => { - const { shouldPinParametersPanel } = ui; - return { - shouldPinParametersPanel, - activeTabName, - }; - }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } -); - -type InvokeWorkareaProps = BoxProps & { - parametersPanelContent: ReactNode; - children: ReactNode; -}; - -const InvokeWorkarea = (props: InvokeWorkareaProps) => { - const { parametersPanelContent, children, ...rest } = props; - const dispatch = useAppDispatch(); - const { activeTabName } = useAppSelector(workareaSelector); - - const getImageByUuid = useGetImageByUuid(); - - const handleDrop = (e: DragEvent) => { - const uuid = e.dataTransfer.getData('invokeai/imageUuid'); - const image = getImageByUuid(uuid); - if (!image) return; - if (activeTabName === 'unifiedCanvas') { - dispatch(setInitialCanvasImage(image)); - } - }; - - return ( - - {parametersPanelContent} - - - {children} - - - - ); -}; - -export default InvokeWorkarea; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersDrawer.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersDrawer.tsx new file mode 100644 index 0000000000..7a969bc396 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/ParametersDrawer.tsx @@ -0,0 +1,98 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { memo, useMemo } from 'react'; +import { Box, Flex } from '@chakra-ui/react'; +import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent'; +import OverlayScrollable from './common/OverlayScrollable'; +import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; +import { + activeTabNameSelector, + uiSelector, +} from 'features/ui/store/uiSelectors'; +import { setShouldShowParametersPanel } from 'features/ui/store/uiSlice'; +import ResizableDrawer from './common/ResizableDrawer/ResizableDrawer'; +import PinParametersPanelButton from './PinParametersPanelButton'; +import TextToImageTabParameters from './tabs/TextToImage/TextToImageTabParameters'; +import ImageToImageTabParameters from './tabs/ImageToImage/ImageToImageTabParameters'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import UnifiedCanvasParameters from './tabs/UnifiedCanvas/UnifiedCanvasParameters'; + +const selector = createSelector( + [uiSelector, activeTabNameSelector, lightboxSelector], + (ui, activeTabName, lightbox) => { + const { + shouldPinParametersPanel, + shouldShowParametersPanel, + shouldShowImageParameters, + } = ui; + + const { isLightboxOpen } = lightbox; + + return { + activeTabName, + shouldPinParametersPanel, + shouldShowParametersPanel, + shouldShowImageParameters, + }; + }, + defaultSelectorOptions +); + +const ParametersDrawer = () => { + const dispatch = useAppDispatch(); + const { shouldPinParametersPanel, shouldShowParametersPanel, activeTabName } = + useAppSelector(selector); + + const handleClosePanel = () => { + dispatch(setShouldShowParametersPanel(false)); + }; + + const drawerContent = useMemo(() => { + if (activeTabName === 'txt2img') { + return ; + } + + if (activeTabName === 'img2img') { + return ; + } + + if (activeTabName === 'unifiedCanvas') { + return ; + } + + return null; + }, [activeTabName]); + + if (shouldPinParametersPanel) { + return null; + } + + return ( + + + + + + + + {drawerContent} + + + + ); +}; + +export default memo(ParametersDrawer); diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx deleted file mode 100644 index b36199e263..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Flex } from '@chakra-ui/react'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; - -import { memo, ReactNode } from 'react'; - -import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; -import ResizableDrawer from 'features/ui/components/common/ResizableDrawer/ResizableDrawer'; -import { - setShouldShowParametersPanel, - toggleParametersPanel, - togglePinParametersPanel, -} from 'features/ui/store/uiSlice'; -import { useHotkeys } from 'react-hotkeys-hook'; -import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent'; -import Scrollable from './common/Scrollable'; -import PinParametersPanelButton from './PinParametersPanelButton'; -import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; -import { createSelector } from '@reduxjs/toolkit'; -import { activeTabNameSelector, uiSelector } from '../store/uiSelectors'; -import { isEqual } from 'lodash-es'; -import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; -import useResolution from 'common/hooks/useResolution'; - -const parametersPanelSelector = createSelector( - [uiSelector, activeTabNameSelector, lightboxSelector], - (ui, activeTabName, lightbox) => { - const { shouldPinParametersPanel, shouldShowParametersPanel } = ui; - const { isLightboxOpen } = lightbox; - - return { - shouldPinParametersPanel, - shouldShowParametersPanel, - isResizable: activeTabName !== 'unifiedCanvas', - isLightboxOpen, - }; - }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } -); - -type ParametersPanelProps = { - children: ReactNode; -}; - -const ParametersPanel = ({ children }: ParametersPanelProps) => { - const dispatch = useAppDispatch(); - - const { - shouldPinParametersPanel, - shouldShowParametersPanel, - isResizable, - isLightboxOpen, - } = useAppSelector(parametersPanelSelector); - - const closeParametersPanel = () => { - dispatch(setShouldShowParametersPanel(false)); - }; - - const resolution = useResolution(); - - useHotkeys( - 'o', - () => { - dispatch(toggleParametersPanel()); - shouldPinParametersPanel && dispatch(requestCanvasRescale()); - }, - { enabled: () => !isLightboxOpen }, - [shouldPinParametersPanel, isLightboxOpen] - ); - - useHotkeys( - 'esc', - () => { - dispatch(setShouldShowParametersPanel(false)); - }, - { - enabled: () => !shouldPinParametersPanel, - preventDefault: true, - }, - [shouldPinParametersPanel] - ); - - useHotkeys( - 'shift+o', - () => { - dispatch(togglePinParametersPanel()); - dispatch(requestCanvasRescale()); - }, - [] - ); - - const parametersPanelContent = () => { - return ( - - {!shouldPinParametersPanel && ( - - - {resolution == 'desktop' && } - - )} - {children} - {shouldPinParametersPanel && resolution == 'desktop' && ( - - )} - - ); - }; - - const resizableParametersPanelContent = () => { - return ( - - {parametersPanelContent()} - - ); - }; - - const renderParametersPanel = () => { - if (['mobile', 'tablet'].includes(resolution)) - return parametersPanelContent(); - return resizableParametersPanelContent(); - }; - - return renderParametersPanel(); -}; - -export default memo(ParametersPanel); diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPinnedWrapper.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPinnedWrapper.tsx new file mode 100644 index 0000000000..407187294c --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPinnedWrapper.tsx @@ -0,0 +1,58 @@ +import { Box, Flex } from '@chakra-ui/react'; +import { PropsWithChildren, memo } from 'react'; +import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; +import OverlayScrollable from './common/OverlayScrollable'; +import PinParametersPanelButton from './PinParametersPanelButton'; +import { createSelector } from '@reduxjs/toolkit'; +import { uiSelector } from '../store/uiSelectors'; +import { useAppSelector } from 'app/store/storeHooks'; + +const selector = createSelector(uiSelector, (ui) => { + const { shouldPinParametersPanel, shouldShowParametersPanel } = ui; + + return { + shouldPinParametersPanel, + shouldShowParametersPanel, + }; +}); + +type ParametersPinnedWrapperProps = PropsWithChildren; + +const ParametersPinnedWrapper = (props: ParametersPinnedWrapperProps) => { + const { shouldPinParametersPanel, shouldShowParametersPanel } = + useAppSelector(selector); + + if (!(shouldPinParametersPanel && shouldShowParametersPanel)) { + return null; + } + + return ( + + + + {props.children} + + + + + ); +}; + +export default memo(ParametersPinnedWrapper); diff --git a/invokeai/frontend/web/src/features/ui/components/common/OverlayScrollable.tsx b/invokeai/frontend/web/src/features/ui/components/common/OverlayScrollable.tsx new file mode 100644 index 0000000000..71413fd01a --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/common/OverlayScrollable.tsx @@ -0,0 +1,24 @@ +import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; +import { PropsWithChildren, memo } from 'react'; + +const OverlayScrollable = (props: PropsWithChildren) => { + return ( + + {props.children} + + ); +}; + +export default memo(OverlayScrollable); diff --git a/invokeai/frontend/web/src/features/ui/components/common/ParametersSlide.tsx b/invokeai/frontend/web/src/features/ui/components/common/ParametersSlide.tsx deleted file mode 100644 index 3342a9338b..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/common/ParametersSlide.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { Box, Flex, useOutsideClick } from '@chakra-ui/react'; -import { Slide } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { uiSelector } from 'features/ui/store/uiSelectors'; -import { isEqual } from 'lodash-es'; -import { memo, PropsWithChildren, useRef } from 'react'; -import PinParametersPanelButton from 'features/ui/components/PinParametersPanelButton'; -import { - setShouldShowParametersPanel, - toggleParametersPanel, - togglePinParametersPanel, -} from 'features/ui/store/uiSlice'; -import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent'; -import Scrollable from 'features/ui/components/common/Scrollable'; -import { useLangDirection } from 'features/ui/hooks/useDirection'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; -import { generationSelector } from 'features/parameters/store/generationSelectors'; -import AnimatedImageToImagePanel from 'features/parameters/components/AnimatedImageToImagePanel'; - -const parametersSlideSelector = createSelector( - [uiSelector, generationSelector], - (ui, generation) => { - const { shouldPinParametersPanel, shouldShowParametersPanel } = ui; - const { isImageToImageEnabled } = generation; - - return { - shouldPinParametersPanel, - shouldShowParametersPanel, - isImageToImageEnabled, - }; - }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } -); - -type ParametersSlideProps = PropsWithChildren; - -const ParametersSlide = (props: ParametersSlideProps) => { - const dispatch = useAppDispatch(); - - const { - shouldShowParametersPanel, - isImageToImageEnabled, - shouldPinParametersPanel, - } = useAppSelector(parametersSlideSelector); - - const langDirection = useLangDirection(); - - const outsideClickRef = useRef(null); - - const closeParametersPanel = () => { - dispatch(setShouldShowParametersPanel(false)); - }; - - useOutsideClick({ - ref: outsideClickRef, - handler: () => { - closeParametersPanel(); - }, - enabled: shouldShowParametersPanel && !shouldPinParametersPanel, - }); - - useHotkeys( - 'o', - () => { - dispatch(toggleParametersPanel()); - shouldPinParametersPanel && dispatch(requestCanvasRescale()); - }, - [shouldPinParametersPanel] - ); - - useHotkeys( - 'esc', - () => { - dispatch(setShouldShowParametersPanel(false)); - }, - { - enabled: () => !shouldPinParametersPanel, - preventDefault: true, - }, - [shouldPinParametersPanel] - ); - - useHotkeys( - 'shift+o', - () => { - dispatch(togglePinParametersPanel()); - dispatch(requestCanvasRescale()); - }, - [] - ); - - return ( - - - - - - - - - {props.children} - - - - - - ); -}; - -export default memo(ParametersSlide); diff --git a/invokeai/frontend/web/src/features/ui/components/common/ResizableDrawer/ResizableDrawer.tsx b/invokeai/frontend/web/src/features/ui/components/common/ResizableDrawer/ResizableDrawer.tsx index cabe58ccf2..993dd45adb 100644 --- a/invokeai/frontend/web/src/features/ui/components/common/ResizableDrawer/ResizableDrawer.tsx +++ b/invokeai/frontend/web/src/features/ui/components/common/ResizableDrawer/ResizableDrawer.tsx @@ -20,13 +20,11 @@ import { getMinMaxDimensions, getSlideDirection, getStyles, - parseAndPadSize, } from './util'; type ResizableDrawerProps = ResizableProps & { children: ReactNode; isResizable: boolean; - isPinned: boolean; isOpen: boolean; onClose: () => void; direction?: SlideDirection; @@ -51,7 +49,6 @@ const ChakraResizeable = chakra(Resizable, { const ResizableDrawer = ({ direction = 'left', isResizable, - isPinned, isOpen, onClose, children, @@ -74,7 +71,7 @@ const ResizableDrawer = ({ () => initialWidth ?? minWidth ?? - (['left', 'right'].includes(direction) ? 500 : '100%'), + (['left', 'right'].includes(direction) ? 'auto' : '100%'), [initialWidth, minWidth, direction] ); @@ -82,7 +79,7 @@ const ResizableDrawer = ({ () => initialHeight ?? minHeight ?? - (['top', 'bottom'].includes(direction) ? 500 : '100%'), + (['top', 'bottom'].includes(direction) ? 'auto' : '100%'), [initialHeight, minHeight, direction] ); @@ -95,7 +92,7 @@ const ResizableDrawer = ({ handler: () => { onClose(); }, - enabled: isOpen && !isPinned, + enabled: isOpen, }); const handleEnables = useMemo( @@ -107,30 +104,21 @@ const ResizableDrawer = ({ () => getMinMaxDimensions({ direction, - minWidth: isResizable - ? parseAndPadSize(minWidth, 18) - : parseAndPadSize(minWidth), - maxWidth: isResizable - ? parseAndPadSize(maxWidth, 18) - : parseAndPadSize(maxWidth), - minHeight: isResizable - ? parseAndPadSize(minHeight, 18) - : parseAndPadSize(minHeight), - maxHeight: isResizable - ? parseAndPadSize(maxHeight, 18) - : parseAndPadSize(maxHeight), + minWidth, + maxWidth, + minHeight, + maxHeight, }), - [minWidth, maxWidth, minHeight, maxHeight, direction, isResizable] + [minWidth, maxWidth, minHeight, maxHeight, direction] ); const { containerStyles, handleStyles } = useMemo( () => getStyles({ - isPinned, isResizable, direction, }), - [isPinned, isResizable, direction] + [isResizable, direction] ); const slideDirection = useMemo( @@ -140,34 +128,21 @@ const ResizableDrawer = ({ useEffect(() => { if (['left', 'right'].includes(direction)) { - setHeight(isPinned ? '100%' : '100vh'); + setHeight('100vh'); + // setHeight(isPinned ? '100%' : '100vh'); } if (['top', 'bottom'].includes(direction)) { - setWidth(isPinned ? '100%' : '100vw'); + setWidth('100vw'); + // setWidth(isPinned ? '100%' : '100vw'); } - }, [isPinned, direction]); + }, [direction]); return ( { - if (!isResizable) { - return { containerStyles: {}, handleStyles: {} }; - } - - const handleWidth = isPinned ? HANDLE_WIDTH_PINNED : HANDLE_WIDTH_UNPINNED; + // if (!isResizable) { + // return { containerStyles: {}, handleStyles: {} }; + // } // Calculate the positioning offset of the handle hitbox so it is centered over the handle - const handleOffset = `calc((2 * ${HANDLE_INTERACT_PADDING} + ${handleWidth}) / -2)`; + const handleOffset = `calc((2 * ${HANDLE_INTERACT_PADDING} + ${HANDLE_WIDTH}) / -2)`; if (direction === 'top') { return { containerStyles: { - borderBottomWidth: handleWidth, + borderBottomWidth: HANDLE_WIDTH, paddingBottom: HANDLE_PADDING, }, - handleStyles: { - top: { - paddingTop: HANDLE_INTERACT_PADDING, - paddingBottom: HANDLE_INTERACT_PADDING, - bottom: handleOffset, - }, - }, + handleStyles: isResizable + ? { + top: { + paddingTop: HANDLE_INTERACT_PADDING, + paddingBottom: HANDLE_INTERACT_PADDING, + bottom: handleOffset, + }, + } + : {}, }; } if (direction === 'left') { return { containerStyles: { - borderInlineEndWidth: handleWidth, + borderInlineEndWidth: HANDLE_WIDTH, paddingInlineEnd: HANDLE_PADDING, }, - handleStyles: { - right: { - paddingInlineStart: HANDLE_INTERACT_PADDING, - paddingInlineEnd: HANDLE_INTERACT_PADDING, - insetInlineEnd: handleOffset, - }, - }, + handleStyles: isResizable + ? { + right: { + paddingInlineStart: HANDLE_INTERACT_PADDING, + paddingInlineEnd: HANDLE_INTERACT_PADDING, + insetInlineEnd: handleOffset, + }, + } + : {}, }; } if (direction === 'bottom') { return { containerStyles: { - borderTopWidth: handleWidth, + borderTopWidth: HANDLE_WIDTH, paddingTop: HANDLE_PADDING, }, - handleStyles: { - bottom: { - paddingTop: HANDLE_INTERACT_PADDING, - paddingBottom: HANDLE_INTERACT_PADDING, - top: handleOffset, - }, - }, + handleStyles: isResizable + ? { + bottom: { + paddingTop: HANDLE_INTERACT_PADDING, + paddingBottom: HANDLE_INTERACT_PADDING, + top: handleOffset, + }, + } + : {}, }; } if (direction === 'right') { return { containerStyles: { - borderInlineStartWidth: handleWidth, + borderInlineStartWidth: HANDLE_WIDTH, paddingInlineStart: HANDLE_PADDING, }, - handleStyles: { - left: { - paddingInlineStart: HANDLE_INTERACT_PADDING, - paddingInlineEnd: HANDLE_INTERACT_PADDING, - insetInlineStart: handleOffset, - }, - }, + handleStyles: isResizable + ? { + left: { + paddingInlineStart: HANDLE_INTERACT_PADDING, + paddingInlineEnd: HANDLE_INTERACT_PADDING, + insetInlineStart: handleOffset, + }, + } + : {}, }; } @@ -279,16 +281,3 @@ export const getSlideDirection = ( return 'left'; }; - -// Hack to correct the width of panels while pinned and unpinned, due to different padding in pinned vs unpinned -export const parseAndPadSize = (size?: number, padding?: number) => { - if (!size) { - return undefined; - } - - if (!padding) { - return size; - } - - return size + padding; -}; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateParameters.tsx deleted file mode 100644 index 5b56fa5b0c..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateParameters.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { - AspectRatio, - Box, - Flex, - Select, - Slider, - SliderFilledTrack, - SliderThumb, - SliderTrack, - Text, -} from '@chakra-ui/react'; -import { Feature } from 'app/features'; -import IAISlider from 'common/components/IAISlider'; -import IAISwitch from 'common/components/IAISwitch'; -import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings'; -import ImageToImageToggle from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle'; -import OutputSettings from 'features/parameters/components/AdvancedParameters/Output/OutputSettings'; -import SymmetrySettings from 'features/parameters/components/AdvancedParameters/Output/SymmetrySettings'; -import SymmetryToggle from 'features/parameters/components/AdvancedParameters/Output/SymmetryToggle'; -import RandomizeSeed from 'features/parameters/components/AdvancedParameters/Seed/RandomizeSeed'; -import SeedSettings from 'features/parameters/components/AdvancedParameters/Seed/SeedSettings'; -import GenerateVariationsToggle from 'features/parameters/components/AdvancedParameters/Variations/GenerateVariations'; -import VariationsSettings from 'features/parameters/components/AdvancedParameters/Variations/VariationsSettings'; -import DimensionsSettings from 'features/parameters/components/ImageDimensions/DimensionsSettings'; -import MainSettings from 'features/parameters/components/MainParameters/MainSettings'; -import ParametersAccordion, { - ParametersAccordionItems, -} from 'features/parameters/components/ParametersAccordion'; -import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; -import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput'; -import PromptInput from 'features/parameters/components/PromptInput/PromptInput'; -import { findIndex } from 'lodash-es'; -import { memo, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; - -const GenerateParameters = () => { - const { t } = useTranslation(); - - const generateAccordionItems: ParametersAccordionItems = useMemo( - () => ({ - // general: { - // name: 'general', - // header: `${t('parameters.general')}`, - // feature: undefined, - // content: , - // }, - seed: { - name: 'seed', - header: `${t('parameters.seed')}`, - feature: Feature.SEED, - content: , - additionalHeaderComponents: , - }, - // imageToImage: { - // name: 'imageToImage', - // header: `${t('parameters.imageToImage')}`, - // feature: undefined, - // content: , - // additionalHeaderComponents: , - // }, - variations: { - name: 'variations', - header: `${t('parameters.variations')}`, - feature: Feature.VARIATIONS, - content: , - additionalHeaderComponents: , - }, - symmetry: { - name: 'symmetry', - header: `${t('parameters.symmetry')}`, - content: , - additionalHeaderComponents: , - }, - other: { - name: 'other', - header: `${t('parameters.otherOptions')}`, - feature: Feature.OTHER, - content: , - }, - }), - [t] - ); - - return ( - - - - - - - - - - - ); -}; - -export default memo(GenerateParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateWorkspace.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateWorkspace.tsx deleted file mode 100644 index df201af6ac..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateWorkspace.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Box, Flex } from '@chakra-ui/react'; -import { useAppSelector } from 'app/store/storeHooks'; -import { memo } from 'react'; -import GenerateContent from './GenerateContent'; -import GenerateParameters from './GenerateParameters'; -import PinParametersPanelButton from '../../PinParametersPanelButton'; -import { RootState } from 'app/store/store'; -import Scrollable from '../../common/Scrollable'; -import ParametersSlide from '../../common/ParametersSlide'; -import AnimatedImageToImagePanel from 'features/parameters/components/AnimatedImageToImagePanel'; - -const GenerateWorkspace = () => { - const shouldPinParametersPanel = useAppSelector( - (state: RootState) => state.ui.shouldPinParametersPanel - ); - - return ( - - {shouldPinParametersPanel ? ( - - - - - - - - - - ) : ( - - - - )} - - - ); -}; - -export default memo(GenerateWorkspace); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTab.tsx new file mode 100644 index 0000000000..cbd261f455 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTab.tsx @@ -0,0 +1,64 @@ +import { Box, Flex } from '@chakra-ui/react'; +import { memo, useCallback, useRef } from 'react'; +import { Panel, PanelGroup } from 'react-resizable-panels'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; +import ResizeHandle from '../ResizeHandle'; +import ImageToImageTabParameters from './ImageToImageTabParameters'; +import TextToImageTabMain from '../TextToImage/TextToImageTabMain'; +import { ImperativePanelGroupHandle } from 'react-resizable-panels'; +import ParametersPinnedWrapper from '../../ParametersPinnedWrapper'; +import InitialImageDisplay from 'features/parameters/components/Parameters/ImageToImage/InitialImageDisplay'; + +const ImageToImageTab = () => { + const dispatch = useAppDispatch(); + const panelGroupRef = useRef(null); + + const handleDoubleClickHandle = useCallback(() => { + if (!panelGroupRef.current) { + return; + } + + panelGroupRef.current.setLayout([50, 50]); + }, []); + + return ( + + + + + + + + + + + { + dispatch(requestCanvasRescale()); + }} + > + + + + + + ); +}; + +export default memo(ImageToImageTab); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabCoreParameters.tsx new file mode 100644 index 0000000000..5d85230140 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabCoreParameters.tsx @@ -0,0 +1,85 @@ +import { memo } from 'react'; +import { Box, Flex } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { uiSelector } from 'features/ui/store/uiSelectors'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations'; +import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps'; +import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; +import ParamWidth from 'features/parameters/components/Parameters/Core/ParamWidth'; +import ParamHeight from 'features/parameters/components/Parameters/Core/ParamHeight'; +import ParamSampler from 'features/parameters/components/Parameters/Core/ParamSampler'; +import ModelSelect from 'features/system/components/ModelSelect'; +import ImageToImageStrength from 'features/parameters/components/Parameters/ImageToImage/ImageToImageStrength'; +import ImageToImageFit from 'features/parameters/components/Parameters/ImageToImage/ImageToImageFit'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; + +const selector = createSelector( + [uiSelector, generationSelector], + (ui, generation) => { + const { shouldUseSliders } = ui; + const { shouldFitToWidthHeight } = generation; + + return { shouldUseSliders, shouldFitToWidthHeight }; + }, + defaultSelectorOptions +); + +const ImageToImageTabCoreParameters = () => { + const { shouldUseSliders, shouldFitToWidthHeight } = useAppSelector(selector); + + return ( + + {shouldUseSliders ? ( + + + + + + + + + + + + + + + + + + ) : ( + + + + + + + + + + + + + + + + + + + + )} + + ); +}; + +export default memo(ImageToImageTabCoreParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx new file mode 100644 index 0000000000..3b3daeaa4c --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx @@ -0,0 +1,28 @@ +import { memo } from 'react'; +import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; +import ParamPositiveConditioning from 'features/parameters/components/Parameters/Core/ParamPositiveConditioning'; +import ParamNegativeConditioning from 'features/parameters/components/Parameters/Core/ParamNegativeConditioning'; +import ParamSeedCollapse from 'features/parameters/components/Parameters/Seed/ParamSeedCollapse'; +import ParamVariationCollapse from 'features/parameters/components/Parameters/Variations/ParamVariationCollapse'; +import ParamNoiseCollapse from 'features/parameters/components/Parameters/Noise/ParamNoiseCollapse'; +import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Symmetry/ParamSymmetryCollapse'; +import ParamSeamlessCollapse from 'features/parameters/components/Parameters/Seamless/ParamSeamlessCollapse'; +import ImageToImageTabCoreParameters from './ImageToImageTabCoreParameters'; + +const ImageToImageTabParameters = () => { + return ( + <> + + + + + + + + + + > + ); +}; + +export default memo(ImageToImageTabParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/Nodes/NodesTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Nodes/NodesTab.tsx new file mode 100644 index 0000000000..aff0a7ce07 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/Nodes/NodesTab.tsx @@ -0,0 +1,8 @@ +import { memo } from 'react'; +import NodeEditor from 'features/nodes/components/NodeEditor'; + +const NodesTab = () => { + return ; +}; + +export default memo(NodesTab); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ResizeHandle.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ResizeHandle.tsx new file mode 100644 index 0000000000..d53a4d4fef --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ResizeHandle.tsx @@ -0,0 +1,47 @@ +import { Box, Flex, FlexProps } from '@chakra-ui/react'; +import { memo } from 'react'; +import { PanelResizeHandle } from 'react-resizable-panels'; + +type ResizeHandleProps = FlexProps & { + direction?: 'horizontal' | 'vertical'; +}; + +const ResizeHandle = (props: ResizeHandleProps) => { + const { direction = 'horizontal', ...rest } = props; + + if (direction === 'horizontal') { + return ( + + + + + + ); + } + + return ( + + + + + + ); +}; + +export default memo(ResizeHandle); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTab.tsx new file mode 100644 index 0000000000..87e77cc3ba --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTab.tsx @@ -0,0 +1,18 @@ +import { Flex } from '@chakra-ui/react'; +import { memo } from 'react'; +import TextToImageTabMain from './TextToImageTabMain'; +import TextToImageTabParameters from './TextToImageTabParameters'; +import ParametersPinnedWrapper from '../../ParametersPinnedWrapper'; + +const TextToImageTab = () => { + return ( + + + + + + + ); +}; + +export default memo(TextToImageTab); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx new file mode 100644 index 0000000000..d7edef148c --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabCoreParameters.tsx @@ -0,0 +1,77 @@ +import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations'; +import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps'; +import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; +import ParamWidth from 'features/parameters/components/Parameters/Core/ParamWidth'; +import ParamHeight from 'features/parameters/components/Parameters/Core/ParamHeight'; +import ParamSampler from 'features/parameters/components/Parameters/Core/ParamSampler'; +import ModelSelect from 'features/system/components/ModelSelect'; +import { Box, Flex } from '@chakra-ui/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import { createSelector } from '@reduxjs/toolkit'; +import { uiSelector } from 'features/ui/store/uiSelectors'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { memo } from 'react'; + +const selector = createSelector( + uiSelector, + (ui) => { + const { shouldUseSliders } = ui; + + return { shouldUseSliders }; + }, + defaultSelectorOptions +); + +const TextToImageTabCoreParameters = () => { + const { shouldUseSliders } = useAppSelector(selector); + + return ( + + {shouldUseSliders ? ( + + + + + + + + + + + + + + + + ) : ( + + + + + + + + + + + + + + + + + + )} + + ); +}; + +export default memo(TextToImageTabCoreParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabMain.tsx similarity index 64% rename from invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx rename to invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabMain.tsx index de7b738956..b6cfcf72c3 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabMain.tsx @@ -1,8 +1,7 @@ import { Box, Flex } from '@chakra-ui/react'; import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay'; -import ProgressImagePreview from 'features/parameters/components/ProgressImagePreview'; -const GenerateContent = () => { +const TextToImageTabMain = () => { return ( { height: '100%', borderRadius: 'base', bg: 'base.850', + p: 4, }} > - + ); }; -export default GenerateContent; +export default TextToImageTabMain; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx new file mode 100644 index 0000000000..0976e3eef2 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx @@ -0,0 +1,30 @@ +import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; +import { memo } from 'react'; +import ParamPositiveConditioning from 'features/parameters/components/Parameters/Core/ParamPositiveConditioning'; +import ParamNegativeConditioning from 'features/parameters/components/Parameters/Core/ParamNegativeConditioning'; +import ParamSeedCollapse from 'features/parameters/components/Parameters/Seed/ParamSeedCollapse'; +import ParamVariationCollapse from 'features/parameters/components/Parameters/Variations/ParamVariationCollapse'; +import ParamNoiseCollapse from 'features/parameters/components/Parameters/Noise/ParamNoiseCollapse'; +import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Symmetry/ParamSymmetryCollapse'; +import ParamHiresCollapse from 'features/parameters/components/Parameters/Hires/ParamHiresCollapse'; +import ParamSeamlessCollapse from 'features/parameters/components/Parameters/Seamless/ParamSeamlessCollapse'; +import TextToImageTabCoreParameters from './TextToImageTabCoreParameters'; + +const TextToImageTabParameters = () => { + return ( + <> + + + + + + + + + + + > + ); +}; + +export default memo(TextToImageTabParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSettings.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSettings.tsx index a2b21368d4..bfaa7cdae8 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSettings.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolSettings/UnifiedCanvasSettings.tsx @@ -6,6 +6,7 @@ import IAIIconButton from 'common/components/IAIIconButton'; import IAIPopover from 'common/components/IAIPopover'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { + setShouldAntialias, setShouldAutoSave, setShouldCropToBoundingBoxOnSave, setShouldShowCanvasDebugInfo, @@ -27,6 +28,7 @@ export const canvasControlsSelector = createSelector( shouldCropToBoundingBoxOnSave, shouldShowCanvasDebugInfo, shouldShowIntermediates, + shouldAntialias, } = canvas; return { @@ -34,6 +36,7 @@ export const canvasControlsSelector = createSelector( shouldCropToBoundingBoxOnSave, shouldShowCanvasDebugInfo, shouldShowIntermediates, + shouldAntialias, }; }, { @@ -52,6 +55,7 @@ const UnifiedCanvasSettings = () => { shouldCropToBoundingBoxOnSave, shouldShowCanvasDebugInfo, shouldShowIntermediates, + shouldAntialias, } = useAppSelector(canvasControlsSelector); return ( @@ -95,6 +99,11 @@ const UnifiedCanvasSettings = () => { dispatch(setShouldShowCanvasDebugInfo(e.target.checked)) } /> + dispatch(setShouldAntialias(e.target.checked))} + /> diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasContent.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasContent.tsx index 6cc63a8446..e56e6126a5 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasContent.tsx @@ -1,6 +1,5 @@ import { Box, Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; -// import IAICanvas from 'features/canvas/components/IAICanvas'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAICanvas from 'features/canvas/components/IAICanvas'; import IAICanvasResizer from 'features/canvas/components/IAICanvasResizer'; @@ -9,7 +8,7 @@ import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { isEqual } from 'lodash-es'; -import { useLayoutEffect } from 'react'; +import { memo, useLayoutEffect } from 'react'; const selector = createSelector( [canvasSelector], @@ -80,4 +79,4 @@ const UnifiedCanvasContent = () => { ); }; -export default UnifiedCanvasContent; +export default memo(UnifiedCanvasContent); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx new file mode 100644 index 0000000000..cc03ef560d --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasCoreParameters.tsx @@ -0,0 +1,83 @@ +import { memo } from 'react'; +import { Box, Flex } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { uiSelector } from 'features/ui/store/uiSelectors'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import ModelSelect from 'features/system/components/ModelSelect'; +import ParamIterations from 'features/parameters/components/Parameters/Core/ParamIterations'; +import ParamSteps from 'features/parameters/components/Parameters/Core/ParamSteps'; +import ParamCFGScale from 'features/parameters/components/Parameters/Core/ParamCFGScale'; +import ParamWidth from 'features/parameters/components/Parameters/Core/ParamWidth'; +import ParamHeight from 'features/parameters/components/Parameters/Core/ParamHeight'; +import ImageToImageStrength from 'features/parameters/components/Parameters/ImageToImage/ImageToImageStrength'; +import ImageToImageFit from 'features/parameters/components/Parameters/ImageToImage/ImageToImageFit'; +import ParamSampler from 'features/parameters/components/Parameters/Core/ParamSampler'; + +const selector = createSelector( + uiSelector, + (ui) => { + const { shouldUseSliders } = ui; + + return { shouldUseSliders }; + }, + defaultSelectorOptions +); + +const UnifiedCanvasCoreParameters = () => { + const { shouldUseSliders } = useAppSelector(selector); + + return ( + + {shouldUseSliders ? ( + + + + + + + + + + + + + + + + + + ) : ( + + + + + + + + + + + + + + + + + + + + )} + + ); +}; + +export default memo(UnifiedCanvasCoreParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx index d0b009a6f5..4aa68ad56a 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx @@ -1,84 +1,30 @@ -import { Flex } from '@chakra-ui/react'; -import { Feature } from 'app/features'; -import BoundingBoxSettings from 'features/parameters/components/AdvancedParameters/Canvas/BoundingBox/BoundingBoxSettings'; -import InfillAndScalingSettings from 'features/parameters/components/AdvancedParameters/Canvas/InfillAndScalingSettings'; -import SeamCorrectionSettings from 'features/parameters/components/AdvancedParameters/Canvas/SeamCorrection/SeamCorrectionSettings'; -import ImageToImageStrength from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength'; -import SymmetrySettings from 'features/parameters/components/AdvancedParameters/Output/SymmetrySettings'; -import SymmetryToggle from 'features/parameters/components/AdvancedParameters/Output/SymmetryToggle'; -import SeedSettings from 'features/parameters/components/AdvancedParameters/Seed/SeedSettings'; -import GenerateVariationsToggle from 'features/parameters/components/AdvancedParameters/Variations/GenerateVariations'; -import VariationsSettings from 'features/parameters/components/AdvancedParameters/Variations/VariationsSettings'; -import MainSettings from 'features/parameters/components/MainParameters/MainSettings'; -import ParametersAccordion, { - ParametersAccordionItems, -} from 'features/parameters/components/ParametersAccordion'; import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; -import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput'; -import PromptInput from 'features/parameters/components/PromptInput/PromptInput'; -import { useTranslation } from 'react-i18next'; - -export default function UnifiedCanvasParameters() { - const { t } = useTranslation(); - - const unifiedCanvasAccordions: ParametersAccordionItems = { - general: { - name: 'general', - header: `${t('parameters.general')}`, - feature: undefined, - content: , - }, - unifiedCanvasImg2Img: { - name: 'unifiedCanvasImg2Img', - header: `${t('parameters.imageToImage')}`, - feature: undefined, - content: , - }, - seed: { - name: 'seed', - header: `${t('parameters.seed')}`, - feature: Feature.SEED, - content: , - }, - boundingBox: { - name: 'boundingBox', - header: `${t('parameters.boundingBoxHeader')}`, - feature: Feature.BOUNDING_BOX, - content: , - }, - seamCorrection: { - name: 'seamCorrection', - header: `${t('parameters.seamCorrectionHeader')}`, - feature: Feature.SEAM_CORRECTION, - content: , - }, - infillAndScaling: { - name: 'infillAndScaling', - header: `${t('parameters.infillScalingHeader')}`, - feature: Feature.INFILL_AND_SCALING, - content: , - }, - variations: { - name: 'variations', - header: `${t('parameters.variations')}`, - feature: Feature.VARIATIONS, - content: , - additionalHeaderComponents: , - }, - symmetry: { - name: 'symmetry', - header: `${t('parameters.symmetry')}`, - content: , - additionalHeaderComponents: , - }, - }; +import ParamSeedCollapse from 'features/parameters/components/Parameters/Seed/ParamSeedCollapse'; +import ParamVariationCollapse from 'features/parameters/components/Parameters/Variations/ParamVariationCollapse'; +import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Symmetry/ParamSymmetryCollapse'; +import ParamBoundingBoxCollapse from 'features/parameters/components/Parameters/Canvas/BoundingBox/ParamBoundingBoxCollapse'; +import ParamInfillAndScalingCollapse from 'features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillAndScalingCollapse'; +import ParamSeamCorrectionCollapse from 'features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamCorrectionCollapse'; +import UnifiedCanvasCoreParameters from './UnifiedCanvasCoreParameters'; +import { memo } from 'react'; +import ParamPositiveConditioning from 'features/parameters/components/Parameters/Core/ParamPositiveConditioning'; +import ParamNegativeConditioning from 'features/parameters/components/Parameters/Core/ParamNegativeConditioning'; +const UnifiedCanvasParameters = () => { return ( - - - + <> + + - - + + + + + + + + > ); -} +}; + +export default memo(UnifiedCanvasParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasTab.tsx new file mode 100644 index 0000000000..2d591d1ecc --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasTab.tsx @@ -0,0 +1,36 @@ +import { Flex } from '@chakra-ui/react'; +import { memo } from 'react'; +import { createSelector } from '@reduxjs/toolkit'; +import { uiSelector } from 'features/ui/store/uiSelectors'; +import { useAppSelector } from 'app/store/storeHooks'; +import UnifiedCanvasContent from './UnifiedCanvasContent'; +import UnifiedCanvasParameters from './UnifiedCanvasParameters'; +import UnifiedCanvasContentBeta from './UnifiedCanvasBeta/UnifiedCanvasContentBeta'; +import ParametersPinnedWrapper from '../../ParametersPinnedWrapper'; + +const selector = createSelector(uiSelector, (ui) => { + const { shouldUseCanvasBetaLayout } = ui; + + return { + shouldUseCanvasBetaLayout, + }; +}); + +const UnifiedCanvasTab = () => { + const { shouldUseCanvasBetaLayout } = useAppSelector(selector); + + return ( + + + + + {shouldUseCanvasBetaLayout ? ( + + ) : ( + + )} + + ); +}; + +export default memo(UnifiedCanvasTab); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasWorkarea.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasWorkarea.tsx index dd32295f3c..dbf4041edf 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasWorkarea.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasWorkarea.tsx @@ -1,76 +1,18 @@ -// import { RootState } from 'app/store/store'; -// import { useAppSelector } from 'app/store/storeHooks'; -// import InvokeWorkarea from 'features/ui/components/InvokeWorkarea'; -// import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -// import UnifiedCanvasContentBeta from './UnifiedCanvasBeta/UnifiedCanvasContentBeta'; -// import UnifiedCanvasContent from './UnifiedCanvasContent'; -// import UnifiedCanvasParameters from './UnifiedCanvasParameters'; - -// export default function UnifiedCanvasWorkarea() { -// const shouldUseCanvasBetaLayout = useAppSelector( -// (state: RootState) => state.ui.shouldUseCanvasBetaLayout -// ); - -// const activeTabName = useAppSelector(activeTabNameSelector); - -// return ( -// }> -// {activeTabName === 'unifiedCanvas' && -// (shouldUseCanvasBetaLayout ? ( -// -// ) : ( -// -// ))} -// -// ); -// } -import { Box, Flex } from '@chakra-ui/react'; import { useAppSelector } from 'app/store/storeHooks'; import { memo } from 'react'; -import PinParametersPanelButton from '../../PinParametersPanelButton'; import { RootState } from 'app/store/store'; -import Scrollable from '../../common/Scrollable'; -import ParametersSlide from '../../common/ParametersSlide'; -import UnifiedCanvasParameters from './UnifiedCanvasParameters'; import UnifiedCanvasContentBeta from './UnifiedCanvasBeta/UnifiedCanvasContentBeta'; import UnifiedCanvasContent from './UnifiedCanvasContent'; const CanvasWorkspace = () => { - const shouldPinParametersPanel = useAppSelector( - (state: RootState) => state.ui.shouldPinParametersPanel - ); - const shouldUseCanvasBetaLayout = useAppSelector( (state: RootState) => state.ui.shouldUseCanvasBetaLayout ); - return ( - - {shouldPinParametersPanel ? ( - - - - - - - ) : ( - - - - )} - {shouldUseCanvasBetaLayout ? ( - - ) : ( - - )} - + return shouldUseCanvasBetaLayout ? ( + + ) : ( + ); }; diff --git a/invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts b/invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts index 4e72d1dce9..527e0b1740 100644 --- a/invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts @@ -6,15 +6,13 @@ type HotkeysState = { shift: boolean; }; -const initialHotkeysState: HotkeysState = { +export const initialHotkeysState: HotkeysState = { shift: false, }; -const initialState: HotkeysState = initialHotkeysState; - export const hotkeysSlice = createSlice({ name: 'hotkeys', - initialState, + initialState: initialHotkeysState, reducers: { shiftKeyPressed: (state, action: PayloadAction) => { state.shift = action.payload; diff --git a/invokeai/frontend/web/src/features/ui/store/tabMap.ts b/invokeai/frontend/web/src/features/ui/store/tabMap.ts index fe6e2d033a..becf52886e 100644 --- a/invokeai/frontend/web/src/features/ui/store/tabMap.ts +++ b/invokeai/frontend/web/src/features/ui/store/tabMap.ts @@ -1,7 +1,7 @@ export const tabMap = [ - // 'txt2img', - // 'img2img', - 'generate', + 'txt2img', + 'img2img', + // 'generate', 'unifiedCanvas', 'nodes', // 'postprocessing', diff --git a/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts b/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts index ae357e7899..7f469e1f1a 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts @@ -4,6 +4,9 @@ import { UIState } from './uiTypes'; * UI slice persist denylist */ const itemsToDenylist: (keyof UIState)[] = ['floatingProgressImageRect']; +export const uiPersistDenylist: (keyof UIState)[] = [ + 'floatingProgressImageRect', +]; export const uiDenylist = itemsToDenylist.map( (denylistItem) => `ui.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 11abf6a20d..b99ebb2c51 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -3,8 +3,10 @@ import { createSlice } from '@reduxjs/toolkit'; import { setActiveTabReducer } from './extraReducers'; import { InvokeTabName, tabMap } from './tabMap'; import { AddNewModelType, Coordinates, Rect, UIState } from './uiTypes'; +import { initialImageSelected } from 'features/parameters/store/actions'; +import { initialImageChanged } from 'features/parameters/store/generationSlice'; -const initialUIState: UIState = { +export const initialUIState: UIState = { activeTab: 0, currentTheme: 'dark', parametersPanelScrollPosition: 0, @@ -18,19 +20,18 @@ const initialUIState: UIState = { shouldPinGallery: true, shouldShowGallery: true, shouldHidePreview: false, - openLinearAccordionItems: [], - openGenerateAccordionItems: [], - openUnifiedCanvasAccordionItems: [], + textTabAccordionState: [], + imageTabAccordionState: [], + canvasTabAccordionState: [], floatingProgressImageRect: { x: 0, y: 0, width: 0, height: 0 }, shouldShowProgressImages: false, - shouldAutoShowProgressImages: false, + shouldShowProgressInViewer: false, + shouldShowImageParameters: false, }; -const initialState: UIState = initialUIState; - export const uiSlice = createSlice({ name: 'ui', - initialState, + initialState: initialUIState, reducers: { setActiveTab: (state, action: PayloadAction) => { setActiveTabReducer(state, action.payload); @@ -80,9 +81,15 @@ export const uiSlice = createSlice({ }, togglePinGalleryPanel: (state) => { state.shouldPinGallery = !state.shouldPinGallery; + if (!state.shouldPinGallery) { + state.shouldShowGallery = true; + } }, togglePinParametersPanel: (state) => { state.shouldPinParametersPanel = !state.shouldPinParametersPanel; + if (!state.shouldPinParametersPanel) { + state.shouldShowParametersPanel = true; + } }, toggleParametersPanel: (state) => { state.shouldShowParametersPanel = !state.shouldShowParametersPanel; @@ -100,12 +107,16 @@ export const uiSlice = createSlice({ } }, openAccordionItemsChanged: (state, action: PayloadAction) => { - if (tabMap[state.activeTab] === 'generate') { - state.openGenerateAccordionItems = action.payload; + if (tabMap[state.activeTab] === 'txt2img') { + state.textTabAccordionState = action.payload; + } + + if (tabMap[state.activeTab] === 'img2img') { + state.imageTabAccordionState = action.payload; } if (tabMap[state.activeTab] === 'unifiedCanvas') { - state.openUnifiedCanvasAccordionItems = action.payload; + state.canvasTabAccordionState = action.payload; } }, floatingProgressImageMoved: (state, action: PayloadAction) => { @@ -126,13 +137,21 @@ export const uiSlice = createSlice({ setShouldShowProgressImages: (state, action: PayloadAction) => { state.shouldShowProgressImages = action.payload; }, - setShouldAutoShowProgressImages: ( + setShouldShowProgressInViewer: (state, action: PayloadAction) => { + state.shouldShowProgressInViewer = action.payload; + }, + shouldShowImageParametersChanged: ( state, action: PayloadAction ) => { - state.shouldAutoShowProgressImages = action.payload; + state.shouldShowImageParameters = action.payload; }, }, + extraReducers(builder) { + builder.addCase(initialImageChanged, (state) => { + setActiveTabReducer(state, 'img2img'); + }); + }, }); export const { @@ -158,7 +177,8 @@ export const { floatingProgressImageMoved, floatingProgressImageResized, setShouldShowProgressImages, - setShouldAutoShowProgressImages, + setShouldShowProgressInViewer, + shouldShowImageParametersChanged, } = uiSlice.actions; export default uiSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index bdcf0a3c30..030ec4f1ce 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -26,10 +26,11 @@ export interface UIState { shouldHidePreview: boolean; shouldPinGallery: boolean; shouldShowGallery: boolean; - openLinearAccordionItems: number[]; - openGenerateAccordionItems: number[]; - openUnifiedCanvasAccordionItems: number[]; + textTabAccordionState: number[]; + imageTabAccordionState: number[]; + canvasTabAccordionState: number[]; floatingProgressImageRect: Rect; shouldShowProgressImages: boolean; - shouldAutoShowProgressImages: boolean; + shouldShowProgressInViewer: boolean; + shouldShowImageParameters: boolean; } diff --git a/invokeai/frontend/web/src/i18n.ts b/invokeai/frontend/web/src/i18n.ts index ed71d583b3..f28365db20 100644 --- a/invokeai/frontend/web/src/i18n.ts +++ b/invokeai/frontend/web/src/i18n.ts @@ -3,7 +3,8 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import Backend from 'i18next-http-backend'; import { initReactI18next } from 'react-i18next'; -import translationEN from '../public/locales/en.json'; +import translationEN from '../dist/locales/en.json'; +import { LOCALSTORAGE_PREFIX } from 'app/store/constants'; if (import.meta.env.MODE === 'package') { i18n.use(initReactI18next).init({ @@ -20,7 +21,11 @@ if (import.meta.env.MODE === 'package') { } else { i18n .use(Backend) - .use(LanguageDetector) + .use( + new LanguageDetector(null, { + lookupLocalStorage: `${LOCALSTORAGE_PREFIX}lng`, + }) + ) .use(initReactI18next) .init({ fallbackLng: 'en', diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index 2a34837715..24eeb458b6 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -12,9 +12,14 @@ export type { Body_upload_image } from './models/Body_upload_image'; export type { CkptModelInfo } from './models/CkptModelInfo'; export type { CollectInvocation } from './models/CollectInvocation'; export type { CollectInvocationOutput } from './models/CollectInvocationOutput'; +export type { ColorField } from './models/ColorField'; +export type { CompelInvocation } from './models/CompelInvocation'; +export type { CompelOutput } from './models/CompelOutput'; +export type { ConditioningField } from './models/ConditioningField'; export type { CreateModelRequest } from './models/CreateModelRequest'; export type { CropImageInvocation } from './models/CropImageInvocation'; export type { CvInpaintInvocation } from './models/CvInpaintInvocation'; +export type { DataURLToImageInvocation } from './models/DataURLToImageInvocation'; export type { DiffusersModelInfo } from './models/DiffusersModelInfo'; export type { DivideInvocation } from './models/DivideInvocation'; export type { Edge } from './models/Edge'; @@ -29,7 +34,11 @@ export type { ImageOutput } from './models/ImageOutput'; export type { ImageResponse } from './models/ImageResponse'; export type { ImageResponseMetadata } from './models/ImageResponseMetadata'; export type { ImageToImageInvocation } from './models/ImageToImageInvocation'; +export type { ImageToLatentsInvocation } from './models/ImageToLatentsInvocation'; export type { ImageType } from './models/ImageType'; +export type { InfillColorInvocation } from './models/InfillColorInvocation'; +export type { InfillPatchMatchInvocation } from './models/InfillPatchMatchInvocation'; +export type { InfillTileInvocation } from './models/InfillTileInvocation'; export type { InpaintInvocation } from './models/InpaintInvocation'; export type { IntCollectionOutput } from './models/IntCollectionOutput'; export type { IntOutput } from './models/IntOutput'; @@ -45,6 +54,7 @@ export type { LerpInvocation } from './models/LerpInvocation'; export type { LoadImageInvocation } from './models/LoadImageInvocation'; export type { MaskFromAlphaInvocation } from './models/MaskFromAlphaInvocation'; export type { MaskOutput } from './models/MaskOutput'; +export type { MetadataColorField } from './models/MetadataColorField'; export type { MetadataImageField } from './models/MetadataImageField'; export type { MetadataLatentsField } from './models/MetadataLatentsField'; export type { ModelsList } from './models/ModelsList'; @@ -75,9 +85,14 @@ export { $Body_upload_image } from './schemas/$Body_upload_image'; export { $CkptModelInfo } from './schemas/$CkptModelInfo'; export { $CollectInvocation } from './schemas/$CollectInvocation'; export { $CollectInvocationOutput } from './schemas/$CollectInvocationOutput'; +export { $ColorField } from './schemas/$ColorField'; +export { $CompelInvocation } from './schemas/$CompelInvocation'; +export { $CompelOutput } from './schemas/$CompelOutput'; +export { $ConditioningField } from './schemas/$ConditioningField'; export { $CreateModelRequest } from './schemas/$CreateModelRequest'; export { $CropImageInvocation } from './schemas/$CropImageInvocation'; export { $CvInpaintInvocation } from './schemas/$CvInpaintInvocation'; +export { $DataURLToImageInvocation } from './schemas/$DataURLToImageInvocation'; export { $DiffusersModelInfo } from './schemas/$DiffusersModelInfo'; export { $DivideInvocation } from './schemas/$DivideInvocation'; export { $Edge } from './schemas/$Edge'; @@ -92,7 +107,11 @@ export { $ImageOutput } from './schemas/$ImageOutput'; export { $ImageResponse } from './schemas/$ImageResponse'; export { $ImageResponseMetadata } from './schemas/$ImageResponseMetadata'; export { $ImageToImageInvocation } from './schemas/$ImageToImageInvocation'; +export { $ImageToLatentsInvocation } from './schemas/$ImageToLatentsInvocation'; export { $ImageType } from './schemas/$ImageType'; +export { $InfillColorInvocation } from './schemas/$InfillColorInvocation'; +export { $InfillPatchMatchInvocation } from './schemas/$InfillPatchMatchInvocation'; +export { $InfillTileInvocation } from './schemas/$InfillTileInvocation'; export { $InpaintInvocation } from './schemas/$InpaintInvocation'; export { $IntCollectionOutput } from './schemas/$IntCollectionOutput'; export { $IntOutput } from './schemas/$IntOutput'; @@ -108,6 +127,7 @@ export { $LerpInvocation } from './schemas/$LerpInvocation'; export { $LoadImageInvocation } from './schemas/$LoadImageInvocation'; export { $MaskFromAlphaInvocation } from './schemas/$MaskFromAlphaInvocation'; export { $MaskOutput } from './schemas/$MaskOutput'; +export { $MetadataColorField } from './schemas/$MetadataColorField'; export { $MetadataImageField } from './schemas/$MetadataImageField'; export { $MetadataLatentsField } from './schemas/$MetadataLatentsField'; export { $ModelsList } from './schemas/$ModelsList'; diff --git a/invokeai/frontend/web/src/services/api/models/ColorField.ts b/invokeai/frontend/web/src/services/api/models/ColorField.ts new file mode 100644 index 0000000000..e0a609ec12 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ColorField.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type ColorField = { + /** + * The red component + */ + 'r': number; + /** + * The green component + */ + 'g': number; + /** + * The blue component + */ + 'b': number; + /** + * The alpha component + */ + 'a': number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CompelInvocation.ts b/invokeai/frontend/web/src/services/api/models/CompelInvocation.ts new file mode 100644 index 0000000000..f03d53a841 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CompelInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Parse prompt using compel package to conditioning. + */ +export type CompelInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'compel'; + /** + * Prompt + */ + prompt?: string; + /** + * Model to use + */ + model?: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CompelOutput.ts b/invokeai/frontend/web/src/services/api/models/CompelOutput.ts new file mode 100644 index 0000000000..94f1fcb282 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CompelOutput.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ConditioningField } from './ConditioningField'; + +/** + * Compel parser output + */ +export type CompelOutput = { + type?: 'compel_output'; + /** + * Conditioning + */ + conditioning?: ConditioningField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ConditioningField.ts b/invokeai/frontend/web/src/services/api/models/ConditioningField.ts new file mode 100644 index 0000000000..7e53a63b42 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ConditioningField.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type ConditioningField = { + /** + * The name of conditioning data + */ + conditioning_name: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/DataURLToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/DataURLToImageInvocation.ts new file mode 100644 index 0000000000..b1e35d9e0c --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/DataURLToImageInvocation.ts @@ -0,0 +1,19 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Outputs an image from a data URL. + */ +export type DataURLToImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'dataURL_image'; + /** + * The b64 data URL + */ + dataURL: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/Graph.ts b/invokeai/frontend/web/src/services/api/models/Graph.ts index 57a9178290..9d56276e62 100644 --- a/invokeai/frontend/web/src/services/api/models/Graph.ts +++ b/invokeai/frontend/web/src/services/api/models/Graph.ts @@ -5,12 +5,18 @@ import type { AddInvocation } from './AddInvocation'; import type { BlurInvocation } from './BlurInvocation'; import type { CollectInvocation } from './CollectInvocation'; +import type { CompelInvocation } from './CompelInvocation'; import type { CropImageInvocation } from './CropImageInvocation'; import type { CvInpaintInvocation } from './CvInpaintInvocation'; +import type { DataURLToImageInvocation } from './DataURLToImageInvocation'; import type { DivideInvocation } from './DivideInvocation'; import type { Edge } from './Edge'; import type { GraphInvocation } from './GraphInvocation'; import type { ImageToImageInvocation } from './ImageToImageInvocation'; +import type { ImageToLatentsInvocation } from './ImageToLatentsInvocation'; +import type { InfillColorInvocation } from './InfillColorInvocation'; +import type { InfillPatchMatchInvocation } from './InfillPatchMatchInvocation'; +import type { InfillTileInvocation } from './InfillTileInvocation'; import type { InpaintInvocation } from './InpaintInvocation'; import type { InverseLerpInvocation } from './InverseLerpInvocation'; import type { IterateInvocation } from './IterateInvocation'; @@ -42,7 +48,7 @@ export type Graph = { /** * The nodes in this graph */ - nodes?: Record; + nodes?: Record; /** * The connections between nodes and their fields in this graph */ diff --git a/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts b/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts index 2243542480..2e54601e7c 100644 --- a/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts +++ b/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts @@ -3,6 +3,7 @@ /* eslint-disable */ import type { CollectInvocationOutput } from './CollectInvocationOutput'; +import type { CompelOutput } from './CompelOutput'; import type { Graph } from './Graph'; import type { GraphInvocationOutput } from './GraphInvocationOutput'; import type { ImageOutput } from './ImageOutput'; @@ -41,7 +42,7 @@ export type GraphExecutionState = { /** * The results of node executions */ - results: Record; + results: Record; /** * Errors raised when executing nodes */ diff --git a/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts index d65ceeee3a..0dfb893213 100644 --- a/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts @@ -18,7 +18,7 @@ export type ImageToImageInvocation = { */ prompt?: string; /** - * The seed to use (-1 for a random seed) + * The seed to use (omit for random) */ seed?: number; /** @@ -41,18 +41,10 @@ export type ImageToImageInvocation = { * The scheduler to use */ scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; - /** - * Whether or not to generate an image that can tile without seams - */ - seamless?: boolean; /** * The model to use (currently ignored) */ model?: string; - /** - * Whether or not to produce progress images during generation - */ - progress_images?: boolean; /** * The input image */ diff --git a/invokeai/frontend/web/src/services/api/models/ImageToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageToLatentsInvocation.ts new file mode 100644 index 0000000000..f72d446615 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageToLatentsInvocation.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Encodes an image into latents. + */ +export type ImageToLatentsInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'i2l'; + /** + * The image to encode + */ + image?: ImageField; + /** + * The model to use + */ + model?: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/InfillColorInvocation.ts b/invokeai/frontend/web/src/services/api/models/InfillColorInvocation.ts new file mode 100644 index 0000000000..a0335eab89 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/InfillColorInvocation.ts @@ -0,0 +1,26 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ColorField } from './ColorField'; +import type { ImageField } from './ImageField'; + +/** + * Infills transparent areas of an image with a color + */ +export type InfillColorInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'infill_rgba'; + /** + * The image to infill + */ + image?: ImageField; + /** + * The color to use to infill + */ + color?: ColorField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/InfillPatchMatchInvocation.ts b/invokeai/frontend/web/src/services/api/models/InfillPatchMatchInvocation.ts new file mode 100644 index 0000000000..6d6c1074a5 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/InfillPatchMatchInvocation.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Infills transparent areas of an image with tiles of the image + */ +export type InfillPatchMatchInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'infill_patchmatch'; + /** + * The image to infill + */ + image?: ImageField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/InfillTileInvocation.ts b/invokeai/frontend/web/src/services/api/models/InfillTileInvocation.ts new file mode 100644 index 0000000000..12113f57f5 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/InfillTileInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Infills transparent areas of an image with tiles of the image + */ +export type InfillTileInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'infill_tile'; + /** + * The image to infill + */ + image?: ImageField; + /** + * The tile size (px) + */ + tile_size?: number; + /** + * The seed to use for tile generation (omit for random) + */ + seed?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts b/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts index 7ea6a89f62..c4b125902a 100644 --- a/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts @@ -2,6 +2,7 @@ /* tslint:disable */ /* eslint-disable */ +import type { ColorField } from './ColorField'; import type { ImageField } from './ImageField'; /** @@ -18,7 +19,7 @@ export type InpaintInvocation = { */ prompt?: string; /** - * The seed to use (-1 for a random seed) + * The seed to use (omit for random) */ seed?: number; /** @@ -41,18 +42,10 @@ export type InpaintInvocation = { * The scheduler to use */ scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; - /** - * Whether or not to generate an image that can tile without seams - */ - seamless?: boolean; /** * The model to use (currently ignored) */ model?: string; - /** - * Whether or not to produce progress images during generation - */ - progress_images?: boolean; /** * The input image */ @@ -69,6 +62,42 @@ export type InpaintInvocation = { * The mask */ mask?: ImageField; + /** + * The seam inpaint size (px) + */ + seam_size?: number; + /** + * The seam inpaint blur radius (px) + */ + seam_blur?: number; + /** + * The seam inpaint strength + */ + seam_strength?: number; + /** + * The number of steps to use for seam inpaint + */ + seam_steps?: number; + /** + * The tile infill method size (px) + */ + tile_size?: number; + /** + * The method used to infill empty regions (px) + */ + infill_method?: 'patchmatch' | 'tile' | 'solid'; + /** + * The width of the inpaint region (px) + */ + inpaint_width?: number; + /** + * The height of the inpaint region (px) + */ + inpaint_height?: number; + /** + * The solid infill method color + */ + inpaint_fill?: ColorField; /** * The amount by which to replace masked areas with latent noise */ diff --git a/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts b/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts index a6bc3f7744..ba80199f9a 100644 --- a/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts +++ b/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts @@ -2,11 +2,12 @@ /* tslint:disable */ /* eslint-disable */ +import type { MetadataColorField } from './MetadataColorField'; import type { MetadataImageField } from './MetadataImageField'; import type { MetadataLatentsField } from './MetadataLatentsField'; export type InvokeAIMetadata = { session_id?: string; - node?: Record; + node?: Record; }; diff --git a/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts index d04885bf85..7795ce2b21 100644 --- a/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts @@ -2,6 +2,7 @@ /* tslint:disable */ /* eslint-disable */ +import type { ConditioningField } from './ConditioningField'; import type { LatentsField } from './LatentsField'; /** @@ -14,9 +15,13 @@ export type LatentsToLatentsInvocation = { id: string; type?: 'l2l'; /** - * The prompt to generate an image from + * Positive conditioning for generation */ - prompt?: string; + positive_conditioning?: ConditioningField; + /** + * Negative conditioning for generation + */ + negative_conditioning?: ConditioningField; /** * The noise to use */ @@ -33,22 +38,10 @@ export type LatentsToLatentsInvocation = { * The scheduler to use */ scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; - /** - * Whether or not to generate an image that can tile without seams - */ - seamless?: boolean; - /** - * The axes to tile the image on, 'x' and/or 'y' - */ - seamless_axes?: string; /** * The model to use (currently ignored) */ model?: string; - /** - * Whether or not to produce progress images during generation - */ - progress_images?: boolean; /** * The latents to use as a base image */ diff --git a/invokeai/frontend/web/src/services/api/models/MetadataColorField.ts b/invokeai/frontend/web/src/services/api/models/MetadataColorField.ts new file mode 100644 index 0000000000..897a0123dd --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/MetadataColorField.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type MetadataColorField = { + 'r': number; + 'g': number; + 'b': number; + 'a': number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts b/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts index 55a94ec46d..c1f80042a6 100644 --- a/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts @@ -24,7 +24,7 @@ export type RandomRangeInvocation = { */ size?: number; /** - * The seed for the RNG + * The seed for the RNG (omit for random) */ seed?: number; }; diff --git a/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts index b1ff7a3525..d928515c76 100644 --- a/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts @@ -16,7 +16,7 @@ export type TextToImageInvocation = { */ prompt?: string; /** - * The seed to use (-1 for a random seed) + * The seed to use (omit for random) */ seed?: number; /** @@ -39,17 +39,9 @@ export type TextToImageInvocation = { * The scheduler to use */ scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; - /** - * Whether or not to generate an image that can tile without seams - */ - seamless?: boolean; /** * The model to use (currently ignored) */ model?: string; - /** - * Whether or not to produce progress images during generation - */ - progress_images?: boolean; }; diff --git a/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts index 217b917f18..a67170d6c8 100644 --- a/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts @@ -2,10 +2,11 @@ /* tslint:disable */ /* eslint-disable */ +import type { ConditioningField } from './ConditioningField'; import type { LatentsField } from './LatentsField'; /** - * Generates latents from a prompt. + * Generates latents from conditionings. */ export type TextToLatentsInvocation = { /** @@ -14,9 +15,13 @@ export type TextToLatentsInvocation = { id: string; type?: 't2l'; /** - * The prompt to generate an image from + * Positive conditioning for generation */ - prompt?: string; + positive_conditioning?: ConditioningField; + /** + * Negative conditioning for generation + */ + negative_conditioning?: ConditioningField; /** * The noise to use */ @@ -33,21 +38,9 @@ export type TextToLatentsInvocation = { * The scheduler to use */ scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; - /** - * Whether or not to generate an image that can tile without seams - */ - seamless?: boolean; - /** - * The axes to tile the image on, 'x' and/or 'y' - */ - seamless_axes?: string; /** * The model to use (currently ignored) */ model?: string; - /** - * Whether or not to produce progress images during generation - */ - progress_images?: boolean; }; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ColorField.ts b/invokeai/frontend/web/src/services/api/schemas/$ColorField.ts new file mode 100644 index 0000000000..e38788dae2 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ColorField.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ColorField = { + properties: { + 'r': { + type: 'number', + description: `The red component`, + isRequired: true, + maximum: 255, + }, + 'g': { + type: 'number', + description: `The green component`, + isRequired: true, + maximum: 255, + }, + 'b': { + type: 'number', + description: `The blue component`, + isRequired: true, + maximum: 255, + }, + 'a': { + type: 'number', + description: `The alpha component`, + isRequired: true, + maximum: 255, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CompelInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$CompelInvocation.ts new file mode 100644 index 0000000000..61139412ad --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CompelInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CompelInvocation = { + description: `Parse prompt using compel package to conditioning.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + prompt: { + type: 'string', + description: `Prompt`, + }, + model: { + type: 'string', + description: `Model to use`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CompelOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$CompelOutput.ts new file mode 100644 index 0000000000..03a429040a --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CompelOutput.ts @@ -0,0 +1,18 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CompelOutput = { + description: `Compel parser output`, + properties: { + type: { + type: 'Enum', + }, + conditioning: { + type: 'all-of', + description: `Conditioning`, + contains: [{ + type: 'ConditioningField', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ConditioningField.ts b/invokeai/frontend/web/src/services/api/schemas/$ConditioningField.ts new file mode 100644 index 0000000000..fcbd449af2 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ConditioningField.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ConditioningField = { + properties: { + conditioning_name: { + type: 'string', + description: `The name of conditioning data`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$DataURLToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$DataURLToImageInvocation.ts new file mode 100644 index 0000000000..f875cd0a11 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$DataURLToImageInvocation.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $DataURLToImageInvocation = { + description: `Outputs an image from a data URL.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + dataURL: { + type: 'string', + description: `The b64 data URL`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$Graph.ts b/invokeai/frontend/web/src/services/api/schemas/$Graph.ts index 6fd8117db8..f3d5fe0edd 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$Graph.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$Graph.ts @@ -15,6 +15,8 @@ export const $Graph = { type: 'LoadImageInvocation', }, { type: 'ShowImageInvocation', + }, { + type: 'DataURLToImageInvocation', }, { type: 'CropImageInvocation', }, { @@ -27,6 +29,8 @@ export const $Graph = { type: 'LerpInvocation', }, { type: 'InverseLerpInvocation', + }, { + type: 'CompelInvocation', }, { type: 'NoiseInvocation', }, { @@ -37,6 +41,8 @@ export const $Graph = { type: 'ResizeLatentsInvocation', }, { type: 'ScaleLatentsInvocation', + }, { + type: 'ImageToLatentsInvocation', }, { type: 'AddInvocation', }, { @@ -59,6 +65,12 @@ export const $Graph = { type: 'RestoreFaceInvocation', }, { type: 'TextToImageInvocation', + }, { + type: 'InfillColorInvocation', + }, { + type: 'InfillTileInvocation', + }, { + type: 'InfillPatchMatchInvocation', }, { type: 'GraphInvocation', }, { diff --git a/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts b/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts index a21419a6a4..c0a2264877 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts @@ -47,6 +47,8 @@ export const $GraphExecutionState = { type: 'ImageOutput', }, { type: 'MaskOutput', + }, { + type: 'CompelOutput', }, { type: 'LatentsOutput', }, { diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts index 4b77f03ca3..098009d182 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts @@ -18,9 +18,8 @@ export const $ImageToImageInvocation = { }, seed: { type: 'number', - description: `The seed to use (-1 for a random seed)`, - maximum: 4294967295, - minimum: -1, + description: `The seed to use (omit for random)`, + maximum: 2147483647, }, steps: { type: 'number', @@ -29,32 +28,25 @@ export const $ImageToImageInvocation = { width: { type: 'number', description: `The width of the resulting image`, - multipleOf: 64, + multipleOf: 8, }, height: { type: 'number', description: `The height of the resulting image`, - multipleOf: 64, + multipleOf: 8, }, cfg_scale: { type: 'number', description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + minimum: 1, }, scheduler: { type: 'Enum', }, - seamless: { - type: 'boolean', - description: `Whether or not to generate an image that can tile without seams`, - }, model: { type: 'string', description: `The model to use (currently ignored)`, }, - progress_images: { - type: 'boolean', - description: `Whether or not to produce progress images during generation`, - }, image: { type: 'all-of', description: `The input image`, diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageToLatentsInvocation.ts new file mode 100644 index 0000000000..48e28f1315 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageToLatentsInvocation.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageToLatentsInvocation = { + description: `Encodes an image into latents.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to encode`, + contains: [{ + type: 'ImageField', + }], + }, + model: { + type: 'string', + description: `The model to use`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InfillColorInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InfillColorInvocation.ts new file mode 100644 index 0000000000..42af32c9b2 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$InfillColorInvocation.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $InfillColorInvocation = { + description: `Infills transparent areas of an image with a color`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to infill`, + contains: [{ + type: 'ImageField', + }], + }, + color: { + type: 'all-of', + description: `The color to use to infill`, + contains: [{ + type: 'ColorField', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InfillPatchMatchInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InfillPatchMatchInvocation.ts new file mode 100644 index 0000000000..0278dafd35 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$InfillPatchMatchInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $InfillPatchMatchInvocation = { + description: `Infills transparent areas of an image with tiles of the image`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to infill`, + contains: [{ + type: 'ImageField', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InfillTileInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InfillTileInvocation.ts new file mode 100644 index 0000000000..7a14d94e5a --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$InfillTileInvocation.ts @@ -0,0 +1,33 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $InfillTileInvocation = { + description: `Infills transparent areas of an image with tiles of the image`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to infill`, + contains: [{ + type: 'ImageField', + }], + }, + tile_size: { + type: 'number', + description: `The tile size (px)`, + minimum: 1, + }, + seed: { + type: 'number', + description: `The seed to use for tile generation (omit for random)`, + maximum: 2147483647, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts index ab022825b3..1225cde1b6 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts @@ -18,9 +18,8 @@ export const $InpaintInvocation = { }, seed: { type: 'number', - description: `The seed to use (-1 for a random seed)`, - maximum: 4294967295, - minimum: -1, + description: `The seed to use (omit for random)`, + maximum: 2147483647, }, steps: { type: 'number', @@ -29,32 +28,25 @@ export const $InpaintInvocation = { width: { type: 'number', description: `The width of the resulting image`, - multipleOf: 64, + multipleOf: 8, }, height: { type: 'number', description: `The height of the resulting image`, - multipleOf: 64, + multipleOf: 8, }, cfg_scale: { type: 'number', description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + minimum: 1, }, scheduler: { type: 'Enum', }, - seamless: { - type: 'boolean', - description: `Whether or not to generate an image that can tile without seams`, - }, model: { type: 'string', description: `The model to use (currently ignored)`, }, - progress_images: { - type: 'boolean', - description: `Whether or not to produce progress images during generation`, - }, image: { type: 'all-of', description: `The input image`, @@ -78,6 +70,50 @@ export const $InpaintInvocation = { type: 'ImageField', }], }, + seam_size: { + type: 'number', + description: `The seam inpaint size (px)`, + minimum: 1, + }, + seam_blur: { + type: 'number', + description: `The seam inpaint blur radius (px)`, + }, + seam_strength: { + type: 'number', + description: `The seam inpaint strength`, + maximum: 1, + }, + seam_steps: { + type: 'number', + description: `The number of steps to use for seam inpaint`, + minimum: 1, + }, + tile_size: { + type: 'number', + description: `The tile infill method size (px)`, + minimum: 1, + }, + infill_method: { + type: 'Enum', + }, + inpaint_width: { + type: 'number', + description: `The width of the inpaint region (px)`, + multipleOf: 8, + }, + inpaint_height: { + type: 'number', + description: `The height of the inpaint region (px)`, + multipleOf: 8, + }, + inpaint_fill: { + type: 'all-of', + description: `The solid infill method color`, + contains: [{ + type: 'ColorField', + }], + }, inpaint_replace: { type: 'number', description: `The amount by which to replace masked areas with latent noise`, diff --git a/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts b/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts index 2d0b8e2db1..f2895f6646 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts @@ -22,6 +22,8 @@ export const $InvokeAIMetadata = { type: 'MetadataImageField', }, { type: 'MetadataLatentsField', + }, { + type: 'MetadataColorField', }], }, }, diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts index b20ee88a52..38df3ad5cc 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts @@ -12,9 +12,19 @@ export const $LatentsToLatentsInvocation = { type: { type: 'Enum', }, - prompt: { - type: 'string', - description: `The prompt to generate an image from`, + positive_conditioning: { + type: 'all-of', + description: `Positive conditioning for generation`, + contains: [{ + type: 'ConditioningField', + }], + }, + negative_conditioning: { + type: 'all-of', + description: `Negative conditioning for generation`, + contains: [{ + type: 'ConditioningField', + }], }, noise: { type: 'all-of', @@ -34,22 +44,10 @@ export const $LatentsToLatentsInvocation = { scheduler: { type: 'Enum', }, - seamless: { - type: 'boolean', - description: `Whether or not to generate an image that can tile without seams`, - }, - seamless_axes: { - type: 'string', - description: `The axes to tile the image on, 'x' and/or 'y'`, - }, model: { type: 'string', description: `The model to use (currently ignored)`, }, - progress_images: { - type: 'boolean', - description: `Whether or not to produce progress images during generation`, - }, latents: { type: 'all-of', description: `The latents to use as a base image`, diff --git a/invokeai/frontend/web/src/services/api/schemas/$MetadataColorField.ts b/invokeai/frontend/web/src/services/api/schemas/$MetadataColorField.ts new file mode 100644 index 0000000000..234bd3e2f6 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$MetadataColorField.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $MetadataColorField = { + properties: { + 'r': { + type: 'number', + isRequired: true, + }, + 'g': { + type: 'number', + isRequired: true, + }, + 'b': { + type: 'number', + isRequired: true, + }, + 'a': { + type: 'number', + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts index 446e77e747..eade3611b7 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts @@ -15,17 +15,17 @@ export const $NoiseInvocation = { seed: { type: 'number', description: `The seed to use`, - maximum: 4294967295, + maximum: 2147483647, }, width: { type: 'number', description: `The width of the resulting noise`, - multipleOf: 64, + multipleOf: 8, }, height: { type: 'number', description: `The height of the resulting noise`, - multipleOf: 64, + multipleOf: 8, }, }, } as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts index f13e1a8332..a71b223ba0 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts @@ -26,7 +26,7 @@ export const $RandomRangeInvocation = { }, seed: { type: 'number', - description: `The seed for the RNG`, + description: `The seed for the RNG (omit for random)`, maximum: 2147483647, }, }, diff --git a/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts index 70c5858012..0f583dd2d0 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts @@ -18,9 +18,8 @@ export const $TextToImageInvocation = { }, seed: { type: 'number', - description: `The seed to use (-1 for a random seed)`, - maximum: 4294967295, - minimum: -1, + description: `The seed to use (omit for random)`, + maximum: 2147483647, }, steps: { type: 'number', @@ -29,31 +28,24 @@ export const $TextToImageInvocation = { width: { type: 'number', description: `The width of the resulting image`, - multipleOf: 64, + multipleOf: 8, }, height: { type: 'number', description: `The height of the resulting image`, - multipleOf: 64, + multipleOf: 8, }, cfg_scale: { type: 'number', description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + minimum: 1, }, scheduler: { type: 'Enum', }, - seamless: { - type: 'boolean', - description: `Whether or not to generate an image that can tile without seams`, - }, model: { type: 'string', description: `The model to use (currently ignored)`, }, - progress_images: { - type: 'boolean', - description: `Whether or not to produce progress images during generation`, - }, }, } as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts index 06376824c6..1080890606 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts @@ -2,7 +2,7 @@ /* tslint:disable */ /* eslint-disable */ export const $TextToLatentsInvocation = { - description: `Generates latents from a prompt.`, + description: `Generates latents from conditionings.`, properties: { id: { type: 'string', @@ -12,9 +12,19 @@ export const $TextToLatentsInvocation = { type: { type: 'Enum', }, - prompt: { - type: 'string', - description: `The prompt to generate an image from`, + positive_conditioning: { + type: 'all-of', + description: `Positive conditioning for generation`, + contains: [{ + type: 'ConditioningField', + }], + }, + negative_conditioning: { + type: 'all-of', + description: `Negative conditioning for generation`, + contains: [{ + type: 'ConditioningField', + }], }, noise: { type: 'all-of', @@ -34,21 +44,9 @@ export const $TextToLatentsInvocation = { scheduler: { type: 'Enum', }, - seamless: { - type: 'boolean', - description: `Whether or not to generate an image that can tile without seams`, - }, - seamless_axes: { - type: 'string', - description: `The axes to tile the image on, 'x' and/or 'y'`, - }, model: { type: 'string', description: `The model to use (currently ignored)`, }, - progress_images: { - type: 'boolean', - description: `Whether or not to produce progress images during generation`, - }, }, } as const; diff --git a/invokeai/frontend/web/src/services/api/services/ImagesService.ts b/invokeai/frontend/web/src/services/api/services/ImagesService.ts index 504b75f6bf..9dc63688fc 100644 --- a/invokeai/frontend/web/src/services/api/services/ImagesService.ts +++ b/invokeai/frontend/web/src/services/api/services/ImagesService.ts @@ -114,13 +114,18 @@ export class ImagesService { * @throws ApiError */ public static uploadImage({ + imageType, formData, }: { + imageType: ImageType, formData: Body_upload_image, }): CancelablePromise { return __request(OpenAPI, { method: 'POST', url: '/api/v1/images/uploads/', + query: { + 'image_type': imageType, + }, formData: formData, mediaType: 'multipart/form-data', errors: { diff --git a/invokeai/frontend/web/src/services/api/services/SessionsService.ts b/invokeai/frontend/web/src/services/api/services/SessionsService.ts index dad455fc80..694f3822cb 100644 --- a/invokeai/frontend/web/src/services/api/services/SessionsService.ts +++ b/invokeai/frontend/web/src/services/api/services/SessionsService.ts @@ -4,14 +4,20 @@ import type { AddInvocation } from '../models/AddInvocation'; import type { BlurInvocation } from '../models/BlurInvocation'; import type { CollectInvocation } from '../models/CollectInvocation'; +import type { CompelInvocation } from '../models/CompelInvocation'; import type { CropImageInvocation } from '../models/CropImageInvocation'; import type { CvInpaintInvocation } from '../models/CvInpaintInvocation'; +import type { DataURLToImageInvocation } from '../models/DataURLToImageInvocation'; import type { DivideInvocation } from '../models/DivideInvocation'; import type { Edge } from '../models/Edge'; import type { Graph } from '../models/Graph'; import type { GraphExecutionState } from '../models/GraphExecutionState'; import type { GraphInvocation } from '../models/GraphInvocation'; import type { ImageToImageInvocation } from '../models/ImageToImageInvocation'; +import type { ImageToLatentsInvocation } from '../models/ImageToLatentsInvocation'; +import type { InfillColorInvocation } from '../models/InfillColorInvocation'; +import type { InfillPatchMatchInvocation } from '../models/InfillPatchMatchInvocation'; +import type { InfillTileInvocation } from '../models/InfillTileInvocation'; import type { InpaintInvocation } from '../models/InpaintInvocation'; import type { InverseLerpInvocation } from '../models/InverseLerpInvocation'; import type { IterateInvocation } from '../models/IterateInvocation'; @@ -144,7 +150,7 @@ export class SessionsService { * The id of the session */ sessionId: string, - requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + requestBody: (LoadImageInvocation | ShowImageInvocation | DataURLToImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), }): CancelablePromise { return __request(OpenAPI, { method: 'POST', @@ -181,7 +187,7 @@ export class SessionsService { * The path to the node in the graph */ nodePath: string, - requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + requestBody: (LoadImageInvocation | ShowImageInvocation | DataURLToImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | CompelInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | ImageToLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | InfillColorInvocation | InfillTileInvocation | InfillPatchMatchInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), }): CancelablePromise { return __request(OpenAPI, { method: 'PUT', diff --git a/invokeai/frontend/web/src/services/events/middleware.ts b/invokeai/frontend/web/src/services/events/middleware.ts index 65652ae7ee..bd1d60099a 100644 --- a/invokeai/frontend/web/src/services/events/middleware.ts +++ b/invokeai/frontend/web/src/services/events/middleware.ts @@ -5,20 +5,11 @@ import { ClientToServerEvents, ServerToClientEvents, } from 'services/events/types'; -import { - invocationComplete, - socketSubscribed, - socketUnsubscribed, -} from './actions'; -import { AppDispatch, RootState } from 'app/store/store'; +import { socketSubscribed, socketUnsubscribed } from './actions'; +import { AppThunkDispatch, RootState } from 'app/store/store'; import { getTimestamp } from 'common/util/getTimestamp'; -import { - sessionInvoked, - isFulfilledSessionCreatedAction, -} from 'services/thunks/session'; +import { sessionInvoked, sessionCreated } from 'services/thunks/session'; import { OpenAPI } from 'services/api'; -import { isImageOutput } from 'services/types/guards'; -import { imageReceived, thumbnailReceived } from 'services/thunks/image'; import { setEventListeners } from 'services/events/util/setEventListeners'; import { log } from 'app/logging/useLogger'; @@ -56,20 +47,22 @@ export const socketMiddleware = () => { ); const middleware: Middleware = - (store: MiddlewareAPI) => (next) => (action) => { - const { dispatch, getState } = store; + (storeApi: MiddlewareAPI) => + (next) => + (action) => { + const { dispatch, getState } = storeApi; // Set listeners for `connect` and `disconnect` events once // Must happen in middleware to get access to `dispatch` if (!areListenersSet) { - setEventListeners({ store, socket, log: socketioLog }); + setEventListeners({ storeApi, socket, log: socketioLog }); areListenersSet = true; socket.connect(); } - if (isFulfilledSessionCreatedAction(action)) { + if (sessionCreated.fulfilled.match(action)) { const sessionId = action.payload.id; const sessionLog = socketioLog.child({ sessionId }); const oldSessionId = getState().system.sessionId; @@ -107,26 +100,6 @@ export const socketMiddleware = () => { dispatch(sessionInvoked({ sessionId })); } - if (invocationComplete.match(action)) { - const { config } = getState(); - - if (config.shouldFetchImages) { - const { result } = action.payload.data; - if (isImageOutput(result)) { - const imageName = result.image.image_name; - const imageType = result.image.image_type; - - dispatch(imageReceived({ imageName, imageType })); - dispatch( - thumbnailReceived({ - thumbnailName: imageName, - thumbnailType: imageType, - }) - ); - } - } - } - next(action); }; diff --git a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts index 581363e446..88bb11147c 100644 --- a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts +++ b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts @@ -27,13 +27,13 @@ import { addToast } from '../../../features/system/store/systemSlice'; type SetEventListenersArg = { socket: Socket; - store: MiddlewareAPI; + storeApi: MiddlewareAPI; log: Logger; }; export const setEventListeners = (arg: SetEventListenersArg) => { - const { socket, store, log } = arg; - const { dispatch, getState } = store; + const { socket, storeApi, log } = arg; + const { dispatch, getState } = storeApi; /** * Connect @@ -82,11 +82,18 @@ export const setEventListeners = (arg: SetEventListenersArg) => { socket.on('connect_error', (error) => { if (error && error.message) { - dispatch( - addToast( - makeToast({ title: error.message, status: 'error', duration: 10000 }) - ) - ); + const data: string | undefined = (error as any).data; + if (data === 'ERR_UNAUTHENTICATED') { + dispatch( + addToast( + makeToast({ + title: error.message, + status: 'error', + duration: 10000, + }) + ) + ); + } } }); diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts index a0d8f504b7..de1361be38 100644 --- a/invokeai/frontend/web/src/services/thunks/image.ts +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -1,8 +1,5 @@ -import { isFulfilled, isRejected } from '@reduxjs/toolkit'; import { log } from 'app/logging/useLogger'; import { createAppAsyncThunk } from 'app/store/storeUtils'; -import { imageSelected } from 'features/gallery/store/gallerySlice'; -import { clamp, isString } from 'lodash-es'; import { ImagesService } from 'services/api'; import { getHeaders } from 'services/util/getHeaders'; @@ -15,7 +12,7 @@ type ImageReceivedArg = Parameters<(typeof ImagesService)['getImage']>[0]; */ export const imageReceived = createAppAsyncThunk( 'api/imageReceived', - async (arg: ImageReceivedArg, _thunkApi) => { + async (arg: ImageReceivedArg) => { const response = await ImagesService.getImage(arg); imagesLog.info({ arg, response }, 'Received image'); @@ -33,7 +30,7 @@ type ThumbnailReceivedArg = Parameters< */ export const thumbnailReceived = createAppAsyncThunk( 'api/thumbnailReceived', - async (arg: ThumbnailReceivedArg, _thunkApi) => { + async (arg: ThumbnailReceivedArg) => { const response = await ImagesService.getThumbnail(arg); imagesLog.info({ arg, response }, 'Received thumbnail'); @@ -49,7 +46,7 @@ type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0]; */ export const imageUploaded = createAppAsyncThunk( 'api/imageUploaded', - async (arg: ImageUploadedArg, _thunkApi) => { + async (arg: ImageUploadedArg) => { const response = await ImagesService.uploadImage(arg); const { location } = getHeaders(response); @@ -62,11 +59,6 @@ export const imageUploaded = createAppAsyncThunk( } ); -/** - * Function to check if an action is a fulfilled `ImagesService.uploadImage()` thunk - */ -export const isFulfilledImageUploadedAction = isFulfilled(imageUploaded); - type ImageDeletedArg = Parameters<(typeof ImagesService)['deleteImage']>[0]; /** @@ -74,45 +66,7 @@ type ImageDeletedArg = Parameters<(typeof ImagesService)['deleteImage']>[0]; */ export const imageDeleted = createAppAsyncThunk( 'api/imageDeleted', - async (arg: ImageDeletedArg, { getState, dispatch }) => { - const { imageType, imageName } = arg; - - if (imageType !== 'uploads' && imageType !== 'results') { - return; - } - - // TODO: move this logic to another thunk? - // Determine which image should replace the deleted image, if the deleted image is the selected image. - // Unfortunately, we have to do this here, because the resultsSlice and uploadsSlice cannot change - // the selected image. - const selectedImageName = getState().gallery.selectedImage?.name; - - if (selectedImageName === imageName) { - const allIds = getState()[imageType].ids; - - const deletedImageIndex = allIds.findIndex( - (result) => result.toString() === imageName - ); - - const filteredIds = allIds.filter((id) => id.toString() !== imageName); - - const newSelectedImageIndex = clamp( - deletedImageIndex, - 0, - filteredIds.length - 1 - ); - - const newSelectedImageId = filteredIds[newSelectedImageIndex]; - - if (newSelectedImageId) { - dispatch( - imageSelected({ name: newSelectedImageId as string, type: imageType }) - ); - } else { - dispatch(imageSelected()); - } - } - + async (arg: ImageDeletedArg) => { const response = await ImagesService.deleteImage(arg); imagesLog.info( diff --git a/invokeai/frontend/web/src/services/thunks/session.ts b/invokeai/frontend/web/src/services/thunks/session.ts index 6bd7f01c26..dca4134886 100644 --- a/invokeai/frontend/web/src/services/thunks/session.ts +++ b/invokeai/frontend/web/src/services/thunks/session.ts @@ -1,52 +1,10 @@ import { createAppAsyncThunk } from 'app/store/storeUtils'; import { SessionsService } from 'services/api'; -import { buildLinearGraph as buildGenerateGraph } from 'features/nodes/util/linearGraphBuilder/buildLinearGraph'; -import { isAnyOf, isFulfilled } from '@reduxjs/toolkit'; -import { buildNodesGraph } from 'features/nodes/util/nodesGraphBuilder/buildNodesGraph'; import { log } from 'app/logging/useLogger'; import { serializeError } from 'serialize-error'; const sessionLog = log.child({ namespace: 'session' }); -export const generateGraphBuilt = createAppAsyncThunk( - 'api/generateGraphBuilt', - async (_, { dispatch, getState, rejectWithValue }) => { - try { - const graph = buildGenerateGraph(getState()); - dispatch(sessionCreated({ graph })); - return graph; - } catch (err: any) { - sessionLog.error( - { error: serializeError(err) }, - 'Problem building graph' - ); - return rejectWithValue(err.message); - } - } -); - -export const nodesGraphBuilt = createAppAsyncThunk( - 'api/nodesGraphBuilt', - async (_, { dispatch, getState, rejectWithValue }) => { - try { - const graph = buildNodesGraph(getState()); - dispatch(sessionCreated({ graph })); - return graph; - } catch (err: any) { - sessionLog.error( - { error: serializeError(err) }, - 'Problem building graph' - ); - return rejectWithValue(err.message); - } - } -); - -export const isFulfilledAnyGraphBuilt = isAnyOf( - generateGraphBuilt.fulfilled, - nodesGraphBuilt.fulfilled -); - type SessionCreatedArg = { graph: Parameters< (typeof SessionsService)['createSession'] @@ -58,22 +16,25 @@ type SessionCreatedArg = { */ export const sessionCreated = createAppAsyncThunk( 'api/sessionCreated', - async (arg: SessionCreatedArg, { dispatch, getState }) => { - const response = await SessionsService.createSession({ - requestBody: arg.graph, - }); - - sessionLog.info({ arg, response }, `Session created (${response.id})`); - - return response; + async (arg: SessionCreatedArg, { rejectWithValue }) => { + try { + const response = await SessionsService.createSession({ + requestBody: arg.graph, + }); + sessionLog.info({ arg, response }, `Session created (${response.id})`); + return response; + } catch (err: any) { + sessionLog.error( + { + error: serializeError(err), + }, + 'Problem creating session' + ); + return rejectWithValue(err.message); + } } ); -/** - * Function to check if an action is a fulfilled `SessionsService.createSession()` thunk - */ -export const isFulfilledSessionCreatedAction = isFulfilled(sessionCreated); - type NodeAddedArg = Parameters<(typeof SessionsService)['addNode']>[0]; /** diff --git a/invokeai/frontend/web/src/services/util/deserializeImageField.ts b/invokeai/frontend/web/src/services/util/deserializeImageField.ts index adda71ccdd..74d63117a4 100644 --- a/invokeai/frontend/web/src/services/util/deserializeImageField.ts +++ b/invokeai/frontend/web/src/services/util/deserializeImageField.ts @@ -1,6 +1,4 @@ -import { Image } from 'app/types/invokeai'; -import { ImageField, ImageType } from 'services/api'; -import { AnyInvocation } from 'services/events/types'; +import { ImageType } from 'services/api'; export const buildImageUrls = ( imageType: ImageType, diff --git a/invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts b/invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts index 386ca972b1..837a053664 100644 --- a/invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts +++ b/invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts @@ -12,12 +12,12 @@ export const makeGraphOfXImages = (numberOfImages: string) => prompt: 'pizza', steps: 50, seed: 123, - sampler_name: 'ddim', + scheduler: 'ddim', }) ) .reduce( (acc, val: TextToImageInvocation) => { - acc.nodes![val.id] = val; + if (acc.nodes) acc.nodes[val.id] = val; return acc; }, { nodes: {} } as Graph diff --git a/invokeai/frontend/web/src/theme/components/popover.ts b/invokeai/frontend/web/src/theme/components/popover.ts index 449f1d926c..c8d6ae20d8 100644 --- a/invokeai/frontend/web/src/theme/components/popover.ts +++ b/invokeai/frontend/web/src/theme/components/popover.ts @@ -20,9 +20,6 @@ const invokeAIContent = defineStyle((_props) => { minW: 'unset', width: 'unset', p: 4, - borderWidth: '2px', - borderStyle: 'solid', - borderColor: 'base.600', bg: 'base.800', }; }); diff --git a/invokeai/frontend/web/src/theme/util/constants.ts b/invokeai/frontend/web/src/theme/util/constants.ts index 38b859913c..722b899407 100644 --- a/invokeai/frontend/web/src/theme/util/constants.ts +++ b/invokeai/frontend/web/src/theme/util/constants.ts @@ -11,7 +11,7 @@ export const APP_GALLERY_POPOVER_HEIGHT = `calc(100vh - (${APP_CONTENT_HEIGHT_CU export const APP_METADATA_HEIGHT = `calc(100vh - (${APP_CONTENT_HEIGHT_CUTOFF} + 4.4rem))`; // this is in pixels -export const PARAMETERS_PANEL_WIDTH = 384; +// export const PARAMETERS_PANEL_WIDTH = 384; // do not touch ffs export const APP_TEXT_TO_IMAGE_HEIGHT = @@ -19,3 +19,5 @@ export const APP_TEXT_TO_IMAGE_HEIGHT = // option bar export const OPTIONS_BAR_MAX_WIDTH = '22.5rem'; + +export const PARAMETERS_PANEL_WIDTH = '28rem'; diff --git a/invokeai/frontend/web/tsconfig.json b/invokeai/frontend/web/tsconfig.json index 8276f461eb..3c777b9318 100644 --- a/invokeai/frontend/web/tsconfig.json +++ b/invokeai/frontend/web/tsconfig.json @@ -20,7 +20,13 @@ "*": ["./src/*"] } }, - "include": ["src/**/*.ts", "src/**/*.tsx", "*.d.ts"], + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "*.d.ts", + "src/app/store/middleware/listenerMiddleware", + "src/features/nodes/util/edgeBuilders" + ], "exclude": ["src/services/fixtures/*", "node_modules", "dist"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index 59a2607db7..90bc4055ac 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -1836,7 +1836,7 @@ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== -"@types/hoist-non-react-statics@^3.3.1": +"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== @@ -1907,6 +1907,16 @@ dependencies: "@types/react" "*" +"@types/react-redux@^7.1.25": + version "7.1.25" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88" + integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-transition-group@^4.4.5": version "4.4.5" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" @@ -5569,6 +5579,11 @@ react-remove-scroll@^2.5.5: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-resizable-panels@^0.0.42: + version "0.0.42" + resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-0.0.42.tgz#e1a5d7fde7be4d18f32d0e021a0b4edb28b9edfe" + integrity sha512-nOaN9DeUTsmKjN3MFGaLd35kngGyO29SHRLdBRafYR1SV2F/LbWbpVUKVPwL2fBBTnQe2/rqOQwT4k+3cKeK+w== + react-rnd@^10.4.1: version "10.4.1" resolved "https://registry.yarnpkg.com/react-rnd/-/react-rnd-10.4.1.tgz#9e1c3f244895d7862ef03be98b2a620848c3fba1" @@ -5682,12 +5697,17 @@ redux-persist@^6.0.0: resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ== +redux-remember@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/redux-remember/-/redux-remember-3.3.1.tgz#fad0b3af81458d8e40a54cd30be148c17e40bda9" + integrity sha512-x30eZpdryapH8+hinYcyoTiGCSmtPUPdvL7OxjpMeRgTckJrVW57FgRAmiv41COqi/q4H+qn65Uftsasqj+F9A== + redux-thunk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== -redux@^4.2.1: +redux@^4.0.0, redux@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== diff --git a/tests/nodes/test_png_metadata_service.py b/tests/nodes/test_png_metadata_service.py index 3075af5a4b..c724074518 100644 --- a/tests/nodes/test_png_metadata_service.py +++ b/tests/nodes/test_png_metadata_service.py @@ -18,9 +18,7 @@ valid_metadata = { "height": 512, "cfg_scale": 7.5, "scheduler": "k_lms", - "seamless": False, "model": "stable-diffusion-1.5", - "progress_images": True, }, }
{metadataJSON}
{t('parameters.shuffle')}