Merge branch 'main' into feat/compel_node

This commit is contained in:
StAlKeR7779 2023-04-27 14:53:10 +03:00 committed by GitHub
commit 0b0068ab86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
118 changed files with 3044 additions and 1661 deletions

View File

@ -33,6 +33,8 @@
</div>
_**Note: The UI is not fully functional on `main`. If you need a stable UI based on `main`, use the `pre-nodes` tag while we [migrate to a new backend](https://github.com/invoke-ai/InvokeAI/discussions/3246).**_
InvokeAI is a leading creative engine built to empower professionals and enthusiasts alike. Generate and create stunning visual media using the latest AI-driven technologies. InvokeAI offers an industry leading Web Interface, interactive Command Line Interface, and also serves as the foundation for multiple commercial products.
**Quick links**: [[How to Install](https://invoke-ai.github.io/InvokeAI/#installation)] [<a href="https://discord.gg/ZmtBAhwWhy">Discord Server</a>] [<a href="https://invoke-ai.github.io/InvokeAI/">Documentation and Tutorials</a>] [<a href="https://github.com/invoke-ai/InvokeAI/">Code and Downloads</a>] [<a href="https://github.com/invoke-ai/InvokeAI/issues">Bug Reports</a>] [<a href="https://github.com/invoke-ai/InvokeAI/discussions">Discussion, Ideas & Q&A</a>]

View File

@ -32,3 +32,9 @@ class ProgressImage(BaseModel):
width: int = Field(description="The effective width of the image in pixels")
height: int = Field(description="The effective height of the image in pixels")
dataURL: str = Field(description="The image data as a b64 data URL")
class SavedImage(BaseModel):
image_name: str = Field(description="The name of the saved image")
thumbnail_name: str = Field(description="The name of the saved thumbnail")
created: int = Field(description="The created timestamp of the saved image")

View File

@ -6,12 +6,14 @@ import os
from typing import Any
import uuid
from fastapi import HTTPException, Path, Query, Request, UploadFile
from fastapi import Body, HTTPException, Path, Query, Request, UploadFile
from fastapi.responses import FileResponse, Response
from fastapi.routing import APIRouter
from PIL import Image
from invokeai.app.api.models.images import ImageResponse, ImageResponseMetadata
from invokeai.app.services.metadata import InvokeAIMetadata
from invokeai.app.api.models.images import (
ImageResponse,
ImageResponseMetadata,
)
from invokeai.app.services.item_storage import PaginatedResults
from ...services.image_storage import ImageType
@ -24,8 +26,8 @@ images_router = APIRouter(prefix="/v1/images", tags=["images"])
async def get_image(
image_type: ImageType = Path(description="The type of image to get"),
image_name: str = Path(description="The name of the image to get"),
) -> FileResponse | Response:
"""Gets a result"""
) -> FileResponse:
"""Gets an image"""
path = ApiDependencies.invoker.services.images.get_path(
image_type=image_type, image_name=image_name
@ -37,17 +39,29 @@ async def get_image(
raise HTTPException(status_code=404)
@images_router.delete("/{image_type}/{image_name}", operation_id="delete_image")
async def delete_image(
image_type: ImageType = Path(description="The type of image to delete"),
image_name: str = Path(description="The name of the image to delete"),
) -> None:
"""Deletes an image and its thumbnail"""
ApiDependencies.invoker.services.images.delete(
image_type=image_type, image_name=image_name
)
@images_router.get(
"/{image_type}/thumbnails/{image_name}", operation_id="get_thumbnail"
"/{thumbnail_type}/thumbnails/{thumbnail_name}", operation_id="get_thumbnail"
)
async def get_thumbnail(
image_type: ImageType = Path(description="The type of image to get"),
image_name: str = Path(description="The name of the image to get"),
thumbnail_type: ImageType = Path(description="The type of thumbnail to get"),
thumbnail_name: str = Path(description="The name of the thumbnail to get"),
) -> FileResponse | Response:
"""Gets a thumbnail"""
path = ApiDependencies.invoker.services.images.get_path(
image_type=image_type, image_name=image_name, is_thumbnail=True
image_type=thumbnail_type, image_name=thumbnail_name, is_thumbnail=True
)
if ApiDependencies.invoker.services.images.validate_path(path):
@ -84,19 +98,27 @@ async def upload_image(
filename = f"{uuid.uuid4()}_{str(int(datetime.now(timezone.utc).timestamp()))}.png"
(image_path, thumbnail_path, ctime) = ApiDependencies.invoker.services.images.save(
saved_image = ApiDependencies.invoker.services.images.save(
ImageType.UPLOAD, 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
)
thumbnail_url = ApiDependencies.invoker.services.images.get_uri(
ImageType.UPLOAD, saved_image.image_name, True
)
res = ImageResponse(
image_type=ImageType.UPLOAD,
image_name=filename,
image_url=f"api/v1/images/{ImageType.UPLOAD.value}/{filename}",
thumbnail_url=f"api/v1/images/{ImageType.UPLOAD.value}/thumbnails/{os.path.splitext(filename)[0]}.webp",
image_name=saved_image.image_name,
image_url=image_url,
thumbnail_url=thumbnail_url,
metadata=ImageResponseMetadata(
created=ctime,
created=saved_image.created,
width=img.width,
height=img.height,
invokeai=invokeai_metadata,
@ -104,9 +126,7 @@ async def upload_image(
)
response.status_code = 201
response.headers["Location"] = request.url_for(
"get_image", image_type=ImageType.UPLOAD.value, image_name=filename
)
response.headers["Location"] = image_url
return res

View File

@ -2,8 +2,7 @@
from typing import Annotated, List, Optional, Union
from fastapi import Body, Path, Query
from fastapi.responses import Response
from fastapi import Body, HTTPException, Path, Query, Response
from fastapi.routing import APIRouter
from pydantic.fields import Field
@ -76,7 +75,7 @@ async def get_session(
"""Gets a session"""
session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id)
if session is None:
return Response(status_code=404)
raise HTTPException(status_code=404)
else:
return session
@ -99,7 +98,7 @@ async def add_node(
"""Adds a node to the graph"""
session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id)
if session is None:
return Response(status_code=404)
raise HTTPException(status_code=404)
try:
session.add_node(node)
@ -108,9 +107,9 @@ async def add_node(
) # TODO: can this be done automatically, or add node through an API?
return session.id
except NodeAlreadyExecutedError:
return Response(status_code=400)
raise HTTPException(status_code=400)
except IndexError:
return Response(status_code=400)
raise HTTPException(status_code=400)
@session_router.put(
@ -132,7 +131,7 @@ async def update_node(
"""Updates a node in the graph and removes all linked edges"""
session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id)
if session is None:
return Response(status_code=404)
raise HTTPException(status_code=404)
try:
session.update_node(node_path, node)
@ -141,9 +140,9 @@ async def update_node(
) # TODO: can this be done automatically, or add node through an API?
return session
except NodeAlreadyExecutedError:
return Response(status_code=400)
raise HTTPException(status_code=400)
except IndexError:
return Response(status_code=400)
raise HTTPException(status_code=400)
@session_router.delete(
@ -162,7 +161,7 @@ async def delete_node(
"""Deletes a node in the graph and removes all linked edges"""
session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id)
if session is None:
return Response(status_code=404)
raise HTTPException(status_code=404)
try:
session.delete_node(node_path)
@ -171,9 +170,9 @@ async def delete_node(
) # TODO: can this be done automatically, or add node through an API?
return session
except NodeAlreadyExecutedError:
return Response(status_code=400)
raise HTTPException(status_code=400)
except IndexError:
return Response(status_code=400)
raise HTTPException(status_code=400)
@session_router.post(
@ -192,7 +191,7 @@ async def add_edge(
"""Adds an edge to the graph"""
session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id)
if session is None:
return Response(status_code=404)
raise HTTPException(status_code=404)
try:
session.add_edge(edge)
@ -201,9 +200,9 @@ async def add_edge(
) # TODO: can this be done automatically, or add node through an API?
return session
except NodeAlreadyExecutedError:
return Response(status_code=400)
raise HTTPException(status_code=400)
except IndexError:
return Response(status_code=400)
raise HTTPException(status_code=400)
# TODO: the edge being in the path here is really ugly, find a better solution
@ -226,7 +225,7 @@ async def delete_edge(
"""Deletes an edge from the graph"""
session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id)
if session is None:
return Response(status_code=404)
raise HTTPException(status_code=404)
try:
edge = Edge(
@ -239,9 +238,9 @@ async def delete_edge(
) # TODO: can this be done automatically, or add node through an API?
return session
except NodeAlreadyExecutedError:
return Response(status_code=400)
raise HTTPException(status_code=400)
except IndexError:
return Response(status_code=400)
raise HTTPException(status_code=400)
@session_router.put(
@ -259,14 +258,14 @@ async def invoke_session(
all: bool = Query(
default=False, description="Whether or not to invoke all remaining invocations"
),
) -> None:
) -> Response:
"""Invokes a session"""
session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id)
if session is None:
return Response(status_code=404)
raise HTTPException(status_code=404)
if session.is_complete():
return Response(status_code=400)
raise HTTPException(status_code=400)
ApiDependencies.invoker.invoke(session, invoke_all=all)
return Response(status_code=202)
@ -281,7 +280,7 @@ async def invoke_session(
)
async def cancel_session_invoke(
session_id: str = Path(description="The id of the session to cancel"),
) -> None:
) -> Response:
"""Invokes a session"""
ApiDependencies.invoker.cancel(session_id)
return Response(status_code=202)

View File

@ -5,11 +5,16 @@ from glob import glob
from abc import ABC, abstractmethod
from pathlib import Path
from queue import Queue
from typing import Dict, List, Tuple
from typing import Dict, List
from PIL.Image import Image
import PIL.Image as PILImage
from invokeai.app.api.models.images import ImageResponse, ImageResponseMetadata
from send2trash import send2trash
from invokeai.app.api.models.images import (
ImageResponse,
ImageResponseMetadata,
SavedImage,
)
from invokeai.app.models.image import ImageType
from invokeai.app.services.metadata import (
InvokeAIMetadata,
@ -41,7 +46,15 @@ class ImageStorageBase(ABC):
def get_path(
self, image_type: ImageType, image_name: str, is_thumbnail: bool = False
) -> str:
"""Gets the path to an image or its thumbnail."""
"""Gets the internal path to an image or its thumbnail."""
pass
# TODO: make this a bit more flexible for e.g. cloud storage
@abstractmethod
def get_uri(
self, image_type: ImageType, image_name: str, is_thumbnail: bool = False
) -> str:
"""Gets the external URI to an image or its thumbnail."""
pass
# TODO: make this a bit more flexible for e.g. cloud storage
@ -57,8 +70,8 @@ class ImageStorageBase(ABC):
image_name: str,
image: Image,
metadata: InvokeAIMetadata | None = None,
) -> Tuple[str, str, int]:
"""Saves an image and a 256x256 WEBP thumbnail. Returns a tuple of the image path, thumbnail path, and created timestamp."""
) -> SavedImage:
"""Saves an image and a 256x256 WEBP thumbnail. Returns a tuple of the image name, thumbnail name, and created timestamp."""
pass
@abstractmethod
@ -126,8 +139,8 @@ class DiskImageStorage(ImageStorageBase):
image_type=image_type.value,
image_name=filename,
# TODO: DiskImageStorage should not be building URLs...?
image_url=f"api/v1/images/{image_type.value}/{filename}",
thumbnail_url=f"api/v1/images/{image_type.value}/thumbnails/{os.path.splitext(filename)[0]}.webp",
image_url=self.get_uri(image_type, filename),
thumbnail_url=self.get_uri(image_type, filename, True),
# TODO: Creation of this object should happen elsewhere (?), just making it fit here so it works
metadata=ImageResponseMetadata(
created=int(os.path.getctime(path)),
@ -174,7 +187,23 @@ class DiskImageStorage(ImageStorageBase):
else:
path = os.path.join(self.__output_folder, image_type, basename)
return path
abspath = os.path.abspath(path)
return abspath
def get_uri(
self, image_type: ImageType, image_name: str, is_thumbnail: bool = False
) -> str:
# strip out any relative path shenanigans
basename = os.path.basename(image_name)
if is_thumbnail:
thumbnail_basename = get_thumbnail_name(basename)
uri = f"api/v1/images/{image_type.value}/thumbnails/{thumbnail_basename}"
else:
uri = f"api/v1/images/{image_type.value}/{basename}"
return uri
def validate_path(self, path: str) -> bool:
try:
@ -189,7 +218,7 @@ class DiskImageStorage(ImageStorageBase):
image_name: str,
image: Image,
metadata: InvokeAIMetadata | None = None,
) -> Tuple[str, str, int]:
) -> SavedImage:
image_path = self.get_path(image_type, image_name)
# TODO: Reading the image and then saving it strips the metadata...
@ -197,7 +226,7 @@ class DiskImageStorage(ImageStorageBase):
pnginfo = build_invokeai_metadata_pnginfo(metadata=metadata)
image.save(image_path, "PNG", pnginfo=pnginfo)
else:
image.save(image_path) # this saved image has an empty info
image.save(image_path) # this saved image has an empty info
thumbnail_name = get_thumbnail_name(image_name)
thumbnail_path = self.get_path(image_type, thumbnail_name, is_thumbnail=True)
@ -207,24 +236,30 @@ class DiskImageStorage(ImageStorageBase):
self.__set_cache(image_path, image)
self.__set_cache(thumbnail_path, thumbnail_image)
return (image_path, thumbnail_path, int(os.path.getctime(image_path)))
return SavedImage(
image_name=image_name,
thumbnail_name=thumbnail_name,
created=int(os.path.getctime(image_path)),
)
def delete(self, image_type: ImageType, image_name: str) -> None:
image_path = self.get_path(image_type, image_name)
thumbnail_path = self.get_path(image_type, image_name, True)
if os.path.exists(image_path):
os.remove(image_path)
basename = os.path.basename(image_name)
image_path = self.get_path(image_type, basename)
if os.path.exists(image_path):
send2trash(image_path)
if image_path in self.__cache:
del self.__cache[image_path]
if os.path.exists(thumbnail_path):
os.remove(thumbnail_path)
thumbnail_name = get_thumbnail_name(image_name)
thumbnail_path = self.get_path(image_type, thumbnail_name, True)
if os.path.exists(thumbnail_path):
send2trash(thumbnail_path)
if thumbnail_path in self.__cache:
del self.__cache[thumbnail_path]
def __get_cache(self, image_name: str) -> Image:
def __get_cache(self, image_name: str) -> Image | None:
return None if image_name not in self.__cache else self.__cache[image_name]
def __set_cache(self, image_name: str, image: Image):

View File

@ -10,7 +10,7 @@ from .generator import (
Img2Img,
Inpaint
)
from .model_management import ModelManager
from .model_management import ModelManager, SDModelComponent
from .safety_checker import SafetyChecker
from .args import Args
from .globals import Globals

View File

@ -5,6 +5,7 @@ from .convert_ckpt_to_diffusers import (
convert_ckpt_to_diffusers,
load_pipeline_from_original_stable_diffusion_ckpt,
)
from .model_manager import ModelManager
from .model_manager import ModelManager,SDModelComponent

View File

@ -445,8 +445,15 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
@property
def _submodels(self) -> Sequence[torch.nn.Module]:
module_names, _, _ = self.extract_init_dict(dict(self.config))
values = [getattr(self, name) for name in module_names.keys()]
return [m for m in values if isinstance(m, torch.nn.Module)]
submodels = []
for name in module_names.keys():
if hasattr(self, name):
value = getattr(self, name)
else:
value = getattr(self.config, name)
if isinstance(value, torch.nn.Module):
submodels.append(value)
return submodels
def image_from_embeddings(
self,
@ -544,7 +551,7 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
yield PipelineIntermediateState(
run_id=run_id,
step=-1,
timestep=self.scheduler.num_train_timesteps,
timestep=self.scheduler.config.num_train_timesteps,
latents=latents,
)
@ -915,7 +922,7 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline):
@property
def channels(self) -> int:
"""Compatible with DiffusionWrapper"""
return self.unet.in_channels
return self.unet.config.in_channels
def decode_latents(self, latents):
# Explicit call to get the vae loaded, since `decode` isn't the forward method.

View File

@ -10,8 +10,7 @@ import diffusers
import psutil
import torch
from compel.cross_attention_control import Arguments
from diffusers.models.cross_attention import AttnProcessor
from diffusers.models.unet_2d_condition import UNet2DConditionModel
from diffusers.models.attention_processor import AttentionProcessor
from torch import nn
from ...util import torch_dtype
@ -188,7 +187,7 @@ class Context:
class InvokeAICrossAttentionMixin:
"""
Enable InvokeAI-flavoured CrossAttention calculation, which does aggressive low-memory slicing and calls
Enable InvokeAI-flavoured Attention calculation, which does aggressive low-memory slicing and calls
through both to an attention_slice_wrangler and a slicing_strategy_getter for custom attention map wrangling
and dymamic slicing strategy selection.
"""
@ -209,7 +208,7 @@ class InvokeAICrossAttentionMixin:
Set custom attention calculator to be called when attention is calculated
:param wrangler: Callback, with args (module, suggested_attention_slice, dim, offset, slice_size),
which returns either the suggested_attention_slice or an adjusted equivalent.
`module` is the current CrossAttention module for which the callback is being invoked.
`module` is the current Attention module for which the callback is being invoked.
`suggested_attention_slice` is the default-calculated attention slice
`dim` is -1 if the attenion map has not been sliced, or 0 or 1 for dimension-0 or dimension-1 slicing.
If `dim` is >= 0, `offset` and `slice_size` specify the slice start and length.
@ -345,11 +344,11 @@ class InvokeAICrossAttentionMixin:
def restore_default_cross_attention(
model,
is_running_diffusers: bool,
restore_attention_processor: Optional[AttnProcessor] = None,
restore_attention_processor: Optional[AttentionProcessor] = None,
):
if is_running_diffusers:
unet = model
unet.set_attn_processor(restore_attention_processor or CrossAttnProcessor())
unet.set_attn_processor(restore_attention_processor or AttnProcessor())
else:
remove_attention_function(model)
@ -408,12 +407,9 @@ def override_cross_attention(model, context: Context, is_running_diffusers=False
def get_cross_attention_modules(
model, which: CrossAttentionType
) -> list[tuple[str, InvokeAICrossAttentionMixin]]:
from ldm.modules.attention import CrossAttention # avoid circular import
cross_attention_class: type = (
InvokeAIDiffusersCrossAttention
if isinstance(model, UNet2DConditionModel)
else CrossAttention
)
which_attn = "attn1" if which is CrossAttentionType.SELF else "attn2"
attention_module_tuples = [
@ -428,10 +424,10 @@ def get_cross_attention_modules(
print(
f"Error! CrossAttentionControl found an unexpected number of {cross_attention_class} modules in the model "
+ f"(expected {expected_count}, found {cross_attention_modules_in_model_count}). Either monkey-patching failed "
+ f"or some assumption has changed about the structure of the model itself. Please fix the monkey-patching, "
+ "or some assumption has changed about the structure of the model itself. Please fix the monkey-patching, "
+ f"and/or update the {expected_count} above to an appropriate number, and/or find and inform someone who knows "
+ f"what it means. This error is non-fatal, but it is likely that .swap() and attention map display will not "
+ f"work properly until it is fixed."
+ "what it means. This error is non-fatal, but it is likely that .swap() and attention map display will not "
+ "work properly until it is fixed."
)
return attention_module_tuples
@ -550,7 +546,7 @@ def get_mem_free_total(device):
class InvokeAIDiffusersCrossAttention(
diffusers.models.attention.CrossAttention, InvokeAICrossAttentionMixin
diffusers.models.attention.Attention, InvokeAICrossAttentionMixin
):
def __init__(self, **kwargs):
super().__init__(**kwargs)
@ -572,8 +568,8 @@ class InvokeAIDiffusersCrossAttention(
"""
# base implementation
class CrossAttnProcessor:
def __call__(self, attn: CrossAttention, hidden_states, encoder_hidden_states=None, attention_mask=None):
class AttnProcessor:
def __call__(self, attn: Attention, hidden_states, encoder_hidden_states=None, attention_mask=None):
batch_size, sequence_length, _ = hidden_states.shape
attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length)
@ -601,9 +597,9 @@ class CrossAttnProcessor:
from dataclasses import dataclass, field
import torch
from diffusers.models.cross_attention import (
CrossAttention,
CrossAttnProcessor,
from diffusers.models.attention_processor import (
Attention,
AttnProcessor,
SlicedAttnProcessor,
)
@ -653,7 +649,7 @@ class SlicedSwapCrossAttnProcesser(SlicedAttnProcessor):
def __call__(
self,
attn: CrossAttention,
attn: Attention,
hidden_states,
encoder_hidden_states=None,
attention_mask=None,

View File

@ -5,7 +5,7 @@ from typing import Any, Callable, Dict, Optional, Union
import numpy as np
import torch
from diffusers.models.cross_attention import AttnProcessor
from diffusers.models.attention_processor import AttentionProcessor
from typing_extensions import TypeAlias
from invokeai.backend.globals import Globals
@ -101,7 +101,7 @@ class InvokeAIDiffuserComponent:
def override_cross_attention(
self, conditioning: ExtraConditioningInfo, step_count: int
) -> Dict[str, AttnProcessor]:
) -> Dict[str, AttentionProcessor]:
"""
setup cross attention .swap control. for diffusers this replaces the attention processor, so
the previous attention processor is returned so that the caller can restore it later.
@ -118,7 +118,7 @@ class InvokeAIDiffuserComponent:
)
def restore_default_cross_attention(
self, restore_attention_processor: Optional["AttnProcessor"] = None
self, restore_attention_processor: Optional["AttentionProcessor"] = None
):
self.conditioning = None
self.cross_attention_control_context = None
@ -262,7 +262,7 @@ class InvokeAIDiffuserComponent:
# TODO remove when compvis codepath support is dropped
if step_index is None and sigma is None:
raise ValueError(
f"Either step_index or sigma is required when doing cross attention control, but both are None."
"Either step_index or sigma is required when doing cross attention control, but both are None."
)
percent_through = self.estimate_percent_through(step_index, sigma)
return percent_through
@ -599,7 +599,6 @@ class InvokeAIDiffuserComponent:
)
# below is fugly omg
num_actual_conditionings = len(c_or_weighted_c_list)
conditionings = [uc] + [c for c, weight in weighted_cond_list]
weights = [1] + [weight for c, weight in weighted_cond_list]
chunk_count = ceil(len(conditionings) / 2)

View File

@ -1,10 +1,9 @@
"""
'''
Minimalist updater script. Prompts user for the tag or branch to update to and runs
pip install <path_to_git_source>.
"""
'''
import os
import platform
import requests
from rich import box, print
from rich.console import Console, Group, group
@ -16,8 +15,10 @@ from rich.text import Text
from invokeai.version import __version__
INVOKE_AI_SRC = "https://github.com/invoke-ai/InvokeAI/archive"
INVOKE_AI_REL = "https://api.github.com/repos/invoke-ai/InvokeAI/releases"
INVOKE_AI_SRC="https://github.com/invoke-ai/InvokeAI/archive"
INVOKE_AI_TAG="https://github.com/invoke-ai/InvokeAI/archive/refs/tags"
INVOKE_AI_BRANCH="https://github.com/invoke-ai/InvokeAI/archive/refs/heads"
INVOKE_AI_REL="https://api.github.com/repos/invoke-ai/InvokeAI/releases"
OS = platform.uname().system
ARCH = platform.uname().machine
@ -28,22 +29,22 @@ if OS == "Windows":
else:
console = Console(style=Style(color="grey74", bgcolor="grey19"))
def get_versions() -> dict:
def get_versions()->dict:
return requests.get(url=INVOKE_AI_REL).json()
def welcome(versions: dict):
@group()
def text():
yield f"InvokeAI Version: [bold yellow]{__version__}"
yield ""
yield "This script will update InvokeAI to the latest release, or to a development version of your choice."
yield ""
yield "[bold yellow]Options:"
yield f"""[1] Update to the latest official release ([italic]{versions[0]['tag_name']}[/italic])
yield f'InvokeAI Version: [bold yellow]{__version__}'
yield ''
yield 'This script will update InvokeAI to the latest release, or to a development version of your choice.'
yield ''
yield '[bold yellow]Options:'
yield f'''[1] Update to the latest official release ([italic]{versions[0]['tag_name']}[/italic])
[2] Update to the bleeding-edge development version ([italic]main[/italic])
[3] Manually enter the tag or branch name you wish to update"""
[3] Manually enter the [bold]tag name[/bold] for the version you wish to update to
[4] Manually enter the [bold]branch name[/bold] for the version you wish to update to'''
console.rule()
print(
@ -59,33 +60,41 @@ def welcome(versions: dict):
)
console.line()
def main():
versions = get_versions()
welcome(versions)
tag = None
choice = Prompt.ask("Choice:", choices=["1", "2", "3"], default="1")
branch = None
release = None
choice = Prompt.ask('Choice:',choices=['1','2','3','4'],default='1')
if choice=='1':
release = versions[0]['tag_name']
elif choice=='2':
release = 'main'
elif choice=='3':
tag = Prompt.ask('Enter an InvokeAI tag name')
elif choice=='4':
branch = Prompt.ask('Enter an InvokeAI branch name')
if choice == "1":
tag = versions[0]["tag_name"]
elif choice == "2":
tag = "main"
elif choice == "3":
tag = Prompt.ask("Enter an InvokeAI tag or branch name")
print(f":crossed_fingers: Upgrading to [yellow]{tag}[/yellow]")
cmd = f"pip install {INVOKE_AI_SRC}/{tag}.zip --use-pep517"
print("")
print("")
if os.system(cmd) == 0:
print(f":heavy_check_mark: Upgrade successful")
print(f':crossed_fingers: Upgrading to [yellow]{tag if tag else release}[/yellow]')
if release:
cmd = f'pip install {INVOKE_AI_SRC}/{release}.zip --use-pep517 --upgrade'
elif tag:
cmd = f'pip install {INVOKE_AI_TAG}/{tag}.zip --use-pep517 --upgrade'
else:
print(f":exclamation: [bold red]Upgrade failed[/red bold]")
cmd = f'pip install {INVOKE_AI_BRANCH}/{branch}.zip --use-pep517 --upgrade'
print('')
print('')
if os.system(cmd)==0:
print(f':heavy_check_mark: Upgrade successful')
else:
print(f':exclamation: [bold red]Upgrade failed[/red bold]')
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
pass

View File

@ -110,6 +110,7 @@
"prettier": "^2.8.4",
"rollup-plugin-visualizer": "^5.9.0",
"terser": "^5.16.4",
"ts-toolbelt": "^9.6.0",
"typescript": "4.9.5",
"vite": "^4.1.2",
"vite-plugin-eslint": "^1.8.1",

View File

@ -63,7 +63,7 @@
"postProcessDesc3": "The Invoke AI Command Line Interface offers various other features including Embiggen.",
"training": "Training",
"trainingDesc1": "A dedicated workflow for training your own embeddings and checkpoints using Textual Inversion and Dreambooth from the web interface.",
"trainingDesc2": "InvokeAI already supports training custom embeddings using Textual Inversion using the main script.",
"trainingDesc2": "InvokeAI already supports training custom embeddourings using Textual Inversion using the main script.",
"upload": "Upload",
"close": "Close",
"cancel": "Cancel",
@ -97,7 +97,12 @@
"statusMergedModels": "Models Merged",
"pinOptionsPanel": "Pin Options Panel",
"loading": "Loading",
"loadingInvokeAI": "Loading Invoke AI"
"loadingInvokeAI": "Loading Invoke AI",
"random": "Random",
"generate": "Generate",
"openInNewTab": "Open in New Tab",
"dontAskMeAgain": "Don't ask me again",
"areYouSure": "Are you sure?"
},
"gallery": {
"generations": "Generations",
@ -113,7 +118,10 @@
"pinGallery": "Pin Gallery",
"allImagesLoaded": "All Images Loaded",
"loadMore": "Load More",
"noImagesInGallery": "No Images In Gallery"
"noImagesInGallery": "No Images In Gallery",
"deleteImage": "Delete Image",
"deleteImageBin": "Deleted images will be sent to your operating system's Bin.",
"deleteImagePermanent": "Deleted images cannot be restored."
},
"hotkeys": {
"keyboardShortcuts": "Keyboard Shortcuts",
@ -505,7 +513,6 @@
"useAll": "Use All",
"useInitImg": "Use Initial Image",
"info": "Info",
"deleteImage": "Delete Image",
"initialImage": "Initial Image",
"showOptionsPanel": "Show Options Panel",
"hidePreview": "Hide Preview",

View File

@ -1,39 +0,0 @@
import { Flex, Spinner, Text } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
interface LoaderProps {
showText?: boolean;
text?: string;
}
// This component loads before the theme so we cannot use theme tokens here
const Loading = (props: LoaderProps) => {
const { t } = useTranslation();
const { showText = false, text = t('common.loadingInvokeAI') } = props;
return (
<Flex
width="100vw"
height="100vh"
alignItems="center"
justifyContent="center"
bg="#121212"
flexDirection="column"
rowGap={4}
>
<Spinner color="grey" w="5rem" h="5rem" />
{showText && (
<Text
color="grey"
fontWeight="semibold"
fontFamily="'Inter', sans-serif"
>
{text}
</Text>
)}
</Flex>
);
};
export default Loading;

View File

@ -14,55 +14,52 @@ import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
import ImageGalleryPanel from 'features/gallery/components/ImageGalleryPanel';
import Lightbox from 'features/lightbox/components/Lightbox';
import { useAppDispatch, useAppSelector } from './storeHooks';
import { PropsWithChildren, useEffect } from 'react';
import { setDisabledPanels, setDisabledTabs } from 'features/ui/store/uiSlice';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { shouldTransformUrlsChanged } from 'features/system/store/systemSlice';
import { setShouldFetchImages } from 'features/gallery/store/resultsSlice';
import { PropsWithChildren, useCallback, useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import Loading from 'common/components/Loading/Loading';
import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady';
import { PartialAppConfig } from './invokeai';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
import { configChanged } from 'features/system/store/configSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
keepGUIAlive();
interface Props extends PropsWithChildren {
options: {
disabledPanels: string[];
disabledTabs: InvokeTabName[];
shouldTransformUrls?: boolean;
shouldFetchImages: boolean;
};
config?: PartialAppConfig;
}
const App = (props: Props) => {
const App = ({ config = {}, children }: Props) => {
useToastWatcher();
useGlobalHotkeys();
const currentTheme = useAppSelector((state) => state.ui.currentTheme);
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
const isApplicationReady = useIsApplicationReady();
const [loadingOverridden, setLoadingOverridden] = useState(false);
const { setColorMode } = useColorMode();
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setDisabledPanels(props.options.disabledPanels));
}, [dispatch, props.options.disabledPanels]);
useEffect(() => {
dispatch(setDisabledTabs(props.options.disabledTabs));
}, [dispatch, props.options.disabledTabs]);
useEffect(() => {
dispatch(
shouldTransformUrlsChanged(Boolean(props.options.shouldTransformUrls))
);
}, [dispatch, props.options.shouldTransformUrls]);
useEffect(() => {
dispatch(setShouldFetchImages(props.options.shouldFetchImages));
}, [dispatch, props.options.shouldFetchImages]);
console.log('Received config: ', config);
dispatch(configChanged(config));
}, [dispatch, config]);
useEffect(() => {
setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark');
}, [setColorMode, currentTheme]);
const handleOverrideClicked = useCallback(() => {
setLoadingOverridden(true);
}, []);
return (
<Grid w="100vw" h="100vh">
<Lightbox />
<Grid w="100vw" h="100vh" position="relative">
{isLightboxEnabled && <Lightbox />}
<ImageUploader>
<ProgressBar />
<Grid
@ -72,7 +69,7 @@ const App = (props: Props) => {
w={APP_WIDTH}
h={APP_HEIGHT}
>
{props.children || <SiteHeader />}
{children || <SiteHeader />}
<Flex
gap={4}
w={{ base: '100vw', xl: 'full' }}
@ -83,16 +80,43 @@ const App = (props: Props) => {
<ImageGalleryPanel />
</Flex>
</Grid>
<Box>
<Console />
</Box>
</ImageUploader>
<AnimatePresence>
{!isApplicationReady && !loadingOverridden && (
<motion.div
key="loading"
initial={{ opacity: 1 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
style={{ zIndex: 3 }}
>
<Box position="absolute" top={0} left={0} w="100vw" h="100vh">
<Loading />
</Box>
<Box
onClick={handleOverrideClicked}
position="absolute"
top={0}
right={0}
cursor="pointer"
w="2rem"
h="2rem"
/>
</motion.div>
)}
</AnimatePresence>
<Portal>
<FloatingParametersPanelButtons />
</Portal>
<Portal>
<FloatingGalleryButton />
</Portal>
<Portal>
<Console />
</Portal>
</Grid>
);
};

View File

@ -12,10 +12,12 @@
* 'gfpgan'.
*/
import { FacetoolType } from 'features/parameters/store/postprocessingSlice';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types';
import { ImageMetadata, ImageType } from 'services/api';
import { AnyInvocation } from 'services/events/types';
import { O } from 'ts-toolbelt';
/**
* TODO:
@ -334,3 +336,96 @@ export declare type UploadOutpaintingMergeImagePayload = {
dataURL: string;
name: string;
};
/**
* A disable-able application feature
*/
export declare type AppFeature =
| 'faceRestore'
| 'upscaling'
| 'lightbox'
| 'modelManager'
| 'githubLink'
| 'discordLink'
| 'bugLink'
| 'localization';
/**
* A disable-able Stable Diffusion feature
*/
export declare type StableDiffusionFeature =
| 'noiseConfig'
| 'variations'
| 'symmetry'
| 'tiling'
| 'hires';
/**
* Configuration options for the InvokeAI UI.
* Distinct from system settings which may be changed inside the app.
*/
export declare type AppConfig = {
/**
* Whether or not URLs should be transformed to use a different host
*/
shouldTransformUrls: boolean;
/**
* Whether or not we need to re-fetch images
*/
shouldFetchImages: boolean;
disabledTabs: InvokeTabName[];
disabledFeatures: AppFeature[];
canRestoreDeletedImagesFromBin: boolean;
sd: {
iterations: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
fineStep: number;
coarseStep: number;
};
width: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
fineStep: number;
coarseStep: number;
};
height: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
fineStep: number;
coarseStep: number;
};
steps: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
fineStep: number;
coarseStep: number;
};
guidance: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
fineStep: number;
coarseStep: number;
};
img2imgStrength: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
fineStep: number;
coarseStep: number;
};
};
};
export declare type PartialAppConfig = O.Partial<AppConfig, 'deep'>;

View File

@ -13,21 +13,23 @@ import lightboxReducer from 'features/lightbox/store/lightboxSlice';
import generationReducer from 'features/parameters/store/generationSlice';
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
import systemReducer from 'features/system/store/systemSlice';
import configReducer from 'features/system/store/configSlice';
import uiReducer from 'features/ui/store/uiSlice';
import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import modelsReducer from 'features/system/store/modelSlice';
import nodesReducer from 'features/nodes/store/nodesSlice';
import { socketioMiddleware } from './socketio/middleware';
import { socketMiddleware } from 'services/events/middleware';
import { canvasBlacklist } from 'features/canvas/store/canvasPersistBlacklist';
import { galleryBlacklist } from 'features/gallery/store/galleryPersistBlacklist';
import { generationBlacklist } from 'features/parameters/store/generationPersistBlacklist';
import { lightboxBlacklist } from 'features/lightbox/store/lightboxPersistBlacklist';
import { modelsBlacklist } from 'features/system/store/modelsPersistBlacklist';
import { nodesBlacklist } from 'features/nodes/store/nodesPersistBlacklist';
import { postprocessingBlacklist } from 'features/parameters/store/postprocessingPersistBlacklist';
import { systemBlacklist } from 'features/system/store/systemPersistsBlacklist';
import { uiBlacklist } from 'features/ui/store/uiPersistBlacklist';
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/systemPersistsDenylist';
import { uiDenylist } from 'features/ui/store/uiPersistDenylist';
/**
* redux-persist provides an easy and reliable way to persist state across reloads.
@ -38,9 +40,9 @@ import { uiBlacklist } from 'features/ui/store/uiPersistBlacklist';
* - Connection/processing status
* - Availability of external libraries like ESRGAN/GFPGAN
*
* These can be blacklisted in redux-persist.
* These can be denylisted in redux-persist.
*
* The necesssary nested persistors with blacklists are configured below.
* The necesssary nested persistors with denylists are configured below.
*/
const rootReducer = combineReducers({
@ -53,8 +55,10 @@ const rootReducer = combineReducers({
postprocessing: postprocessingReducer,
results: resultsReducer,
system: systemReducer,
config: configReducer,
ui: uiReducer,
uploads: uploadsReducer,
hotkeys: hotkeysReducer,
});
const rootPersistConfig = getPersistConfig({
@ -62,19 +66,21 @@ const rootPersistConfig = getPersistConfig({
storage,
rootReducer,
blacklist: [
...canvasBlacklist,
...galleryBlacklist,
...generationBlacklist,
...lightboxBlacklist,
...modelsBlacklist,
...nodesBlacklist,
...postprocessingBlacklist,
// ...resultsBlacklist,
...canvasDenylist,
...galleryDenylist,
...generationDenylist,
...lightboxDenylist,
...modelsDenylist,
...nodesDenylist,
...postprocessingDenylist,
// ...resultsDenylist,
'results',
...systemBlacklist,
...uiBlacklist,
// ...uploadsBlacklist,
...systemDenylist,
...uiDenylist,
// ...uploadsDenylist,
'uploads',
'hotkeys',
'config',
],
debounce: 300,
});

View File

@ -26,9 +26,18 @@ import {
import { clamp } from 'lodash';
import { useTranslation } from 'react-i18next';
import { FocusEvent, memo, useEffect, useMemo, useState } from 'react';
import {
FocusEvent,
memo,
MouseEvent,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { BiReset } from 'react-icons/bi';
import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton';
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
export type IAIFullSliderProps = {
label: string;
@ -108,31 +117,52 @@ const IAISlider = (props: IAIFullSliderProps) => {
[max, sliderNumberInputProps?.max]
);
const handleSliderChange = (v: number) => {
onChange(v);
};
const handleSliderChange = useCallback(
(v: number) => {
onChange(v);
},
[onChange]
);
const handleInputBlur = (e: FocusEvent<HTMLInputElement>) => {
if (e.target.value === '') e.target.value = String(min);
const clamped = clamp(
isInteger ? Math.floor(Number(e.target.value)) : Number(localInputValue),
min,
numberInputMax
);
onChange(clamped);
};
const handleInputBlur = useCallback(
(e: FocusEvent<HTMLInputElement>) => {
if (e.target.value === '') {
e.target.value = String(min);
}
const clamped = clamp(
isInteger
? Math.floor(Number(e.target.value))
: Number(localInputValue),
min,
numberInputMax
);
const quantized = roundDownToMultiple(clamped, step);
onChange(quantized);
setLocalInputValue(quantized);
},
[isInteger, localInputValue, min, numberInputMax, onChange, step]
);
const handleInputChange = (v: number | string) => {
const handleInputChange = useCallback((v: number | string) => {
setLocalInputValue(v);
};
}, []);
const handleResetDisable = () => {
if (!handleReset) return;
const handleResetDisable = useCallback(() => {
if (!handleReset) {
return;
}
handleReset();
};
}, [handleReset]);
const forceInputBlur = useCallback((e: MouseEvent) => {
if (e.target instanceof HTMLDivElement) {
e.target.focus();
}
}, []);
return (
<FormControl
onClick={forceInputBlur}
sx={
isCompact
? {
@ -215,6 +245,7 @@ const IAISlider = (props: IAIFullSliderProps) => {
value={localInputValue}
onChange={handleInputChange}
onBlur={handleInputBlur}
focusInputOnChange={false}
{...sliderNumberInputProps}
>
<NumberInputField
@ -237,7 +268,7 @@ const IAISlider = (props: IAIFullSliderProps) => {
<IAIIconButton
size="sm"
aria-label={t('accessibility.reset')}
tooltip="Reset"
tooltip={t('accessibility.reset')}
icon={<BiReset />}
isDisabled={isDisabled}
onClick={handleResetDisable}

View File

@ -34,10 +34,9 @@ const IAISwitch = (props: Props) => {
display="flex"
gap={4}
alignItems="center"
justifyContent="space-between"
{...formControlProps}
>
<FormLabel my={1} {...formLabelProps}>
<FormLabel my={1} flexGrow={1} {...formLabelProps}>
{label}
</FormLabel>
<Switch {...rest} />

View File

@ -37,34 +37,12 @@ const ImageToImageOverlay = ({
position: 'absolute',
}}
>
<ButtonGroup
<Flex
sx={{
position: 'absolute',
top: 0,
right: 0,
p: 2,
}}
>
<IAIIconButton
size="sm"
isDisabled={!isImageToImageEnabled}
icon={<FaUndo />}
aria-label={t('accessibility.reset')}
onClick={handleResetInitialImage}
/>
<IAIIconButton
size="sm"
isDisabled={!isImageToImageEnabled}
icon={<FaUpload />}
aria-label={t('common.upload')}
/>
</ButtonGroup>
<Flex
sx={{
position: 'absolute',
bottom: 0,
left: 0,
p: 2,
alignItems: 'flex-start',
}}
>

View File

@ -0,0 +1,62 @@
import {
Box,
ButtonGroup,
Collapse,
Flex,
Heading,
HStack,
Image,
Spacer,
Text,
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 { FaUndo, FaUpload } from 'react-icons/fa';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { RootState } from 'app/store';
import { useCallback } from 'react';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
const ImageToImageSettingsHeader = () => {
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleResetInitialImage = useCallback(() => {
dispatch(clearInitialImage());
}, [dispatch]);
return (
<Flex w="full" alignItems="center">
<Text size="sm" fontWeight={500} color="base.300">
Image to Image
</Text>
<Spacer />
<ButtonGroup>
<IAIIconButton
size="sm"
icon={<FaUndo />}
aria-label={t('accessibility.reset')}
onClick={handleResetInitialImage}
/>
<IAIIconButton
size="sm"
icon={<FaUpload />}
aria-label={t('common.upload')}
/>
</ButtonGroup>
</Flex>
);
};
export default ImageToImageSettingsHeader;

View File

@ -0,0 +1,32 @@
import { Flex, Image, Spinner } from '@chakra-ui/react';
import InvokeAILogoImage from 'assets/images/logo.png';
// This component loads before the theme so we cannot use theme tokens here
const Loading = () => {
return (
<Flex
position="relative"
width="100vw"
height="100vh"
alignItems="center"
justifyContent="center"
bg="#151519"
>
<Image src={InvokeAILogoImage} w="8rem" h="8rem" />
<Spinner
label="Loading"
color="grey"
position="absolute"
size="sm"
width="24px !important"
height="24px !important"
right="1.5rem"
bottom="1.5rem"
speed="1.2s"
/>
</Flex>
);
};
export default Loading;

View File

@ -3,7 +3,17 @@ import { FaImage } from 'react-icons/fa';
const SelectImagePlaceholder = () => {
return (
<Flex sx={{ h: 36, alignItems: 'center', justifyContent: 'center' }}>
<Flex
sx={{
w: 'full',
h: 'full',
bg: 'base.800',
borderRadius: 'base',
alignItems: 'center',
justifyContent: 'center',
aspectRatio: '1/1',
}}
>
<Icon color="base.400" boxSize={32} as={FaImage}></Icon>
</Flex>
);

View File

@ -1,160 +0,0 @@
// import WorkInProgress from './WorkInProgress';
// import ReactFlow, {
// applyEdgeChanges,
// applyNodeChanges,
// Background,
// Controls,
// Edge,
// Handle,
// Node,
// NodeTypes,
// OnEdgesChange,
// OnNodesChange,
// Position,
// } from 'reactflow';
// import 'reactflow/dist/style.css';
// import {
// Fragment,
// FunctionComponent,
// ReactNode,
// useCallback,
// useMemo,
// useState,
// } from 'react';
// import { OpenAPIV3 } from 'openapi-types';
// import { filter, map, reduce } from 'lodash';
// import {
// Box,
// Flex,
// FormControl,
// FormLabel,
// Input,
// Select,
// Switch,
// Text,
// NumberInput,
// NumberInputField,
// NumberInputStepper,
// NumberIncrementStepper,
// NumberDecrementStepper,
// Tooltip,
// chakra,
// Badge,
// Heading,
// VStack,
// HStack,
// Menu,
// MenuButton,
// MenuList,
// MenuItem,
// MenuItemOption,
// MenuGroup,
// MenuOptionGroup,
// MenuDivider,
// IconButton,
// } from '@chakra-ui/react';
// import { FaPlus } from 'react-icons/fa';
// import {
// FIELD_NAMES as FIELD_NAMES,
// FIELDS,
// INVOCATION_NAMES as INVOCATION_NAMES,
// INVOCATIONS,
// } from 'features/nodeEditor/constants';
// console.log('invocations', INVOCATIONS);
// const nodeTypes = reduce(
// INVOCATIONS,
// (acc, val, key) => {
// acc[key] = val.component;
// return acc;
// },
// {} as NodeTypes
// );
// console.log('nodeTypes', nodeTypes);
// // make initial nodes one of every node for now
// let n = 0;
// const initialNodes = map(INVOCATIONS, (i) => ({
// id: i.type,
// type: i.title,
// position: { x: (n += 20), y: (n += 20) },
// data: {},
// }));
// console.log('initialNodes', initialNodes);
// export default function NodesWIP() {
// const [nodes, setNodes] = useState<Node[]>([]);
// const [edges, setEdges] = useState<Edge[]>([]);
// const onNodesChange: OnNodesChange = useCallback(
// (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
// []
// );
// const onEdgesChange: OnEdgesChange = useCallback(
// (changes) => setEdges((eds: Edge[]) => applyEdgeChanges(changes, eds)),
// []
// );
// return (
// <Box
// sx={{
// position: 'relative',
// width: 'full',
// height: 'full',
// borderRadius: 'md',
// }}
// >
// <ReactFlow
// nodeTypes={nodeTypes}
// nodes={nodes}
// edges={edges}
// onNodesChange={onNodesChange}
// onEdgesChange={onEdgesChange}
// >
// <Background />
// <Controls />
// </ReactFlow>
// <HStack sx={{ position: 'absolute', top: 2, right: 2 }}>
// {FIELD_NAMES.map((field) => (
// <Badge
// key={field}
// colorScheme={FIELDS[field].color}
// sx={{ userSelect: 'none' }}
// >
// {field}
// </Badge>
// ))}
// </HStack>
// <Menu>
// <MenuButton
// as={IconButton}
// aria-label="Options"
// icon={<FaPlus />}
// sx={{ position: 'absolute', top: 2, left: 2 }}
// />
// <MenuList>
// {INVOCATION_NAMES.map((name) => {
// const invocation = INVOCATIONS[name];
// return (
// <Tooltip
// key={name}
// label={invocation.description}
// placement="end"
// hasArrow
// >
// <MenuItem>{invocation.title}</MenuItem>
// </Tooltip>
// );
// })}
// </MenuList>
// </Menu>
// </Box>
// );
// }
export default {};

View File

@ -0,0 +1,39 @@
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice';
import { isEqual } from 'lodash';
import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook';
const globalHotkeysSelector = createSelector(
(state: RootState) => state.hotkeys,
(hotkeys) => {
const { shift } = hotkeys;
return { shift };
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
// TODO: Does not catch keypresses while focused in an input. Maybe there is a way?
export const useGlobalHotkeys = () => {
const dispatch = useAppDispatch();
const { shift } = useAppSelector(globalHotkeysSelector);
useHotkeys(
'*',
() => {
if (isHotkeyPressed('shift')) {
!shift && dispatch(shiftKeyPressed(true));
} else {
shift && dispatch(shiftKeyPressed(false));
}
},
{ keyup: true, keydown: true },
[shift]
);
};

View File

@ -12,7 +12,7 @@ export const getUrlAlt = (url: string, shouldTransformUrls: boolean) => {
export const useGetUrl = () => {
const shouldTransformUrls = useAppSelector(
(state: RootState) => state.system.shouldTransformUrls
(state: RootState) => state.config.shouldTransformUrls
);
return {

View File

@ -1,10 +1,9 @@
import React, { lazy, PropsWithChildren, useEffect, useState } from 'react';
import React, { lazy, PropsWithChildren, useEffect } from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { buildMiddleware, store } from './app/store';
import { persistor } from './persistor';
import { OpenAPI } from 'services/api';
import { InvokeTabName } from 'features/ui/store/tabMap';
import '@fontsource/inter/100.css';
import '@fontsource/inter/200.css';
import '@fontsource/inter/300.css';
@ -15,33 +14,22 @@ import '@fontsource/inter/700.css';
import '@fontsource/inter/800.css';
import '@fontsource/inter/900.css';
import Loading from './Loading';
// Localization
import './i18n';
import Loading from './common/components/Loading/Loading';
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
import { PartialAppConfig } from 'app/invokeai';
import './i18n';
const App = lazy(() => import('./app/App'));
const ThemeLocaleProvider = lazy(() => import('./app/ThemeLocaleProvider'));
interface Props extends PropsWithChildren {
apiUrl?: string;
disabledPanels?: string[];
disabledTabs?: InvokeTabName[];
token?: string;
shouldTransformUrls?: boolean;
shouldFetchImages?: boolean;
config?: PartialAppConfig;
}
export default function Component({
apiUrl,
disabledPanels = [],
disabledTabs = [],
token,
children,
shouldTransformUrls,
shouldFetchImages = false,
}: Props) {
export default function Component({ apiUrl, token, config, children }: Props) {
useEffect(() => {
// configure API client token
if (token) {
@ -69,18 +57,9 @@ export default function Component({
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={<Loading />} persistor={persistor}>
<React.Suspense fallback={<Loading showText />}>
<React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider>
<App
options={{
disabledPanels,
disabledTabs,
shouldTransformUrls,
shouldFetchImages,
}}
>
{children}
</App>
<App config={config}>{children}</App>
</ThemeLocaleProvider>
</React.Suspense>
</PersistGate>

View File

@ -1,14 +0,0 @@
import { CanvasState } from './canvasTypes';
/**
* Canvas slice persist blacklist
*/
const itemsToBlacklist: (keyof CanvasState)[] = [
'cursorPosition',
'isCanvasInitialized',
'doesCanvasNeedScaling',
];
export const canvasBlacklist = itemsToBlacklist.map(
(blacklistItem) => `canvas.${blacklistItem}`
);

View File

@ -0,0 +1,14 @@
import { CanvasState } from './canvasTypes';
/**
* Canvas slice persist denylist
*/
const itemsToDenylist: (keyof CanvasState)[] = [
'cursorPosition',
'isCanvasInitialized',
'doesCanvasNeedScaling',
];
export const canvasDenylist = itemsToDenylist.map(
(denylistItem) => `canvas.${denylistItem}`
);

View File

@ -1,7 +1,15 @@
import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
import { ButtonGroup, Flex, FlexProps, Link, useToast } from '@chakra-ui/react';
import {
ButtonGroup,
Flex,
FlexProps,
FormControl,
Link,
useDisclosure,
useToast,
} from '@chakra-ui/react';
import { runESRGAN, runFacetool } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIButton from 'common/components/IAIButton';
@ -58,6 +66,8 @@ import { useCallback } from 'react';
import useSetBothPrompts from 'features/parameters/hooks/usePrompt';
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';
const currentImageButtonsSelector = createSelector(
[
@ -69,17 +79,14 @@ const currentImageButtonsSelector = createSelector(
activeTabNameSelector,
selectedImageSelector,
],
(
system: SystemState,
gallery: GalleryState,
postprocessing,
ui,
lightbox,
activeTabName,
selectedImage
) => {
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
system;
(system, gallery, postprocessing, ui, lightbox, activeTabName, image) => {
const {
isProcessing,
isConnected,
isGFPGANAvailable,
isESRGANAvailable,
shouldConfirmOnDelete,
} = system;
const { upscalingLevel, facetoolStrength } = postprocessing;
@ -90,6 +97,8 @@ const currentImageButtonsSelector = createSelector(
const { intermediateImage, currentImage } = gallery;
return {
canDeleteImage: isConnected && !isProcessing,
shouldConfirmOnDelete,
isProcessing,
isConnected,
isGFPGANAvailable,
@ -102,7 +111,7 @@ const currentImageButtonsSelector = createSelector(
activeTabName,
isLightboxOpen,
shouldHidePreview,
selectedImage,
image,
};
},
{
@ -133,29 +142,48 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
isLightboxOpen,
activeTabName,
shouldHidePreview,
selectedImage,
image,
canDeleteImage,
shouldConfirmOnDelete,
} = useAppSelector(currentImageButtonsSelector);
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled;
const { getUrl, shouldTransformUrls } = useGetUrl();
const {
isOpen: isDeleteDialogOpen,
onOpen: onDeleteDialogOpen,
onClose: onDeleteDialogClose,
} = useDisclosure();
const toast = useToast();
const { t } = useTranslation();
const setBothPrompts = useSetBothPrompts();
const handleClickUseAsInitialImage = () => {
if (!selectedImage) return;
if (!image) return;
if (isLightboxOpen) dispatch(setIsLightboxOpen(false));
dispatch(initialImageSelected(selectedImage.name));
dispatch(initialImageSelected(image.name));
// dispatch(setInitialImage(currentImage));
// dispatch(setActiveTab('img2img'));
};
const handleCopyImage = async () => {
if (!selectedImage) return;
if (!image?.url) {
return;
}
const blob = await fetch(getUrl(selectedImage.url)).then((res) =>
res.blob()
);
const url = getUrl(image.url);
if (!url) {
return;
}
const blob = await fetch(url).then((res) => res.blob());
const data = [new ClipboardItem({ [blob.type]: blob })];
await navigator.clipboard.write(data);
@ -169,12 +197,16 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
};
const handleCopyImageLink = () => {
const url = selectedImage
const url = image
? shouldTransformUrls
? getUrl(selectedImage.url)
: window.location.toString() + selectedImage.url
? getUrl(image.url)
: window.location.toString() + image.url
: '';
if (!url) {
return;
}
navigator.clipboard.writeText(url).then(() => {
toast({
title: t('toast.imageLinkCopied'),
@ -188,7 +220,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys(
'shift+i',
() => {
if (selectedImage) {
if (image) {
handleClickUseAsInitialImage();
toast({
title: t('toast.sentToImageToImage'),
@ -206,7 +238,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
});
}
},
[selectedImage]
[image]
);
const handlePreviewVisibility = () => {
@ -214,7 +246,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
};
const handleClickUseAllParameters = () => {
if (!selectedImage) return;
if (!image) return;
// selectedImage.metadata &&
// dispatch(setAllParameters(selectedImage.metadata));
// if (selectedImage.metadata?.image.type === 'img2img') {
@ -227,11 +259,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys(
'a',
() => {
if (
['txt2img', 'img2img'].includes(
selectedImage?.metadata?.sd_metadata?.type
)
) {
if (['txt2img', 'img2img'].includes(image?.metadata?.sd_metadata?.type)) {
handleClickUseAllParameters();
toast({
title: t('toast.parametersSet'),
@ -249,18 +277,17 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
});
}
},
[selectedImage]
[image]
);
const handleClickUseSeed = () => {
selectedImage?.metadata &&
dispatch(setSeed(selectedImage.metadata.sd_metadata.seed));
image?.metadata && dispatch(setSeed(image.metadata.sd_metadata.seed));
};
useHotkeys(
's',
() => {
if (selectedImage?.metadata?.sd_metadata?.seed) {
if (image?.metadata?.sd_metadata?.seed) {
handleClickUseSeed();
toast({
title: t('toast.seedSet'),
@ -278,19 +305,19 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
});
}
},
[selectedImage]
[image]
);
const handleClickUsePrompt = useCallback(() => {
if (selectedImage?.metadata?.sd_metadata?.prompt) {
setBothPrompts(selectedImage?.metadata?.sd_metadata?.prompt);
if (image?.metadata?.sd_metadata?.prompt) {
setBothPrompts(image?.metadata?.sd_metadata?.prompt);
}
}, [selectedImage?.metadata?.sd_metadata?.prompt, setBothPrompts]);
}, [image?.metadata?.sd_metadata?.prompt, setBothPrompts]);
useHotkeys(
'p',
() => {
if (selectedImage?.metadata?.sd_metadata?.prompt) {
if (image?.metadata?.sd_metadata?.prompt) {
handleClickUsePrompt();
toast({
title: t('toast.promptSet'),
@ -308,7 +335,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
});
}
},
[selectedImage]
[image]
);
const handleClickUpscale = () => {
@ -318,25 +345,22 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys(
'Shift+U',
() => {
if (
isESRGANAvailable &&
!shouldDisableToolbarButtons &&
isConnected &&
!isProcessing &&
upscalingLevel
) {
handleClickUpscale();
} else {
toast({
title: t('toast.upscalingFailed'),
status: 'error',
duration: 2500,
isClosable: true,
});
}
handleClickUpscale();
},
{
enabled: () =>
Boolean(
isUpscalingEnabled &&
isESRGANAvailable &&
!shouldDisableToolbarButtons &&
isConnected &&
!isProcessing &&
upscalingLevel
),
},
[
selectedImage,
isUpscalingEnabled,
image,
isESRGANAvailable,
shouldDisableToolbarButtons,
isConnected,
@ -352,25 +376,23 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys(
'Shift+R',
() => {
if (
isGFPGANAvailable &&
!shouldDisableToolbarButtons &&
isConnected &&
!isProcessing &&
facetoolStrength
) {
handleClickFixFaces();
} else {
toast({
title: t('toast.faceRestoreFailed'),
status: 'error',
duration: 2500,
isClosable: true,
});
}
handleClickFixFaces();
},
{
enabled: () =>
Boolean(
isFaceRestoreEnabled &&
isGFPGANAvailable &&
!shouldDisableToolbarButtons &&
isConnected &&
!isProcessing &&
facetoolStrength
),
},
[
selectedImage,
isFaceRestoreEnabled,
image,
isGFPGANAvailable,
shouldDisableToolbarButtons,
isConnected,
@ -383,7 +405,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
dispatch(setShouldShowImageDetails(!shouldShowImageDetails));
const handleSendToCanvas = () => {
if (!selectedImage) return;
if (!image) return;
if (isLightboxOpen) dispatch(setIsLightboxOpen(false));
// dispatch(setInitialCanvasImage(selectedImage));
@ -404,7 +426,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys(
'i',
() => {
if (selectedImage) {
if (image) {
handleClickShowImageDetails();
} else {
toast({
@ -415,217 +437,255 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
});
}
},
[selectedImage, shouldShowImageDetails]
[image, shouldShowImageDetails]
);
const handleInitiateDelete = () => {
if (shouldConfirmOnDelete) {
onDeleteDialogOpen();
} else {
handleDelete();
}
};
const handleDelete = () => {
if (canDeleteImage && image) {
dispatch(imageDeleted({ imageType: image.type, imageName: image.name }));
}
};
useHotkeys('delete', handleInitiateDelete, [
image,
shouldConfirmOnDelete,
isConnected,
isProcessing,
]);
const handleLightBox = () => {
dispatch(setIsLightboxOpen(!isLightboxOpen));
};
return (
<Flex
sx={{
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'center',
gap: 2,
}}
{...props}
>
<ButtonGroup isAttached={true}>
<IAIPopover
triggerComponent={
<IAIIconButton
aria-label={`${t('parameters.sendTo')}...`}
icon={<FaShareAlt />}
/>
}
>
<Flex
sx={{
flexDirection: 'column',
rowGap: 2,
}}
<>
<Flex
sx={{
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'center',
gap: 2,
}}
{...props}
>
<ButtonGroup isAttached={true}>
<IAIPopover
triggerComponent={
<IAIIconButton
isDisabled={!image}
aria-label={`${t('parameters.sendTo')}...`}
icon={<FaShareAlt />}
/>
}
>
<IAIButton
size="sm"
onClick={handleClickUseAsInitialImage}
leftIcon={<FaShare />}
<Flex
sx={{
flexDirection: 'column',
rowGap: 2,
}}
>
{t('parameters.sendToImg2Img')}
</IAIButton>
<IAIButton
size="sm"
onClick={handleSendToCanvas}
leftIcon={<FaShare />}
>
{t('parameters.sendToUnifiedCanvas')}
</IAIButton>
<IAIButton
size="sm"
onClick={handleCopyImage}
leftIcon={<FaCopy />}
>
{t('parameters.copyImage')}
</IAIButton>
<IAIButton
size="sm"
onClick={handleCopyImageLink}
leftIcon={<FaCopy />}
>
{t('parameters.copyImageToLink')}
</IAIButton>
<Link download={true} href={getUrl(selectedImage!.url)}>
<IAIButton leftIcon={<FaDownload />} size="sm" w="100%">
{t('parameters.downloadImage')}
<IAIButton
size="sm"
onClick={handleClickUseAsInitialImage}
leftIcon={<FaShare />}
>
{t('parameters.sendToImg2Img')}
</IAIButton>
<IAIButton
size="sm"
onClick={handleSendToCanvas}
leftIcon={<FaShare />}
>
{t('parameters.sendToUnifiedCanvas')}
</IAIButton>
</Link>
</Flex>
</IAIPopover>
<IAIIconButton
icon={shouldHidePreview ? <FaEyeSlash /> : <FaEye />}
tooltip={
!shouldHidePreview
? t('parameters.hidePreview')
: t('parameters.showPreview')
}
aria-label={
!shouldHidePreview
? t('parameters.hidePreview')
: t('parameters.showPreview')
}
isChecked={shouldHidePreview}
onClick={handlePreviewVisibility}
/>
<IAIIconButton
icon={<FaExpand />}
tooltip={
!isLightboxOpen
? `${t('parameters.openInViewer')} (Z)`
: `${t('parameters.closeViewer')} (Z)`
}
aria-label={
!isLightboxOpen
? `${t('parameters.openInViewer')} (Z)`
: `${t('parameters.closeViewer')} (Z)`
}
isChecked={isLightboxOpen}
onClick={handleLightBox}
/>
</ButtonGroup>
<ButtonGroup isAttached={true}>
<IAIIconButton
icon={<FaQuoteRight />}
tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.usePrompt')} (P)`}
isDisabled={!selectedImage?.metadata?.sd_metadata?.prompt}
onClick={handleClickUsePrompt}
/>
<IAIButton
size="sm"
onClick={handleCopyImage}
leftIcon={<FaCopy />}
>
{t('parameters.copyImage')}
</IAIButton>
<IAIButton
size="sm"
onClick={handleCopyImageLink}
leftIcon={<FaCopy />}
>
{t('parameters.copyImageToLink')}
</IAIButton>
<IAIIconButton
icon={<FaSeedling />}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={!selectedImage?.metadata?.sd_metadata?.seed}
onClick={handleClickUseSeed}
/>
<IAIIconButton
icon={<FaAsterisk />}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={
!['txt2img', 'img2img'].includes(
selectedImage?.metadata?.sd_metadata?.type
)
}
onClick={handleClickUseAllParameters}
/>
</ButtonGroup>
<ButtonGroup isAttached={true}>
<IAIPopover
triggerComponent={
<Link download={true} href={getUrl(image?.url ?? '')}>
<IAIButton leftIcon={<FaDownload />} size="sm" w="100%">
{t('parameters.downloadImage')}
</IAIButton>
</Link>
</Flex>
</IAIPopover>
<IAIIconButton
icon={shouldHidePreview ? <FaEyeSlash /> : <FaEye />}
tooltip={
!shouldHidePreview
? t('parameters.hidePreview')
: t('parameters.showPreview')
}
aria-label={
!shouldHidePreview
? t('parameters.hidePreview')
: t('parameters.showPreview')
}
isChecked={shouldHidePreview}
onClick={handlePreviewVisibility}
/>
{isLightboxEnabled && (
<IAIIconButton
icon={<FaGrinStars />}
aria-label={t('parameters.restoreFaces')}
/>
}
>
<Flex
sx={{
flexDirection: 'column',
rowGap: 4,
}}
>
<FaceRestoreSettings />
<IAIButton
isDisabled={
!isGFPGANAvailable ||
!selectedImage ||
!(isConnected && !isProcessing) ||
!facetoolStrength
icon={<FaExpand />}
tooltip={
!isLightboxOpen
? `${t('parameters.openInViewer')} (Z)`
: `${t('parameters.closeViewer')} (Z)`
}
onClick={handleClickFixFaces}
>
{t('parameters.restoreFaces')}
</IAIButton>
</Flex>
</IAIPopover>
<IAIPopover
triggerComponent={
<IAIIconButton
icon={<FaExpandArrowsAlt />}
aria-label={t('parameters.upscale')}
/>
}
>
<Flex
sx={{
flexDirection: 'column',
gap: 4,
}}
>
<UpscaleSettings />
<IAIButton
isDisabled={
!isESRGANAvailable ||
!selectedImage ||
!(isConnected && !isProcessing) ||
!upscalingLevel
aria-label={
!isLightboxOpen
? `${t('parameters.openInViewer')} (Z)`
: `${t('parameters.closeViewer')} (Z)`
}
onClick={handleClickUpscale}
>
{t('parameters.upscaleImage')}
</IAIButton>
</Flex>
</IAIPopover>
</ButtonGroup>
isChecked={isLightboxOpen}
onClick={handleLightBox}
/>
)}
</ButtonGroup>
<ButtonGroup isAttached={true}>
<IAIIconButton
icon={<FaCode />}
tooltip={`${t('parameters.info')} (I)`}
aria-label={`${t('parameters.info')} (I)`}
isChecked={shouldShowImageDetails}
onClick={handleClickShowImageDetails}
/>
</ButtonGroup>
<ButtonGroup isAttached={true}>
<IAIIconButton
icon={<FaQuoteRight />}
tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.usePrompt')} (P)`}
isDisabled={!image?.metadata?.sd_metadata?.prompt}
onClick={handleClickUsePrompt}
/>
<IAIIconButton
icon={<FaSeedling />}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={!image?.metadata?.sd_metadata?.seed}
onClick={handleClickUseSeed}
/>
<IAIIconButton
icon={<FaAsterisk />}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={
!['txt2img', 'img2img'].includes(
image?.metadata?.sd_metadata?.type
)
}
onClick={handleClickUseAllParameters}
/>
</ButtonGroup>
{(isUpscalingEnabled || isFaceRestoreEnabled) && (
<ButtonGroup isAttached={true}>
{isFaceRestoreEnabled && (
<IAIPopover
triggerComponent={
<IAIIconButton
icon={<FaGrinStars />}
aria-label={t('parameters.restoreFaces')}
/>
}
>
<Flex
sx={{
flexDirection: 'column',
rowGap: 4,
}}
>
<FaceRestoreSettings />
<IAIButton
isDisabled={
!isGFPGANAvailable ||
!image ||
!(isConnected && !isProcessing) ||
!facetoolStrength
}
onClick={handleClickFixFaces}
>
{t('parameters.restoreFaces')}
</IAIButton>
</Flex>
</IAIPopover>
)}
{isUpscalingEnabled && (
<IAIPopover
triggerComponent={
<IAIIconButton
icon={<FaExpandArrowsAlt />}
aria-label={t('parameters.upscale')}
/>
}
>
<Flex
sx={{
flexDirection: 'column',
gap: 4,
}}
>
<UpscaleSettings />
<IAIButton
isDisabled={
!isESRGANAvailable ||
!image ||
!(isConnected && !isProcessing) ||
!upscalingLevel
}
onClick={handleClickUpscale}
>
{t('parameters.upscaleImage')}
</IAIButton>
</Flex>
</IAIPopover>
)}
</ButtonGroup>
)}
<ButtonGroup isAttached={true}>
<IAIIconButton
icon={<FaCode />}
tooltip={`${t('parameters.info')} (I)`}
aria-label={`${t('parameters.info')} (I)`}
isChecked={shouldShowImageDetails}
onClick={handleClickShowImageDetails}
/>
</ButtonGroup>
{/* <DeleteImageModal image={selectedImage}>
<IAIIconButton
onClick={handleInitiateDelete}
icon={<FaTrash />}
tooltip={`${t('parameters.deleteImage')} (Del)`}
aria-label={`${t('parameters.deleteImage')} (Del)`}
isDisabled={!selectedImage || !isConnected || isProcessing}
tooltip={`${t('gallery.deleteImage')} (Del)`}
aria-label={`${t('gallery.deleteImage')} (Del)`}
isDisabled={!image || !isConnected}
colorScheme="error"
/>
</DeleteImageModal> */}
</Flex>
</Flex>
{image && (
<DeleteImageModal
isOpen={isDeleteDialogOpen}
onClose={onDeleteDialogClose}
handleDelete={handleDelete}
/>
)}
</>
);
};

View File

@ -1,23 +1,21 @@
import { Flex, Icon } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors';
import { isEqual } from 'lodash';
import { MdPhoto } from 'react-icons/md';
import {
gallerySelector,
selectedImageSelector,
} from '../store/gallerySelectors';
import { selectedImageSelector } from '../store/gallerySelectors';
import CurrentImageButtons from './CurrentImageButtons';
import CurrentImagePreview from './CurrentImagePreview';
export const currentImageDisplaySelector = createSelector(
[gallerySelector, selectedImageSelector],
(gallery, selectedImage) => {
const { currentImage, intermediateImage } = gallery;
[systemSelector, selectedImageSelector],
(system, selectedImage) => {
const { progressImage } = system;
return {
hasAnImageToDisplay: selectedImage || intermediateImage,
hasAnImageToDisplay: selectedImage || progressImage,
};
},
{
@ -36,27 +34,32 @@ const CurrentImageDisplay = () => {
return (
<Flex
sx={{
position: 'relative',
flexDirection: 'column',
height: '100%',
width: '100%',
rowGap: 4,
borderRadius: 'base',
alignItems: 'center',
justifyContent: 'center',
}}
>
{hasAnImageToDisplay ? (
<>
<CurrentImageButtons />
<CurrentImagePreview />
</>
) : (
<Flex
sx={{
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
}}
>
<Flex
flexDirection="column"
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
}}
>
{hasAnImageToDisplay ? (
<>
<CurrentImageButtons />
<CurrentImagePreview />
</>
) : (
<Icon
as={MdPhoto}
sx={{
@ -64,8 +67,8 @@ const CurrentImageDisplay = () => {
color: 'base.500',
}}
/>
</Flex>
)}
)}
</Flex>
</Flex>
);
};

View File

@ -5,8 +5,6 @@ import { useGetUrl } from 'common/util/getUrl';
import { systemSelector } from 'features/system/store/systemSelectors';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash';
import { ReactEventHandler } from 'react';
import { APP_METADATA_HEIGHT } from 'theme/util/constants';
import { selectedImageSelector } from '../store/gallerySelectors';
import CurrentImageFallback from './CurrentImageFallback';
@ -110,7 +108,6 @@ export default function CurrentImagePreview() {
height: '100%',
borderRadius: 'base',
overflow: 'scroll',
maxHeight: APP_METADATA_HEIGHT,
}}
>
<ImageMetadataViewer image={imageToDisplay.image} />

View File

@ -5,38 +5,27 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
forwardRef,
Flex,
Text,
useDisclosure,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/invokeai';
import { deleteImage } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAISwitch from 'common/components/IAISwitch';
import { configSelector } from 'features/system/store/configSelectors';
import { systemSelector } from 'features/system/store/systemSelectors';
import {
setShouldConfirmOnDelete,
SystemState,
} from 'features/system/store/systemSlice';
import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
import { isEqual } from 'lodash';
import {
ChangeEvent,
cloneElement,
ReactElement,
SyntheticEvent,
useRef,
} from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { ChangeEvent, memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
const deleteImageModalSelector = createSelector(
systemSelector,
(system: SystemState) => {
const { shouldConfirmOnDelete, isConnected, isProcessing } = system;
return { shouldConfirmOnDelete, isConnected, isProcessing };
const selector = createSelector(
[systemSelector, configSelector],
(system, config) => {
const { shouldConfirmOnDelete } = system;
const { canRestoreDeletedImagesFromBin } = config;
return { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin };
},
{
memoizeOptions: {
@ -44,104 +33,77 @@ const deleteImageModalSelector = createSelector(
},
}
);
interface DeleteImageModalProps {
/**
* Component which, on click, should delete the image/open the modal.
*/
children: ReactElement;
/**
* The image to delete.
*/
image?: InvokeAI._Image;
isOpen: boolean;
onClose: () => void;
handleDelete: () => void;
}
/**
* Needs a child, which will act as the button to delete an image.
* If system.shouldConfirmOnDelete is true, a confirmation modal is displayed.
* If it is false, the image is deleted immediately.
* The confirmation modal has a "Don't ask me again" switch to set the boolean.
*/
const DeleteImageModal = forwardRef(
({ image, children }: DeleteImageModalProps, ref) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const dispatch = useAppDispatch();
const { shouldConfirmOnDelete, isConnected, isProcessing } = useAppSelector(
deleteImageModalSelector
);
const cancelRef = useRef<HTMLButtonElement>(null);
const DeleteImageModal = ({
isOpen,
onClose,
handleDelete,
}: DeleteImageModalProps) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } =
useAppSelector(selector);
const cancelRef = useRef<HTMLButtonElement>(null);
const handleClickDelete = (e: SyntheticEvent) => {
e.stopPropagation();
shouldConfirmOnDelete ? onOpen() : handleDelete();
};
const handleChangeShouldConfirmOnDelete = useCallback(
(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldConfirmOnDelete(!e.target.checked)),
[dispatch]
);
const handleDelete = () => {
if (isConnected && !isProcessing && image) {
dispatch(deleteImage(image));
}
onClose();
};
const handleClickDelete = useCallback(() => {
handleDelete();
onClose();
}, [handleDelete, onClose]);
useHotkeys(
'delete',
() => {
shouldConfirmOnDelete ? onOpen() : handleDelete();
},
[image, shouldConfirmOnDelete, isConnected, isProcessing]
);
return (
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
isCentered
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{t('gallery.deleteImage')}
</AlertDialogHeader>
const handleChangeShouldConfirmOnDelete = (
e: ChangeEvent<HTMLInputElement>
) => dispatch(setShouldConfirmOnDelete(!e.target.checked));
<AlertDialogBody>
<Flex direction="column" gap={5}>
<Flex direction="column" gap={2}>
<Text>{t('common.areYouSure')}</Text>
<Text>
{canRestoreDeletedImagesFromBin
? t('gallery.deleteImageBin')
: t('gallery.deleteImagePermanent')}
</Text>
</Flex>
<IAISwitch
label={t('common.dontAskMeAgain')}
isChecked={!shouldConfirmOnDelete}
onChange={handleChangeShouldConfirmOnDelete}
/>
</Flex>
</AlertDialogBody>
<AlertDialogFooter>
<IAIButton ref={cancelRef} onClick={onClose}>
Cancel
</IAIButton>
<IAIButton colorScheme="error" onClick={handleClickDelete} ml={3}>
Delete
</IAIButton>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
);
};
return (
<>
{cloneElement(children, {
// TODO: This feels wrong.
onClick: image ? handleClickDelete : undefined,
ref: ref,
})}
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
Delete image
</AlertDialogHeader>
<AlertDialogBody>
<Flex direction="column" gap={5}>
<Text>
Are you sure? Deleted images will be sent to the Bin. You
can restore from there if you wish to.
</Text>
<IAISwitch
label="Don't ask me again"
isChecked={!shouldConfirmOnDelete}
onChange={handleChangeShouldConfirmOnDelete}
/>
</Flex>
</AlertDialogBody>
<AlertDialogFooter>
<IAIButton ref={cancelRef} onClick={onClose}>
Cancel
</IAIButton>
<IAIButton colorScheme="error" onClick={handleDelete} ml={3}>
Delete
</IAIButton>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
);
}
);
DeleteImageModal.displayName = 'DeleteImageModal';
export default DeleteImageModal;
export default memo(DeleteImageModal);

View File

@ -5,6 +5,8 @@ import {
Image,
MenuItem,
MenuList,
Text,
useDisclosure,
useTheme,
useToast,
} from '@chakra-ui/react';
@ -20,7 +22,14 @@ import {
setSeed,
} from 'features/parameters/store/generationSlice';
import { DragEvent, memo, useState } from 'react';
import { FaCheck, FaTrashAlt } from 'react-icons/fa';
import {
FaCheck,
FaExpand,
FaLink,
FaShare,
FaTrash,
FaTrashAlt,
} from 'react-icons/fa';
import DeleteImageModal from './DeleteImageModal';
import { ContextMenu } from 'chakra-ui-contextmenu';
import * as InvokeAI from 'app/invokeai';
@ -28,13 +37,60 @@ import {
resizeAndScaleCanvas,
setInitialCanvasImage,
} from 'features/canvas/store/canvasSlice';
import { hoverableImageSelector } from 'features/gallery/store/gallerySelectors';
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { useTranslation } from 'react-i18next';
import useSetBothPrompts from 'features/parameters/hooks/usePrompt';
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import IAIIconButton from 'common/components/IAIIconButton';
import { useGetUrl } from 'common/util/getUrl';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { BiZoomIn } from 'react-icons/bi';
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 { configSelector } from 'features/system/store/configSelectors';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash';
export const selector = createSelector(
[
gallerySelector,
systemSelector,
configSelector,
lightboxSelector,
activeTabNameSelector,
],
(gallery, system, config, lightbox, activeTabName) => {
const {
galleryImageObjectFit,
galleryImageMinimumWidth,
shouldUseSingleGalleryColumn,
} = gallery;
const { isLightboxOpen } = lightbox;
const { disabledFeatures } = config;
const { isConnected, isProcessing, shouldConfirmOnDelete } = system;
return {
canDeleteImage: isConnected && !isProcessing,
shouldConfirmOnDelete,
galleryImageObjectFit,
galleryImageMinimumWidth,
shouldUseSingleGalleryColumn,
activeTabName,
isLightboxOpen,
disabledFeatures,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
interface HoverableImageProps {
image: InvokeAI.Image;
@ -55,9 +111,16 @@ const HoverableImage = memo((props: HoverableImageProps) => {
activeTabName,
galleryImageObjectFit,
galleryImageMinimumWidth,
mayDeleteImage,
canDeleteImage,
shouldUseSingleGalleryColumn,
} = useAppSelector(hoverableImageSelector);
disabledFeatures,
shouldConfirmOnDelete,
} = useAppSelector(selector);
const {
isOpen: isDeleteDialogOpen,
onOpen: onDeleteDialogOpen,
onClose: onDeleteDialogClose,
} = useDisclosure();
const { image, isSelected } = props;
const { url, thumbnail, name, metadata } = image;
const { getUrl } = useGetUrl();
@ -73,6 +136,20 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleMouseOut = () => setIsHovered(false);
const handleInitiateDelete = () => {
if (shouldConfirmOnDelete) {
onDeleteDialogOpen();
} else {
handleDelete();
}
};
const handleDelete = () => {
if (canDeleteImage && image) {
dispatch(imageDeleted({ imageType: image.type, imageName: image.name }));
}
};
const handleUsePrompt = () => {
if (image.metadata?.sd_metadata?.prompt) {
setBothPrompts(image.metadata?.sd_metadata?.prompt);
@ -133,7 +210,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
metadata.sd_metadata?.image?.init_image_path
);
if (response.ok) {
dispatch(setActiveTab('img2img'));
dispatch(setAllImageToImageParameters(metadata?.sd_metadata));
toast({
title: t('toast.initialImageSet'),
@ -158,7 +234,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
};
const handleDragStart = (e: DragEvent<HTMLDivElement>) => {
console.log('drag started');
e.dataTransfer.setData('invokeai/imageName', image.name);
e.dataTransfer.setData('invokeai/imageType', image.type);
e.dataTransfer.effectAllowed = 'move';
@ -169,145 +244,172 @@ const HoverableImage = memo((props: HoverableImageProps) => {
// dispatch(setIsLightboxOpen(true));
};
const handleOpenInNewTab = () => {
window.open(getUrl(image.url), '_blank');
};
return (
<ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => (
<MenuList>
<MenuItem onClickCapture={handleLightBox}>
{t('parameters.openInViewer')}
</MenuItem>
<MenuItem
onClickCapture={handleUsePrompt}
isDisabled={image?.metadata?.sd_metadata?.prompt === undefined}
>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem
onClickCapture={handleUseSeed}
isDisabled={image?.metadata?.sd_metadata?.seed === undefined}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
onClickCapture={handleUseAllParameters}
isDisabled={
!['txt2img', 'img2img'].includes(
image?.metadata?.sd_metadata?.type
)
}
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
onClickCapture={handleUseInitialImage}
isDisabled={image?.metadata?.sd_metadata?.type !== 'img2img'}
>
{t('parameters.useInitImg')}
</MenuItem>
<MenuItem onClickCapture={handleSendToImageToImage}>
{t('parameters.sendToImg2Img')}
</MenuItem>
<MenuItem onClickCapture={handleSendToCanvas}>
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
<MenuItem data-warning>
{/* <DeleteImageModal image={image}>
<p>{t('parameters.deleteImage')}</p>
</DeleteImageModal> */}
</MenuItem>
</MenuList>
)}
>
{(ref) => (
<Box
position="relative"
key={name}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
userSelect="none"
draggable={true}
onDragStart={handleDragStart}
ref={ref}
sx={{
padding: 2,
display: 'flex',
justifyContent: 'center',
transition: 'transform 0.2s ease-out',
_hover: {
cursor: 'pointer',
zIndex: 2,
},
_before: { content: '""', display: 'block', paddingBottom: '100%' },
}}
>
<Image
objectFit={
shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
}
rounded="md"
src={getUrl(thumbnail || url)}
loading="lazy"
sx={{
position: 'absolute',
width: '100%',
height: '100%',
maxWidth: '100%',
maxHeight: '100%',
top: '50%',
transform: 'translate(-50%,-50%)',
...(direction === 'rtl'
? { insetInlineEnd: '50%' }
: { insetInlineStart: '50%' }),
}}
/>
<Flex
onClick={handleSelectImage}
sx={{
position: 'absolute',
top: '0',
insetInlineStart: '0',
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
}}
>
{isSelected && (
<Icon
as={FaCheck}
sx={{
width: '50%',
height: '50%',
fill: 'ok.500',
}}
/>
<>
<ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => (
<MenuList>
<MenuItem
icon={<ExternalLinkIcon />}
onClickCapture={handleOpenInNewTab}
>
{t('common.openInNewTab')}
</MenuItem>
{!disabledFeatures.includes('lightbox') && (
<MenuItem icon={<FaExpand />} onClickCapture={handleLightBox}>
{t('parameters.openInViewer')}
</MenuItem>
)}
</Flex>
{isHovered && galleryImageMinimumWidth >= 64 && (
<Box
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUsePrompt}
isDisabled={image?.metadata?.sd_metadata?.prompt === undefined}
>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseSeed}
isDisabled={image?.metadata?.sd_metadata?.seed === undefined}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseInitialImage}
isDisabled={image?.metadata?.sd_metadata?.type !== 'img2img'}
>
{t('parameters.useInitImg')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseAllParameters}
isDisabled={
!['txt2img', 'img2img'].includes(
image?.metadata?.sd_metadata?.type
)
}
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
icon={<FaShare />}
onClickCapture={handleSendToImageToImage}
>
{t('parameters.sendToImg2Img')}
</MenuItem>
<MenuItem icon={<FaShare />} onClickCapture={handleSendToCanvas}>
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
<MenuItem icon={<FaTrash />} onClickCapture={onDeleteDialogOpen}>
{t('gallery.deleteImage')}
</MenuItem>
</MenuList>
)}
>
{(ref) => (
<Box
position="relative"
key={name}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
userSelect="none"
draggable={true}
onDragStart={handleDragStart}
ref={ref}
sx={{
padding: 2,
display: 'flex',
justifyContent: 'center',
transition: 'transform 0.2s ease-out',
_hover: {
cursor: 'pointer',
zIndex: 2,
},
_before: {
content: '""',
display: 'block',
paddingBottom: '100%',
},
}}
>
<Image
objectFit={
shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
}
rounded="md"
src={getUrl(thumbnail || url)}
loading="lazy"
sx={{
position: 'absolute',
top: 1,
insetInlineEnd: 1,
width: '100%',
height: '100%',
maxWidth: '100%',
maxHeight: '100%',
top: '50%',
transform: 'translate(-50%,-50%)',
...(direction === 'rtl'
? { insetInlineEnd: '50%' }
: { insetInlineStart: '50%' }),
}}
/>
<Flex
onClick={handleSelectImage}
sx={{
position: 'absolute',
top: '0',
insetInlineStart: '0',
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
}}
>
{/* <DeleteImageModal image={image}>
{isSelected && (
<Icon
as={FaCheck}
sx={{
width: '50%',
height: '50%',
fill: 'ok.500',
}}
/>
)}
</Flex>
{isHovered && galleryImageMinimumWidth >= 64 && (
<Box
sx={{
position: 'absolute',
top: 1,
insetInlineEnd: 1,
}}
>
<IAIIconButton
aria-label={t('parameters.deleteImage')}
icon={<FaTrashAlt />}
onClickCapture={handleInitiateDelete}
aria-label={t('gallery.deleteImage')}
icon={<FaTrash />}
size="xs"
fontSize={14}
isDisabled={!mayDeleteImage}
isDisabled={!canDeleteImage}
/>
</DeleteImageModal> */}
</Box>
)}
</Box>
)}
</ContextMenu>
</Box>
)}
</Box>
)}
</ContextMenu>
<DeleteImageModal
isOpen={isDeleteDialogOpen}
onClose={onDeleteDialogClose}
handleDelete={handleDelete}
/>
</>
);
}, memoEqualityCheck);

View File

@ -35,7 +35,7 @@ const GALLERY_TAB_WIDTHS: Record<
> = {
// txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 },
linear: { galleryMinWidth: 200, galleryMaxWidth: 500 },
generate: { galleryMinWidth: 200, galleryMaxWidth: 500 },
unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 },
nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 },
// postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 },

View File

@ -1,9 +1,9 @@
import { GalleryState } from './gallerySlice';
/**
* Gallery slice persist blacklist
* Gallery slice persist denylist
*/
const itemsToBlacklist: (keyof GalleryState)[] = [
const itemsToDenylist: (keyof GalleryState)[] = [
'categories',
'currentCategory',
'currentImage',
@ -12,6 +12,6 @@ const itemsToBlacklist: (keyof GalleryState)[] = [
'intermediateImage',
];
export const galleryBlacklist = itemsToBlacklist.map(
(blacklistItem) => `gallery.${blacklistItem}`
export const galleryDenylist = itemsToDenylist.map(
(denylistItem) => `gallery.${denylistItem}`
);

View File

@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { configSelector } from 'features/system/store/configSelectors';
import { systemSelector } from 'features/system/store/systemSelectors';
import {
activeTabNameSelector,
@ -67,25 +68,6 @@ export const imageGallerySelector = createSelector(
}
);
export const hoverableImageSelector = createSelector(
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
(gallery, system, lightbox, activeTabName) => {
return {
mayDeleteImage: system.isConnected && !system.isProcessing,
galleryImageObjectFit: gallery.galleryImageObjectFit,
galleryImageMinimumWidth: gallery.galleryImageMinimumWidth,
shouldUseSingleGalleryColumn: gallery.shouldUseSingleGalleryColumn,
activeTabName,
isLightboxOpen: lightbox.isLightboxOpen,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
export const selectedImageSelector = createSelector(
[gallerySelector, selectResultsEntities, selectUploadsEntities],
(gallery, allResults, allUploads) => {

View File

@ -6,6 +6,7 @@ import { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types';
import { clamp } from 'lodash';
import { isImageOutput } from 'services/types/guards';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import { imageUploaded } from 'services/thunks/image';
export type GalleryCategory = 'user' | 'result';
@ -295,9 +296,10 @@ export const gallerySlice = createSlice({
* Upload Image - FULFILLED
*/
builder.addCase(imageUploaded.fulfilled, (state, action) => {
const { location } = action.payload;
const imageName = location.split('/').pop() || '';
state.selectedImageName = imageName;
const { response } = action.payload;
const uploadedImage = deserializeImageResponse(response);
state.selectedImageName = uploadedImage.name;
});
},
});

View File

@ -1,12 +0,0 @@
import { ResultsState } from './resultsSlice';
/**
* Results slice persist blacklist
*
* Currently blacklisting results slice entirely, see persist config in store.ts
*/
const itemsToBlacklist: (keyof ResultsState)[] = [];
export const resultsBlacklist = itemsToBlacklist.map(
(blacklistItem) => `results.${blacklistItem}`
);

View File

@ -0,0 +1,12 @@
import { ResultsState } from './resultsSlice';
/**
* Results slice persist denylist
*
* Currently denylisting results slice entirely, see persist config in store.ts
*/
const itemsToDenylist: (keyof ResultsState)[] = [];
export const resultsDenylist = itemsToDenylist.map(
(denylistItem) => `results.${denylistItem}`
);

View File

@ -1,8 +1,4 @@
import {
PayloadAction,
createEntityAdapter,
createSlice,
} from '@reduxjs/toolkit';
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import { Image } from 'app/invokeai';
import { invocationComplete } from 'services/events/actions';
@ -17,39 +13,30 @@ import {
extractTimestampFromImageName,
} from 'services/util/deserializeImageField';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import { imageReceived, thumbnailReceived } from 'services/thunks/image';
import {
imageDeleted,
imageReceived,
thumbnailReceived,
} from 'services/thunks/image';
// use `createEntityAdapter` to create a slice for results images
// https://redux-toolkit.js.org/api/createEntityAdapter#overview
// the "Entity" is InvokeAI.ResultImage, while the "entities" are instances of that type
export const resultsAdapter = createEntityAdapter<Image>({
// Provide a callback to get a stable, unique identifier for each entity. This defaults to
// `(item) => item.id`, but for our result images, the `name` is the unique identifier.
selectId: (image) => image.name,
// Order all images by their time (in descending order)
sortComparer: (a, b) => b.metadata.created - a.metadata.created,
});
// This type is intersected with the Entity type to create the shape of the state
type AdditionalResultsState = {
// these are a bit misleading; they refer to sessions, not results, but we don't have a route
// to list all images directly at this time...
page: number; // current page we are on
pages: number; // the total number of pages available
isLoading: boolean; // whether we are loading more images or not, mostly a placeholder
nextPage: number; // the next page to request
shouldFetchImages: boolean; // whether we need to re-fetch images or not
page: number;
pages: number;
isLoading: boolean;
nextPage: number;
};
export const initialResultsState =
resultsAdapter.getInitialState<AdditionalResultsState>({
// provide the additional initial state
page: 0,
pages: 0,
isLoading: false,
nextPage: 0,
shouldFetchImages: false,
});
export type ResultsState = typeof initialResultsState;
@ -58,21 +45,9 @@ const resultsSlice = createSlice({
name: 'results',
initialState: initialResultsState,
reducers: {
// the adapter provides some helper reducers; see the docs for all of them
// can use them as helper functions within a reducer, or use the function itself as a reducer
// here we just use the function itself as the reducer. we'll call this on `invocation_complete`
// to add a single result
resultAdded: resultsAdapter.upsertOne,
setShouldFetchImages: (state, action: PayloadAction<boolean>) => {
state.shouldFetchImages = action.payload;
},
},
extraReducers: (builder) => {
// here we can respond to a fulfilled call of the `getNextResultsPage` thunk
// because we pass in the fulfilled thunk action creator, everything is typed
/**
* Received Result Images Page - PENDING
*/
@ -90,7 +65,6 @@ const resultsSlice = createSlice({
deserializeImageResponse(image)
);
// use the adapter reducer to append all the results to state
resultsAdapter.addMany(state, resultImages);
state.page = page;
@ -103,14 +77,15 @@ const resultsSlice = createSlice({
* Invocation Complete
*/
builder.addCase(invocationComplete, (state, action) => {
const { data } = action.payload;
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 } = state.shouldFetchImages
const { url, thumbnail } = shouldFetchImages
? { url: '', thumbnail: '' }
: buildImageUrls(type, name);
@ -123,7 +98,7 @@ const resultsSlice = createSlice({
thumbnail,
metadata: {
created: timestamp,
width: result.width, // TODO: add tese dimensions
width: result.width,
height: result.height,
invokeai: {
session_id: graph_execution_state_id,
@ -136,6 +111,9 @@ const resultsSlice = createSlice({
}
});
/**
* Image Received - FULFILLED
*/
builder.addCase(imageReceived.fulfilled, (state, action) => {
const { imagePath } = action.payload;
const { imageName } = action.meta.arg;
@ -148,22 +126,34 @@ const resultsSlice = createSlice({
});
});
/**
* Thumbnail Received - FULFILLED
*/
builder.addCase(thumbnailReceived.fulfilled, (state, action) => {
const { thumbnailPath } = action.payload;
const { imageName } = action.meta.arg;
const { thumbnailName } = action.meta.arg;
resultsAdapter.updateOne(state, {
id: imageName,
id: thumbnailName,
changes: {
thumbnail: thumbnailPath,
},
});
});
/**
* Delete Image - FULFILLED
*/
builder.addCase(imageDeleted.fulfilled, (state, action) => {
const { imageType, imageName } = action.meta.arg;
if (imageType === 'results') {
resultsAdapter.removeOne(state, imageName);
}
});
},
});
// Create a set of memoized selectors based on the location of this entity state
// to be used as selectors in a `useAppSelector()` call
export const {
selectAll: selectResultsAll,
selectById: selectResultsById,
@ -172,6 +162,6 @@ export const {
selectTotal: selectResultsTotal,
} = resultsAdapter.getSelectors<RootState>((state) => state.results);
export const { resultAdded, setShouldFetchImages } = resultsSlice.actions;
export const { resultAdded } = resultsSlice.actions;
export default resultsSlice.reducer;

View File

@ -1,12 +0,0 @@
import { UploadsState } from './uploadsSlice';
/**
* Uploads slice persist blacklist
*
* Currently blacklisting uploads slice entirely, see persist config in store.ts
*/
const itemsToBlacklist: (keyof UploadsState)[] = [];
export const uploadsBlacklist = itemsToBlacklist.map(
(blacklistItem) => `uploads.${blacklistItem}`
);

View File

@ -0,0 +1,12 @@
import { UploadsState } from './uploadsSlice';
/**
* Uploads slice persist denylist
*
* Currently denylisting uploads slice entirely, see persist config in store.ts
*/
const itemsToDenylist: (keyof UploadsState)[] = [];
export const uploadsDenylist = itemsToDenylist.map(
(denylistItem) => `uploads.${denylistItem}`
);

View File

@ -6,7 +6,7 @@ import {
receivedUploadImagesPage,
IMAGES_PER_PAGE,
} from 'services/thunks/gallery';
import { imageUploaded } from 'services/thunks/image';
import { imageDeleted, imageUploaded } from 'services/thunks/image';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
export const uploadsAdapter = createEntityAdapter<Image>({
@ -71,6 +71,17 @@ const uploadsSlice = createSlice({
uploadsAdapter.addOne(state, uploadedImage);
});
/**
* Delete Image - FULFILLED
*/
builder.addCase(imageDeleted.fulfilled, (state, action) => {
const { imageType, imageName } = action.meta.arg;
if (imageType === 'uploads') {
uploadsAdapter.removeOne(state, imageName);
}
});
},
});

View File

@ -1,10 +0,0 @@
import { LightboxState } from './lightboxSlice';
/**
* Lightbox slice persist blacklist
*/
const itemsToBlacklist: (keyof LightboxState)[] = ['isLightboxOpen'];
export const lightboxBlacklist = itemsToBlacklist.map(
(blacklistItem) => `lightbox.${blacklistItem}`
);

View File

@ -0,0 +1,10 @@
import { LightboxState } from './lightboxSlice';
/**
* Lightbox slice persist denylist
*/
const itemsToDenylist: (keyof LightboxState)[] = ['isLightboxOpen'];
export const lightboxDenylist = itemsToDenylist.map(
(denylistItem) => `lightbox.${denylistItem}`
);

View File

@ -31,7 +31,12 @@ const NumberInputFieldComponent = (
};
return (
<NumberInput onChange={handleValueChanged} value={field.value}>
<NumberInput
onChange={handleValueChanged}
value={field.value}
step={props.template.type === 'integer' ? 1 : 0.1}
precision={props.template.type === 'integer' ? 0 : 3}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />

View File

@ -1,4 +1,7 @@
import { InputFieldTemplate, InputFieldValue } from 'features/nodes/types';
import {
InputFieldTemplate,
InputFieldValue,
} from 'features/nodes/types/types';
export type FieldComponentProps<
V extends InputFieldValue,

View File

@ -15,8 +15,7 @@ import { buildInputFieldValue } from '../util/fieldValueBuilders';
const templatesSelector = createSelector(
[(state: RootState) => state.nodes],
(nodes) => nodes.invocationTemplates,
{ memoizeOptions: { resultEqualityCheck: (a, b) => true } }
(nodes) => nodes.invocationTemplates
);
export const useBuildInvocation = () => {

View File

@ -1,10 +0,0 @@
import { NodesState } from './nodesSlice';
/**
* Nodes slice persist blacklist
*/
const itemsToBlacklist: (keyof NodesState)[] = ['schema', 'invocations'];
export const nodesBlacklist = itemsToBlacklist.map(
(blacklistItem) => `nodes.${blacklistItem}`
);

View File

@ -0,0 +1,10 @@
import { NodesState } from './nodesSlice';
/**
* Nodes slice persist denylist
*/
const itemsToDenylist: (keyof NodesState)[] = ['schema', 'invocationTemplates'];
export const nodesDenylist = itemsToDenylist.map(
(denylistItem) => `nodes.${denylistItem}`
);

View File

@ -85,8 +85,7 @@ const nodesSlice = createSlice({
},
extraReducers(builder) {
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
state.schema = action.payload;
state.invocationTemplates = parseSchema(action.payload);
state.invocationTemplates = action.payload;
});
builder.addMatcher(isFulfilledAnyGraphBuilt, (state, action) => {

View File

@ -13,7 +13,7 @@ import {
buildOutputFieldTemplates,
} from './fieldTemplateBuilders';
const invocationBlacklist = ['Graph', 'Collect', 'LoadImage'];
const invocationDenylist = ['Graph', 'Collect', 'LoadImage'];
export const parseSchema = (openAPI: OpenAPIV3.Document) => {
// filter out non-invocation schemas, plus some tricky invocations for now
@ -22,7 +22,7 @@ export const parseSchema = (openAPI: OpenAPIV3.Document) => {
(schema, key) =>
key.includes('Invocation') &&
!key.includes('InvocationOutput') &&
!invocationBlacklist.some((blacklistItem) => key.includes(blacklistItem))
!invocationDenylist.some((denylistItem) => key.includes(denylistItem))
) as (OpenAPIV3.ReferenceObject | InvocationSchemaObject)[];
const invocations = filteredSchemas.reduce<
@ -114,7 +114,5 @@ export const parseSchema = (openAPI: OpenAPIV3.Document) => {
return acc;
}, {});
console.debug('Generated invocations: ', invocations);
return invocations;
};

View File

@ -27,7 +27,7 @@ export default function InvokeAccordionItem({
{header}
</Box>
{additionalHeaderComponents}
{feature && <GuideIcon feature={feature} />}
{/* {feature && <GuideIcon feature={feature} />} */}
<AccordionIcon />
</Flex>
</AccordionButton>

View File

@ -12,10 +12,6 @@ export default function ImageFit() {
(state: RootState) => state.generation.shouldFitToWidthHeight
);
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
const handleChangeFit = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldFitToWidthHeight(e.target.checked));
@ -23,7 +19,6 @@ export default function ImageFit() {
return (
<IAISwitch
isDisabled={!isImageToImageEnabled}
label={t('parameters.imageFit')}
isChecked={shouldFitToWidthHeight}
onChange={handleChangeFit}

View File

@ -1,14 +1,33 @@
import { Flex, Image, VStack } from '@chakra-ui/react';
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 (
<VStack gap={2} alignItems="stretch">
<VStack gap={2} w="full" alignItems="stretch">
<ImageToImageSettingsHeader />
<InitialImagePreview />
<ImageToImageStrength label={t('parameters.img2imgStrength')} />
<ImageFit />

View File

@ -1,46 +1,73 @@
import { RootState } from 'app/store';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setImg2imgStrength } from 'features/parameters/store/generationSlice';
import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
interface ImageToImageStrengthProps {
label?: string;
}
const selector = createSelector(
[generationSelector, hotkeysSelector, configSelector],
(generation, hotkeys, config) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.img2imgStrength;
const { img2imgStrength, isImageToImageEnabled } = generation;
export default function ImageToImageStrength(props: ImageToImageStrengthProps) {
const { t } = useTranslation();
const { label = `${t('parameters.strength')}` } = props;
const img2imgStrength = useAppSelector(
(state: RootState) => state.generation.img2imgStrength
);
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
const step = hotkeys.shift ? fineStep : coarseStep;
return {
img2imgStrength,
isImageToImageEnabled,
initial,
min,
sliderMax,
inputMax,
step,
};
}
);
const ImageToImageStrength = () => {
const {
img2imgStrength,
isImageToImageEnabled,
initial,
min,
sliderMax,
inputMax,
step,
} = useAppSelector(selector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChangeStrength = (v: number) => dispatch(setImg2imgStrength(v));
const handleChange = useCallback(
(v: number) => dispatch(setImg2imgStrength(v)),
[dispatch]
);
const handleImg2ImgStrengthReset = () => {
dispatch(setImg2imgStrength(0.75));
};
const handleReset = useCallback(() => {
dispatch(setImg2imgStrength(initial));
}, [dispatch, initial]);
return (
<IAISlider
label={label}
step={0.01}
min={0.01}
max={1}
onChange={handleChangeStrength}
label={`${t('parameters.strength')}`}
step={step}
min={min}
max={sliderMax}
onChange={handleChange}
handleReset={handleReset}
value={img2imgStrength}
isInteger={false}
withInput
withSliderMarks
inputWidth={22}
withReset
handleReset={handleImg2ImgStrengthReset}
isDisabled={!isImageToImageEnabled}
sliderNumberInputProps={{ max: inputMax }}
/>
);
}
};
export default memo(ImageToImageStrength);

View File

@ -1,24 +1,36 @@
import { Flex } from '@chakra-ui/react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/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<HTMLInputElement>) =>
dispatch(isImageToImageEnabledChanged(e.target.checked));
return (
<IAISwitch
isChecked={isImageToImageEnabled}
width="auto"
onChange={handleChange}
/>
<Flex background="base.800" py={1.5} px={4} borderRadius={4}>
<IAISwitch
label={t('common.img2img')}
isChecked={isImageToImageEnabled}
width="full"
onChange={handleChange}
justifyContent="space-between"
formLabelProps={{
fontWeight: 'bold',
color: 'base.200',
}}
/>
</Flex>
);
}

View File

@ -87,47 +87,44 @@ const InitialImagePreview = () => {
}}
onDrop={handleDrop}
>
<Box
sx={{
height: 'full',
width: 'full',
opacity: isImageToImageEnabled ? 1 : 0.5,
filter: isImageToImageEnabled ? 'none' : 'auto',
blur: '5px',
position: 'relative',
}}
>
{initialImage?.url && (
<>
<Image
sx={{
fit: 'contain',
borderRadius: 'base',
}}
src={getUrl(initialImage?.url)}
onError={onError}
onLoad={() => {
setIsLoaded(true);
}}
fallback={
<Flex
sx={{ h: 36, alignItems: 'center', justifyContent: 'center' }}
>
<Spinner color="grey" w="5rem" h="5rem" />
</Flex>
}
{initialImage?.url && (
<Box
sx={{
height: 'full',
width: 'full',
opacity: isImageToImageEnabled ? 1 : 0.5,
filter: isImageToImageEnabled ? 'none' : 'auto',
blur: '5px',
position: 'relative',
}}
>
<Image
sx={{
fit: 'contain',
borderRadius: 'base',
}}
src={getUrl(initialImage?.url)}
onError={onError}
onLoad={() => {
setIsLoaded(true);
}}
fallback={
<Flex
sx={{ h: 36, alignItems: 'center', justifyContent: 'center' }}
>
<Spinner color="grey" w="5rem" h="5rem" />
</Flex>
}
/>
{isLoaded && (
<ImageToImageOverlay
setIsLoaded={setIsLoaded}
image={initialImage}
/>
{isLoaded && (
<ImageToImageOverlay
setIsLoaded={setIsLoaded}
image={initialImage}
/>
)}
</>
)}
{!initialImage?.url && <SelectImagePlaceholder />}
</Box>
)}
</Box>
)}
{!initialImage?.url && <SelectImagePlaceholder />}
{!isImageToImageEnabled && (
<Flex
sx={{

View File

@ -1,12 +1,33 @@
import { ChangeEvent } from 'react';
import { ChangeEvent, memo } from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/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';
export default function RandomizeSeed() {
// export default function RandomizeSeed() {
// const dispatch = useAppDispatch();
// const { t } = useTranslation();
// const shouldRandomizeSeed = useAppSelector(
// (state: RootState) => state.generation.shouldRandomizeSeed
// );
// const handleChangeShouldRandomizeSeed = (e: ChangeEvent<HTMLInputElement>) =>
// dispatch(setShouldRandomizeSeed(e.target.checked));
// return (
// <Switch
// aria-label={t('parameters.randomizeSeed')}
// isChecked={shouldRandomizeSeed}
// onChange={handleChangeShouldRandomizeSeed}
// />
// );
// }
const SeedToggle = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
@ -15,13 +36,15 @@ export default function RandomizeSeed() {
);
const handleChangeShouldRandomizeSeed = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldRandomizeSeed(e.target.checked));
dispatch(setShouldRandomizeSeed(!e.target.checked));
return (
<IAISwitch
label={t('parameters.randomizeSeed')}
isChecked={shouldRandomizeSeed}
<Switch
aria-label={t('parameters.randomizeSeed')}
isChecked={!shouldRandomizeSeed}
onChange={handleChangeShouldRandomizeSeed}
/>
);
}
};
export default memo(SeedToggle);

View File

@ -10,7 +10,6 @@ import Threshold from './Threshold';
const SeedSettings = () => {
return (
<VStack gap={2} alignItems="stretch">
<RandomizeSeed />
<Seed />
<Threshold />
<Perlin />

View File

@ -3,8 +3,10 @@ import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import randomInt from 'common/util/randomInt';
import { IAIIconButton } from 'exports';
import { setSeed } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
import { FaRandom } from 'react-icons/fa';
export default function ShuffleSeed() {
const dispatch = useAppDispatch();
@ -17,13 +19,20 @@ export default function ShuffleSeed() {
dispatch(setSeed(randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)));
return (
<Button
<IAIIconButton
size="sm"
isDisabled={shouldRandomizeSeed}
aria-label={t('parameters.shuffle')}
tooltip={t('parameters.shuffle')}
icon={<FaRandom />}
onClick={handleClickRandomizeSeed}
padding="0 1.5rem"
>
<p>{t('parameters.shuffle')}</p>
</Button>
/>
// <Button
// size="sm"
// onClick={handleClickRandomizeSeed}
// padding="0 1.5rem"
// >
// <p>{t('parameters.shuffle')}</p>
// </Button>
);
}

View File

@ -0,0 +1,32 @@
import { memo, useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings';
import { useAppSelector } from 'app/storeHooks';
import { RootState } from 'app/store';
import { Box } from '@chakra-ui/react';
const AnimatedImageToImagePanel = () => {
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
return (
<AnimatePresence>
{isImageToImageEnabled && (
<motion.div
initial={{ opacity: 0, scaleX: 0, width: 0 }}
animate={{ opacity: 1, scaleX: 1, width: '28rem' }}
exit={{ opacity: 0, scaleX: 0, width: 0 }}
transition={{ type: 'spring', bounce: 0, duration: 0.35 }}
>
<Box sx={{ h: 'full', w: 'full', pl: 4 }}>
<ImageToImageSettings />
</Box>
</motion.div>
)}
</AnimatePresence>
);
};
export default memo(AnimatedImageToImagePanel);

View File

@ -0,0 +1,75 @@
import { Flex, Text } from '@chakra-ui/react';
import { memo, useMemo } from 'react';
export const ratioToCSSString = (
ratio: AspectRatio,
orientation: Orientation
) => {
if (orientation === 'portrait') {
return `${ratio[0]}/${ratio[1]}`;
}
return `${ratio[1]}/${ratio[0]}`;
};
export const ratioToDisplayString = (
ratio: AspectRatio,
orientation: Orientation
) => {
if (orientation === 'portrait') {
return `${ratio[0]}:${ratio[1]}`;
}
return `${ratio[1]}:${ratio[0]}`;
};
type AspectRatioPreviewProps = {
ratio: AspectRatio;
orientation: Orientation;
size: string;
};
export type AspectRatio = [number, number];
export type Orientation = 'portrait' | 'landscape';
const AspectRatioPreview = (props: AspectRatioPreviewProps) => {
const { ratio, size, orientation } = props;
const ratioCSSString = useMemo(() => {
if (orientation === 'portrait') {
return `${ratio[0]}/${ratio[1]}`;
}
return `${ratio[1]}/${ratio[0]}`;
}, [ratio, orientation]);
const ratioDisplayString = useMemo(() => `${ratio[0]}:${ratio[1]}`, [ratio]);
return (
<Flex
sx={{
alignItems: 'center',
justifyContent: 'center',
w: size,
h: size,
}}
>
<Flex
sx={{
alignItems: 'center',
justifyContent: 'center',
bg: 'base.700',
color: 'base.400',
borderRadius: 'base',
aspectRatio: ratioCSSString,
objectFit: 'contain',
...(orientation === 'landscape' ? { h: 'full' } : { w: 'full' }),
}}
>
<Text sx={{ size: 'xs', userSelect: 'none' }}>
{ratioDisplayString}
</Text>
</Flex>
</Flex>
);
};
export default memo(AspectRatioPreview);

View File

@ -0,0 +1,76 @@
import { Box, Flex, FormControl, FormLabel, Select } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setWidth } from 'features/parameters/store/generationSlice';
import { memo, useState } from 'react';
import AspectRatioPreview, {
AspectRatio,
Orientation,
} from './AspectRatioPreview';
const RATIOS: AspectRatio[] = [
[1, 1],
[5, 4],
[3, 2],
[16, 10],
[16, 9],
];
RATIOS.forEach((r) => {
const float = r[0] / r[1];
console.log((512 * float) / 8);
});
const dimensionsSettingsSelector = createSelector(
(state: RootState) => state.generation,
(generation) => {
const { width, height } = generation;
return { width, height };
}
);
const DimensionsSettings = () => {
const { width, height } = useAppSelector(dimensionsSettingsSelector);
const dispatch = useAppDispatch();
const [ratioIndex, setRatioIndex] = useState(4);
const [orientation, setOrientation] = useState<Orientation>('portrait');
return (
<Flex gap={3}>
<Box flexShrink={0}>
<AspectRatioPreview
ratio={RATIOS[ratioIndex]}
orientation={orientation}
size="4rem"
/>
</Box>
<FormControl>
<FormLabel>Aspect Ratio</FormLabel>
<Select
onChange={(e) => {
setRatioIndex(Number(e.target.value));
}}
>
{RATIOS.map((r, i) => (
<option key={r.join()} value={i}>{`${r[0]}:${r[1]}`}</option>
))}
</Select>
</FormControl>
<IAISlider
label="Size"
value={width}
min={64}
max={2048}
step={8}
onChange={(v) => {
dispatch(setWidth(v));
}}
/>
</Flex>
);
};
export default memo(DimensionsSettings);

View File

@ -0,0 +1,58 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider 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';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
[generationSelector, hotkeysSelector, configSelector],
(generation, hotkeys, config) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.height;
const { height } = generation;
const step = hotkeys.shift ? fineStep : coarseStep;
return { height, initial, min, sliderMax, inputMax, step };
}
);
const HeightSlider = () => {
const { height, initial, min, sliderMax, inputMax, step } =
useAppSelector(selector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChange = useCallback(
(v: number) => {
dispatch(setHeight(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setHeight(initial));
}, [dispatch, initial]);
return (
<IAISlider
label={t('parameters.height')}
value={height}
min={min}
step={step}
max={sliderMax}
onChange={handleChange}
handleReset={handleReset}
withInput
withReset
withSliderMarks
sliderNumberInputProps={{ max: inputMax }}
/>
);
};
export default memo(HeightSlider);

View File

@ -1,46 +1,85 @@
import { RootState } from 'app/store';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setCfgScale } from 'features/parameters/store/generationSlice';
import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export default function MainCFGScale() {
const selector = createSelector(
[generationSelector, configSelector, uiSelector, hotkeysSelector],
(generation, config, ui, hotkeys) => {
const { initial, min, sliderMax, inputMax } = config.sd.guidance;
const { cfgScale } = generation;
const { shouldUseSliders } = ui;
const { shift } = hotkeys;
return {
cfgScale,
initial,
min,
sliderMax,
inputMax,
shouldUseSliders,
shift,
};
}
);
const GuidanceScale = () => {
const {
cfgScale,
initial,
min,
sliderMax,
inputMax,
shouldUseSliders,
shift,
} = useAppSelector(selector);
const dispatch = useAppDispatch();
const cfgScale = useAppSelector(
(state: RootState) => state.generation.cfgScale
);
const shouldUseSliders = useAppSelector(
(state: RootState) => state.ui.shouldUseSliders
);
const { t } = useTranslation();
const handleChangeCfgScale = (v: number) => dispatch(setCfgScale(v));
const handleChange = useCallback(
(v: number) => dispatch(setCfgScale(v)),
[dispatch]
);
const handleReset = useCallback(
() => dispatch(setCfgScale(initial)),
[dispatch, initial]
);
return shouldUseSliders ? (
<IAISlider
label={t('parameters.cfgScale')}
step={0.5}
min={1.01}
max={30}
onChange={handleChangeCfgScale}
handleReset={() => dispatch(setCfgScale(7.5))}
step={shift ? 0.1 : 0.5}
min={min}
max={sliderMax}
onChange={handleChange}
handleReset={handleReset}
value={cfgScale}
sliderNumberInputProps={{ max: 200 }}
sliderNumberInputProps={{ max: inputMax }}
withInput
withReset
withSliderMarks
isInteger={false}
/>
) : (
<IAINumberInput
label={t('parameters.cfgScale')}
step={0.5}
min={1.01}
max={200}
onChange={handleChangeCfgScale}
min={min}
max={inputMax}
onChange={handleChange}
value={cfgScale}
isInteger={false}
numberInputFieldProps={{ textAlign: 'center' }}
/>
);
}
};
export default memo(GuidanceScale);

View File

@ -23,7 +23,7 @@ export default function MainHeight() {
label={t('parameters.height')}
value={height}
min={64}
step={64}
step={8}
max={2048}
onChange={(v) => dispatch(setHeight(v))}
handleReset={() => dispatch(setHeight(512))}

View File

@ -1,48 +1,86 @@
import type { RootState } from 'app/store';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setIterations } from 'features/parameters/store/generationSlice';
import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export default function MainIterations() {
const iterations = useAppSelector(
(state: RootState) => state.generation.iterations
);
const selector = createSelector(
[generationSelector, configSelector, uiSelector, hotkeysSelector],
(generation, config, ui, hotkeys) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.iterations;
const { iterations } = generation;
const { shouldUseSliders } = ui;
const shouldUseSliders = useAppSelector(
(state: RootState) => state.ui.shouldUseSliders
);
const step = hotkeys.shift ? fineStep : coarseStep;
return {
iterations,
initial,
min,
sliderMax,
inputMax,
step,
shouldUseSliders,
};
}
);
const MainIterations = () => {
const {
iterations,
initial,
min,
sliderMax,
inputMax,
step,
shouldUseSliders,
} = useAppSelector(selector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChangeIterations = (v: number) => dispatch(setIterations(v));
const handleChange = useCallback(
(v: number) => {
dispatch(setIterations(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setIterations(initial));
}, [dispatch, initial]);
return shouldUseSliders ? (
<IAISlider
label={t('parameters.images')}
step={1}
min={1}
max={16}
onChange={handleChangeIterations}
handleReset={() => dispatch(setIterations(1))}
step={step}
min={min}
max={sliderMax}
onChange={handleChange}
handleReset={handleReset}
value={iterations}
withInput
withReset
withSliderMarks
sliderNumberInputProps={{ max: 9999 }}
sliderNumberInputProps={{ max: inputMax }}
/>
) : (
<IAINumberInput
label={t('parameters.images')}
step={1}
min={1}
max={9999}
onChange={handleChangeIterations}
step={step}
min={min}
max={inputMax}
onChange={handleChange}
value={iterations}
numberInputFieldProps={{ textAlign: 'center' }}
/>
);
}
};
export default memo(MainIterations);

View File

@ -1,32 +1,32 @@
import { DIFFUSERS_SAMPLERS, SAMPLERS } from 'app/constants';
import { DIFFUSERS_SAMPLERS } from 'app/constants';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISelect from 'common/components/IAISelect';
import { setSampler } from 'features/parameters/store/generationSlice';
import { activeModelSelector } from 'features/system/store/systemSelectors';
import { ChangeEvent } from 'react';
import { ChangeEvent, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export default function MainSampler() {
const Scheduler = () => {
const sampler = useAppSelector(
(state: RootState) => state.generation.sampler
);
const activeModel = useAppSelector(activeModelSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChangeSampler = (e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setSampler(e.target.value));
const handleChange = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => dispatch(setSampler(e.target.value)),
[dispatch]
);
return (
<IAISelect
label={t('parameters.sampler')}
value={sampler}
onChange={handleChangeSampler}
validValues={
activeModel.format === 'diffusers' ? DIFFUSERS_SAMPLERS : SAMPLERS
}
onChange={handleChange}
validValues={DIFFUSERS_SAMPLERS}
minWidth={36}
/>
);
}
};
export default memo(Scheduler);

View File

@ -1,14 +1,16 @@
import { Flex, VStack } from '@chakra-ui/react';
import { Box, Flex, VStack } from '@chakra-ui/react';
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import { ModelSelect } from 'exports';
import { memo } from 'react';
import HeightSlider from './HeightSlider';
import MainCFGScale from './MainCFGScale';
import MainHeight from './MainHeight';
import MainIterations from './MainIterations';
import MainSampler from './MainSampler';
import MainSteps from './MainSteps';
import MainWidth from './MainWidth';
import WidthSlider from './WidthSlider';
export default function MainSettings() {
const MainSettings = () => {
const shouldUseSliders = useAppSelector(
(state: RootState) => state.ui.shouldUseSliders
);
@ -18,22 +20,36 @@ export default function MainSettings() {
<MainIterations />
<MainSteps />
<MainCFGScale />
<MainWidth />
<MainHeight />
<MainSampler />
<WidthSlider />
<HeightSlider />
<Flex gap={3} w="full">
<Box flexGrow={2}>
<MainSampler />
</Box>
<Box flexGrow={3}>
<ModelSelect />
</Box>
</Flex>
</VStack>
) : (
<Flex rowGap={2} flexDirection="column">
<Flex columnGap={1}>
<Flex gap={3} flexDirection="column">
<Flex gap={3}>
<MainIterations />
<MainSteps />
<MainCFGScale />
</Flex>
<Flex columnGap={1}>
<MainWidth />
<MainHeight />
<MainSampler />
<WidthSlider />
<HeightSlider />
<Flex gap={3} w="full">
<Box flexGrow={2}>
<MainSampler />
</Box>
<Box flexGrow={3}>
<ModelSelect />
</Box>
</Flex>
</Flex>
);
}
};
export default memo(MainSettings);

View File

@ -1,53 +1,87 @@
import { RootState } from 'app/store';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import {
clampSymmetrySteps,
setSteps,
} from 'features/parameters/store/generationSlice';
import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export default function MainSteps() {
const selector = createSelector(
[generationSelector, configSelector, uiSelector, hotkeysSelector],
(generation, config, ui, hotkeys) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.steps;
const { steps } = generation;
const { shouldUseSliders } = ui;
const step = hotkeys.shift ? fineStep : coarseStep;
return {
steps,
initial,
min,
sliderMax,
inputMax,
step,
shouldUseSliders,
};
}
);
const MainSteps = () => {
const { steps, initial, min, sliderMax, inputMax, step, shouldUseSliders } =
useAppSelector(selector);
const dispatch = useAppDispatch();
const steps = useAppSelector((state: RootState) => state.generation.steps);
const shouldUseSliders = useAppSelector(
(state: RootState) => state.ui.shouldUseSliders
);
const { t } = useTranslation();
const handleChangeSteps = (v: number) => {
dispatch(setSteps(v));
};
const handleChange = useCallback(
(v: number) => {
dispatch(setSteps(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setSteps(initial));
}, [dispatch, initial]);
const handleBlur = () => {
const handleBlur = useCallback(() => {
dispatch(clampSymmetrySteps());
};
}, [dispatch]);
return shouldUseSliders ? (
<IAISlider
label={t('parameters.steps')}
min={1}
step={1}
onChange={handleChangeSteps}
handleReset={() => dispatch(setSteps(20))}
min={min}
max={sliderMax}
step={step}
onChange={handleChange}
handleReset={handleReset}
value={steps}
withInput
withReset
withSliderMarks
sliderNumberInputProps={{ max: 9999 }}
sliderNumberInputProps={{ max: inputMax }}
/>
) : (
<IAINumberInput
label={t('parameters.steps')}
min={1}
max={9999}
step={1}
onChange={handleChangeSteps}
min={min}
max={inputMax}
step={step}
onChange={handleChange}
value={steps}
numberInputFieldProps={{ textAlign: 'center' }}
onBlur={handleBlur}
/>
);
}
};
export default memo(MainSteps);

View File

@ -22,7 +22,7 @@ export default function MainWidth() {
isDisabled={activeTabName === 'unifiedCanvas'}
label={t('parameters.width')}
value={width}
min={64}
min={8}
step={64}
max={2048}
onChange={(v) => dispatch(setWidth(v))}

View File

@ -0,0 +1,58 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider 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';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
[generationSelector, hotkeysSelector, configSelector],
(generation, hotkeys, config) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.width;
const { width } = generation;
const step = hotkeys.shift ? fineStep : coarseStep;
return { width, initial, min, sliderMax, inputMax, step };
}
);
const WidthSlider = () => {
const { width, initial, min, sliderMax, inputMax, step } =
useAppSelector(selector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChange = useCallback(
(v: number) => {
dispatch(setWidth(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setWidth(initial));
}, [dispatch, initial]);
return (
<IAISlider
label={t('parameters.width')}
value={width}
min={min}
step={step}
max={sliderMax}
onChange={handleChange}
handleReset={handleReset}
withInput
withReset
withSliderMarks
sliderNumberInputProps={{ max: inputMax }}
/>
);
};
export default memo(WidthSlider);

View File

@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { tabMap } from 'features/ui/store/tabMap';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { openAccordionItemsChanged } from 'features/ui/store/uiSlice';
import { filter } from 'lodash';
import { map } from 'lodash';
import { ReactNode, useCallback } from 'react';
import InvokeAccordionItem from './AccordionItems/InvokeAccordionItem';
@ -14,12 +14,11 @@ const parametersAccordionSelector = createSelector([uiSelector], (uiSlice) => {
activeTab,
openLinearAccordionItems,
openUnifiedCanvasAccordionItems,
disabledParameterPanels,
} = uiSlice;
let openAccordions: number[] = [];
if (tabMap[activeTab] === 'linear') {
if (tabMap[activeTab] === 'generate') {
openAccordions = openLinearAccordionItems;
}
@ -29,7 +28,6 @@ const parametersAccordionSelector = createSelector([uiSelector], (uiSlice) => {
return {
openAccordions,
disabledParameterPanels,
};
});
@ -53,9 +51,7 @@ type ParametersAccordionProps = {
* Main container for generation and processing parameters.
*/
const ParametersAccordion = ({ accordionItems }: ParametersAccordionProps) => {
const { openAccordions, disabledParameterPanels } = useAppSelector(
parametersAccordionSelector
);
const { openAccordions } = useAppSelector(parametersAccordionSelector);
const dispatch = useAppDispatch();
@ -68,20 +64,16 @@ const ParametersAccordion = ({ accordionItems }: ParametersAccordionProps) => {
};
// Render function for accordion items
const renderAccordionItems = useCallback(() => {
// Filter out disabled accordions
const filteredAccordionItems = filter(
accordionItems,
(item) => disabledParameterPanels.indexOf(item.name) === -1
);
return filteredAccordionItems.map((accordionItem) => (
<InvokeAccordionItem
key={accordionItem.name}
accordionItem={accordionItem}
/>
));
}, [disabledParameterPanels, accordionItems]);
const renderAccordionItems = useCallback(
() =>
map(accordionItems, (accordionItem) => (
<InvokeAccordionItem
key={accordionItem.name}
accordionItem={accordionItem}
/>
)),
[accordionItems]
);
return (
<Accordion

View File

@ -11,7 +11,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaPlay } from 'react-icons/fa';
import { linearGraphBuilt, sessionCreated } from 'services/thunks/session';
import { generateGraphBuilt, sessionCreated } from 'services/thunks/session';
interface InvokeButton
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
@ -26,7 +26,7 @@ export default function InvokeButton(props: InvokeButton) {
const handleClickGenerate = () => {
// dispatch(generateImage(activeTabName));
dispatch(linearGraphBuilt());
dispatch(generateGraphBuilt());
};
const { t } = useTranslation();

View File

@ -1,10 +0,0 @@
import { GenerationState } from './generationSlice';
/**
* Generation slice persist blacklist
*/
const itemsToBlacklist: (keyof GenerationState)[] = [];
export const generationBlacklist = itemsToBlacklist.map(
(blacklistItem) => `generation.${blacklistItem}`
);

View File

@ -0,0 +1,10 @@
import { GenerationState } from './generationSlice';
/**
* Generation slice persist denylist
*/
const itemsToDenylist: (keyof GenerationState)[] = [];
export const generationDenylist = itemsToDenylist.map(
(denylistItem) => `generation.${denylistItem}`
);

View File

@ -1,10 +0,0 @@
import { PostprocessingState } from './postprocessingSlice';
/**
* Postprocessing slice persist blacklist
*/
const itemsToBlacklist: (keyof PostprocessingState)[] = [];
export const postprocessingBlacklist = itemsToBlacklist.map(
(blacklistItem) => `postprocessing.${blacklistItem}`
);

View File

@ -0,0 +1,10 @@
import { PostprocessingState } from './postprocessingSlice';
/**
* Postprocessing slice persist denylist
*/
const itemsToDenylist: (keyof PostprocessingState)[] = [];
export const postprocessingDenylist = itemsToDenylist.map(
(denylistItem) => `postprocessing.${denylistItem}`
);

View File

@ -110,7 +110,7 @@ const Console = () => {
position: 'fixed',
insetInlineStart: 0,
bottom: 0,
zIndex: 9999,
zIndex: 1,
}}
maxHeight="90vh"
>
@ -128,6 +128,7 @@ const Console = () => {
borderTopWidth: 5,
bg: 'base.850',
borderColor: 'base.700',
zIndex: 2,
}}
ref={viewerRef}
onScroll={handleOnScroll}
@ -166,7 +167,7 @@ const Console = () => {
position: 'fixed',
insetInlineStart: 2,
bottom: 12,
zIndex: '10000',
zIndex: 1,
}}
/>
</Tooltip>
@ -184,7 +185,7 @@ const Console = () => {
position: 'fixed',
insetInlineStart: 2,
bottom: 2,
zIndex: '10000',
zIndex: 1,
}}
colorScheme={hasError || !wasErrorSeen ? 'error' : 'base'}
/>

View File

@ -1,6 +1,5 @@
import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { ChangeEvent } from 'react';
import { ChangeEvent, memo } from 'react';
import { isEqual } from 'lodash';
import { useTranslation } from 'react-i18next';
@ -39,21 +38,16 @@ const ModelSelect = () => {
};
return (
<Flex
style={{
paddingInlineStart: 1.5,
}}
>
<IAISelect
style={{ fontSize: 'sm' }}
aria-label={t('accessibility.modelSelect')}
tooltip={selectedModel?.description || ''}
value={selectedModel?.name || undefined}
validValues={allModelNames}
onChange={handleChangeModel}
/>
</Flex>
<IAISelect
label={t('modelManager.model')}
style={{ fontSize: 'sm' }}
aria-label={t('accessibility.modelSelect')}
tooltip={selectedModel?.description || ''}
value={selectedModel?.name || undefined}
validValues={allModelNames}
onChange={handleChangeModel}
/>
);
};
export default ModelSelect;
export default memo(ModelSelect);

View File

@ -34,8 +34,6 @@ const SiteHeader = () => {
>
<StatusIndicator />
<ModelSelect />
{resolution === 'desktop' ? (
<SiteHeaderMenu />
) : (

View File

@ -8,27 +8,38 @@ import ModelManagerModal from './ModelManager/ModelManagerModal';
import SettingsModal from './SettingsModal/SettingsModal';
import ThemeChanger from './ThemeChanger';
import IAIIconButton from 'common/components/IAIIconButton';
import { useFeatureStatus } from '../hooks/useFeatureStatus';
const SiteHeaderMenu = () => {
const { t } = useTranslation();
const isModelManagerEnabled =
useFeatureStatus('modelManager').isFeatureEnabled;
const isLocalizationEnabled =
useFeatureStatus('localization').isFeatureEnabled;
const isBugLinkEnabled = useFeatureStatus('bugLink').isFeatureEnabled;
const isDiscordLinkEnabled = useFeatureStatus('discordLink').isFeatureEnabled;
const isGithubLinkEnabled = useFeatureStatus('githubLink').isFeatureEnabled;
return (
<Flex
alignItems="center"
flexDirection={{ base: 'column', xl: 'row' }}
gap={{ base: 4, xl: 1 }}
>
<ModelManagerModal>
<IAIIconButton
aria-label={t('modelManager.modelManager')}
tooltip={t('modelManager.modelManager')}
size="sm"
variant="link"
data-variant="link"
fontSize={20}
icon={<FaCube />}
/>
</ModelManagerModal>
{isModelManagerEnabled && (
<ModelManagerModal>
<IAIIconButton
aria-label={t('modelManager.modelManager')}
tooltip={t('modelManager.modelManager')}
size="sm"
variant="link"
data-variant="link"
fontSize={20}
icon={<FaCube />}
/>
</ModelManagerModal>
)}
<HotkeysModal>
<IAIIconButton
@ -44,55 +55,61 @@ const SiteHeaderMenu = () => {
<ThemeChanger />
<LanguagePicker />
{isLocalizationEnabled && <LanguagePicker />}
<Link
isExternal
href="http://github.com/invoke-ai/InvokeAI/issues"
marginBottom="-0.25rem"
>
<IAIIconButton
aria-label={t('common.reportBugLabel')}
tooltip={t('common.reportBugLabel')}
variant="link"
data-variant="link"
fontSize={20}
size="sm"
icon={<FaBug />}
/>
</Link>
{isBugLinkEnabled && (
<Link
isExternal
href="http://github.com/invoke-ai/InvokeAI/issues"
marginBottom="-0.25rem"
>
<IAIIconButton
aria-label={t('common.reportBugLabel')}
tooltip={t('common.reportBugLabel')}
variant="link"
data-variant="link"
fontSize={20}
size="sm"
icon={<FaBug />}
/>
</Link>
)}
<Link
isExternal
href="http://github.com/invoke-ai/InvokeAI"
marginBottom="-0.25rem"
>
<IAIIconButton
aria-label={t('common.githubLabel')}
tooltip={t('common.githubLabel')}
variant="link"
data-variant="link"
fontSize={20}
size="sm"
icon={<FaGithub />}
/>
</Link>
{isGithubLinkEnabled && (
<Link
isExternal
href="http://github.com/invoke-ai/InvokeAI"
marginBottom="-0.25rem"
>
<IAIIconButton
aria-label={t('common.githubLabel')}
tooltip={t('common.githubLabel')}
variant="link"
data-variant="link"
fontSize={20}
size="sm"
icon={<FaGithub />}
/>
</Link>
)}
<Link
isExternal
href="https://discord.gg/ZmtBAhwWhy"
marginBottom="-0.25rem"
>
<IAIIconButton
aria-label={t('common.discordLabel')}
tooltip={t('common.discordLabel')}
variant="link"
data-variant="link"
fontSize={20}
size="sm"
icon={<FaDiscord />}
/>
</Link>
{isDiscordLinkEnabled && (
<Link
isExternal
href="https://discord.gg/ZmtBAhwWhy"
marginBottom="-0.25rem"
>
<IAIIconButton
aria-label={t('common.discordLabel')}
tooltip={t('common.discordLabel')}
variant="link"
data-variant="link"
fontSize={20}
size="sm"
icon={<FaDiscord />}
/>
</Link>
)}
<SettingsModal>
<IAIIconButton

View File

@ -0,0 +1,22 @@
import { AppFeature } from 'app/invokeai';
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import { useMemo } from 'react';
export const useFeatureStatus = (feature: AppFeature) => {
const disabledFeatures = useAppSelector(
(state: RootState) => state.config.disabledFeatures
);
const isFeatureDisabled = useMemo(
() => disabledFeatures.includes(feature),
[disabledFeatures, feature]
);
const isFeatureEnabled = useMemo(
() => !disabledFeatures.includes(feature),
[disabledFeatures, feature]
);
return { isFeatureDisabled, isFeatureEnabled };
};

View File

@ -0,0 +1,41 @@
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import { useMemo } from 'react';
import { configSelector } from '../store/configSelectors';
import { systemSelector } from '../store/systemSelectors';
const isApplicationReadySelector = createSelector(
[systemSelector, configSelector],
(system, config) => {
const { wereModelsReceived, wasSchemaParsed } = system;
const { disabledTabs } = config;
return {
disabledTabs,
wereModelsReceived,
wasSchemaParsed,
};
}
);
export const useIsApplicationReady = () => {
const { disabledTabs, wereModelsReceived, wasSchemaParsed } = useAppSelector(
isApplicationReadySelector
);
const isApplicationReady = useMemo(() => {
if (!wereModelsReceived) {
return false;
}
if (!disabledTabs.includes('nodes') && !wasSchemaParsed) {
return false;
}
return true;
}, [disabledTabs, wereModelsReceived, wasSchemaParsed]);
return isApplicationReady;
};

View File

@ -0,0 +1,17 @@
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { useCallback } from 'react';
export const useIsTabDisabled = () => {
const disabledTabs = useAppSelector(
(state: RootState) => state.config.disabledTabs
);
const isTabDisabled = useCallback(
(tab: InvokeTabName) => disabledTabs.includes(tab),
[disabledTabs]
);
return isTabDisabled;
};

View File

@ -0,0 +1,3 @@
import { RootState } from 'app/store';
export const configSelector = (state: RootState) => state.config;

View File

@ -0,0 +1,76 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { AppConfig, PartialAppConfig } from 'app/invokeai';
import { merge } from 'lodash';
const initialConfigState: AppConfig = {
shouldTransformUrls: false,
shouldFetchImages: false,
disabledTabs: [],
disabledFeatures: [],
canRestoreDeletedImagesFromBin: true,
sd: {
iterations: {
initial: 1,
min: 1,
sliderMax: 20,
inputMax: 9999,
fineStep: 1,
coarseStep: 1,
},
width: {
initial: 512,
min: 64,
sliderMax: 1536,
inputMax: 4096,
fineStep: 8,
coarseStep: 64,
},
height: {
initial: 512,
min: 64,
sliderMax: 1536,
inputMax: 4096,
fineStep: 8,
coarseStep: 64,
},
steps: {
initial: 30,
min: 1,
sliderMax: 100,
inputMax: 500,
fineStep: 1,
coarseStep: 1,
},
guidance: {
initial: 7,
min: 1,
sliderMax: 20,
inputMax: 200,
fineStep: 0.1,
coarseStep: 0.5,
},
img2imgStrength: {
initial: 0.7,
min: 0,
sliderMax: 1,
inputMax: 1,
fineStep: 0.01,
coarseStep: 0.05,
},
},
};
export const configSlice = createSlice({
name: 'config',
initialState: initialConfigState,
reducers: {
configChanged: (state, action: PayloadAction<PartialAppConfig>) => {
merge(state, action.payload);
},
},
});
export const { configChanged } = configSlice.actions;
export default configSlice.reducer;

View File

@ -1,10 +0,0 @@
import { ModelsState } from './modelSlice';
/**
* Models slice persist blacklist
*/
const itemsToBlacklist: (keyof ModelsState)[] = ['entities', 'ids'];
export const modelsBlacklist = itemsToBlacklist.map(
(blacklistItem) => `models.${blacklistItem}`
);

View File

@ -0,0 +1,10 @@
import { ModelsState } from './modelSlice';
/**
* Models slice persist denylist
*/
const itemsToDenylist: (keyof ModelsState)[] = ['entities', 'ids'];
export const modelsDenylist = itemsToDenylist.map(
(denylistItem) => `models.${denylistItem}`
);

View File

@ -1,9 +1,9 @@
import { SystemState } from './systemSlice';
/**
* System slice persist blacklist
* System slice persist denylist
*/
const itemsToBlacklist: (keyof SystemState)[] = [
const itemsToDenylist: (keyof SystemState)[] = [
'currentIteration',
'currentStatus',
'currentStep',
@ -19,8 +19,10 @@ const itemsToBlacklist: (keyof SystemState)[] = [
'isCancelScheduled',
'sessionId',
'progressImage',
'wereModelsReceived',
'wasSchemaParsed',
];
export const systemBlacklist = itemsToBlacklist.map(
(blacklistItem) => `system.${blacklistItem}`
export const systemDenylist = itemsToDenylist.map(
(denylistItem) => `system.${denylistItem}`
);

View File

@ -19,6 +19,9 @@ 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 { InvokeTabName } from 'features/ui/store/tabMap';
import { receivedModels } from 'services/thunks/model';
import { receivedOpenAPISchema } from 'services/thunks/schema';
export type LogLevel = 'info' | 'warning' | 'error';
@ -32,11 +35,6 @@ export interface Log {
[index: number]: LogEntry;
}
export type ReadinessPayload = {
isReady: boolean;
reasonsWhyNotReady: string[];
};
export type InProgressImageType = 'none' | 'full-res' | 'latents';
export type CancelType = 'immediate' | 'scheduled';
@ -92,10 +90,26 @@ export interface SystemState
* Array of node IDs that we want to handle when events received
*/
subscribedNodeIds: string[];
// /**
// * Whether or not URLs should be transformed to use a different host
// */
// shouldTransformUrls: boolean;
// /**
// * Array of disabled tabs
// */
// disabledTabs: InvokeTabName[];
// /**
// * Array of disabled features
// */
// disabledFeatures: InvokeAI.AppFeature[];
/**
* Whether or not URLs should be transformed to use a different host
* Whether or not the available models were received
*/
shouldTransformUrls: boolean;
wereModelsReceived: boolean;
/**
* Whether or not the OpenAPI schema was received and parsed
*/
wasSchemaParsed: boolean;
}
const initialSystemState: SystemState = {
@ -143,7 +157,11 @@ const initialSystemState: SystemState = {
cancelType: 'immediate',
isCancelScheduled: false,
subscribedNodeIds: [],
shouldTransformUrls: false,
// shouldTransformUrls: false,
// disabledTabs: [],
// disabledFeatures: [],
wereModelsReceived: false,
wasSchemaParsed: false,
};
export const systemSlice = createSlice({
@ -341,12 +359,27 @@ export const systemSlice = createSlice({
subscribedNodeIdsSet: (state, action: PayloadAction<string[]>) => {
state.subscribedNodeIds = action.payload;
},
/**
* `shouldTransformUrls` was changed
*/
shouldTransformUrlsChanged: (state, action: PayloadAction<boolean>) => {
state.shouldTransformUrls = action.payload;
},
// /**
// * `shouldTransformUrls` was changed
// */
// shouldTransformUrlsChanged: (state, action: PayloadAction<boolean>) => {
// state.shouldTransformUrls = action.payload;
// },
// /**
// * `disabledTabs` was changed
// */
// disabledTabsChanged: (state, action: PayloadAction<InvokeTabName[]>) => {
// state.disabledTabs = action.payload;
// },
// /**
// * `disabledFeatures` was changed
// */
// disabledFeaturesChanged: (
// state,
// action: PayloadAction<InvokeAI.AppFeature[]>
// ) => {
// state.disabledFeatures = action.payload;
// },
},
extraReducers(builder) {
/**
@ -417,7 +450,8 @@ export const systemSlice = createSlice({
step,
total_steps,
progress_image,
invocation,
node,
source_node_id,
graph_execution_state_id,
} = action.payload.data;
@ -514,6 +548,20 @@ export const systemSlice = createSlice({
builder.addCase(initialImageSelected, (state) => {
state.toastQueue.push(makeToast(i18n.t('toast.sentToImageToImage')));
});
/**
* Received available models from the backend
*/
builder.addCase(receivedModels.fulfilled, (state, action) => {
state.wereModelsReceived = true;
});
/**
* OpenAPI schema was received and parsed
*/
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
state.wasSchemaParsed = true;
});
},
});
@ -553,7 +601,9 @@ export const {
scheduledCancelAborted,
cancelTypeChanged,
subscribedNodeIdsSet,
shouldTransformUrlsChanged,
// shouldTransformUrlsChanged,
// disabledTabsChanged,
// disabledFeaturesChanged,
} = systemSlice.actions;
export default systemSlice.reducer;

View File

@ -39,7 +39,7 @@ export const floatingParametersPanelButtonSelector = createSelector(
const shouldShowParametersPanelButton =
!canvasBetaLayoutCheck &&
(!shouldPinParametersPanel || !shouldShowParametersPanel) &&
['linear', 'unifiedCanvas'].includes(activeTabName);
['generate', 'unifiedCanvas'].includes(activeTabName);
return {
shouldPinParametersPanel,

View File

@ -23,8 +23,11 @@ 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 LinearWorkarea from './tabs/Linear/LinearWorkarea';
import GenerateWorkspace from './tabs/Generate/GenerateWorkspace';
import { FaImage } from 'react-icons/fa';
import { createSelector } from '@reduxjs/toolkit';
import { BsLightningChargeFill, BsLightningFill } from 'react-icons/bs';
import { configSelector } from 'features/system/store/configSelectors';
export interface InvokeTabInfo {
id: InvokeTabName;
@ -36,53 +39,55 @@ const tabIconStyles: ChakraProps['sx'] = {
boxSize: 6,
};
const buildTabs = (disabledTabs: InvokeTabName[]): InvokeTabInfo[] => {
const tabs: InvokeTabInfo[] = [
{
id: 'linear',
icon: <Icon as={FaImage} sx={tabIconStyles} />,
workarea: <LinearWorkarea />,
},
{
id: 'unifiedCanvas',
icon: <Icon as={MdGridOn} sx={tabIconStyles} />,
workarea: <UnifiedCanvasWorkarea />,
},
{
id: 'nodes',
icon: <Icon as={MdDeviceHub} sx={tabIconStyles} />,
workarea: <NodeEditor />,
},
];
const tabs: InvokeTabInfo[] = [
{
id: 'generate',
icon: <Icon as={BsLightningChargeFill} sx={{ boxSize: 5 }} />,
workarea: <GenerateWorkspace />,
},
{
id: 'unifiedCanvas',
icon: <Icon as={MdGridOn} sx={{ boxSize: 6 }} />,
workarea: <UnifiedCanvasWorkarea />,
},
{
id: 'nodes',
icon: <Icon as={MdDeviceHub} sx={{ boxSize: 6 }} />,
workarea: <NodeEditor />,
},
];
const enabledTabsSelector = createSelector(configSelector, (config) => {
const { disabledTabs } = config;
return tabs.filter((tab) => !disabledTabs.includes(tab.id));
};
});
export default function InvokeTabs() {
const activeTab = useAppSelector(activeTabIndexSelector);
const enabledTabs = useAppSelector(enabledTabsSelector);
const isLightBoxOpen = useAppSelector(
(state: RootState) => state.lightbox.isLightboxOpen
);
const { shouldPinGallery, disabledTabs, shouldPinParametersPanel } =
useAppSelector((state: RootState) => state.ui);
const activeTabs = buildTabs(disabledTabs);
const { shouldPinGallery, shouldPinParametersPanel } = useAppSelector(
(state: RootState) => state.ui
);
const { t } = useTranslation();
const dispatch = useAppDispatch();
useHotkeys('1', () => {
dispatch(setActiveTab(0));
dispatch(setActiveTab('generate'));
});
useHotkeys('2', () => {
dispatch(setActiveTab(1));
dispatch(setActiveTab('unifiedCanvas'));
});
useHotkeys('3', () => {
dispatch(setActiveTab(2));
dispatch(setActiveTab('nodes'));
});
// Lightbox Hotkey
@ -106,7 +111,7 @@ export default function InvokeTabs() {
const tabs = useMemo(
() =>
activeTabs.map((tab) => (
enabledTabs.map((tab) => (
<Tooltip
key={tab.id}
hasArrow
@ -121,13 +126,15 @@ export default function InvokeTabs() {
</Tab>
</Tooltip>
)),
[t, activeTabs]
[t, enabledTabs]
);
const tabPanels = useMemo(
() =>
activeTabs.map((tab) => <TabPanel key={tab.id}>{tab.workarea}</TabPanel>),
[activeTabs]
enabledTabs.map((tab) => (
<TabPanel key={tab.id}>{tab.workarea}</TabPanel>
)),
[enabledTabs]
);
return (

View File

@ -0,0 +1,143 @@
import { Box, Flex, useOutsideClick } from '@chakra-ui/react';
import { Slide } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash';
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<HTMLDivElement>(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 (
<Slide
direction={langDirection === 'rtl' ? 'right' : 'left'}
in={shouldShowParametersPanel}
motionProps={{ initial: false }}
style={{ zIndex: 99 }}
>
<Flex
sx={{
boxShadow: '0 0 4rem 0 rgba(0, 0, 0, 0.8)',
pl: 4,
py: 4,
h: 'full',
w: 'min',
bg: 'base.900',
borderInlineEndWidth: 4,
borderInlineEndColor: 'base.800',
}}
>
<Flex ref={outsideClickRef} position="relative" height="full" pr={4}>
<Flex
sx={{
flexDirection: 'column',
width: '28rem',
flexShrink: 0,
}}
>
<Flex
paddingTop={1.5}
paddingBottom={4}
justifyContent="space-between"
alignItems="center"
>
<InvokeAILogoComponent />
<PinParametersPanelButton />
</Flex>
<Scrollable>{props.children}</Scrollable>
</Flex>
<AnimatedImageToImagePanel />
</Flex>
</Flex>
</Slide>
);
};
export default memo(ParametersSlide);

View File

@ -1,7 +1,7 @@
import { Box, Flex } from '@chakra-ui/react';
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
const LinearContent = () => {
const GenerateContent = () => {
return (
<Box
sx={{
@ -18,4 +18,4 @@ const LinearContent = () => {
);
};
export default LinearContent;
export default GenerateContent;

View File

@ -0,0 +1,108 @@
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';
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: <MainSettings />,
// },
seed: {
name: 'seed',
header: `${t('parameters.seed')}`,
feature: Feature.SEED,
content: <SeedSettings />,
additionalHeaderComponents: <RandomizeSeed />,
},
// imageToImage: {
// name: 'imageToImage',
// header: `${t('parameters.imageToImage')}`,
// feature: undefined,
// content: <ImageToImageSettings />,
// additionalHeaderComponents: <ImageToImageToggle />,
// },
variations: {
name: 'variations',
header: `${t('parameters.variations')}`,
feature: Feature.VARIATIONS,
content: <VariationsSettings />,
additionalHeaderComponents: <GenerateVariationsToggle />,
},
symmetry: {
name: 'symmetry',
header: `${t('parameters.symmetry')}`,
content: <SymmetrySettings />,
additionalHeaderComponents: <SymmetryToggle />,
},
other: {
name: 'other',
header: `${t('parameters.otherOptions')}`,
feature: Feature.OTHER,
content: <OutputSettings />,
},
}),
[t]
);
return (
<Flex flexDir="column" gap={2}>
<PromptInput />
<NegativePromptInput />
<ProcessButtons />
<Flex
sx={{
flexDirection: 'column',
gap: 2,
bg: 'base.800',
p: 4,
pb: 6,
borderRadius: 'base',
}}
>
<MainSettings />
</Flex>
<ImageToImageToggle />
<ParametersAccordion accordionItems={generateAccordionItems} />
</Flex>
);
};
export default memo(GenerateParameters);

View File

@ -0,0 +1,53 @@
import { Box, Flex } from '@chakra-ui/react';
import { useAppSelector } from 'app/storeHooks';
import { memo } from 'react';
import GenerateContent from './GenerateContent';
import GenerateParameters from './GenerateParameters';
import PinParametersPanelButton from '../../PinParametersPanelButton';
import { RootState } from 'app/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 (
<Flex
flexDirection={{ base: 'column-reverse', xl: 'row' }}
w="full"
h="full"
gap={4}
>
{shouldPinParametersPanel ? (
<Flex sx={{ flexDirection: 'row-reverse' }}>
<AnimatedImageToImagePanel />
<Flex
sx={{
flexDirection: 'column',
width: '28rem',
flexShrink: 0,
position: 'relative',
}}
>
<Scrollable>
<GenerateParameters />
</Scrollable>
<PinParametersPanelButton
sx={{ position: 'absolute', top: 0, insetInlineEnd: 0 }}
/>
</Flex>
</Flex>
) : (
<ParametersSlide>
<GenerateParameters />
</ParametersSlide>
)}
<GenerateContent />
</Flex>
);
};
export default memo(GenerateWorkspace);

View File

@ -1,75 +0,0 @@
import { Flex } from '@chakra-ui/react';
import { Feature } from 'app/features';
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 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 { memo } from 'react';
import { useTranslation } from 'react-i18next';
const LinearParameters = () => {
const { t } = useTranslation();
const linearAccordions: ParametersAccordionItems = {
general: {
name: 'general',
header: `${t('parameters.general')}`,
feature: undefined,
content: <MainSettings />,
},
seed: {
name: 'seed',
header: `${t('parameters.seed')}`,
feature: Feature.SEED,
content: <SeedSettings />,
},
imageToImage: {
name: 'imageToImage',
header: `${t('parameters.imageToImage')}`,
feature: undefined,
content: <ImageToImageSettings />,
additionalHeaderComponents: <ImageToImageToggle />,
},
variations: {
name: 'variations',
header: `${t('parameters.variations')}`,
feature: Feature.VARIATIONS,
content: <VariationsSettings />,
additionalHeaderComponents: <GenerateVariationsToggle />,
},
symmetry: {
name: 'symmetry',
header: `${t('parameters.symmetry')}`,
content: <SymmetrySettings />,
additionalHeaderComponents: <SymmetryToggle />,
},
other: {
name: 'other',
header: `${t('parameters.otherOptions')}`,
feature: Feature.OTHER,
content: <OutputSettings />,
},
};
return (
<Flex flexDir="column" gap={2}>
<PromptInput />
<NegativePromptInput />
<ProcessButtons />
<ParametersAccordion accordionItems={linearAccordions} />
</Flex>
);
};
export default memo(LinearParameters);

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