mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into feat/compel_node
This commit is contained in:
commit
0b0068ab86
@ -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>]
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
95
invokeai/frontend/web/src/app/invokeai.d.ts
vendored
95
invokeai/frontend/web/src/app/invokeai.d.ts
vendored
@ -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'>;
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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}
|
||||
|
@ -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} />
|
||||
|
@ -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',
|
||||
}}
|
||||
>
|
||||
|
@ -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;
|
@ -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;
|
@ -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>
|
||||
);
|
||||
|
@ -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 {};
|
39
invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
Normal file
39
invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
Normal 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]
|
||||
);
|
||||
};
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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}`
|
||||
);
|
@ -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}`
|
||||
);
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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} />
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 },
|
||||
|
@ -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}`
|
||||
);
|
@ -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) => {
|
||||
|
@ -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;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -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}`
|
||||
);
|
@ -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}`
|
||||
);
|
@ -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;
|
||||
|
@ -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}`
|
||||
);
|
@ -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}`
|
||||
);
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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}`
|
||||
);
|
@ -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}`
|
||||
);
|
@ -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 />
|
||||
|
@ -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,
|
||||
|
@ -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 = () => {
|
||||
|
@ -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}`
|
||||
);
|
@ -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}`
|
||||
);
|
@ -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) => {
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ export default function InvokeAccordionItem({
|
||||
{header}
|
||||
</Box>
|
||||
{additionalHeaderComponents}
|
||||
{feature && <GuideIcon feature={feature} />}
|
||||
{/* {feature && <GuideIcon feature={feature} />} */}
|
||||
<AccordionIcon />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
|
@ -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}
|
||||
|
@ -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 />
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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={{
|
||||
|
@ -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);
|
||||
|
@ -10,7 +10,6 @@ import Threshold from './Threshold';
|
||||
const SeedSettings = () => {
|
||||
return (
|
||||
<VStack gap={2} alignItems="stretch">
|
||||
<RandomizeSeed />
|
||||
<Seed />
|
||||
<Threshold />
|
||||
<Perlin />
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
||||
|
@ -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))}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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))}
|
||||
|
@ -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);
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { GenerationState } from './generationSlice';
|
||||
|
||||
/**
|
||||
* Generation slice persist blacklist
|
||||
*/
|
||||
const itemsToBlacklist: (keyof GenerationState)[] = [];
|
||||
|
||||
export const generationBlacklist = itemsToBlacklist.map(
|
||||
(blacklistItem) => `generation.${blacklistItem}`
|
||||
);
|
@ -0,0 +1,10 @@
|
||||
import { GenerationState } from './generationSlice';
|
||||
|
||||
/**
|
||||
* Generation slice persist denylist
|
||||
*/
|
||||
const itemsToDenylist: (keyof GenerationState)[] = [];
|
||||
|
||||
export const generationDenylist = itemsToDenylist.map(
|
||||
(denylistItem) => `generation.${denylistItem}`
|
||||
);
|
@ -1,10 +0,0 @@
|
||||
import { PostprocessingState } from './postprocessingSlice';
|
||||
|
||||
/**
|
||||
* Postprocessing slice persist blacklist
|
||||
*/
|
||||
const itemsToBlacklist: (keyof PostprocessingState)[] = [];
|
||||
|
||||
export const postprocessingBlacklist = itemsToBlacklist.map(
|
||||
(blacklistItem) => `postprocessing.${blacklistItem}`
|
||||
);
|
@ -0,0 +1,10 @@
|
||||
import { PostprocessingState } from './postprocessingSlice';
|
||||
|
||||
/**
|
||||
* Postprocessing slice persist denylist
|
||||
*/
|
||||
const itemsToDenylist: (keyof PostprocessingState)[] = [];
|
||||
|
||||
export const postprocessingDenylist = itemsToDenylist.map(
|
||||
(denylistItem) => `postprocessing.${denylistItem}`
|
||||
);
|
@ -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'}
|
||||
/>
|
||||
|
@ -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);
|
||||
|
@ -34,8 +34,6 @@ const SiteHeader = () => {
|
||||
>
|
||||
<StatusIndicator />
|
||||
|
||||
<ModelSelect />
|
||||
|
||||
{resolution === 'desktop' ? (
|
||||
<SiteHeaderMenu />
|
||||
) : (
|
||||
|
@ -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
|
||||
|
@ -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 };
|
||||
};
|
@ -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;
|
||||
};
|
@ -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;
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
import { RootState } from 'app/store';
|
||||
|
||||
export const configSelector = (state: RootState) => state.config;
|
@ -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;
|
@ -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}`
|
||||
);
|
@ -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}`
|
||||
);
|
@ -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}`
|
||||
);
|
@ -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;
|
||||
|
@ -39,7 +39,7 @@ export const floatingParametersPanelButtonSelector = createSelector(
|
||||
const shouldShowParametersPanelButton =
|
||||
!canvasBetaLayoutCheck &&
|
||||
(!shouldPinParametersPanel || !shouldShowParametersPanel) &&
|
||||
['linear', 'unifiedCanvas'].includes(activeTabName);
|
||||
['generate', 'unifiedCanvas'].includes(activeTabName);
|
||||
|
||||
return {
|
||||
shouldPinParametersPanel,
|
||||
|
@ -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 (
|
||||
|
@ -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);
|
@ -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;
|
@ -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);
|
@ -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);
|
@ -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
Loading…
Reference in New Issue
Block a user