mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into stalker7779/modular_seamless
This commit is contained in:
commit
c57a7afb87
@ -55,6 +55,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
|||||||
FROM node:20-slim AS web-builder
|
FROM node:20-slim AS web-builder
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
RUN corepack use pnpm@8.x
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
@ -6,7 +6,7 @@ import pathlib
|
|||||||
import traceback
|
import traceback
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any, Dict, List, Optional, Type
|
from typing import List, Optional, Type
|
||||||
|
|
||||||
from fastapi import Body, Path, Query, Response, UploadFile
|
from fastapi import Body, Path, Query, Response, UploadFile
|
||||||
from fastapi.responses import FileResponse, HTMLResponse
|
from fastapi.responses import FileResponse, HTMLResponse
|
||||||
@ -430,13 +430,11 @@ async def delete_model_image(
|
|||||||
async def install_model(
|
async def install_model(
|
||||||
source: str = Query(description="Model source to install, can be a local path, repo_id, or remote URL"),
|
source: str = Query(description="Model source to install, can be a local path, repo_id, or remote URL"),
|
||||||
inplace: Optional[bool] = Query(description="Whether or not to install a local model in place", default=False),
|
inplace: Optional[bool] = Query(description="Whether or not to install a local model in place", default=False),
|
||||||
# TODO(MM2): Can we type this?
|
access_token: Optional[str] = Query(description="access token for the remote resource", default=None),
|
||||||
config: Optional[Dict[str, Any]] = Body(
|
config: ModelRecordChanges = Body(
|
||||||
description="Dict of fields that override auto-probed values in the model config record, such as name, description and prediction_type ",
|
description="Object containing fields that override auto-probed values in the model config record, such as name, description and prediction_type ",
|
||||||
default=None,
|
|
||||||
example={"name": "string", "description": "string"},
|
example={"name": "string", "description": "string"},
|
||||||
),
|
),
|
||||||
access_token: Optional[str] = None,
|
|
||||||
) -> ModelInstallJob:
|
) -> ModelInstallJob:
|
||||||
"""Install a model using a string identifier.
|
"""Install a model using a string identifier.
|
||||||
|
|
||||||
@ -451,8 +449,9 @@ async def install_model(
|
|||||||
- model/name:fp16:path/to/model.safetensors
|
- model/name:fp16:path/to/model.safetensors
|
||||||
- model/name::path/to/model.safetensors
|
- model/name::path/to/model.safetensors
|
||||||
|
|
||||||
`config` is an optional dict containing model configuration values that will override
|
`config` is a ModelRecordChanges object. Fields in this object will override
|
||||||
the ones that are probed automatically.
|
the ones that are probed automatically. Pass an empty object to accept
|
||||||
|
all the defaults.
|
||||||
|
|
||||||
`access_token` is an optional access token for use with Urls that require
|
`access_token` is an optional access token for use with Urls that require
|
||||||
authentication.
|
authentication.
|
||||||
@ -737,7 +736,7 @@ async def convert_model(
|
|||||||
# write the converted file to the convert path
|
# write the converted file to the convert path
|
||||||
raw_model = converted_model.model
|
raw_model = converted_model.model
|
||||||
assert hasattr(raw_model, "save_pretrained")
|
assert hasattr(raw_model, "save_pretrained")
|
||||||
raw_model.save_pretrained(convert_path)
|
raw_model.save_pretrained(convert_path) # type: ignore
|
||||||
assert convert_path.exists()
|
assert convert_path.exists()
|
||||||
|
|
||||||
# temporarily rename the original safetensors file so that there is no naming conflict
|
# temporarily rename the original safetensors file so that there is no naming conflict
|
||||||
@ -750,12 +749,12 @@ async def convert_model(
|
|||||||
try:
|
try:
|
||||||
new_key = installer.install_path(
|
new_key = installer.install_path(
|
||||||
convert_path,
|
convert_path,
|
||||||
config={
|
config=ModelRecordChanges(
|
||||||
"name": original_name,
|
name=original_name,
|
||||||
"description": model_config.description,
|
description=model_config.description,
|
||||||
"hash": model_config.hash,
|
hash=model_config.hash,
|
||||||
"source": model_config.source,
|
source=model_config.source,
|
||||||
},
|
),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
|
@ -23,7 +23,7 @@ from invokeai.backend.tiles.tiles import calc_tiles_min_overlap
|
|||||||
from invokeai.backend.tiles.utils import TBLR, Tile
|
from invokeai.backend.tiles.utils import TBLR, Tile
|
||||||
|
|
||||||
|
|
||||||
@invocation("spandrel_image_to_image", title="Image-to-Image", tags=["upscale"], category="upscale", version="1.2.0")
|
@invocation("spandrel_image_to_image", title="Image-to-Image", tags=["upscale"], category="upscale", version="1.3.0")
|
||||||
class SpandrelImageToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
class SpandrelImageToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||||
"""Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel)."""
|
"""Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel)."""
|
||||||
|
|
||||||
@ -36,16 +36,6 @@ class SpandrelImageToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
|||||||
tile_size: int = InputField(
|
tile_size: int = InputField(
|
||||||
default=512, description="The tile size for tiled image-to-image. Set to 0 to disable tiling."
|
default=512, description="The tile size for tiled image-to-image. Set to 0 to disable tiling."
|
||||||
)
|
)
|
||||||
scale: float = InputField(
|
|
||||||
default=4.0,
|
|
||||||
gt=0.0,
|
|
||||||
le=16.0,
|
|
||||||
description="The final scale of the output image. If the model does not upscale the image, this will be ignored.",
|
|
||||||
)
|
|
||||||
fit_to_multiple_of_8: bool = InputField(
|
|
||||||
default=False,
|
|
||||||
description="If true, the output image will be resized to the nearest multiple of 8 in both dimensions.",
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def scale_tile(cls, tile: Tile, scale: int) -> Tile:
|
def scale_tile(cls, tile: Tile, scale: int) -> Tile:
|
||||||
@ -152,6 +142,47 @@ class SpandrelImageToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
|
|||||||
|
|
||||||
return pil_image
|
return pil_image
|
||||||
|
|
||||||
|
@torch.inference_mode()
|
||||||
|
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||||
|
# Images are converted to RGB, because most models don't support an alpha channel. In the future, we may want to
|
||||||
|
# revisit this.
|
||||||
|
image = context.images.get_pil(self.image.image_name, mode="RGB")
|
||||||
|
|
||||||
|
# Load the model.
|
||||||
|
spandrel_model_info = context.models.load(self.image_to_image_model)
|
||||||
|
|
||||||
|
# Do the upscaling.
|
||||||
|
with spandrel_model_info as spandrel_model:
|
||||||
|
assert isinstance(spandrel_model, SpandrelImageToImageModel)
|
||||||
|
|
||||||
|
# Upscale the image
|
||||||
|
pil_image = self.upscale_image(image, self.tile_size, spandrel_model, context.util.is_canceled)
|
||||||
|
|
||||||
|
image_dto = context.images.save(image=pil_image)
|
||||||
|
return ImageOutput.build(image_dto)
|
||||||
|
|
||||||
|
|
||||||
|
@invocation(
|
||||||
|
"spandrel_image_to_image_autoscale",
|
||||||
|
title="Image-to-Image (Autoscale)",
|
||||||
|
tags=["upscale"],
|
||||||
|
category="upscale",
|
||||||
|
version="1.0.0",
|
||||||
|
)
|
||||||
|
class SpandrelImageToImageAutoscaleInvocation(SpandrelImageToImageInvocation):
|
||||||
|
"""Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel) until the target scale is reached."""
|
||||||
|
|
||||||
|
scale: float = InputField(
|
||||||
|
default=4.0,
|
||||||
|
gt=0.0,
|
||||||
|
le=16.0,
|
||||||
|
description="The final scale of the output image. If the model does not upscale the image, this will be ignored.",
|
||||||
|
)
|
||||||
|
fit_to_multiple_of_8: bool = InputField(
|
||||||
|
default=False,
|
||||||
|
description="If true, the output image will be resized to the nearest multiple of 8 in both dimensions.",
|
||||||
|
)
|
||||||
|
|
||||||
@torch.inference_mode()
|
@torch.inference_mode()
|
||||||
def invoke(self, context: InvocationContext) -> ImageOutput:
|
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||||
# Images are converted to RGB, because most models don't support an alpha channel. In the future, we may want to
|
# Images are converted to RGB, because most models don't support an alpha channel. In the future, we may want to
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from pydantic.networks import AnyHttpUrl
|
from pydantic.networks import AnyHttpUrl
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ from invokeai.app.services.download import DownloadQueueServiceBase
|
|||||||
from invokeai.app.services.events.events_base import EventServiceBase
|
from invokeai.app.services.events.events_base import EventServiceBase
|
||||||
from invokeai.app.services.invoker import Invoker
|
from invokeai.app.services.invoker import Invoker
|
||||||
from invokeai.app.services.model_install.model_install_common import ModelInstallJob, ModelSource
|
from invokeai.app.services.model_install.model_install_common import ModelInstallJob, ModelSource
|
||||||
from invokeai.app.services.model_records import ModelRecordServiceBase
|
from invokeai.app.services.model_records import ModelRecordChanges, ModelRecordServiceBase
|
||||||
from invokeai.backend.model_manager import AnyModelConfig
|
from invokeai.backend.model_manager import AnyModelConfig
|
||||||
|
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ class ModelInstallServiceBase(ABC):
|
|||||||
def register_path(
|
def register_path(
|
||||||
self,
|
self,
|
||||||
model_path: Union[Path, str],
|
model_path: Union[Path, str],
|
||||||
config: Optional[Dict[str, Any]] = None,
|
config: Optional[ModelRecordChanges] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Probe and register the model at model_path.
|
Probe and register the model at model_path.
|
||||||
@ -72,7 +72,7 @@ class ModelInstallServiceBase(ABC):
|
|||||||
This keeps the model in its current location.
|
This keeps the model in its current location.
|
||||||
|
|
||||||
:param model_path: Filesystem Path to the model.
|
:param model_path: Filesystem Path to the model.
|
||||||
:param config: Dict of attributes that will override autoassigned values.
|
:param config: ModelRecordChanges object that will override autoassigned model record values.
|
||||||
:returns id: The string ID of the registered model.
|
:returns id: The string ID of the registered model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ class ModelInstallServiceBase(ABC):
|
|||||||
def install_path(
|
def install_path(
|
||||||
self,
|
self,
|
||||||
model_path: Union[Path, str],
|
model_path: Union[Path, str],
|
||||||
config: Optional[Dict[str, Any]] = None,
|
config: Optional[ModelRecordChanges] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Probe, register and install the model in the models directory.
|
Probe, register and install the model in the models directory.
|
||||||
@ -101,7 +101,7 @@ class ModelInstallServiceBase(ABC):
|
|||||||
the models directory handled by InvokeAI.
|
the models directory handled by InvokeAI.
|
||||||
|
|
||||||
:param model_path: Filesystem Path to the model.
|
:param model_path: Filesystem Path to the model.
|
||||||
:param config: Dict of attributes that will override autoassigned values.
|
:param config: ModelRecordChanges object that will override autoassigned model record values.
|
||||||
:returns id: The string ID of the registered model.
|
:returns id: The string ID of the registered model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -109,14 +109,14 @@ class ModelInstallServiceBase(ABC):
|
|||||||
def heuristic_import(
|
def heuristic_import(
|
||||||
self,
|
self,
|
||||||
source: str,
|
source: str,
|
||||||
config: Optional[Dict[str, Any]] = None,
|
config: Optional[ModelRecordChanges] = None,
|
||||||
access_token: Optional[str] = None,
|
access_token: Optional[str] = None,
|
||||||
inplace: Optional[bool] = False,
|
inplace: Optional[bool] = False,
|
||||||
) -> ModelInstallJob:
|
) -> ModelInstallJob:
|
||||||
r"""Install the indicated model using heuristics to interpret user intentions.
|
r"""Install the indicated model using heuristics to interpret user intentions.
|
||||||
|
|
||||||
:param source: String source
|
:param source: String source
|
||||||
:param config: Optional dict. Any fields in this dict
|
:param config: Optional ModelRecordChanges object. Any fields in this object
|
||||||
will override corresponding autoassigned probe fields in the
|
will override corresponding autoassigned probe fields in the
|
||||||
model's config record as described in `import_model()`.
|
model's config record as described in `import_model()`.
|
||||||
:param access_token: Optional access token for remote sources.
|
:param access_token: Optional access token for remote sources.
|
||||||
@ -147,7 +147,7 @@ class ModelInstallServiceBase(ABC):
|
|||||||
def import_model(
|
def import_model(
|
||||||
self,
|
self,
|
||||||
source: ModelSource,
|
source: ModelSource,
|
||||||
config: Optional[Dict[str, Any]] = None,
|
config: Optional[ModelRecordChanges] = None,
|
||||||
) -> ModelInstallJob:
|
) -> ModelInstallJob:
|
||||||
"""Install the indicated model.
|
"""Install the indicated model.
|
||||||
|
|
||||||
|
@ -2,13 +2,14 @@ import re
|
|||||||
import traceback
|
import traceback
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Literal, Optional, Set, Union
|
from typing import Literal, Optional, Set, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, PrivateAttr, field_validator
|
from pydantic import BaseModel, Field, PrivateAttr, field_validator
|
||||||
from pydantic.networks import AnyHttpUrl
|
from pydantic.networks import AnyHttpUrl
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
from invokeai.app.services.download import DownloadJob, MultiFileDownloadJob
|
from invokeai.app.services.download import DownloadJob, MultiFileDownloadJob
|
||||||
|
from invokeai.app.services.model_records import ModelRecordChanges
|
||||||
from invokeai.backend.model_manager import AnyModelConfig, ModelRepoVariant
|
from invokeai.backend.model_manager import AnyModelConfig, ModelRepoVariant
|
||||||
from invokeai.backend.model_manager.config import ModelSourceType
|
from invokeai.backend.model_manager.config import ModelSourceType
|
||||||
from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata
|
from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata
|
||||||
@ -133,8 +134,9 @@ class ModelInstallJob(BaseModel):
|
|||||||
id: int = Field(description="Unique ID for this job")
|
id: int = Field(description="Unique ID for this job")
|
||||||
status: InstallStatus = Field(default=InstallStatus.WAITING, description="Current status of install process")
|
status: InstallStatus = Field(default=InstallStatus.WAITING, description="Current status of install process")
|
||||||
error_reason: Optional[str] = Field(default=None, description="Information about why the job failed")
|
error_reason: Optional[str] = Field(default=None, description="Information about why the job failed")
|
||||||
config_in: Dict[str, Any] = Field(
|
config_in: ModelRecordChanges = Field(
|
||||||
default_factory=dict, description="Configuration information (e.g. 'description') to apply to model."
|
default_factory=ModelRecordChanges,
|
||||||
|
description="Configuration information (e.g. 'description') to apply to model.",
|
||||||
)
|
)
|
||||||
config_out: Optional[AnyModelConfig] = Field(
|
config_out: Optional[AnyModelConfig] = Field(
|
||||||
default=None, description="After successful installation, this will hold the configuration object."
|
default=None, description="After successful installation, this will hold the configuration object."
|
||||||
|
@ -163,26 +163,27 @@ class ModelInstallService(ModelInstallServiceBase):
|
|||||||
def register_path(
|
def register_path(
|
||||||
self,
|
self,
|
||||||
model_path: Union[Path, str],
|
model_path: Union[Path, str],
|
||||||
config: Optional[Dict[str, Any]] = None,
|
config: Optional[ModelRecordChanges] = None,
|
||||||
) -> str: # noqa D102
|
) -> str: # noqa D102
|
||||||
model_path = Path(model_path)
|
model_path = Path(model_path)
|
||||||
config = config or {}
|
config = config or ModelRecordChanges()
|
||||||
if not config.get("source"):
|
if not config.source:
|
||||||
config["source"] = model_path.resolve().as_posix()
|
config.source = model_path.resolve().as_posix()
|
||||||
config["source_type"] = ModelSourceType.Path
|
config.source_type = ModelSourceType.Path
|
||||||
return self._register(model_path, config)
|
return self._register(model_path, config)
|
||||||
|
|
||||||
def install_path(
|
def install_path(
|
||||||
self,
|
self,
|
||||||
model_path: Union[Path, str],
|
model_path: Union[Path, str],
|
||||||
config: Optional[Dict[str, Any]] = None,
|
config: Optional[ModelRecordChanges] = None,
|
||||||
) -> str: # noqa D102
|
) -> str: # noqa D102
|
||||||
model_path = Path(model_path)
|
model_path = Path(model_path)
|
||||||
config = config or {}
|
config = config or ModelRecordChanges()
|
||||||
|
info: AnyModelConfig = ModelProbe.probe(
|
||||||
|
Path(model_path), config.model_dump(), hash_algo=self._app_config.hashing_algorithm
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
info: AnyModelConfig = ModelProbe.probe(Path(model_path), config, hash_algo=self._app_config.hashing_algorithm)
|
if preferred_name := config.name:
|
||||||
|
|
||||||
if preferred_name := config.get("name"):
|
|
||||||
preferred_name = Path(preferred_name).with_suffix(model_path.suffix)
|
preferred_name = Path(preferred_name).with_suffix(model_path.suffix)
|
||||||
|
|
||||||
dest_path = (
|
dest_path = (
|
||||||
@ -204,7 +205,7 @@ class ModelInstallService(ModelInstallServiceBase):
|
|||||||
def heuristic_import(
|
def heuristic_import(
|
||||||
self,
|
self,
|
||||||
source: str,
|
source: str,
|
||||||
config: Optional[Dict[str, Any]] = None,
|
config: Optional[ModelRecordChanges] = None,
|
||||||
access_token: Optional[str] = None,
|
access_token: Optional[str] = None,
|
||||||
inplace: Optional[bool] = False,
|
inplace: Optional[bool] = False,
|
||||||
) -> ModelInstallJob:
|
) -> ModelInstallJob:
|
||||||
@ -216,7 +217,7 @@ class ModelInstallService(ModelInstallServiceBase):
|
|||||||
source_obj.access_token = access_token
|
source_obj.access_token = access_token
|
||||||
return self.import_model(source_obj, config)
|
return self.import_model(source_obj, config)
|
||||||
|
|
||||||
def import_model(self, source: ModelSource, config: Optional[Dict[str, Any]] = None) -> ModelInstallJob: # noqa D102
|
def import_model(self, source: ModelSource, config: Optional[ModelRecordChanges] = None) -> ModelInstallJob: # noqa D102
|
||||||
similar_jobs = [x for x in self.list_jobs() if x.source == source and not x.in_terminal_state]
|
similar_jobs = [x for x in self.list_jobs() if x.source == source and not x.in_terminal_state]
|
||||||
if similar_jobs:
|
if similar_jobs:
|
||||||
self._logger.warning(f"There is already an active install job for {source}. Not enqueuing.")
|
self._logger.warning(f"There is already an active install job for {source}. Not enqueuing.")
|
||||||
@ -318,16 +319,17 @@ class ModelInstallService(ModelInstallServiceBase):
|
|||||||
model_path = self._app_config.models_path / model_path
|
model_path = self._app_config.models_path / model_path
|
||||||
model_path = model_path.resolve()
|
model_path = model_path.resolve()
|
||||||
|
|
||||||
config: dict[str, Any] = {}
|
config = ModelRecordChanges(
|
||||||
config["name"] = model_name
|
name=model_name,
|
||||||
config["description"] = stanza.get("description")
|
description=stanza.get("description"),
|
||||||
|
)
|
||||||
legacy_config_path = stanza.get("config")
|
legacy_config_path = stanza.get("config")
|
||||||
if legacy_config_path:
|
if legacy_config_path:
|
||||||
# In v3, these paths were relative to the root. Migrate them to be relative to the legacy_conf_dir.
|
# In v3, these paths were relative to the root. Migrate them to be relative to the legacy_conf_dir.
|
||||||
legacy_config_path = self._app_config.root_path / legacy_config_path
|
legacy_config_path = self._app_config.root_path / legacy_config_path
|
||||||
if legacy_config_path.is_relative_to(self._app_config.legacy_conf_path):
|
if legacy_config_path.is_relative_to(self._app_config.legacy_conf_path):
|
||||||
legacy_config_path = legacy_config_path.relative_to(self._app_config.legacy_conf_path)
|
legacy_config_path = legacy_config_path.relative_to(self._app_config.legacy_conf_path)
|
||||||
config["config_path"] = str(legacy_config_path)
|
config.config_path = str(legacy_config_path)
|
||||||
try:
|
try:
|
||||||
id = self.register_path(model_path=model_path, config=config)
|
id = self.register_path(model_path=model_path, config=config)
|
||||||
self._logger.info(f"Migrated {model_name} with id {id}")
|
self._logger.info(f"Migrated {model_name} with id {id}")
|
||||||
@ -500,11 +502,11 @@ class ModelInstallService(ModelInstallServiceBase):
|
|||||||
job.total_bytes = self._stat_size(job.local_path)
|
job.total_bytes = self._stat_size(job.local_path)
|
||||||
job.bytes = job.total_bytes
|
job.bytes = job.total_bytes
|
||||||
self._signal_job_running(job)
|
self._signal_job_running(job)
|
||||||
job.config_in["source"] = str(job.source)
|
job.config_in.source = str(job.source)
|
||||||
job.config_in["source_type"] = MODEL_SOURCE_TO_TYPE_MAP[job.source.__class__]
|
job.config_in.source_type = MODEL_SOURCE_TO_TYPE_MAP[job.source.__class__]
|
||||||
# enter the metadata, if there is any
|
# enter the metadata, if there is any
|
||||||
if isinstance(job.source_metadata, (HuggingFaceMetadata)):
|
if isinstance(job.source_metadata, (HuggingFaceMetadata)):
|
||||||
job.config_in["source_api_response"] = job.source_metadata.api_response
|
job.config_in.source_api_response = job.source_metadata.api_response
|
||||||
|
|
||||||
if job.inplace:
|
if job.inplace:
|
||||||
key = self.register_path(job.local_path, job.config_in)
|
key = self.register_path(job.local_path, job.config_in)
|
||||||
@ -639,11 +641,11 @@ class ModelInstallService(ModelInstallServiceBase):
|
|||||||
return new_path
|
return new_path
|
||||||
|
|
||||||
def _register(
|
def _register(
|
||||||
self, model_path: Path, config: Optional[Dict[str, Any]] = None, info: Optional[AnyModelConfig] = None
|
self, model_path: Path, config: Optional[ModelRecordChanges] = None, info: Optional[AnyModelConfig] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
config = config or {}
|
config = config or ModelRecordChanges()
|
||||||
|
|
||||||
info = info or ModelProbe.probe(model_path, config, hash_algo=self._app_config.hashing_algorithm)
|
info = info or ModelProbe.probe(model_path, config.model_dump(), hash_algo=self._app_config.hashing_algorithm) # type: ignore
|
||||||
|
|
||||||
model_path = model_path.resolve()
|
model_path = model_path.resolve()
|
||||||
|
|
||||||
@ -674,11 +676,13 @@ class ModelInstallService(ModelInstallServiceBase):
|
|||||||
precision = TorchDevice.choose_torch_dtype()
|
precision = TorchDevice.choose_torch_dtype()
|
||||||
return ModelRepoVariant.FP16 if precision == torch.float16 else None
|
return ModelRepoVariant.FP16 if precision == torch.float16 else None
|
||||||
|
|
||||||
def _import_local_model(self, source: LocalModelSource, config: Optional[Dict[str, Any]]) -> ModelInstallJob:
|
def _import_local_model(
|
||||||
|
self, source: LocalModelSource, config: Optional[ModelRecordChanges] = None
|
||||||
|
) -> ModelInstallJob:
|
||||||
return ModelInstallJob(
|
return ModelInstallJob(
|
||||||
id=self._next_id(),
|
id=self._next_id(),
|
||||||
source=source,
|
source=source,
|
||||||
config_in=config or {},
|
config_in=config or ModelRecordChanges(),
|
||||||
local_path=Path(source.path),
|
local_path=Path(source.path),
|
||||||
inplace=source.inplace or False,
|
inplace=source.inplace or False,
|
||||||
)
|
)
|
||||||
@ -686,7 +690,7 @@ class ModelInstallService(ModelInstallServiceBase):
|
|||||||
def _import_from_hf(
|
def _import_from_hf(
|
||||||
self,
|
self,
|
||||||
source: HFModelSource,
|
source: HFModelSource,
|
||||||
config: Optional[Dict[str, Any]] = None,
|
config: Optional[ModelRecordChanges] = None,
|
||||||
) -> ModelInstallJob:
|
) -> ModelInstallJob:
|
||||||
# Add user's cached access token to HuggingFace requests
|
# Add user's cached access token to HuggingFace requests
|
||||||
if source.access_token is None:
|
if source.access_token is None:
|
||||||
@ -702,7 +706,7 @@ class ModelInstallService(ModelInstallServiceBase):
|
|||||||
def _import_from_url(
|
def _import_from_url(
|
||||||
self,
|
self,
|
||||||
source: URLModelSource,
|
source: URLModelSource,
|
||||||
config: Optional[Dict[str, Any]],
|
config: Optional[ModelRecordChanges] = None,
|
||||||
) -> ModelInstallJob:
|
) -> ModelInstallJob:
|
||||||
remote_files, metadata = self._remote_files_from_source(source)
|
remote_files, metadata = self._remote_files_from_source(source)
|
||||||
return self._import_remote_model(
|
return self._import_remote_model(
|
||||||
@ -717,7 +721,7 @@ class ModelInstallService(ModelInstallServiceBase):
|
|||||||
source: HFModelSource | URLModelSource,
|
source: HFModelSource | URLModelSource,
|
||||||
remote_files: List[RemoteModelFile],
|
remote_files: List[RemoteModelFile],
|
||||||
metadata: Optional[AnyModelRepoMetadata],
|
metadata: Optional[AnyModelRepoMetadata],
|
||||||
config: Optional[Dict[str, Any]],
|
config: Optional[ModelRecordChanges],
|
||||||
) -> ModelInstallJob:
|
) -> ModelInstallJob:
|
||||||
if len(remote_files) == 0:
|
if len(remote_files) == 0:
|
||||||
raise ValueError(f"{source}: No downloadable files found")
|
raise ValueError(f"{source}: No downloadable files found")
|
||||||
@ -730,7 +734,7 @@ class ModelInstallService(ModelInstallServiceBase):
|
|||||||
install_job = ModelInstallJob(
|
install_job = ModelInstallJob(
|
||||||
id=self._next_id(),
|
id=self._next_id(),
|
||||||
source=source,
|
source=source,
|
||||||
config_in=config or {},
|
config_in=config or ModelRecordChanges(),
|
||||||
source_metadata=metadata,
|
source_metadata=metadata,
|
||||||
local_path=destdir, # local path may change once the download has started due to content-disposition handling
|
local_path=destdir, # local path may change once the download has started due to content-disposition handling
|
||||||
bytes=0,
|
bytes=0,
|
||||||
|
@ -18,6 +18,7 @@ from invokeai.backend.model_manager.config import (
|
|||||||
ControlAdapterDefaultSettings,
|
ControlAdapterDefaultSettings,
|
||||||
MainModelDefaultSettings,
|
MainModelDefaultSettings,
|
||||||
ModelFormat,
|
ModelFormat,
|
||||||
|
ModelSourceType,
|
||||||
ModelType,
|
ModelType,
|
||||||
ModelVariantType,
|
ModelVariantType,
|
||||||
SchedulerPredictionType,
|
SchedulerPredictionType,
|
||||||
@ -66,10 +67,16 @@ class ModelRecordChanges(BaseModelExcludeNull):
|
|||||||
"""A set of changes to apply to a model."""
|
"""A set of changes to apply to a model."""
|
||||||
|
|
||||||
# Changes applicable to all models
|
# Changes applicable to all models
|
||||||
|
source: Optional[str] = Field(description="original source of the model", default=None)
|
||||||
|
source_type: Optional[ModelSourceType] = Field(description="type of model source", default=None)
|
||||||
|
source_api_response: Optional[str] = Field(description="metadata from remote source", default=None)
|
||||||
name: Optional[str] = Field(description="Name of the model.", default=None)
|
name: Optional[str] = Field(description="Name of the model.", default=None)
|
||||||
path: Optional[str] = Field(description="Path to the model.", default=None)
|
path: Optional[str] = Field(description="Path to the model.", default=None)
|
||||||
description: Optional[str] = Field(description="Model description", default=None)
|
description: Optional[str] = Field(description="Model description", default=None)
|
||||||
base: Optional[BaseModelType] = Field(description="The base model.", default=None)
|
base: Optional[BaseModelType] = Field(description="The base model.", default=None)
|
||||||
|
type: Optional[ModelType] = Field(description="Type of model", default=None)
|
||||||
|
key: Optional[str] = Field(description="Database ID for this model", default=None)
|
||||||
|
hash: Optional[str] = Field(description="hash of model file", default=None)
|
||||||
trigger_phrases: Optional[set[str]] = Field(description="Set of trigger phrases for this model", default=None)
|
trigger_phrases: Optional[set[str]] = Field(description="Set of trigger phrases for this model", default=None)
|
||||||
default_settings: Optional[MainModelDefaultSettings | ControlAdapterDefaultSettings] = Field(
|
default_settings: Optional[MainModelDefaultSettings | ControlAdapterDefaultSettings] = Field(
|
||||||
description="Default settings for this model", default=None
|
description="Default settings for this model", default=None
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -354,7 +354,7 @@ class CLIPVisionDiffusersConfig(DiffusersConfigBase):
|
|||||||
"""Model config for CLIPVision."""
|
"""Model config for CLIPVision."""
|
||||||
|
|
||||||
type: Literal[ModelType.CLIPVision] = ModelType.CLIPVision
|
type: Literal[ModelType.CLIPVision] = ModelType.CLIPVision
|
||||||
format: Literal[ModelFormat.Diffusers]
|
format: Literal[ModelFormat.Diffusers] = ModelFormat.Diffusers
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_tag() -> Tag:
|
def get_tag() -> Tag:
|
||||||
@ -365,7 +365,7 @@ class T2IAdapterConfig(DiffusersConfigBase, ControlAdapterConfigBase):
|
|||||||
"""Model config for T2I."""
|
"""Model config for T2I."""
|
||||||
|
|
||||||
type: Literal[ModelType.T2IAdapter] = ModelType.T2IAdapter
|
type: Literal[ModelType.T2IAdapter] = ModelType.T2IAdapter
|
||||||
format: Literal[ModelFormat.Diffusers]
|
format: Literal[ModelFormat.Diffusers] = ModelFormat.Diffusers
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_tag() -> Tag:
|
def get_tag() -> Tag:
|
||||||
|
@ -98,6 +98,9 @@ class StableDiffusionDiffusersModel(GenericDiffusersLoader):
|
|||||||
ModelVariantType.Normal: StableDiffusionXLPipeline,
|
ModelVariantType.Normal: StableDiffusionXLPipeline,
|
||||||
ModelVariantType.Inpaint: StableDiffusionXLInpaintPipeline,
|
ModelVariantType.Inpaint: StableDiffusionXLInpaintPipeline,
|
||||||
},
|
},
|
||||||
|
BaseModelType.StableDiffusionXLRefiner: {
|
||||||
|
ModelVariantType.Normal: StableDiffusionXLPipeline,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
assert isinstance(config, MainCheckpointConfig)
|
assert isinstance(config, MainCheckpointConfig)
|
||||||
try:
|
try:
|
||||||
|
@ -187,164 +187,171 @@ STARTER_MODELS: list[StarterModel] = [
|
|||||||
# endregion
|
# endregion
|
||||||
# region ControlNet
|
# region ControlNet
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="QRCode Monster",
|
name="QRCode Monster v2 (SD1.5)",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="monster-labs/control_v1p_sd15_qrcode_monster",
|
source="monster-labs/control_v1p_sd15_qrcode_monster::v2",
|
||||||
description="Controlnet model that generates scannable creative QR codes",
|
description="ControlNet model that generates scannable creative QR codes",
|
||||||
|
type=ModelType.ControlNet,
|
||||||
|
),
|
||||||
|
StarterModel(
|
||||||
|
name="QRCode Monster (SDXL)",
|
||||||
|
base=BaseModelType.StableDiffusionXL,
|
||||||
|
source="monster-labs/control_v1p_sdxl_qrcode_monster",
|
||||||
|
description="ControlNet model that generates scannable creative QR codes",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="canny",
|
name="canny",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11p_sd15_canny",
|
source="lllyasviel/control_v11p_sd15_canny",
|
||||||
description="Controlnet weights trained on sd-1.5 with canny conditioning.",
|
description="ControlNet weights trained on sd-1.5 with canny conditioning.",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="inpaint",
|
name="inpaint",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11p_sd15_inpaint",
|
source="lllyasviel/control_v11p_sd15_inpaint",
|
||||||
description="Controlnet weights trained on sd-1.5 with canny conditioning, inpaint version",
|
description="ControlNet weights trained on sd-1.5 with canny conditioning, inpaint version",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="mlsd",
|
name="mlsd",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11p_sd15_mlsd",
|
source="lllyasviel/control_v11p_sd15_mlsd",
|
||||||
description="Controlnet weights trained on sd-1.5 with canny conditioning, MLSD version",
|
description="ControlNet weights trained on sd-1.5 with canny conditioning, MLSD version",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="depth",
|
name="depth",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11f1p_sd15_depth",
|
source="lllyasviel/control_v11f1p_sd15_depth",
|
||||||
description="Controlnet weights trained on sd-1.5 with depth conditioning",
|
description="ControlNet weights trained on sd-1.5 with depth conditioning",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="normal_bae",
|
name="normal_bae",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11p_sd15_normalbae",
|
source="lllyasviel/control_v11p_sd15_normalbae",
|
||||||
description="Controlnet weights trained on sd-1.5 with normalbae image conditioning",
|
description="ControlNet weights trained on sd-1.5 with normalbae image conditioning",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="seg",
|
name="seg",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11p_sd15_seg",
|
source="lllyasviel/control_v11p_sd15_seg",
|
||||||
description="Controlnet weights trained on sd-1.5 with seg image conditioning",
|
description="ControlNet weights trained on sd-1.5 with seg image conditioning",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="lineart",
|
name="lineart",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11p_sd15_lineart",
|
source="lllyasviel/control_v11p_sd15_lineart",
|
||||||
description="Controlnet weights trained on sd-1.5 with lineart image conditioning",
|
description="ControlNet weights trained on sd-1.5 with lineart image conditioning",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="lineart_anime",
|
name="lineart_anime",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11p_sd15s2_lineart_anime",
|
source="lllyasviel/control_v11p_sd15s2_lineart_anime",
|
||||||
description="Controlnet weights trained on sd-1.5 with anime image conditioning",
|
description="ControlNet weights trained on sd-1.5 with anime image conditioning",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="openpose",
|
name="openpose",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11p_sd15_openpose",
|
source="lllyasviel/control_v11p_sd15_openpose",
|
||||||
description="Controlnet weights trained on sd-1.5 with openpose image conditioning",
|
description="ControlNet weights trained on sd-1.5 with openpose image conditioning",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="scribble",
|
name="scribble",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11p_sd15_scribble",
|
source="lllyasviel/control_v11p_sd15_scribble",
|
||||||
description="Controlnet weights trained on sd-1.5 with scribble image conditioning",
|
description="ControlNet weights trained on sd-1.5 with scribble image conditioning",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="softedge",
|
name="softedge",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11p_sd15_softedge",
|
source="lllyasviel/control_v11p_sd15_softedge",
|
||||||
description="Controlnet weights trained on sd-1.5 with soft edge conditioning",
|
description="ControlNet weights trained on sd-1.5 with soft edge conditioning",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="shuffle",
|
name="shuffle",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11e_sd15_shuffle",
|
source="lllyasviel/control_v11e_sd15_shuffle",
|
||||||
description="Controlnet weights trained on sd-1.5 with shuffle image conditioning",
|
description="ControlNet weights trained on sd-1.5 with shuffle image conditioning",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="tile",
|
name="tile",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11f1e_sd15_tile",
|
source="lllyasviel/control_v11f1e_sd15_tile",
|
||||||
description="Controlnet weights trained on sd-1.5 with tiled image conditioning",
|
description="ControlNet weights trained on sd-1.5 with tiled image conditioning",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="ip2p",
|
name="ip2p",
|
||||||
base=BaseModelType.StableDiffusion1,
|
base=BaseModelType.StableDiffusion1,
|
||||||
source="lllyasviel/control_v11e_sd15_ip2p",
|
source="lllyasviel/control_v11e_sd15_ip2p",
|
||||||
description="Controlnet weights trained on sd-1.5 with ip2p conditioning.",
|
description="ControlNet weights trained on sd-1.5 with ip2p conditioning.",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="canny-sdxl",
|
name="canny-sdxl",
|
||||||
base=BaseModelType.StableDiffusionXL,
|
base=BaseModelType.StableDiffusionXL,
|
||||||
source="xinsir/controlnet-canny-sdxl-1.0",
|
source="xinsir/controlNet-canny-sdxl-1.0",
|
||||||
description="Controlnet weights trained on sdxl-1.0 with canny conditioning, by Xinsir.",
|
description="ControlNet weights trained on sdxl-1.0 with canny conditioning, by Xinsir.",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="depth-sdxl",
|
name="depth-sdxl",
|
||||||
base=BaseModelType.StableDiffusionXL,
|
base=BaseModelType.StableDiffusionXL,
|
||||||
source="diffusers/controlnet-depth-sdxl-1.0",
|
source="diffusers/controlNet-depth-sdxl-1.0",
|
||||||
description="Controlnet weights trained on sdxl-1.0 with depth conditioning.",
|
description="ControlNet weights trained on sdxl-1.0 with depth conditioning.",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="softedge-dexined-sdxl",
|
name="softedge-dexined-sdxl",
|
||||||
base=BaseModelType.StableDiffusionXL,
|
base=BaseModelType.StableDiffusionXL,
|
||||||
source="SargeZT/controlnet-sd-xl-1.0-softedge-dexined",
|
source="SargeZT/controlNet-sd-xl-1.0-softedge-dexined",
|
||||||
description="Controlnet weights trained on sdxl-1.0 with dexined soft edge preprocessing.",
|
description="ControlNet weights trained on sdxl-1.0 with dexined soft edge preprocessing.",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="depth-16bit-zoe-sdxl",
|
name="depth-16bit-zoe-sdxl",
|
||||||
base=BaseModelType.StableDiffusionXL,
|
base=BaseModelType.StableDiffusionXL,
|
||||||
source="SargeZT/controlnet-sd-xl-1.0-depth-16bit-zoe",
|
source="SargeZT/controlNet-sd-xl-1.0-depth-16bit-zoe",
|
||||||
description="Controlnet weights trained on sdxl-1.0 with Zoe's preprocessor (16 bits).",
|
description="ControlNet weights trained on sdxl-1.0 with Zoe's preprocessor (16 bits).",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="depth-zoe-sdxl",
|
name="depth-zoe-sdxl",
|
||||||
base=BaseModelType.StableDiffusionXL,
|
base=BaseModelType.StableDiffusionXL,
|
||||||
source="diffusers/controlnet-zoe-depth-sdxl-1.0",
|
source="diffusers/controlNet-zoe-depth-sdxl-1.0",
|
||||||
description="Controlnet weights trained on sdxl-1.0 with Zoe's preprocessor (32 bits).",
|
description="ControlNet weights trained on sdxl-1.0 with Zoe's preprocessor (32 bits).",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="openpose-sdxl",
|
name="openpose-sdxl",
|
||||||
base=BaseModelType.StableDiffusionXL,
|
base=BaseModelType.StableDiffusionXL,
|
||||||
source="xinsir/controlnet-openpose-sdxl-1.0",
|
source="xinsir/controlNet-openpose-sdxl-1.0",
|
||||||
description="Controlnet weights trained on sdxl-1.0 compatible with the DWPose processor by Xinsir.",
|
description="ControlNet weights trained on sdxl-1.0 compatible with the DWPose processor by Xinsir.",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="scribble-sdxl",
|
name="scribble-sdxl",
|
||||||
base=BaseModelType.StableDiffusionXL,
|
base=BaseModelType.StableDiffusionXL,
|
||||||
source="xinsir/controlnet-scribble-sdxl-1.0",
|
source="xinsir/controlNet-scribble-sdxl-1.0",
|
||||||
description="Controlnet weights trained on sdxl-1.0 compatible with various lineart processors and black/white sketches by Xinsir.",
|
description="ControlNet weights trained on sdxl-1.0 compatible with various lineart processors and black/white sketches by Xinsir.",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
StarterModel(
|
StarterModel(
|
||||||
name="tile-sdxl",
|
name="tile-sdxl",
|
||||||
base=BaseModelType.StableDiffusionXL,
|
base=BaseModelType.StableDiffusionXL,
|
||||||
source="xinsir/controlnet-tile-sdxl-1.0",
|
source="xinsir/controlNet-tile-sdxl-1.0",
|
||||||
description="Controlnet weights trained on sdxl-1.0 with tiled image conditioning",
|
description="ControlNet weights trained on sdxl-1.0 with tiled image conditioning",
|
||||||
type=ModelType.ControlNet,
|
type=ModelType.ControlNet,
|
||||||
),
|
),
|
||||||
# endregion
|
# endregion
|
||||||
|
@ -155,5 +155,8 @@
|
|||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-tsconfig-paths": "^4.3.2",
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": "8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,10 +77,6 @@
|
|||||||
"title": "استعادة الوجوه",
|
"title": "استعادة الوجوه",
|
||||||
"desc": "استعادة الصورة الحالية"
|
"desc": "استعادة الصورة الحالية"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "تحسين الحجم",
|
|
||||||
"desc": "تحسين حجم الصورة الحالية"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "عرض المعلومات",
|
"title": "عرض المعلومات",
|
||||||
"desc": "عرض معلومات البيانات الخاصة بالصورة الحالية"
|
"desc": "عرض معلومات البيانات الخاصة بالصورة الحالية"
|
||||||
@ -255,8 +251,6 @@
|
|||||||
"type": "نوع",
|
"type": "نوع",
|
||||||
"strength": "قوة",
|
"strength": "قوة",
|
||||||
"upscaling": "تصغير",
|
"upscaling": "تصغير",
|
||||||
"upscale": "تصغير",
|
|
||||||
"upscaleImage": "تصغير الصورة",
|
|
||||||
"scale": "مقياس",
|
"scale": "مقياس",
|
||||||
"imageFit": "ملائمة الصورة الأولية لحجم الخرج",
|
"imageFit": "ملائمة الصورة الأولية لحجم الخرج",
|
||||||
"scaleBeforeProcessing": "تحجيم قبل المعالجة",
|
"scaleBeforeProcessing": "تحجيم قبل المعالجة",
|
||||||
|
@ -187,10 +187,6 @@
|
|||||||
"title": "Gesicht restaurieren",
|
"title": "Gesicht restaurieren",
|
||||||
"desc": "Das aktuelle Bild restaurieren"
|
"desc": "Das aktuelle Bild restaurieren"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "Hochskalieren",
|
|
||||||
"desc": "Das aktuelle Bild hochskalieren"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "Info anzeigen",
|
"title": "Info anzeigen",
|
||||||
"desc": "Metadaten des aktuellen Bildes anzeigen"
|
"desc": "Metadaten des aktuellen Bildes anzeigen"
|
||||||
@ -433,8 +429,6 @@
|
|||||||
"type": "Art",
|
"type": "Art",
|
||||||
"strength": "Stärke",
|
"strength": "Stärke",
|
||||||
"upscaling": "Hochskalierung",
|
"upscaling": "Hochskalierung",
|
||||||
"upscale": "Hochskalieren (Shift + U)",
|
|
||||||
"upscaleImage": "Bild hochskalieren",
|
|
||||||
"scale": "Maßstab",
|
"scale": "Maßstab",
|
||||||
"imageFit": "Ausgangsbild an Ausgabegröße anpassen",
|
"imageFit": "Ausgangsbild an Ausgabegröße anpassen",
|
||||||
"scaleBeforeProcessing": "Skalieren vor der Verarbeitung",
|
"scaleBeforeProcessing": "Skalieren vor der Verarbeitung",
|
||||||
|
@ -32,12 +32,14 @@
|
|||||||
"deleteBoardAndImages": "Delete Board and Images",
|
"deleteBoardAndImages": "Delete Board and Images",
|
||||||
"deleteBoardOnly": "Delete Board Only",
|
"deleteBoardOnly": "Delete Board Only",
|
||||||
"deletedBoardsCannotbeRestored": "Deleted boards cannot be restored",
|
"deletedBoardsCannotbeRestored": "Deleted boards cannot be restored",
|
||||||
|
"hideBoards": "Hide Boards",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"menuItemAutoAdd": "Auto-add to this Board",
|
"menuItemAutoAdd": "Auto-add to this Board",
|
||||||
"move": "Move",
|
"move": "Move",
|
||||||
"movingImagesToBoard_one": "Moving {{count}} image to board:",
|
"movingImagesToBoard_one": "Moving {{count}} image to board:",
|
||||||
"movingImagesToBoard_other": "Moving {{count}} images to board:",
|
"movingImagesToBoard_other": "Moving {{count}} images to board:",
|
||||||
"myBoard": "My Board",
|
"myBoard": "My Board",
|
||||||
|
"noBoards": "No {{boardType}} Boards",
|
||||||
"noMatching": "No matching Boards",
|
"noMatching": "No matching Boards",
|
||||||
"private": "Private Boards",
|
"private": "Private Boards",
|
||||||
"searchBoard": "Search Boards...",
|
"searchBoard": "Search Boards...",
|
||||||
@ -46,6 +48,7 @@
|
|||||||
"topMessage": "This board contains images used in the following features:",
|
"topMessage": "This board contains images used in the following features:",
|
||||||
"unarchiveBoard": "Unarchive Board",
|
"unarchiveBoard": "Unarchive Board",
|
||||||
"uncategorized": "Uncategorized",
|
"uncategorized": "Uncategorized",
|
||||||
|
"viewBoards": "View Boards",
|
||||||
"downloadBoard": "Download Board",
|
"downloadBoard": "Download Board",
|
||||||
"imagesWithCount_one": "{{count}} image",
|
"imagesWithCount_one": "{{count}} image",
|
||||||
"imagesWithCount_other": "{{count}} images",
|
"imagesWithCount_other": "{{count}} images",
|
||||||
@ -102,6 +105,7 @@
|
|||||||
"negativePrompt": "Negative Prompt",
|
"negativePrompt": "Negative Prompt",
|
||||||
"discordLabel": "Discord",
|
"discordLabel": "Discord",
|
||||||
"dontAskMeAgain": "Don't ask me again",
|
"dontAskMeAgain": "Don't ask me again",
|
||||||
|
"dontShowMeThese": "Don't show me these",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"file": "File",
|
"file": "File",
|
||||||
@ -373,10 +377,14 @@
|
|||||||
"displayBoardSearch": "Display Board Search",
|
"displayBoardSearch": "Display Board Search",
|
||||||
"displaySearch": "Display Search",
|
"displaySearch": "Display Search",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
|
"exitBoardSearch": "Exit Board Search",
|
||||||
|
"exitSearch": "Exit Search",
|
||||||
"featuresWillReset": "If you delete this image, those features will immediately be reset.",
|
"featuresWillReset": "If you delete this image, those features will immediately be reset.",
|
||||||
"galleryImageSize": "Image Size",
|
"galleryImageSize": "Image Size",
|
||||||
"gallerySettings": "Gallery Settings",
|
"gallerySettings": "Gallery Settings",
|
||||||
|
"go": "Go",
|
||||||
"image": "image",
|
"image": "image",
|
||||||
|
"jump": "Jump",
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"loadMore": "Load More",
|
"loadMore": "Load More",
|
||||||
"newestFirst": "Newest First",
|
"newestFirst": "Newest First",
|
||||||
@ -636,9 +644,9 @@
|
|||||||
"title": "Undo Stroke"
|
"title": "Undo Stroke"
|
||||||
},
|
},
|
||||||
"unifiedCanvasHotkeys": "Unified Canvas",
|
"unifiedCanvasHotkeys": "Unified Canvas",
|
||||||
"upscale": {
|
"postProcess": {
|
||||||
"desc": "Upscale the current image",
|
"desc": "Process the current image using the selected post-processing model",
|
||||||
"title": "Upscale"
|
"title": "Process Image"
|
||||||
},
|
},
|
||||||
"toggleViewer": {
|
"toggleViewer": {
|
||||||
"desc": "Switches between the Image Viewer and workspace for the current tab.",
|
"desc": "Switches between the Image Viewer and workspace for the current tab.",
|
||||||
@ -1035,8 +1043,8 @@
|
|||||||
"symmetry": "Symmetry",
|
"symmetry": "Symmetry",
|
||||||
"tileSize": "Tile Size",
|
"tileSize": "Tile Size",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"upscale": "Upscale (Shift + U)",
|
"postProcessing": "Post-Processing (Shift + U)",
|
||||||
"upscaleImage": "Upscale Image",
|
"processImage": "Process Image",
|
||||||
"upscaling": "Upscaling",
|
"upscaling": "Upscaling",
|
||||||
"useAll": "Use All",
|
"useAll": "Use All",
|
||||||
"useSize": "Use Size",
|
"useSize": "Use Size",
|
||||||
@ -1092,6 +1100,8 @@
|
|||||||
"displayInProgress": "Display Progress Images",
|
"displayInProgress": "Display Progress Images",
|
||||||
"enableImageDebugging": "Enable Image Debugging",
|
"enableImageDebugging": "Enable Image Debugging",
|
||||||
"enableInformationalPopovers": "Enable Informational Popovers",
|
"enableInformationalPopovers": "Enable Informational Popovers",
|
||||||
|
"informationalPopoversDisabled": "Informational Popovers Disabled",
|
||||||
|
"informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.",
|
||||||
"enableInvisibleWatermark": "Enable Invisible Watermark",
|
"enableInvisibleWatermark": "Enable Invisible Watermark",
|
||||||
"enableNSFWChecker": "Enable NSFW Checker",
|
"enableNSFWChecker": "Enable NSFW Checker",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
@ -1499,6 +1509,30 @@
|
|||||||
"seamlessTilingYAxis": {
|
"seamlessTilingYAxis": {
|
||||||
"heading": "Seamless Tiling Y Axis",
|
"heading": "Seamless Tiling Y Axis",
|
||||||
"paragraphs": ["Seamlessly tile an image along the vertical axis."]
|
"paragraphs": ["Seamlessly tile an image along the vertical axis."]
|
||||||
|
},
|
||||||
|
"upscaleModel": {
|
||||||
|
"heading": "Upscale Model",
|
||||||
|
"paragraphs": [
|
||||||
|
"The upscale model scales the image to the output size before details are added. Any supported upscale model may be used, but some are specialized for different kinds of images, like photos or line drawings."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"heading": "Scale",
|
||||||
|
"paragraphs": [
|
||||||
|
"Scale controls the output image size, and is based on a multiple of the input image resolution. For example a 2x upscale on a 1024x1024 image would produce a 2048 x 2048 output."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"creativity": {
|
||||||
|
"heading": "Creativity",
|
||||||
|
"paragraphs": [
|
||||||
|
"Creativity controls the amount of freedom granted to the model when adding details. Low creativity stays close to the original image, while high creativity allows for more change. When using a prompt, high creativity increases the influence of the prompt."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"structure": {
|
||||||
|
"heading": "Structure",
|
||||||
|
"paragraphs": [
|
||||||
|
"Structure controls how closely the output image will keep to the layout of the original. Low structure allows major changes, while high structure strictly maintains the original composition and layout."
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"unifiedCanvas": {
|
"unifiedCanvas": {
|
||||||
@ -1645,7 +1679,9 @@
|
|||||||
"creativity": "Creativity",
|
"creativity": "Creativity",
|
||||||
"structure": "Structure",
|
"structure": "Structure",
|
||||||
"upscaleModel": "Upscale Model",
|
"upscaleModel": "Upscale Model",
|
||||||
|
"postProcessingModel": "Post-Processing Model",
|
||||||
"scale": "Scale",
|
"scale": "Scale",
|
||||||
|
"postProcessingMissingModelWarning": "Visit the <LinkComponent>Model Manager</LinkComponent> to install a post-processing (image to image) model.",
|
||||||
"missingModelsWarning": "Visit the <LinkComponent>Model Manager</LinkComponent> to install the required models:",
|
"missingModelsWarning": "Visit the <LinkComponent>Model Manager</LinkComponent> to install the required models:",
|
||||||
"mainModelDesc": "Main model (SD1.5 or SDXL architecture)",
|
"mainModelDesc": "Main model (SD1.5 or SDXL architecture)",
|
||||||
"tileControlNetModelDesc": "Tile ControlNet model for the chosen main model architecture",
|
"tileControlNetModelDesc": "Tile ControlNet model for the chosen main model architecture",
|
||||||
@ -1654,6 +1690,12 @@
|
|||||||
"missingUpscaleModel": "Missing upscale model",
|
"missingUpscaleModel": "Missing upscale model",
|
||||||
"missingTileControlNetModel": "No valid tile ControlNet models installed"
|
"missingTileControlNetModel": "No valid tile ControlNet models installed"
|
||||||
},
|
},
|
||||||
|
"upsell": {
|
||||||
|
"inviteTeammates": "Invite Teammates",
|
||||||
|
"professional": "Professional",
|
||||||
|
"professionalUpsell": "Available in Invoke’s Professional Edition. Click here or visit invoke.com/pricing for more details.",
|
||||||
|
"shareAccess": "Share Access"
|
||||||
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"generation": "Generation",
|
"generation": "Generation",
|
||||||
|
@ -151,10 +151,6 @@
|
|||||||
"title": "Restaurar rostros",
|
"title": "Restaurar rostros",
|
||||||
"desc": "Restaurar rostros en la imagen actual"
|
"desc": "Restaurar rostros en la imagen actual"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "Aumentar resolución",
|
|
||||||
"desc": "Aumentar la resolución de la imagen actual"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "Mostrar información",
|
"title": "Mostrar información",
|
||||||
"desc": "Mostar metadatos de la imagen actual"
|
"desc": "Mostar metadatos de la imagen actual"
|
||||||
@ -360,8 +356,6 @@
|
|||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"strength": "Fuerza",
|
"strength": "Fuerza",
|
||||||
"upscaling": "Aumento de resolución",
|
"upscaling": "Aumento de resolución",
|
||||||
"upscale": "Aumentar resolución",
|
|
||||||
"upscaleImage": "Aumentar la resolución de la imagen",
|
|
||||||
"scale": "Escala",
|
"scale": "Escala",
|
||||||
"imageFit": "Ajuste tamaño de imagen inicial al tamaño objetivo",
|
"imageFit": "Ajuste tamaño de imagen inicial al tamaño objetivo",
|
||||||
"scaleBeforeProcessing": "Redimensionar antes de procesar",
|
"scaleBeforeProcessing": "Redimensionar antes de procesar",
|
||||||
@ -408,7 +402,12 @@
|
|||||||
"showProgressInViewer": "Mostrar las imágenes del progreso en el visor",
|
"showProgressInViewer": "Mostrar las imágenes del progreso en el visor",
|
||||||
"ui": "Interfaz del usuario",
|
"ui": "Interfaz del usuario",
|
||||||
"generation": "Generación",
|
"generation": "Generación",
|
||||||
"beta": "Beta"
|
"beta": "Beta",
|
||||||
|
"reloadingIn": "Recargando en",
|
||||||
|
"intermediatesClearedFailed": "Error limpiando los intermediarios",
|
||||||
|
"intermediatesCleared_one": "Borrado {{count}} intermediario",
|
||||||
|
"intermediatesCleared_many": "Borrados {{count}} intermediarios",
|
||||||
|
"intermediatesCleared_other": "Borrados {{count}} intermediarios"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"uploadFailed": "Error al subir archivo",
|
"uploadFailed": "Error al subir archivo",
|
||||||
@ -426,7 +425,12 @@
|
|||||||
"parameterSet": "Conjunto de parámetros",
|
"parameterSet": "Conjunto de parámetros",
|
||||||
"parameterNotSet": "Parámetro no configurado",
|
"parameterNotSet": "Parámetro no configurado",
|
||||||
"problemCopyingImage": "No se puede copiar la imagen",
|
"problemCopyingImage": "No se puede copiar la imagen",
|
||||||
"errorCopied": "Error al copiar"
|
"errorCopied": "Error al copiar",
|
||||||
|
"baseModelChanged": "Modelo base cambiado",
|
||||||
|
"addedToBoard": "Añadido al tablero",
|
||||||
|
"baseModelChangedCleared_one": "Borrado o desactivado {{count}} submodelo incompatible",
|
||||||
|
"baseModelChangedCleared_many": "Borrados o desactivados {{count}} submodelos incompatibles",
|
||||||
|
"baseModelChangedCleared_other": "Borrados o desactivados {{count}} submodelos incompatibles"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"feature": {
|
"feature": {
|
||||||
@ -540,7 +544,13 @@
|
|||||||
"downloadBoard": "Descargar panel",
|
"downloadBoard": "Descargar panel",
|
||||||
"deleteBoardOnly": "Borrar solo el panel",
|
"deleteBoardOnly": "Borrar solo el panel",
|
||||||
"myBoard": "Mi panel",
|
"myBoard": "Mi panel",
|
||||||
"noMatching": "No hay paneles que coincidan"
|
"noMatching": "No hay paneles que coincidan",
|
||||||
|
"imagesWithCount_one": "{{count}} imagen",
|
||||||
|
"imagesWithCount_many": "{{count}} imágenes",
|
||||||
|
"imagesWithCount_other": "{{count}} imágenes",
|
||||||
|
"assetsWithCount_one": "{{count}} activo",
|
||||||
|
"assetsWithCount_many": "{{count}} activos",
|
||||||
|
"assetsWithCount_other": "{{count}} activos"
|
||||||
},
|
},
|
||||||
"accordions": {
|
"accordions": {
|
||||||
"compositing": {
|
"compositing": {
|
||||||
@ -590,6 +600,27 @@
|
|||||||
"balanced": "Equilibrado",
|
"balanced": "Equilibrado",
|
||||||
"beginEndStepPercent": "Inicio / Final Porcentaje de pasos",
|
"beginEndStepPercent": "Inicio / Final Porcentaje de pasos",
|
||||||
"detectResolution": "Detectar resolución",
|
"detectResolution": "Detectar resolución",
|
||||||
"beginEndStepPercentShort": "Inicio / Final %"
|
"beginEndStepPercentShort": "Inicio / Final %",
|
||||||
|
"t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))",
|
||||||
|
"controlnet": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.controlNet))",
|
||||||
|
"ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))",
|
||||||
|
"addControlNet": "Añadir $t(common.controlNet)",
|
||||||
|
"addIPAdapter": "Añadir $t(common.ipAdapter)",
|
||||||
|
"controlAdapter_one": "Adaptador de control",
|
||||||
|
"controlAdapter_many": "Adaptadores de control",
|
||||||
|
"controlAdapter_other": "Adaptadores de control",
|
||||||
|
"addT2IAdapter": "Añadir $t(common.t2iAdapter)"
|
||||||
|
},
|
||||||
|
"queue": {
|
||||||
|
"back": "Atrás",
|
||||||
|
"front": "Delante",
|
||||||
|
"batchQueuedDesc_one": "Se agregó {{count}} sesión a {{direction}} la cola",
|
||||||
|
"batchQueuedDesc_many": "Se agregaron {{count}} sesiones a {{direction}} la cola",
|
||||||
|
"batchQueuedDesc_other": "Se agregaron {{count}} sesiones a {{direction}} la cola"
|
||||||
|
},
|
||||||
|
"upsell": {
|
||||||
|
"inviteTeammates": "Invitar compañeros de equipo",
|
||||||
|
"shareAccess": "Compartir acceso",
|
||||||
|
"professionalUpsell": "Disponible en la edición profesional de Invoke. Haz clic aquí o visita invoke.com/pricing para obtener más detalles."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,10 +130,6 @@
|
|||||||
"title": "Restaurer les visages",
|
"title": "Restaurer les visages",
|
||||||
"desc": "Restaurer l'image actuelle"
|
"desc": "Restaurer l'image actuelle"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "Agrandir",
|
|
||||||
"desc": "Agrandir l'image actuelle"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "Afficher les informations",
|
"title": "Afficher les informations",
|
||||||
"desc": "Afficher les informations de métadonnées de l'image actuelle"
|
"desc": "Afficher les informations de métadonnées de l'image actuelle"
|
||||||
@ -308,8 +304,6 @@
|
|||||||
"type": "Type",
|
"type": "Type",
|
||||||
"strength": "Force",
|
"strength": "Force",
|
||||||
"upscaling": "Agrandissement",
|
"upscaling": "Agrandissement",
|
||||||
"upscale": "Agrandir",
|
|
||||||
"upscaleImage": "Image en Agrandissement",
|
|
||||||
"scale": "Echelle",
|
"scale": "Echelle",
|
||||||
"imageFit": "Ajuster Image Initiale à la Taille de Sortie",
|
"imageFit": "Ajuster Image Initiale à la Taille de Sortie",
|
||||||
"scaleBeforeProcessing": "Echelle Avant Traitement",
|
"scaleBeforeProcessing": "Echelle Avant Traitement",
|
||||||
|
@ -90,10 +90,6 @@
|
|||||||
"desc": "שחזור התמונה הנוכחית",
|
"desc": "שחזור התמונה הנוכחית",
|
||||||
"title": "שחזור פרצופים"
|
"title": "שחזור פרצופים"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "הגדלת קנה מידה",
|
|
||||||
"desc": "הגדל את התמונה הנוכחית"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "הצג מידע",
|
"title": "הצג מידע",
|
||||||
"desc": "הצגת פרטי מטא-נתונים של התמונה הנוכחית"
|
"desc": "הצגת פרטי מטא-נתונים של התמונה הנוכחית"
|
||||||
@ -263,8 +259,6 @@
|
|||||||
"seed": "זרע",
|
"seed": "זרע",
|
||||||
"type": "סוג",
|
"type": "סוג",
|
||||||
"strength": "חוזק",
|
"strength": "חוזק",
|
||||||
"upscale": "הגדלת קנה מידה",
|
|
||||||
"upscaleImage": "הגדלת קנה מידת התמונה",
|
|
||||||
"denoisingStrength": "חוזק מנטרל הרעש",
|
"denoisingStrength": "חוזק מנטרל הרעש",
|
||||||
"scaleBeforeProcessing": "שנה קנה מידה לפני עיבוד",
|
"scaleBeforeProcessing": "שנה קנה מידה לפני עיבוד",
|
||||||
"scaledWidth": "קנה מידה לאחר שינוי W",
|
"scaledWidth": "קנה מידה לאחר שינוי W",
|
||||||
|
@ -150,7 +150,11 @@
|
|||||||
"showArchivedBoards": "Mostra le bacheche archiviate",
|
"showArchivedBoards": "Mostra le bacheche archiviate",
|
||||||
"searchImages": "Ricerca per metadati",
|
"searchImages": "Ricerca per metadati",
|
||||||
"displayBoardSearch": "Mostra la ricerca nelle Bacheche",
|
"displayBoardSearch": "Mostra la ricerca nelle Bacheche",
|
||||||
"displaySearch": "Mostra la ricerca"
|
"displaySearch": "Mostra la ricerca",
|
||||||
|
"selectAllOnPage": "Seleziona tutto nella pagina",
|
||||||
|
"selectAllOnBoard": "Seleziona tutto nella bacheca",
|
||||||
|
"exitBoardSearch": "Esci da Ricerca bacheca",
|
||||||
|
"exitSearch": "Esci dalla ricerca"
|
||||||
},
|
},
|
||||||
"hotkeys": {
|
"hotkeys": {
|
||||||
"keyboardShortcuts": "Tasti di scelta rapida",
|
"keyboardShortcuts": "Tasti di scelta rapida",
|
||||||
@ -210,10 +214,6 @@
|
|||||||
"title": "Restaura volti",
|
"title": "Restaura volti",
|
||||||
"desc": "Restaura l'immagine corrente"
|
"desc": "Restaura l'immagine corrente"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "Amplia",
|
|
||||||
"desc": "Amplia l'immagine corrente"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "Mostra informazioni",
|
"title": "Mostra informazioni",
|
||||||
"desc": "Mostra le informazioni sui metadati dell'immagine corrente"
|
"desc": "Mostra le informazioni sui metadati dell'immagine corrente"
|
||||||
@ -377,6 +377,10 @@
|
|||||||
"toggleViewer": {
|
"toggleViewer": {
|
||||||
"title": "Attiva/disattiva il visualizzatore di immagini",
|
"title": "Attiva/disattiva il visualizzatore di immagini",
|
||||||
"desc": "Passa dal visualizzatore immagini all'area di lavoro per la scheda corrente."
|
"desc": "Passa dal visualizzatore immagini all'area di lavoro per la scheda corrente."
|
||||||
|
},
|
||||||
|
"postProcess": {
|
||||||
|
"desc": "Elabora l'immagine corrente utilizzando il modello di post-elaborazione selezionato",
|
||||||
|
"title": "Elabora immagine"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modelManager": {
|
"modelManager": {
|
||||||
@ -505,8 +509,6 @@
|
|||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"strength": "Forza",
|
"strength": "Forza",
|
||||||
"upscaling": "Ampliamento",
|
"upscaling": "Ampliamento",
|
||||||
"upscale": "Amplia (Shift + U)",
|
|
||||||
"upscaleImage": "Amplia Immagine",
|
|
||||||
"scale": "Scala",
|
"scale": "Scala",
|
||||||
"imageFit": "Adatta l'immagine iniziale alle dimensioni di output",
|
"imageFit": "Adatta l'immagine iniziale alle dimensioni di output",
|
||||||
"scaleBeforeProcessing": "Scala prima dell'elaborazione",
|
"scaleBeforeProcessing": "Scala prima dell'elaborazione",
|
||||||
@ -591,7 +593,10 @@
|
|||||||
"infillColorValue": "Colore di riempimento",
|
"infillColorValue": "Colore di riempimento",
|
||||||
"globalSettings": "Impostazioni globali",
|
"globalSettings": "Impostazioni globali",
|
||||||
"globalPositivePromptPlaceholder": "Prompt positivo globale",
|
"globalPositivePromptPlaceholder": "Prompt positivo globale",
|
||||||
"globalNegativePromptPlaceholder": "Prompt negativo globale"
|
"globalNegativePromptPlaceholder": "Prompt negativo globale",
|
||||||
|
"processImage": "Elabora Immagine",
|
||||||
|
"sendToUpscale": "Invia a Ampliare",
|
||||||
|
"postProcessing": "Post-elaborazione (Shift + U)"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"models": "Modelli",
|
"models": "Modelli",
|
||||||
@ -964,7 +969,10 @@
|
|||||||
"boards": "Bacheche",
|
"boards": "Bacheche",
|
||||||
"private": "Bacheche private",
|
"private": "Bacheche private",
|
||||||
"shared": "Bacheche condivise",
|
"shared": "Bacheche condivise",
|
||||||
"addPrivateBoard": "Aggiungi una Bacheca Privata"
|
"addPrivateBoard": "Aggiungi una Bacheca Privata",
|
||||||
|
"noBoards": "Nessuna bacheca {{boardType}}",
|
||||||
|
"hideBoards": "Nascondi bacheche",
|
||||||
|
"viewBoards": "Visualizza bacheche"
|
||||||
},
|
},
|
||||||
"controlnet": {
|
"controlnet": {
|
||||||
"contentShuffleDescription": "Rimescola il contenuto di un'immagine",
|
"contentShuffleDescription": "Rimescola il contenuto di un'immagine",
|
||||||
@ -1684,7 +1692,30 @@
|
|||||||
"models": "Modelli",
|
"models": "Modelli",
|
||||||
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
|
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
|
||||||
"queue": "Coda",
|
"queue": "Coda",
|
||||||
"queueTab": "$t(ui.tabs.queue) $t(common.tab)"
|
"queueTab": "$t(ui.tabs.queue) $t(common.tab)",
|
||||||
}
|
"upscaling": "Ampliamento",
|
||||||
|
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"upscaling": {
|
||||||
|
"creativity": "Creatività",
|
||||||
|
"structure": "Struttura",
|
||||||
|
"upscaleModel": "Modello di Ampliamento",
|
||||||
|
"scale": "Scala",
|
||||||
|
"missingModelsWarning": "Visita <LinkComponent>Gestione modelli</LinkComponent> per installare i modelli richiesti:",
|
||||||
|
"mainModelDesc": "Modello principale (architettura SD1.5 o SDXL)",
|
||||||
|
"tileControlNetModelDesc": "Modello Tile ControlNet per l'architettura del modello principale scelto",
|
||||||
|
"upscaleModelDesc": "Modello per l'ampliamento (da immagine a immagine)",
|
||||||
|
"missingUpscaleInitialImage": "Immagine iniziale mancante per l'ampliamento",
|
||||||
|
"missingUpscaleModel": "Modello per l’ampliamento mancante",
|
||||||
|
"missingTileControlNetModel": "Nessun modello ControlNet Tile valido installato",
|
||||||
|
"postProcessingModel": "Modello di post-elaborazione",
|
||||||
|
"postProcessingMissingModelWarning": "Visita <LinkComponent>Gestione modelli</LinkComponent> per installare un modello di post-elaborazione (da immagine a immagine)."
|
||||||
|
},
|
||||||
|
"upsell": {
|
||||||
|
"inviteTeammates": "Invita collaboratori",
|
||||||
|
"shareAccess": "Condividi l'accesso",
|
||||||
|
"professional": "Professionale",
|
||||||
|
"professionalUpsell": "Disponibile nell'edizione Professional di Invoke. Fai clic qui o visita invoke.com/pricing per ulteriori dettagli."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -199,10 +199,6 @@
|
|||||||
"title": "顔の修復",
|
"title": "顔の修復",
|
||||||
"desc": "現在の画像を修復"
|
"desc": "現在の画像を修復"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "アップスケール",
|
|
||||||
"desc": "現在の画像をアップスケール"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "情報を見る",
|
"title": "情報を見る",
|
||||||
"desc": "現在の画像のメタデータ情報を表示"
|
"desc": "現在の画像のメタデータ情報を表示"
|
||||||
@ -427,8 +423,6 @@
|
|||||||
"shuffle": "シャッフル",
|
"shuffle": "シャッフル",
|
||||||
"strength": "強度",
|
"strength": "強度",
|
||||||
"upscaling": "アップスケーリング",
|
"upscaling": "アップスケーリング",
|
||||||
"upscale": "アップスケール",
|
|
||||||
"upscaleImage": "画像をアップスケール",
|
|
||||||
"scale": "Scale",
|
"scale": "Scale",
|
||||||
"scaleBeforeProcessing": "処理前のスケール",
|
"scaleBeforeProcessing": "処理前のスケール",
|
||||||
"scaledWidth": "幅のスケール",
|
"scaledWidth": "幅のスケール",
|
||||||
|
@ -258,10 +258,6 @@
|
|||||||
"desc": "캔버스 브러시를 선택",
|
"desc": "캔버스 브러시를 선택",
|
||||||
"title": "브러시 선택"
|
"title": "브러시 선택"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"desc": "현재 이미지를 업스케일",
|
|
||||||
"title": "업스케일"
|
|
||||||
},
|
|
||||||
"previousImage": {
|
"previousImage": {
|
||||||
"title": "이전 이미지",
|
"title": "이전 이미지",
|
||||||
"desc": "갤러리에 이전 이미지 표시"
|
"desc": "갤러리에 이전 이미지 표시"
|
||||||
|
@ -168,10 +168,6 @@
|
|||||||
"title": "Herstel gezichten",
|
"title": "Herstel gezichten",
|
||||||
"desc": "Herstelt de huidige afbeelding"
|
"desc": "Herstelt de huidige afbeelding"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "Schaal op",
|
|
||||||
"desc": "Schaalt de huidige afbeelding op"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "Toon info",
|
"title": "Toon info",
|
||||||
"desc": "Toont de metagegevens van de huidige afbeelding"
|
"desc": "Toont de metagegevens van de huidige afbeelding"
|
||||||
@ -412,8 +408,6 @@
|
|||||||
"type": "Soort",
|
"type": "Soort",
|
||||||
"strength": "Sterkte",
|
"strength": "Sterkte",
|
||||||
"upscaling": "Opschalen",
|
"upscaling": "Opschalen",
|
||||||
"upscale": "Vergroot (Shift + U)",
|
|
||||||
"upscaleImage": "Schaal afbeelding op",
|
|
||||||
"scale": "Schaal",
|
"scale": "Schaal",
|
||||||
"imageFit": "Pas initiële afbeelding in uitvoergrootte",
|
"imageFit": "Pas initiële afbeelding in uitvoergrootte",
|
||||||
"scaleBeforeProcessing": "Schalen voor verwerking",
|
"scaleBeforeProcessing": "Schalen voor verwerking",
|
||||||
|
@ -78,10 +78,6 @@
|
|||||||
"title": "Popraw twarze",
|
"title": "Popraw twarze",
|
||||||
"desc": "Uruchamia proces poprawiania twarzy dla aktywnego obrazu"
|
"desc": "Uruchamia proces poprawiania twarzy dla aktywnego obrazu"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "Powiększ",
|
|
||||||
"desc": "Uruchamia proces powiększania aktywnego obrazu"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "Pokaż informacje",
|
"title": "Pokaż informacje",
|
||||||
"desc": "Pokazuje metadane zapisane w aktywnym obrazie"
|
"desc": "Pokazuje metadane zapisane w aktywnym obrazie"
|
||||||
@ -232,8 +228,6 @@
|
|||||||
"type": "Metoda",
|
"type": "Metoda",
|
||||||
"strength": "Siła",
|
"strength": "Siła",
|
||||||
"upscaling": "Powiększanie",
|
"upscaling": "Powiększanie",
|
||||||
"upscale": "Powiększ",
|
|
||||||
"upscaleImage": "Powiększ obraz",
|
|
||||||
"scale": "Skala",
|
"scale": "Skala",
|
||||||
"imageFit": "Przeskaluj oryginalny obraz",
|
"imageFit": "Przeskaluj oryginalny obraz",
|
||||||
"scaleBeforeProcessing": "Tryb skalowania",
|
"scaleBeforeProcessing": "Tryb skalowania",
|
||||||
|
@ -160,10 +160,6 @@
|
|||||||
"title": "Restaurar Rostos",
|
"title": "Restaurar Rostos",
|
||||||
"desc": "Restaurar a imagem atual"
|
"desc": "Restaurar a imagem atual"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "Redimensionar",
|
|
||||||
"desc": "Redimensionar a imagem atual"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "Mostrar Informações",
|
"title": "Mostrar Informações",
|
||||||
"desc": "Mostrar metadados de informações da imagem atual"
|
"desc": "Mostrar metadados de informações da imagem atual"
|
||||||
@ -275,8 +271,6 @@
|
|||||||
"showOptionsPanel": "Mostrar Painel de Opções",
|
"showOptionsPanel": "Mostrar Painel de Opções",
|
||||||
"strength": "Força",
|
"strength": "Força",
|
||||||
"upscaling": "Redimensionando",
|
"upscaling": "Redimensionando",
|
||||||
"upscale": "Redimensionar",
|
|
||||||
"upscaleImage": "Redimensionar Imagem",
|
|
||||||
"scaleBeforeProcessing": "Escala Antes do Processamento",
|
"scaleBeforeProcessing": "Escala Antes do Processamento",
|
||||||
"images": "Imagems",
|
"images": "Imagems",
|
||||||
"steps": "Passos",
|
"steps": "Passos",
|
||||||
|
@ -80,10 +80,6 @@
|
|||||||
"title": "Restaurar Rostos",
|
"title": "Restaurar Rostos",
|
||||||
"desc": "Restaurar a imagem atual"
|
"desc": "Restaurar a imagem atual"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "Redimensionar",
|
|
||||||
"desc": "Redimensionar a imagem atual"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "Mostrar Informações",
|
"title": "Mostrar Informações",
|
||||||
"desc": "Mostrar metadados de informações da imagem atual"
|
"desc": "Mostrar metadados de informações da imagem atual"
|
||||||
@ -268,8 +264,6 @@
|
|||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"strength": "Força",
|
"strength": "Força",
|
||||||
"upscaling": "Redimensionando",
|
"upscaling": "Redimensionando",
|
||||||
"upscale": "Redimensionar",
|
|
||||||
"upscaleImage": "Redimensionar Imagem",
|
|
||||||
"scale": "Escala",
|
"scale": "Escala",
|
||||||
"imageFit": "Caber Imagem Inicial No Tamanho de Saída",
|
"imageFit": "Caber Imagem Inicial No Tamanho de Saída",
|
||||||
"scaleBeforeProcessing": "Escala Antes do Processamento",
|
"scaleBeforeProcessing": "Escala Antes do Processamento",
|
||||||
|
@ -214,10 +214,6 @@
|
|||||||
"title": "Восстановить лица",
|
"title": "Восстановить лица",
|
||||||
"desc": "Восстановить лица на текущем изображении"
|
"desc": "Восстановить лица на текущем изображении"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "Увеличение",
|
|
||||||
"desc": "Увеличить текущеее изображение"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "Показать метаданные",
|
"title": "Показать метаданные",
|
||||||
"desc": "Показать метаданные из текущего изображения"
|
"desc": "Показать метаданные из текущего изображения"
|
||||||
@ -512,8 +508,6 @@
|
|||||||
"type": "Тип",
|
"type": "Тип",
|
||||||
"strength": "Сила",
|
"strength": "Сила",
|
||||||
"upscaling": "Увеличение",
|
"upscaling": "Увеличение",
|
||||||
"upscale": "Увеличить",
|
|
||||||
"upscaleImage": "Увеличить изображение",
|
|
||||||
"scale": "Масштаб",
|
"scale": "Масштаб",
|
||||||
"imageFit": "Уместить изображение",
|
"imageFit": "Уместить изображение",
|
||||||
"scaleBeforeProcessing": "Масштабировать",
|
"scaleBeforeProcessing": "Масштабировать",
|
||||||
|
@ -90,10 +90,6 @@
|
|||||||
"title": "Återskapa ansikten",
|
"title": "Återskapa ansikten",
|
||||||
"desc": "Återskapa nuvarande bild"
|
"desc": "Återskapa nuvarande bild"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "Skala upp",
|
|
||||||
"desc": "Skala upp nuvarande bild"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "Visa info",
|
"title": "Visa info",
|
||||||
"desc": "Visa metadata för nuvarande bild"
|
"desc": "Visa metadata för nuvarande bild"
|
||||||
|
@ -416,10 +416,6 @@
|
|||||||
"desc": "Maske/Taban katmanları arasında geçiş yapar",
|
"desc": "Maske/Taban katmanları arasında geçiş yapar",
|
||||||
"title": "Katmanı Gizle-Göster"
|
"title": "Katmanı Gizle-Göster"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "Büyüt",
|
|
||||||
"desc": "Seçili görseli büyüt"
|
|
||||||
},
|
|
||||||
"setSeed": {
|
"setSeed": {
|
||||||
"title": "Tohumu Kullan",
|
"title": "Tohumu Kullan",
|
||||||
"desc": "Seçili görselin tohumunu kullan"
|
"desc": "Seçili görselin tohumunu kullan"
|
||||||
@ -641,7 +637,6 @@
|
|||||||
"copyImage": "Görseli Kopyala",
|
"copyImage": "Görseli Kopyala",
|
||||||
"height": "Boy",
|
"height": "Boy",
|
||||||
"width": "En",
|
"width": "En",
|
||||||
"upscale": "Büyüt (Shift + U)",
|
|
||||||
"useSize": "Boyutu Kullan",
|
"useSize": "Boyutu Kullan",
|
||||||
"symmetry": "Bakışım",
|
"symmetry": "Bakışım",
|
||||||
"tileSize": "Döşeme Boyutu",
|
"tileSize": "Döşeme Boyutu",
|
||||||
@ -657,7 +652,6 @@
|
|||||||
"showOptionsPanel": "Yan Paneli Göster (O ya da T)",
|
"showOptionsPanel": "Yan Paneli Göster (O ya da T)",
|
||||||
"shuffle": "Kar",
|
"shuffle": "Kar",
|
||||||
"usePrompt": "İstemi Kullan",
|
"usePrompt": "İstemi Kullan",
|
||||||
"upscaleImage": "Görseli Büyüt",
|
|
||||||
"setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (çok küçük olabilir)",
|
"setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (çok küçük olabilir)",
|
||||||
"setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (çok büyük olabilir)",
|
"setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (çok büyük olabilir)",
|
||||||
"cfgRescaleMultiplier": "CFG Rescale Çarpanı",
|
"cfgRescaleMultiplier": "CFG Rescale Çarpanı",
|
||||||
|
@ -85,10 +85,6 @@
|
|||||||
"title": "Відновити обличчя",
|
"title": "Відновити обличчя",
|
||||||
"desc": "Відновити обличчя на поточному зображенні"
|
"desc": "Відновити обличчя на поточному зображенні"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "Збільшення",
|
|
||||||
"desc": "Збільшити поточне зображення"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "Показати метадані",
|
"title": "Показати метадані",
|
||||||
"desc": "Показати метадані з поточного зображення"
|
"desc": "Показати метадані з поточного зображення"
|
||||||
@ -276,8 +272,6 @@
|
|||||||
"type": "Тип",
|
"type": "Тип",
|
||||||
"strength": "Сила",
|
"strength": "Сила",
|
||||||
"upscaling": "Збільшення",
|
"upscaling": "Збільшення",
|
||||||
"upscale": "Збільшити",
|
|
||||||
"upscaleImage": "Збільшити зображення",
|
|
||||||
"scale": "Масштаб",
|
"scale": "Масштаб",
|
||||||
"imageFit": "Вмістити зображення",
|
"imageFit": "Вмістити зображення",
|
||||||
"scaleBeforeProcessing": "Масштабувати",
|
"scaleBeforeProcessing": "Масштабувати",
|
||||||
|
@ -193,10 +193,6 @@
|
|||||||
"title": "面部修复",
|
"title": "面部修复",
|
||||||
"desc": "对当前图像进行面部修复"
|
"desc": "对当前图像进行面部修复"
|
||||||
},
|
},
|
||||||
"upscale": {
|
|
||||||
"title": "放大",
|
|
||||||
"desc": "对当前图像进行放大"
|
|
||||||
},
|
|
||||||
"showInfo": {
|
"showInfo": {
|
||||||
"title": "显示信息",
|
"title": "显示信息",
|
||||||
"desc": "显示当前图像的元数据"
|
"desc": "显示当前图像的元数据"
|
||||||
@ -422,8 +418,6 @@
|
|||||||
"type": "种类",
|
"type": "种类",
|
||||||
"strength": "强度",
|
"strength": "强度",
|
||||||
"upscaling": "放大",
|
"upscaling": "放大",
|
||||||
"upscale": "放大 (Shift + U)",
|
|
||||||
"upscaleImage": "放大图像",
|
|
||||||
"scale": "等级",
|
"scale": "等级",
|
||||||
"imageFit": "使生成图像长宽适配初始图像",
|
"imageFit": "使生成图像长宽适配初始图像",
|
||||||
"scaleBeforeProcessing": "处理前缩放",
|
"scaleBeforeProcessing": "处理前缩放",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { TypedStartListening } from '@reduxjs/toolkit';
|
import type { TypedStartListening } from '@reduxjs/toolkit';
|
||||||
import { createListenerMiddleware } from '@reduxjs/toolkit';
|
import { createListenerMiddleware } from '@reduxjs/toolkit';
|
||||||
|
import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
|
||||||
import { addCommitStagingAreaImageListener } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
|
import { addCommitStagingAreaImageListener } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
|
||||||
import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued';
|
import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued';
|
||||||
import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived';
|
import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived';
|
||||||
@ -47,7 +48,6 @@ import { addModelLoadEventListener } from 'app/store/middleware/listenerMiddlewa
|
|||||||
import { addSocketQueueItemStatusChangedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged';
|
import { addSocketQueueItemStatusChangedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged';
|
||||||
import { addStagingAreaImageSavedListener } from 'app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved';
|
import { addStagingAreaImageSavedListener } from 'app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved';
|
||||||
import { addUpdateAllNodesRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested';
|
import { addUpdateAllNodesRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested';
|
||||||
import { addUpscaleRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
|
|
||||||
import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested';
|
import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested';
|
||||||
import type { AppDispatch, RootState } from 'app/store/store';
|
import type { AppDispatch, RootState } from 'app/store/store';
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ addModelsLoadedListener(startAppListening);
|
|||||||
addAppConfigReceivedListener(startAppListening);
|
addAppConfigReceivedListener(startAppListening);
|
||||||
|
|
||||||
// Ad-hoc upscale workflwo
|
// Ad-hoc upscale workflwo
|
||||||
addUpscaleRequestedListener(startAppListening);
|
addAdHocPostProcessingRequestedListener(startAppListening);
|
||||||
|
|
||||||
// Prompts
|
// Prompts
|
||||||
addDynamicPromptsListener(startAppListening);
|
addDynamicPromptsListener(startAppListening);
|
||||||
|
@ -2,46 +2,28 @@ import { createAction } from '@reduxjs/toolkit';
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import { parseify } from 'common/util/serialize';
|
import { parseify } from 'common/util/serialize';
|
||||||
import { buildAdHocUpscaleGraph } from 'features/nodes/util/graph/buildAdHocUpscaleGraph';
|
import { buildAdHocPostProcessingGraph } from 'features/nodes/util/graph/buildAdHocPostProcessingGraph';
|
||||||
import { createIsAllowedToUpscaleSelector } from 'features/parameters/hooks/useIsAllowedToUpscale';
|
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { queueApi } from 'services/api/endpoints/queue';
|
import { queueApi } from 'services/api/endpoints/queue';
|
||||||
import type { BatchConfig, ImageDTO } from 'services/api/types';
|
import type { BatchConfig, ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
export const upscaleRequested = createAction<{ imageDTO: ImageDTO }>(`upscale/upscaleRequested`);
|
export const adHocPostProcessingRequested = createAction<{ imageDTO: ImageDTO }>(`upscaling/postProcessingRequested`);
|
||||||
|
|
||||||
export const addUpscaleRequestedListener = (startAppListening: AppStartListening) => {
|
export const addAdHocPostProcessingRequestedListener = (startAppListening: AppStartListening) => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: upscaleRequested,
|
actionCreator: adHocPostProcessingRequested,
|
||||||
effect: async (action, { dispatch, getState }) => {
|
effect: async (action, { dispatch, getState }) => {
|
||||||
const log = logger('session');
|
const log = logger('session');
|
||||||
|
|
||||||
const { imageDTO } = action.payload;
|
const { imageDTO } = action.payload;
|
||||||
const { image_name } = imageDTO;
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
||||||
const { isAllowedToUpscale, detailTKey } = createIsAllowedToUpscaleSelector(imageDTO)(state);
|
|
||||||
|
|
||||||
// if we can't upscale, show a toast and return
|
|
||||||
if (!isAllowedToUpscale) {
|
|
||||||
log.error(
|
|
||||||
{ imageDTO },
|
|
||||||
t(detailTKey ?? 'parameters.isAllowedToUpscale.tooLarge') // should never coalesce
|
|
||||||
);
|
|
||||||
toast({
|
|
||||||
id: 'NOT_ALLOWED_TO_UPSCALE',
|
|
||||||
title: t(detailTKey ?? 'parameters.isAllowedToUpscale.tooLarge'), // should never coalesce
|
|
||||||
status: 'error',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enqueueBatchArg: BatchConfig = {
|
const enqueueBatchArg: BatchConfig = {
|
||||||
prepend: true,
|
prepend: true,
|
||||||
batch: {
|
batch: {
|
||||||
graph: buildAdHocUpscaleGraph({
|
graph: await buildAdHocPostProcessingGraph({
|
||||||
image_name,
|
image: imageDTO,
|
||||||
state,
|
state,
|
||||||
}),
|
}),
|
||||||
runs: 1,
|
runs: 1,
|
@ -10,7 +10,7 @@ import { heightChanged, widthChanged } from 'features/controlLayers/store/contro
|
|||||||
import { loraRemoved } from 'features/lora/store/loraSlice';
|
import { loraRemoved } from 'features/lora/store/loraSlice';
|
||||||
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
||||||
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
|
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
|
||||||
import { upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
|
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
|
||||||
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
|
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
|
||||||
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||||
import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice';
|
import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice';
|
||||||
@ -186,21 +186,23 @@ const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log)
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSpandrelImageToImageModels: ModelHandler = (models, state, dispatch, _log) => {
|
const handleSpandrelImageToImageModels: ModelHandler = (models, state, dispatch, _log) => {
|
||||||
const currentUpscaleModel = state.upscale.upscaleModel;
|
const { upscaleModel: currentUpscaleModel, postProcessingModel: currentPostProcessingModel } = state.upscale;
|
||||||
const upscaleModels = models.filter(isSpandrelImageToImageModelConfig);
|
const upscaleModels = models.filter(isSpandrelImageToImageModelConfig);
|
||||||
|
const firstModel = upscaleModels[0] || null;
|
||||||
|
|
||||||
if (currentUpscaleModel) {
|
const isCurrentUpscaleModelAvailable = currentUpscaleModel
|
||||||
const isCurrentUpscaleModelAvailable = upscaleModels.some((m) => m.key === currentUpscaleModel.key);
|
? upscaleModels.some((m) => m.key === currentUpscaleModel.key)
|
||||||
if (isCurrentUpscaleModelAvailable) {
|
: false;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstModel = upscaleModels[0];
|
if (!isCurrentUpscaleModelAvailable) {
|
||||||
if (firstModel) {
|
|
||||||
dispatch(upscaleModelChanged(firstModel));
|
dispatch(upscaleModelChanged(firstModel));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(upscaleModelChanged(null));
|
const isCurrentPostProcessingModelAvailable = currentPostProcessingModel
|
||||||
|
? upscaleModels.some((m) => m.key === currentPostProcessingModel.key)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (!isCurrentPostProcessingModelAvailable) {
|
||||||
|
dispatch(postProcessingModelChanged(firstModel));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,6 @@ import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/no
|
|||||||
import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
|
import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
|
||||||
import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
|
import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
|
||||||
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
|
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
|
||||||
import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice';
|
|
||||||
import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice';
|
import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||||
import { queueSlice } from 'features/queue/store/queueSlice';
|
import { queueSlice } from 'features/queue/store/queueSlice';
|
||||||
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
|
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
|
||||||
@ -53,7 +52,6 @@ const allReducers = {
|
|||||||
[gallerySlice.name]: gallerySlice.reducer,
|
[gallerySlice.name]: gallerySlice.reducer,
|
||||||
[generationSlice.name]: generationSlice.reducer,
|
[generationSlice.name]: generationSlice.reducer,
|
||||||
[nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig),
|
[nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig),
|
||||||
[postprocessingSlice.name]: postprocessingSlice.reducer,
|
|
||||||
[systemSlice.name]: systemSlice.reducer,
|
[systemSlice.name]: systemSlice.reducer,
|
||||||
[configSlice.name]: configSlice.reducer,
|
[configSlice.name]: configSlice.reducer,
|
||||||
[uiSlice.name]: uiSlice.reducer,
|
[uiSlice.name]: uiSlice.reducer,
|
||||||
@ -104,7 +102,6 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
|
|||||||
[galleryPersistConfig.name]: galleryPersistConfig,
|
[galleryPersistConfig.name]: galleryPersistConfig,
|
||||||
[generationPersistConfig.name]: generationPersistConfig,
|
[generationPersistConfig.name]: generationPersistConfig,
|
||||||
[nodesPersistConfig.name]: nodesPersistConfig,
|
[nodesPersistConfig.name]: nodesPersistConfig,
|
||||||
[postprocessingPersistConfig.name]: postprocessingPersistConfig,
|
|
||||||
[systemPersistConfig.name]: systemPersistConfig,
|
[systemPersistConfig.name]: systemPersistConfig,
|
||||||
[workflowPersistConfig.name]: workflowPersistConfig,
|
[workflowPersistConfig.name]: workflowPersistConfig,
|
||||||
[uiPersistConfig.name]: uiPersistConfig,
|
[uiPersistConfig.name]: uiPersistConfig,
|
||||||
|
@ -72,7 +72,6 @@ export type AppConfig = {
|
|||||||
canRestoreDeletedImagesFromBin: boolean;
|
canRestoreDeletedImagesFromBin: boolean;
|
||||||
nodesAllowlist: string[] | undefined;
|
nodesAllowlist: string[] | undefined;
|
||||||
nodesDenylist: string[] | undefined;
|
nodesDenylist: string[] | undefined;
|
||||||
maxUpscalePixels?: number;
|
|
||||||
metadataFetchDebounce?: number;
|
metadataFetchDebounce?: number;
|
||||||
workflowFetchDebounce?: number;
|
workflowFetchDebounce?: number;
|
||||||
isLocal?: boolean;
|
isLocal?: boolean;
|
||||||
|
@ -10,9 +10,12 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
Portal,
|
Portal,
|
||||||
|
Spacer,
|
||||||
Text,
|
Text,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { setShouldEnableInformationalPopovers } from 'features/system/store/systemSlice';
|
||||||
|
import { toast } from 'features/toast/toast';
|
||||||
import { merge, omit } from 'lodash-es';
|
import { merge, omit } from 'lodash-es';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
@ -71,7 +74,7 @@ type ContentProps = {
|
|||||||
|
|
||||||
const Content = ({ data, feature }: ContentProps) => {
|
const Content = ({ data, feature }: ContentProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const heading = useMemo<string | undefined>(() => t(`popovers.${feature}.heading`), [feature, t]);
|
const heading = useMemo<string | undefined>(() => t(`popovers.${feature}.heading`), [feature, t]);
|
||||||
|
|
||||||
const paragraphs = useMemo<string[]>(
|
const paragraphs = useMemo<string[]>(
|
||||||
@ -82,16 +85,25 @@ const Content = ({ data, feature }: ContentProps) => {
|
|||||||
[feature, t]
|
[feature, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const onClickLearnMore = useCallback(() => {
|
||||||
if (!data?.href) {
|
if (!data?.href) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.open(data.href);
|
window.open(data.href);
|
||||||
}, [data?.href]);
|
}, [data?.href]);
|
||||||
|
|
||||||
|
const onClickDontShowMeThese = useCallback(() => {
|
||||||
|
dispatch(setShouldEnableInformationalPopovers(false));
|
||||||
|
toast({
|
||||||
|
title: t('settings.informationalPopoversDisabled'),
|
||||||
|
description: t('settings.informationalPopoversDisabledDesc'),
|
||||||
|
status: 'info',
|
||||||
|
});
|
||||||
|
}, [dispatch, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverContent w={96}>
|
<PopoverContent maxW={300}>
|
||||||
<PopoverCloseButton />
|
<PopoverCloseButton top={2} />
|
||||||
<PopoverBody>
|
<PopoverBody>
|
||||||
<Flex gap={2} flexDirection="column" alignItems="flex-start">
|
<Flex gap={2} flexDirection="column" alignItems="flex-start">
|
||||||
{heading && (
|
{heading && (
|
||||||
@ -116,21 +128,20 @@ const Content = ({ data, feature }: ContentProps) => {
|
|||||||
{paragraphs.map((p) => (
|
{paragraphs.map((p) => (
|
||||||
<Text key={p}>{p}</Text>
|
<Text key={p}>{p}</Text>
|
||||||
))}
|
))}
|
||||||
{data?.href && (
|
|
||||||
<>
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<Button
|
<Flex alignItems="center" justifyContent="space-between" w="full">
|
||||||
pt={1}
|
<Button onClick={onClickDontShowMeThese} variant="link" size="sm">
|
||||||
onClick={handleClick}
|
{t('common.dontShowMeThese')}
|
||||||
leftIcon={<PiArrowSquareOutBold />}
|
</Button>
|
||||||
alignSelf="flex-end"
|
<Spacer />
|
||||||
variant="link"
|
{data?.href && (
|
||||||
>
|
<Button onClick={onClickLearnMore} leftIcon={<PiArrowSquareOutBold />} variant="link" size="sm">
|
||||||
{t('common.learnMore') ?? heading}
|
{t('common.learnMore') ?? heading}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Flex>
|
||||||
</PopoverBody>
|
</PopoverBody>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
);
|
);
|
||||||
|
@ -53,7 +53,11 @@ export type Feature =
|
|||||||
| 'refinerCfgScale'
|
| 'refinerCfgScale'
|
||||||
| 'scaleBeforeProcessing'
|
| 'scaleBeforeProcessing'
|
||||||
| 'seamlessTilingXAxis'
|
| 'seamlessTilingXAxis'
|
||||||
| 'seamlessTilingYAxis';
|
| 'seamlessTilingYAxis'
|
||||||
|
| 'upscaleModel'
|
||||||
|
| 'scale'
|
||||||
|
| 'creativity'
|
||||||
|
| 'structure';
|
||||||
|
|
||||||
export type PopoverData = PopoverProps & {
|
export type PopoverData = PopoverProps & {
|
||||||
image?: string;
|
image?: string;
|
||||||
|
18
invokeai/frontend/web/src/common/hooks/useAssertSingleton.ts
Normal file
18
invokeai/frontend/web/src/common/hooks/useAssertSingleton.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
const IDS = new Set<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that there is only one instance of a singleton entity. It can be a hook or a component.
|
||||||
|
* @param id The ID of the singleton entity.
|
||||||
|
*/
|
||||||
|
export function useAssertSingleton(id: string) {
|
||||||
|
useEffect(() => {
|
||||||
|
assert(!IDS.has(id), `There should be only one instance of ${id}`);
|
||||||
|
IDS.add(id);
|
||||||
|
return () => {
|
||||||
|
IDS.delete(id);
|
||||||
|
};
|
||||||
|
}, [id]);
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import { Flex, Image, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
|
||||||
|
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||||
|
import type { BoardDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
board: BoardDTO | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BoardTooltip = ({ board }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { imagesTotal } = useGetBoardImagesTotalQuery(board?.board_id || 'none', {
|
||||||
|
selectFromResult: ({ data }) => {
|
||||||
|
return { imagesTotal: data?.total ?? 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { assetsTotal } = useGetBoardAssetsTotalQuery(board?.board_id || 'none', {
|
||||||
|
selectFromResult: ({ data }) => {
|
||||||
|
return { assetsTotal: data?.total ?? 0 };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { currentData: coverImage } = useGetImageDTOQuery(board?.cover_image_name ?? skipToken);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" alignItems="center" gap={1}>
|
||||||
|
{coverImage && (
|
||||||
|
<Image
|
||||||
|
src={coverImage.thumbnail_url}
|
||||||
|
draggable={false}
|
||||||
|
objectFit="cover"
|
||||||
|
maxW={150}
|
||||||
|
aspectRatio="1/1"
|
||||||
|
borderRadius="base"
|
||||||
|
borderBottomRadius="lg"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Flex flexDir="column" alignItems="center">
|
||||||
|
<Text noOfLines={1}>
|
||||||
|
{t('boards.imagesWithCount', { count: imagesTotal })}, {t('boards.assetsWithCount', { count: assetsTotal })}
|
||||||
|
</Text>
|
||||||
|
{board?.archived && <Text>({t('boards.archived')})</Text>}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
@ -1,22 +0,0 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
board_id: string;
|
|
||||||
isArchived: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BoardTotalsTooltip = ({ board_id, isArchived }: Props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { imagesTotal } = useGetBoardImagesTotalQuery(board_id, {
|
|
||||||
selectFromResult: ({ data }) => {
|
|
||||||
return { imagesTotal: data?.total ?? 0 };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { assetsTotal } = useGetBoardAssetsTotalQuery(board_id, {
|
|
||||||
selectFromResult: ({ data }) => {
|
|
||||||
return { assetsTotal: data?.total ?? 0 };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return `${t('boards.imagesWithCount', { count: imagesTotal })}, ${t('boards.assetsWithCount', { count: assetsTotal })}${isArchived ? ` (${t('boards.archived')})` : ''}`;
|
|
||||||
};
|
|
@ -1,13 +1,10 @@
|
|||||||
import { Box, Flex, Text } from '@invoke-ai/ui-library';
|
import { Button, Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
|
||||||
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
|
||||||
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
|
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { useMemo } from 'react';
|
||||||
import type { CSSProperties } from 'react';
|
|
||||||
import { memo, useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiCaretDownBold } from 'react-icons/pi';
|
||||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||||
import type { BoardDTO } from 'services/api/types';
|
import type { BoardDTO } from 'services/api/types';
|
||||||
|
|
||||||
@ -15,101 +12,111 @@ import AddBoardButton from './AddBoardButton';
|
|||||||
import GalleryBoard from './GalleryBoard';
|
import GalleryBoard from './GalleryBoard';
|
||||||
import NoBoardBoard from './NoBoardBoard';
|
import NoBoardBoard from './NoBoardBoard';
|
||||||
|
|
||||||
const overlayScrollbarsStyles: CSSProperties = {
|
type Props = {
|
||||||
height: '100%',
|
isPrivate: boolean;
|
||||||
width: '100%',
|
setBoardToDelete: (board?: BoardDTO) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoardsList = () => {
|
export const BoardsList = ({ isPrivate, setBoardToDelete }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||||
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
||||||
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
|
|
||||||
const queryArgs = useAppSelector(selectListBoardsQueryArgs);
|
const queryArgs = useAppSelector(selectListBoardsQueryArgs);
|
||||||
const { data: boards } = useListAllBoardsQuery(queryArgs);
|
const { data: boards } = useListAllBoardsQuery(queryArgs);
|
||||||
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
|
||||||
const { t } = useTranslation();
|
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
||||||
|
|
||||||
const { filteredPrivateBoards, filteredSharedBoards } = useMemo(() => {
|
const filteredBoards = useMemo(() => {
|
||||||
const filteredBoards = boardSearchText
|
if (!boards) {
|
||||||
? boards?.filter((board) => board.board_name.toLowerCase().includes(boardSearchText.toLowerCase()))
|
return EMPTY_ARRAY;
|
||||||
: boards;
|
}
|
||||||
const filteredPrivateBoards = filteredBoards?.filter((board) => board.is_private) ?? EMPTY_ARRAY;
|
|
||||||
const filteredSharedBoards = filteredBoards?.filter((board) => !board.is_private) ?? EMPTY_ARRAY;
|
return boards.filter((board) => {
|
||||||
return { filteredPrivateBoards, filteredSharedBoards };
|
if (boardSearchText.length) {
|
||||||
}, [boardSearchText, boards]);
|
return board.is_private === isPrivate && board.board_name.toLowerCase().includes(boardSearchText.toLowerCase());
|
||||||
|
} else {
|
||||||
|
return board.is_private === isPrivate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [boardSearchText, boards, isPrivate]);
|
||||||
|
|
||||||
|
const boardElements = useMemo(() => {
|
||||||
|
const elements = [];
|
||||||
|
if (allowPrivateBoards && isPrivate && !boardSearchText.length) {
|
||||||
|
elements.push(<NoBoardBoard key="none" isSelected={selectedBoardId === 'none'} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowPrivateBoards && !boardSearchText.length) {
|
||||||
|
elements.push(<NoBoardBoard key="none" isSelected={selectedBoardId === 'none'} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredBoards.forEach((board) => {
|
||||||
|
elements.push(
|
||||||
|
<GalleryBoard
|
||||||
|
board={board}
|
||||||
|
isSelected={selectedBoardId === board.board_id}
|
||||||
|
setBoardToDelete={setBoardToDelete}
|
||||||
|
key={board.board_id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}, [allowPrivateBoards, isPrivate, boardSearchText.length, filteredBoards, selectedBoardId, setBoardToDelete]);
|
||||||
|
|
||||||
|
const boardListTitle = useMemo(() => {
|
||||||
|
if (allowPrivateBoards) {
|
||||||
|
return isPrivate ? t('boards.private') : t('boards.shared');
|
||||||
|
} else {
|
||||||
|
return t('boards.boards');
|
||||||
|
}
|
||||||
|
}, [isPrivate, allowPrivateBoards, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Flex direction="column">
|
||||||
<Box position="relative" w="full" h="full">
|
|
||||||
<Box position="absolute" top={0} right={0} bottom={0} left={0}>
|
|
||||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
|
||||||
{allowPrivateBoards && (
|
|
||||||
<Flex direction="column" gap={1}>
|
|
||||||
<Flex
|
<Flex
|
||||||
position="sticky"
|
position="sticky"
|
||||||
w="full"
|
w="full"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
ps={2}
|
ps={2}
|
||||||
pb={1}
|
py={1}
|
||||||
pt={2}
|
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
top={0}
|
top={0}
|
||||||
bg="base.900"
|
bg="base.900"
|
||||||
>
|
>
|
||||||
<Text fontSize="md" fontWeight="semibold" userSelect="none">
|
{allowPrivateBoards ? (
|
||||||
{t('boards.private')}
|
<Button variant="unstyled" onClick={onToggle}>
|
||||||
</Text>
|
<Flex gap="2" alignItems="center">
|
||||||
<AddBoardButton isPrivateBoard={true} />
|
<Icon
|
||||||
</Flex>
|
boxSize={4}
|
||||||
<Flex direction="column" gap={1}>
|
as={PiCaretDownBold}
|
||||||
<NoBoardBoard isSelected={selectedBoardId === 'none'} />
|
transform={isOpen ? undefined : 'rotate(-90deg)'}
|
||||||
{filteredPrivateBoards.map((board) => (
|
fill="base.500"
|
||||||
<GalleryBoard
|
|
||||||
board={board}
|
|
||||||
isSelected={selectedBoardId === board.board_id}
|
|
||||||
setBoardToDelete={setBoardToDelete}
|
|
||||||
key={board.board_id}
|
|
||||||
/>
|
/>
|
||||||
))}
|
<Text fontSize="sm" fontWeight="semibold" userSelect="none" color="base.500">
|
||||||
</Flex>
|
{boardListTitle}
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Text fontSize="sm" fontWeight="semibold" userSelect="none" color="base.500">
|
||||||
|
{boardListTitle}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
<AddBoardButton isPrivateBoard={isPrivate} />
|
||||||
|
</Flex>
|
||||||
|
<Collapse in={isOpen}>
|
||||||
<Flex direction="column" gap={1}>
|
<Flex direction="column" gap={1}>
|
||||||
<Flex
|
{boardElements.length ? (
|
||||||
position="sticky"
|
boardElements
|
||||||
w="full"
|
) : (
|
||||||
justifyContent="space-between"
|
<Text variant="subtext" textAlign="center">
|
||||||
alignItems="center"
|
{t('boards.noBoards', { boardType: boardSearchText.length ? 'Matching' : '' })}
|
||||||
ps={2}
|
|
||||||
pb={1}
|
|
||||||
pt={2}
|
|
||||||
zIndex={1}
|
|
||||||
top={0}
|
|
||||||
bg="base.900"
|
|
||||||
>
|
|
||||||
<Text fontSize="md" fontWeight="semibold" userSelect="none">
|
|
||||||
{allowPrivateBoards ? t('boards.shared') : t('boards.boards')}
|
|
||||||
</Text>
|
</Text>
|
||||||
<AddBoardButton isPrivateBoard={false} />
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex direction="column" gap={1}>
|
</Collapse>
|
||||||
{!allowPrivateBoards && <NoBoardBoard isSelected={selectedBoardId === 'none'} />}
|
|
||||||
{filteredSharedBoards.map((board) => (
|
|
||||||
<GalleryBoard
|
|
||||||
board={board}
|
|
||||||
isSelected={selectedBoardId === board.board_id}
|
|
||||||
setBoardToDelete={setBoardToDelete}
|
|
||||||
key={board.board_id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
|
||||||
</OverlayScrollbarsComponent>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<DeleteBoardModal boardToDelete={boardToDelete} setBoardToDelete={setBoardToDelete} />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default memo(BoardsList);
|
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
import { Box } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
|
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
||||||
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
import { memo, useState } from 'react';
|
||||||
|
import type { BoardDTO } from 'services/api/types';
|
||||||
|
|
||||||
|
import { BoardsList } from './BoardsList';
|
||||||
|
|
||||||
|
const overlayScrollbarsStyles: CSSProperties = {
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
};
|
||||||
|
|
||||||
|
const BoardsListWrapper = () => {
|
||||||
|
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
|
||||||
|
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box position="relative" w="full" h="full">
|
||||||
|
<Box position="absolute" top={0} right={0} bottom={0} left={0}>
|
||||||
|
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||||
|
{allowPrivateBoards && <BoardsList isPrivate={true} setBoardToDelete={setBoardToDelete} />}
|
||||||
|
<BoardsList isPrivate={false} setBoardToDelete={setBoardToDelete} />
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<DeleteBoardModal boardToDelete={boardToDelete} setBoardToDelete={setBoardToDelete} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default memo(BoardsListWrapper);
|
@ -40,7 +40,7 @@ const BoardsSearch = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup pt={2}>
|
<InputGroup>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('boards.searchBoard')}
|
placeholder={t('boards.searchBoard')}
|
||||||
value={boardSearchText}
|
value={boardSearchText}
|
||||||
|
@ -17,7 +17,7 @@ import IAIDroppable from 'common/components/IAIDroppable';
|
|||||||
import type { AddToBoardDropData } from 'features/dnd/types';
|
import type { AddToBoardDropData } from 'features/dnd/types';
|
||||||
import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
|
import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
|
||||||
import BoardContextMenu from 'features/gallery/components/Boards/BoardContextMenu';
|
import BoardContextMenu from 'features/gallery/components/Boards/BoardContextMenu';
|
||||||
import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip';
|
import { BoardTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTooltip';
|
||||||
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||||
import type { MouseEvent, MouseEventHandler, MutableRefObject } from 'react';
|
import type { MouseEvent, MouseEventHandler, MutableRefObject } from 'react';
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
@ -115,12 +115,7 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
|
|||||||
return (
|
return (
|
||||||
<BoardContextMenu board={board} setBoardToDelete={setBoardToDelete}>
|
<BoardContextMenu board={board} setBoardToDelete={setBoardToDelete}>
|
||||||
{(ref) => (
|
{(ref) => (
|
||||||
<Tooltip
|
<Tooltip label={<BoardTooltip board={board} />} openDelay={1000} placement="left" closeOnScroll p={2}>
|
||||||
label={<BoardTotalsTooltip board_id={board.board_id} isArchived={Boolean(board.archived)} />}
|
|
||||||
openDelay={1000}
|
|
||||||
placement="left"
|
|
||||||
closeOnScroll
|
|
||||||
>
|
|
||||||
<Flex
|
<Flex
|
||||||
position="relative"
|
position="relative"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -131,10 +126,12 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
|
|||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
py={1}
|
py={1}
|
||||||
px={2}
|
ps={1}
|
||||||
gap={2}
|
pe={4}
|
||||||
|
gap={4}
|
||||||
bg={isSelected ? 'base.850' : undefined}
|
bg={isSelected ? 'base.850' : undefined}
|
||||||
_hover={_hover}
|
_hover={_hover}
|
||||||
|
h={12}
|
||||||
>
|
>
|
||||||
<CoverImage board={board} />
|
<CoverImage board={board} />
|
||||||
<Editable
|
<Editable
|
||||||
@ -149,17 +146,17 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
isPreviewFocusable={false}
|
isPreviewFocusable={false}
|
||||||
|
fontSize="sm"
|
||||||
>
|
>
|
||||||
<EditablePreview
|
<EditablePreview
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
p={0}
|
p={0}
|
||||||
fontSize="md"
|
fontSize="sm"
|
||||||
textOverflow="ellipsis"
|
textOverflow="ellipsis"
|
||||||
noOfLines={1}
|
noOfLines={1}
|
||||||
w="fit-content"
|
w="fit-content"
|
||||||
wordBreak="break-all"
|
wordBreak="break-all"
|
||||||
color={isSelected ? 'base.100' : 'base.400'}
|
fontWeight={isSelected ? 'bold' : 'normal'}
|
||||||
fontWeight={isSelected ? 'semibold' : 'normal'}
|
|
||||||
/>
|
/>
|
||||||
<EditableInput sx={editableInputStyles} />
|
<EditableInput sx={editableInputStyles} />
|
||||||
<JankEditableHijack onStartEditingRef={onStartEditingRef} />
|
<JankEditableHijack onStartEditingRef={onStartEditingRef} />
|
||||||
@ -168,7 +165,7 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
|
|||||||
{board.archived && !editingDisclosure.isOpen && <Icon as={PiArchiveBold} fill="base.300" />}
|
{board.archived && !editingDisclosure.isOpen && <Icon as={PiArchiveBold} fill="base.300" />}
|
||||||
{!editingDisclosure.isOpen && <Text variant="subtext">{board.image_count}</Text>}
|
{!editingDisclosure.isOpen && <Text variant="subtext">{board.image_count}</Text>}
|
||||||
|
|
||||||
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
|
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="lg">{t('unifiedCanvas.move')}</Text>} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@ -197,8 +194,8 @@ const CoverImage = ({ board }: { board: BoardDTO }) => {
|
|||||||
src={coverImage.thumbnail_url}
|
src={coverImage.thumbnail_url}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
w={8}
|
w={10}
|
||||||
h={8}
|
h={10}
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
borderBottomRadius="lg"
|
borderBottomRadius="lg"
|
||||||
/>
|
/>
|
||||||
@ -206,8 +203,8 @@ const CoverImage = ({ board }: { board: BoardDTO }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w={8} h={8} justifyContent="center" alignItems="center">
|
<Flex w={10} h={10} justifyContent="center" alignItems="center">
|
||||||
<Icon boxSize={8} as={PiImageSquare} opacity={0.7} color="base.500" />
|
<Icon boxSize={10} as={PiImageSquare} opacity={0.7} color="base.500" />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import IAIDroppable from 'common/components/IAIDroppable';
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
import type { RemoveFromBoardDropData } from 'features/dnd/types';
|
import type { RemoveFromBoardDropData } from 'features/dnd/types';
|
||||||
import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
|
import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
|
||||||
import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTotalsTooltip';
|
import { BoardTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTooltip';
|
||||||
import NoBoardBoardContextMenu from 'features/gallery/components/Boards/NoBoardBoardContextMenu';
|
import NoBoardBoardContextMenu from 'features/gallery/components/Boards/NoBoardBoardContextMenu';
|
||||||
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
@ -46,25 +46,16 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredOut = useMemo(() => {
|
|
||||||
return boardSearchText ? !boardName.toLowerCase().includes(boardSearchText.toLowerCase()) : false;
|
|
||||||
}, [boardName, boardSearchText]);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (filteredOut) {
|
if (boardSearchText.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoBoardBoardContextMenu>
|
<NoBoardBoardContextMenu>
|
||||||
{(ref) => (
|
{(ref) => (
|
||||||
<Tooltip
|
<Tooltip label={<BoardTooltip board={null} />} openDelay={1000} placement="left" closeOnScroll>
|
||||||
label={<BoardTotalsTooltip board_id="none" isArchived={false} />}
|
|
||||||
openDelay={1000}
|
|
||||||
placement="left"
|
|
||||||
closeOnScroll
|
|
||||||
>
|
|
||||||
<Flex
|
<Flex
|
||||||
position="relative"
|
position="relative"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -73,15 +64,17 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
px={2}
|
|
||||||
py={1}
|
py={1}
|
||||||
gap={2}
|
ps={1}
|
||||||
|
pe={4}
|
||||||
|
gap={4}
|
||||||
bg={isSelected ? 'base.850' : undefined}
|
bg={isSelected ? 'base.850' : undefined}
|
||||||
_hover={_hover}
|
_hover={_hover}
|
||||||
|
h={12}
|
||||||
>
|
>
|
||||||
<Flex w={8} h={8} justifyContent="center" alignItems="center">
|
<Flex w="10" justifyContent="space-around">
|
||||||
{/* iconified from public/assets/images/invoke-symbol-wht-lrg.svg */}
|
{/* iconified from public/assets/images/invoke-symbol-wht-lrg.svg */}
|
||||||
<Icon boxSize={6} opacity={1} stroke="base.500" viewBox="0 0 66 66" fill="none">
|
<Icon boxSize={8} opacity={1} stroke="base.500" viewBox="0 0 66 66" fill="none">
|
||||||
<path
|
<path
|
||||||
d="M43.9137 16H63.1211V3H3.12109V16H22.3285L43.9137 50H63.1211V63H3.12109V50H22.3285"
|
d="M43.9137 16H63.1211V3H3.12109V16H22.3285L43.9137 50H63.1211V63H3.12109V50H22.3285"
|
||||||
strokeWidth="5"
|
strokeWidth="5"
|
||||||
@ -89,18 +82,12 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
|||||||
</Icon>
|
</Icon>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Text
|
<Text fontSize="sm" fontWeight={isSelected ? 'bold' : 'normal'} noOfLines={1} flexGrow={1}>
|
||||||
fontSize="md"
|
|
||||||
color={isSelected ? 'base.100' : 'base.400'}
|
|
||||||
fontWeight={isSelected ? 'semibold' : 'normal'}
|
|
||||||
noOfLines={1}
|
|
||||||
flexGrow={1}
|
|
||||||
>
|
|
||||||
{boardName}
|
{boardName}
|
||||||
</Text>
|
</Text>
|
||||||
{autoAddBoardId === 'none' && <AutoAddBadge />}
|
{autoAddBoardId === 'none' && <AutoAddBadge />}
|
||||||
<Text variant="subtext">{imagesTotal}</Text>
|
<Text variant="subtext">{imagesTotal}</Text>
|
||||||
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
|
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="lg">{t('unifiedCanvas.move')}</Text>} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
@ -0,0 +1,105 @@
|
|||||||
|
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Collapse,
|
||||||
|
Flex,
|
||||||
|
IconButton,
|
||||||
|
Spacer,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useGallerySearchTerm } from 'features/gallery/components/ImageGrid/useGallerySearchTerm';
|
||||||
|
import { galleryViewChanged } from 'features/gallery/store/gallerySlice';
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiMagnifyingGlassBold } from 'react-icons/pi';
|
||||||
|
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||||
|
|
||||||
|
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||||
|
import { GalleryPagination } from './ImageGrid/GalleryPagination';
|
||||||
|
import { GallerySearch } from './ImageGrid/GallerySearch';
|
||||||
|
|
||||||
|
const BASE_STYLES: ChakraProps['sx'] = {
|
||||||
|
fontWeight: 'semibold',
|
||||||
|
fontSize: 'sm',
|
||||||
|
color: 'base.300',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SELECTED_STYLES: ChakraProps['sx'] = {
|
||||||
|
borderColor: 'base.800',
|
||||||
|
borderBottomColor: 'base.900',
|
||||||
|
color: 'invokeBlue.300',
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
||||||
|
|
||||||
|
export const Gallery = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const galleryView = useAppSelector((s) => s.gallery.galleryView);
|
||||||
|
const initialSearchTerm = useAppSelector((s) => s.gallery.searchTerm);
|
||||||
|
const searchDisclosure = useDisclosure({ defaultIsOpen: initialSearchTerm.length > 0 });
|
||||||
|
const [searchTerm, onChangeSearchTerm, onResetSearchTerm] = useGallerySearchTerm();
|
||||||
|
|
||||||
|
const handleClickImages = useCallback(() => {
|
||||||
|
dispatch(galleryViewChanged('images'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleClickAssets = useCallback(() => {
|
||||||
|
dispatch(galleryViewChanged('assets'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleClickSearch = useCallback(() => {
|
||||||
|
searchDisclosure.onToggle();
|
||||||
|
onResetSearchTerm();
|
||||||
|
}, [onResetSearchTerm, searchDisclosure]);
|
||||||
|
|
||||||
|
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||||
|
const boardName = useBoardName(selectedBoardId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" pt={1}>
|
||||||
|
<Tabs index={galleryView === 'images' ? 0 : 1} variant="enclosed" display="flex" flexDir="column" w="full">
|
||||||
|
<TabList gap={2} fontSize="sm" borderColor="base.800" alignItems="center" w="full">
|
||||||
|
<Text fontSize="sm" fontWeight="semibold" noOfLines={1} px="2">
|
||||||
|
{boardName}
|
||||||
|
</Text>
|
||||||
|
<Spacer />
|
||||||
|
<Tab sx={BASE_STYLES} _selected={SELECTED_STYLES} onClick={handleClickImages} data-testid="images-tab">
|
||||||
|
{t('parameters.images')}
|
||||||
|
</Tab>
|
||||||
|
<Tab sx={BASE_STYLES} _selected={SELECTED_STYLES} onClick={handleClickAssets} data-testid="assets-tab">
|
||||||
|
{t('gallery.assets')}
|
||||||
|
</Tab>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleClickSearch}
|
||||||
|
tooltip={searchDisclosure.isOpen ? `${t('gallery.exitSearch')}` : `${t('gallery.displaySearch')}`}
|
||||||
|
aria-label={t('gallery.displaySearch')}
|
||||||
|
icon={<PiMagnifyingGlassBold />}
|
||||||
|
colorScheme={searchDisclosure.isOpen ? 'invokeBlue' : 'base'}
|
||||||
|
variant="link"
|
||||||
|
/>
|
||||||
|
</TabList>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Box w="full">
|
||||||
|
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||||
|
<Box w="full" pt={2}>
|
||||||
|
<GallerySearch
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
onChangeSearchTerm={onChangeSearchTerm}
|
||||||
|
onResetSearchTerm={onResetSearchTerm}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
</Box>
|
||||||
|
<GalleryImageGrid />
|
||||||
|
<GalleryPagination />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
@ -1,33 +0,0 @@
|
|||||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onClick: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const GalleryBoardName = (props: Props) => {
|
|
||||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
|
||||||
const boardName = useBoardName(selectedBoardId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
onClick={props.onClick}
|
|
||||||
as="button"
|
|
||||||
h="full"
|
|
||||||
w="full"
|
|
||||||
borderWidth={1}
|
|
||||||
borderRadius="base"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
px={2}
|
|
||||||
>
|
|
||||||
<Text fontWeight="semibold" fontSize="md" noOfLines={1} wordBreak="break-all" color="base.200">
|
|
||||||
{boardName}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(GalleryBoardName);
|
|
@ -3,32 +3,21 @@ import { useStore } from '@nanostores/react';
|
|||||||
import { $projectName, $projectUrl } from 'app/store/nanostores/projectId';
|
import { $projectName, $projectUrl } from 'app/store/nanostores/projectId';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import GalleryBoardName from './GalleryBoardName';
|
export const GalleryHeader = memo(() => {
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onClickBoardName: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GalleryHeader = memo((props: Props) => {
|
|
||||||
const projectName = useStore($projectName);
|
const projectName = useStore($projectName);
|
||||||
const projectUrl = useStore($projectUrl);
|
const projectUrl = useStore($projectUrl);
|
||||||
|
|
||||||
if (projectName && projectUrl) {
|
if (projectName && projectUrl) {
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} w="full" alignItems="center" justifyContent="space-evenly" pe={2}>
|
<Flex gap={2} w="full" alignItems="center" justifyContent="space-evenly" pe={2}>
|
||||||
<Text fontSize="md" fontWeight="semibold" noOfLines={1} w="full" textAlign="center">
|
<Text fontSize="md" fontWeight="semibold" noOfLines={1} wordBreak="break-all" w="full" textAlign="center">
|
||||||
<Link href={projectUrl}>{projectName}</Link>
|
<Link href={projectUrl}>{projectName}</Link>
|
||||||
</Text>
|
</Text>
|
||||||
<GalleryBoardName onClick={props.onClickBoardName} />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return null;
|
||||||
<Flex w="full" pe={2}>
|
|
||||||
<GalleryBoardName onClick={props.onClickBoardName} />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
GalleryHeader.displayName = 'GalleryHeader';
|
GalleryHeader.displayName = 'GalleryHeader';
|
||||||
|
@ -1,57 +1,28 @@
|
|||||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
import { Box, Button, Collapse, Divider, Flex, IconButton, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Collapse,
|
|
||||||
Divider,
|
|
||||||
Flex,
|
|
||||||
IconButton,
|
|
||||||
Spacer,
|
|
||||||
Tab,
|
|
||||||
TabList,
|
|
||||||
Tabs,
|
|
||||||
useDisclosure,
|
|
||||||
} from '@invoke-ai/ui-library';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
||||||
import { galleryViewChanged } from 'features/gallery/store/gallerySlice';
|
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||||
import { usePanel, type UsePanelOptions } from 'features/ui/hooks/usePanel';
|
import { usePanel, type UsePanelOptions } from 'features/ui/hooks/usePanel';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiMagnifyingGlassBold } from 'react-icons/pi';
|
import { PiCaretDownBold, PiCaretUpBold, PiMagnifyingGlassBold } from 'react-icons/pi';
|
||||||
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||||
|
|
||||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
import BoardsListWrapper from './Boards/BoardsList/BoardsListWrapper';
|
||||||
import BoardsSearch from './Boards/BoardsList/BoardsSearch';
|
import BoardsSearch from './Boards/BoardsList/BoardsSearch';
|
||||||
|
import { Gallery } from './Gallery';
|
||||||
import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopover';
|
import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopover';
|
||||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
|
||||||
import { GalleryPagination } from './ImageGrid/GalleryPagination';
|
|
||||||
import { GallerySearch } from './ImageGrid/GallerySearch';
|
|
||||||
|
|
||||||
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
||||||
|
|
||||||
const BASE_STYLES: ChakraProps['sx'] = {
|
|
||||||
fontWeight: 'semibold',
|
|
||||||
fontSize: 'sm',
|
|
||||||
color: 'base.300',
|
|
||||||
};
|
|
||||||
|
|
||||||
const SELECTED_STYLES: ChakraProps['sx'] = {
|
|
||||||
borderColor: 'base.800',
|
|
||||||
borderBottomColor: 'base.900',
|
|
||||||
color: 'invokeBlue.300',
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImageGalleryContent = () => {
|
const ImageGalleryContent = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const galleryView = useAppSelector((s) => s.gallery.galleryView);
|
|
||||||
const searchTerm = useAppSelector((s) => s.gallery.searchTerm);
|
|
||||||
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const searchDisclosure = useDisclosure({ defaultIsOpen: false });
|
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
|
||||||
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: false });
|
|
||||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||||
|
|
||||||
const boardsListPanelOptions = useMemo<UsePanelOptions>(
|
const boardsListPanelOptions = useMemo<UsePanelOptions>(
|
||||||
@ -67,42 +38,58 @@ const ImageGalleryContent = () => {
|
|||||||
);
|
);
|
||||||
const boardsListPanel = usePanel(boardsListPanelOptions);
|
const boardsListPanel = usePanel(boardsListPanelOptions);
|
||||||
|
|
||||||
const handleClickImages = useCallback(() => {
|
const handleClickBoardSearch = useCallback(() => {
|
||||||
dispatch(galleryViewChanged('images'));
|
if (boardSearchText.length) {
|
||||||
}, [dispatch]);
|
dispatch(boardSearchTextChanged(''));
|
||||||
|
}
|
||||||
|
boardSearchDisclosure.onToggle();
|
||||||
|
boardsListPanel.expand();
|
||||||
|
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
|
||||||
|
|
||||||
const handleClickAssets = useCallback(() => {
|
const handleToggleBoardPanel = useCallback(() => {
|
||||||
dispatch(galleryViewChanged('assets'));
|
if (boardSearchText.length) {
|
||||||
}, [dispatch]);
|
dispatch(boardSearchTextChanged(''));
|
||||||
|
}
|
||||||
|
boardSearchDisclosure.onClose();
|
||||||
|
boardsListPanel.toggle();
|
||||||
|
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex position="relative" flexDirection="column" h="full" w="full" pt={2}>
|
<Flex position="relative" flexDirection="column" h="full" w="full" pt={2}>
|
||||||
<Flex alignItems="center" gap={2}>
|
<Flex alignItems="center" gap={0}>
|
||||||
<GalleryHeader onClickBoardName={boardsListPanel.toggle} />
|
<GalleryHeader />
|
||||||
|
<Flex alignItems="center" justifyContent="space-between" w="full">
|
||||||
|
<Button
|
||||||
|
w={112}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleToggleBoardPanel}
|
||||||
|
rightIcon={boardsListPanel.isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
|
||||||
|
>
|
||||||
|
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
|
||||||
|
</Button>
|
||||||
|
<Flex alignItems="center" justifyContent="space-between">
|
||||||
<GallerySettingsPopover />
|
<GallerySettingsPopover />
|
||||||
<Box position="relative" h="full">
|
<Flex>
|
||||||
<IconButton
|
<IconButton
|
||||||
w="full"
|
w="full"
|
||||||
h="full"
|
h="full"
|
||||||
onClick={boardSearchDisclosure.onToggle}
|
onClick={handleClickBoardSearch}
|
||||||
tooltip={`${t('gallery.displayBoardSearch')}`}
|
tooltip={
|
||||||
|
boardSearchDisclosure.isOpen
|
||||||
|
? `${t('gallery.exitBoardSearch')}`
|
||||||
|
: `${t('gallery.displayBoardSearch')}`
|
||||||
|
}
|
||||||
aria-label={t('gallery.displayBoardSearch')}
|
aria-label={t('gallery.displayBoardSearch')}
|
||||||
icon={<PiMagnifyingGlassBold />}
|
icon={<PiMagnifyingGlassBold />}
|
||||||
|
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
|
||||||
variant="link"
|
variant="link"
|
||||||
/>
|
/>
|
||||||
{boardSearchText && (
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
w={2}
|
|
||||||
h={2}
|
|
||||||
bg="invokeBlue.300"
|
|
||||||
borderRadius="full"
|
|
||||||
insetBlockStart={2}
|
|
||||||
insetInlineEnd={2}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<PanelGroup ref={panelGroupRef} direction="vertical">
|
<PanelGroup ref={panelGroupRef} direction="vertical">
|
||||||
<Panel
|
<Panel
|
||||||
id="boards-list-panel"
|
id="boards-list-panel"
|
||||||
@ -115,10 +102,12 @@ const ImageGalleryContent = () => {
|
|||||||
>
|
>
|
||||||
<Flex flexDir="column" w="full" h="full">
|
<Flex flexDir="column" w="full" h="full">
|
||||||
<Collapse in={boardSearchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
<Collapse in={boardSearchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||||
|
<Box w="full" pt={2}>
|
||||||
<BoardsSearch />
|
<BoardsSearch />
|
||||||
|
</Box>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
<Divider pt={2} />
|
<Divider pt={2} />
|
||||||
<BoardsList />
|
<BoardsListWrapper />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Panel>
|
</Panel>
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
@ -127,50 +116,7 @@ const ImageGalleryContent = () => {
|
|||||||
onDoubleClick={boardsListPanel.onDoubleClickHandle}
|
onDoubleClick={boardsListPanel.onDoubleClickHandle}
|
||||||
/>
|
/>
|
||||||
<Panel id="gallery-wrapper-panel" minSize={20}>
|
<Panel id="gallery-wrapper-panel" minSize={20}>
|
||||||
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full">
|
<Gallery />
|
||||||
<Tabs index={galleryView === 'images' ? 0 : 1} variant="enclosed" display="flex" flexDir="column" w="full">
|
|
||||||
<TabList gap={2} fontSize="sm" borderColor="base.800">
|
|
||||||
<Tab sx={BASE_STYLES} _selected={SELECTED_STYLES} onClick={handleClickImages} data-testid="images-tab">
|
|
||||||
{t('parameters.images')}
|
|
||||||
</Tab>
|
|
||||||
<Tab sx={BASE_STYLES} _selected={SELECTED_STYLES} onClick={handleClickAssets} data-testid="assets-tab">
|
|
||||||
{t('gallery.assets')}
|
|
||||||
</Tab>
|
|
||||||
<Spacer />
|
|
||||||
<Box position="relative">
|
|
||||||
<IconButton
|
|
||||||
w="full"
|
|
||||||
h="full"
|
|
||||||
onClick={searchDisclosure.onToggle}
|
|
||||||
tooltip={`${t('gallery.displaySearch')}`}
|
|
||||||
aria-label={t('gallery.displaySearch')}
|
|
||||||
icon={<PiMagnifyingGlassBold />}
|
|
||||||
variant="link"
|
|
||||||
/>
|
|
||||||
{searchTerm && (
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
w={2}
|
|
||||||
h={2}
|
|
||||||
bg="invokeBlue.300"
|
|
||||||
borderRadius="full"
|
|
||||||
insetBlockStart={2}
|
|
||||||
insetInlineEnd={2}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</TabList>
|
|
||||||
</Tabs>
|
|
||||||
<Box w="full">
|
|
||||||
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
|
||||||
<Box w="full" pt={2}>
|
|
||||||
<GallerySearch />
|
|
||||||
</Box>
|
|
||||||
</Collapse>
|
|
||||||
</Box>
|
|
||||||
<GalleryImageGrid />
|
|
||||||
<GalleryPagination />
|
|
||||||
</Flex>
|
|
||||||
</Panel>
|
</Panel>
|
||||||
</PanelGroup>
|
</PanelGroup>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -3,6 +3,8 @@ import { ELLIPSIS, useGalleryPagination } from 'features/gallery/hooks/useGaller
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
|
import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
import { JumpTo } from './JumpTo';
|
||||||
|
|
||||||
export const GalleryPagination = () => {
|
export const GalleryPagination = () => {
|
||||||
const { goPrev, goNext, isPrevEnabled, isNextEnabled, pageButtons, goToPage, currentPage, total } =
|
const { goPrev, goNext, isPrevEnabled, isNextEnabled, pageButtons, goToPage, currentPage, total } =
|
||||||
useGalleryPagination();
|
useGalleryPagination();
|
||||||
@ -20,7 +22,7 @@ export const GalleryPagination = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} alignItems="center" w="full">
|
<Flex justifyContent="center" alignItems="center" w="full" gap={1} pt={2}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label="prev"
|
aria-label="prev"
|
||||||
@ -30,25 +32,9 @@ export const GalleryPagination = () => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
{pageButtons.map((page, i) => {
|
{pageButtons.map((page, i) => (
|
||||||
if (page === ELLIPSIS) {
|
<PageButton key={`${page}_${i}`} page={page} currentPage={currentPage} goToPage={goToPage} />
|
||||||
return (
|
))}
|
||||||
<Button size="sm" key={`ellipsis_${i}`} variant="link" isDisabled>
|
|
||||||
...
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
key={page}
|
|
||||||
onClick={goToPage.bind(null, page - 1)}
|
|
||||||
variant={currentPage === page - 1 ? 'solid' : 'outline'}
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -58,6 +44,28 @@ export const GalleryPagination = () => {
|
|||||||
isDisabled={!isNextEnabled}
|
isDisabled={!isNextEnabled}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
|
<JumpTo />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PageButtonProps = {
|
||||||
|
page: number | typeof ELLIPSIS;
|
||||||
|
currentPage: number;
|
||||||
|
goToPage: (page: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PageButton = ({ page, currentPage, goToPage }: PageButtonProps) => {
|
||||||
|
if (page === ELLIPSIS) {
|
||||||
|
return (
|
||||||
|
<Button size="sm" variant="link" isDisabled>
|
||||||
|
...
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button size="sm" onClick={goToPage.bind(null, page - 1)} variant={currentPage === page - 1 ? 'solid' : 'outline'}>
|
||||||
|
{page}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,59 +1,60 @@
|
|||||||
import { IconButton, Input, InputGroup, InputRightElement, Spinner } from '@invoke-ai/ui-library';
|
import { IconButton, Input, InputGroup, InputRightElement, Spinner } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import { searchTermChanged } from 'features/gallery/store/gallerySlice';
|
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||||
import { debounce } from 'lodash-es';
|
import { useCallback } from 'react';
|
||||||
import type { ChangeEvent } from 'react';
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiXBold } from 'react-icons/pi';
|
import { PiXBold } from 'react-icons/pi';
|
||||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
export const GallerySearch = () => {
|
type Props = {
|
||||||
const dispatch = useAppDispatch();
|
searchTerm: string;
|
||||||
const searchTerm = useAppSelector((s) => s.gallery.searchTerm);
|
onChangeSearchTerm: (value: string) => void;
|
||||||
|
onResetSearchTerm: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GallerySearch = ({ searchTerm, onChangeSearchTerm, onResetSearchTerm }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchTermInput, setSearchTermInput] = useState(searchTerm);
|
|
||||||
const queryArgs = useAppSelector(selectListImagesQueryArgs);
|
const queryArgs = useAppSelector(selectListImagesQueryArgs);
|
||||||
const { isPending } = useListImagesQuery(queryArgs, {
|
const { isPending } = useListImagesQuery(queryArgs, {
|
||||||
selectFromResult: ({ isLoading, isFetching }) => ({ isPending: isLoading || isFetching }),
|
selectFromResult: ({ isLoading, isFetching }) => ({ isPending: isLoading || isFetching }),
|
||||||
});
|
});
|
||||||
const debouncedSetSearchTerm = useMemo(() => {
|
|
||||||
return debounce((value: string) => {
|
|
||||||
dispatch(searchTermChanged(value));
|
|
||||||
}, 1000);
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const handleChangeInput = useCallback(
|
const handleChangeInput = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setSearchTermInput(e.target.value);
|
onChangeSearchTerm(e.target.value);
|
||||||
debouncedSetSearchTerm(e.target.value);
|
|
||||||
},
|
},
|
||||||
[debouncedSetSearchTerm]
|
[onChangeSearchTerm]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClearInput = useCallback(() => {
|
const handleKeydown = useCallback(
|
||||||
setSearchTermInput('');
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
dispatch(searchTermChanged(''));
|
// exit search mode on escape
|
||||||
}, [dispatch]);
|
if (e.key === 'Escape') {
|
||||||
|
onResetSearchTerm();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onResetSearchTerm]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('gallery.searchImages')}
|
placeholder={t('gallery.searchImages')}
|
||||||
value={searchTermInput}
|
value={searchTerm}
|
||||||
onChange={handleChangeInput}
|
onChange={handleChangeInput}
|
||||||
data-testid="image-search-input"
|
data-testid="image-search-input"
|
||||||
|
onKeyDown={handleKeydown}
|
||||||
/>
|
/>
|
||||||
{isPending && (
|
{isPending && (
|
||||||
<InputRightElement h="full" pe={2}>
|
<InputRightElement h="full" pe={2}>
|
||||||
<Spinner size="sm" opacity={0.5} />
|
<Spinner size="sm" opacity={0.5} />
|
||||||
</InputRightElement>
|
</InputRightElement>
|
||||||
)}
|
)}
|
||||||
{!isPending && searchTermInput.length && (
|
{!isPending && searchTerm.length && (
|
||||||
<InputRightElement h="full" pe={2}>
|
<InputRightElement h="full" pe={2}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleClearInput}
|
onClick={onResetSearchTerm}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="link"
|
variant="link"
|
||||||
aria-label={t('boards.clearSearch')}
|
aria-label={t('boards.clearSearch')}
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
CompositeNumberInput,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const JumpTo = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { goToPage, currentPage, pages } = useGalleryPagination();
|
||||||
|
const [newPage, setNewPage] = useState(currentPage);
|
||||||
|
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const onOpen = useCallback(() => {
|
||||||
|
setNewPage(currentPage);
|
||||||
|
setTimeout(() => {
|
||||||
|
const input = ref.current?.querySelector('input');
|
||||||
|
input?.focus();
|
||||||
|
input?.select();
|
||||||
|
}, 0);
|
||||||
|
}, [currentPage]);
|
||||||
|
|
||||||
|
const onChangeJumpTo = useCallback((v: number) => {
|
||||||
|
setNewPage(v - 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onClickGo = useCallback(() => {
|
||||||
|
goToPage(newPage);
|
||||||
|
onClose();
|
||||||
|
}, [newPage, goToPage, onClose]);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'enter',
|
||||||
|
() => {
|
||||||
|
onClickGo();
|
||||||
|
},
|
||||||
|
{ enabled: isOpen, enableOnFormTags: ['input'] },
|
||||||
|
[isOpen, onClickGo]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'esc',
|
||||||
|
() => {
|
||||||
|
setNewPage(currentPage);
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
{ enabled: isOpen, enableOnFormTags: ['input'] },
|
||||||
|
[isOpen, onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNewPage(currentPage);
|
||||||
|
}, [currentPage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover isOpen={isOpen} onClose={onClose} onOpen={onOpen}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button aria-label={t('gallery.jump')} size="sm" onClick={onToggle} variant="outline">
|
||||||
|
{t('gallery.jump')}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverBody>
|
||||||
|
<Flex gap={2} alignItems="center">
|
||||||
|
<FormControl>
|
||||||
|
<CompositeNumberInput
|
||||||
|
ref={ref}
|
||||||
|
size="sm"
|
||||||
|
maxW="60px"
|
||||||
|
value={newPage + 1}
|
||||||
|
min={1}
|
||||||
|
max={pages}
|
||||||
|
step={1}
|
||||||
|
onChange={onChangeJumpTo}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button h="full" size="sm" onClick={onClickGo}>
|
||||||
|
{t('gallery.go')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,37 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||||
|
import { searchTermChanged } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
export const useGallerySearchTerm = () => {
|
||||||
|
// Highlander!
|
||||||
|
useAssertSingleton('gallery-search-state');
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const searchTerm = useAppSelector((s) => s.gallery.searchTerm);
|
||||||
|
|
||||||
|
const [localSearchTerm, setLocalSearchTerm] = useState(searchTerm);
|
||||||
|
|
||||||
|
const debouncedSetSearchTerm = useMemo(() => {
|
||||||
|
return debounce((val: string) => {
|
||||||
|
dispatch(searchTermChanged(val));
|
||||||
|
}, 1000);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(val: string) => {
|
||||||
|
setLocalSearchTerm(val);
|
||||||
|
debouncedSetSearchTerm(val);
|
||||||
|
},
|
||||||
|
[debouncedSetSearchTerm]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onReset = useCallback(() => {
|
||||||
|
debouncedSetSearchTerm.cancel();
|
||||||
|
setLocalSearchTerm('');
|
||||||
|
dispatch(searchTermChanged(''));
|
||||||
|
}, [debouncedSetSearchTerm, dispatch]);
|
||||||
|
|
||||||
|
return [localSearchTerm, onChange, onReset] as const;
|
||||||
|
};
|
@ -2,7 +2,7 @@ import { ButtonGroup, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested';
|
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
|
import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||||
@ -14,7 +14,7 @@ import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors
|
|||||||
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||||
import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers';
|
import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers';
|
||||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||||
import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUpscaleSettings';
|
import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover';
|
||||||
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||||
@ -97,7 +97,7 @@ const CurrentImageButtons = () => {
|
|||||||
if (!imageDTO) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(upscaleRequested({ imageDTO }));
|
dispatch(adHocPostProcessingRequested({ imageDTO }));
|
||||||
}, [dispatch, imageDTO]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
@ -193,7 +193,7 @@ const CurrentImageButtons = () => {
|
|||||||
|
|
||||||
{isUpscalingEnabled && (
|
{isUpscalingEnabled && (
|
||||||
<ButtonGroup isDisabled={isQueueMutationInProgress}>
|
<ButtonGroup isDisabled={isQueueMutationInProgress}>
|
||||||
{isUpscalingEnabled && <ParamUpscalePopover imageDTO={imageDTO} />}
|
{isUpscalingEnabled && <PostProcessingPopover imageDTO={imageDTO} />}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import { offsetChanged } from 'features/gallery/store/gallerySlice';
|
import { offsetChanged } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { throttle } from 'lodash-es';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
@ -80,32 +81,41 @@ export const useGalleryPagination = () => {
|
|||||||
return offset > 0;
|
return offset > 0;
|
||||||
}, [count, offset]);
|
}, [count, offset]);
|
||||||
|
|
||||||
|
const onOffsetChanged = useCallback(
|
||||||
|
(arg: Parameters<typeof offsetChanged>[0]) => {
|
||||||
|
dispatch(offsetChanged(arg));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const throttledOnOffsetChanged = useMemo(() => throttle(onOffsetChanged, 500), [onOffsetChanged]);
|
||||||
|
|
||||||
const goNext = useCallback(
|
const goNext = useCallback(
|
||||||
(withHotkey?: 'arrow' | 'alt+arrow') => {
|
(withHotkey?: 'arrow' | 'alt+arrow') => {
|
||||||
dispatch(offsetChanged({ offset: offset + (limit || 0), withHotkey }));
|
throttledOnOffsetChanged({ offset: offset + (limit || 0), withHotkey });
|
||||||
},
|
},
|
||||||
[dispatch, offset, limit]
|
[throttledOnOffsetChanged, offset, limit]
|
||||||
);
|
);
|
||||||
|
|
||||||
const goPrev = useCallback(
|
const goPrev = useCallback(
|
||||||
(withHotkey?: 'arrow' | 'alt+arrow') => {
|
(withHotkey?: 'arrow' | 'alt+arrow') => {
|
||||||
dispatch(offsetChanged({ offset: Math.max(offset - (limit || 0), 0), withHotkey }));
|
throttledOnOffsetChanged({ offset: Math.max(offset - (limit || 0), 0), withHotkey });
|
||||||
},
|
},
|
||||||
[dispatch, offset, limit]
|
[throttledOnOffsetChanged, offset, limit]
|
||||||
);
|
);
|
||||||
|
|
||||||
const goToPage = useCallback(
|
const goToPage = useCallback(
|
||||||
(page: number) => {
|
(page: number) => {
|
||||||
dispatch(offsetChanged({ offset: page * (limit || 0) }));
|
throttledOnOffsetChanged({ offset: page * (limit || 0) });
|
||||||
},
|
},
|
||||||
[dispatch, limit]
|
[throttledOnOffsetChanged, limit]
|
||||||
);
|
);
|
||||||
const goToFirst = useCallback(() => {
|
const goToFirst = useCallback(() => {
|
||||||
dispatch(offsetChanged({ offset: 0 }));
|
throttledOnOffsetChanged({ offset: 0 });
|
||||||
}, [dispatch]);
|
}, [throttledOnOffsetChanged]);
|
||||||
const goToLast = useCallback(() => {
|
const goToLast = useCallback(() => {
|
||||||
dispatch(offsetChanged({ offset: (pages - 1) * (limit || 0) }));
|
throttledOnOffsetChanged({ offset: (pages - 1) * (limit || 0) });
|
||||||
}, [dispatch, pages, limit]);
|
}, [throttledOnOffsetChanged, pages, limit]);
|
||||||
|
|
||||||
// handle when total/pages decrease and user is on high page number (ie bulk removing or deleting)
|
// handle when total/pages decrease and user is on high page number (ie bulk removing or deleting)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useGetModelConfigWithTypeGuard } from 'services/api/hooks/useGetModelConfigWithTypeGuard';
|
import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
||||||
import { isControlNetOrT2IAdapterModelConfig } from 'services/api/types';
|
|
||||||
|
|
||||||
export const useControlNetOrT2IAdapterDefaultSettings = (modelKey?: string | null) => {
|
|
||||||
const { modelConfig, isLoading } = useGetModelConfigWithTypeGuard(
|
|
||||||
modelKey ?? skipToken,
|
|
||||||
isControlNetOrT2IAdapterModelConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
|
export const useControlNetOrT2IAdapterDefaultSettings = (
|
||||||
|
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig
|
||||||
|
) => {
|
||||||
const defaultSettingsDefaults = useMemo(() => {
|
const defaultSettingsDefaults = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
preprocessor: {
|
preprocessor: {
|
||||||
@ -19,5 +14,5 @@ export const useControlNetOrT2IAdapterDefaultSettings = (modelKey?: string | nul
|
|||||||
};
|
};
|
||||||
}, [modelConfig?.default_settings]);
|
}, [modelConfig?.default_settings]);
|
||||||
|
|
||||||
return { defaultSettingsDefaults, isLoading };
|
return defaultSettingsDefaults;
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useInstallModelMutation } from 'services/api/endpoints/models';
|
import { type InstallModelArg, useInstallModelMutation } from 'services/api/endpoints/models';
|
||||||
|
|
||||||
type InstallModelArg = {
|
type InstallModelArgWithCallbacks = InstallModelArg & {
|
||||||
source: string;
|
|
||||||
inplace?: boolean;
|
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
onError?: (error: unknown) => void;
|
onError?: (error: unknown) => void;
|
||||||
};
|
};
|
||||||
@ -15,8 +13,9 @@ export const useInstallModel = () => {
|
|||||||
const [_installModel, request] = useInstallModelMutation();
|
const [_installModel, request] = useInstallModelMutation();
|
||||||
|
|
||||||
const installModel = useCallback(
|
const installModel = useCallback(
|
||||||
({ source, inplace, onSuccess, onError }: InstallModelArg) => {
|
({ source, inplace, config, onSuccess, onError }: InstallModelArgWithCallbacks) => {
|
||||||
_installModel({ source, inplace })
|
config ||= {};
|
||||||
|
_installModel({ source, inplace, config })
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
|
||||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useGetModelConfigWithTypeGuard } from 'services/api/hooks/useGetModelConfigWithTypeGuard';
|
import type { MainModelConfig } from 'services/api/types';
|
||||||
import { isNonRefinerMainModelConfig } from 'services/api/types';
|
|
||||||
|
|
||||||
const initialStatesSelector = createMemoizedSelector(selectConfigSlice, (config) => {
|
const initialStatesSelector = createMemoizedSelector(selectConfigSlice, (config) => {
|
||||||
const { steps, guidance, scheduler, cfgRescaleMultiplier, vaePrecision, width, height } = config.sd;
|
const { steps, guidance, scheduler, cfgRescaleMultiplier, vaePrecision, width, height } = config.sd;
|
||||||
@ -22,9 +19,7 @@ const initialStatesSelector = createMemoizedSelector(selectConfigSlice, (config)
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useMainModelDefaultSettings = (modelKey?: string | null) => {
|
export const useMainModelDefaultSettings = (modelConfig: MainModelConfig) => {
|
||||||
const { modelConfig, isLoading } = useGetModelConfigWithTypeGuard(modelKey ?? skipToken, isNonRefinerMainModelConfig);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
initialSteps,
|
initialSteps,
|
||||||
initialCfg,
|
initialCfg,
|
||||||
@ -81,5 +76,5 @@ export const useMainModelDefaultSettings = (modelKey?: string | null) => {
|
|||||||
initialHeight,
|
initialHeight,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { defaultSettingsDefaults, isLoading, optimalDimension: getOptimalDimension(modelConfig) };
|
return defaultSettingsDefaults;
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
import type { ModelType } from 'services/api/types';
|
import type { ModelType } from 'services/api/types';
|
||||||
|
|
||||||
export type FilterableModelType = Exclude<ModelType, 'onnx' | 'clip_vision'> | 'refiner';
|
export type FilterableModelType = Exclude<ModelType, 'onnx' | 'clip_vision'> | 'refiner';
|
||||||
@ -50,6 +50,8 @@ export const modelManagerV2Slice = createSlice({
|
|||||||
export const { setSelectedModelKey, setSearchTerm, setFilteredModelType, setSelectedModelMode, setScanPath } =
|
export const { setSelectedModelKey, setSearchTerm, setFilteredModelType, setSelectedModelMode, setScanPath } =
|
||||||
modelManagerV2Slice.actions;
|
modelManagerV2Slice.actions;
|
||||||
|
|
||||||
|
export const selectModelManagerV2Slice = (state: RootState) => state.modelmanagerV2;
|
||||||
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const migrateModelManagerState = (state: any): any => {
|
const migrateModelManagerState = (state: any): any => {
|
||||||
if (!('_version' in state)) {
|
if (!('_version' in state)) {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Button, Flex, FormControl, FormErrorMessage, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library';
|
import { Button, Flex, FormControl, FormErrorMessage, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library';
|
||||||
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
||||||
import type { ChangeEventHandler } from 'react';
|
import type { ChangeEventHandler } from 'react';
|
||||||
import { useCallback, useState } from 'react';
|
import { memo, useCallback, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models';
|
import { useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models';
|
||||||
|
|
||||||
import { HuggingFaceResults } from './HuggingFaceResults';
|
import { HuggingFaceResults } from './HuggingFaceResults';
|
||||||
|
|
||||||
export const HuggingFaceForm = () => {
|
export const HuggingFaceForm = memo(() => {
|
||||||
const [huggingFaceRepo, setHuggingFaceRepo] = useState('');
|
const [huggingFaceRepo, setHuggingFaceRepo] = useState('');
|
||||||
const [displayResults, setDisplayResults] = useState(false);
|
const [displayResults, setDisplayResults] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
@ -66,4 +66,6 @@ export const HuggingFaceForm = () => {
|
|||||||
{data && data.urls && displayResults && <HuggingFaceResults results={data.urls} />}
|
{data && data.urls && displayResults && <HuggingFaceResults results={data.urls} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
HuggingFaceForm.displayName = 'HuggingFaceForm';
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
||||||
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
||||||
import { useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
result: string;
|
result: string;
|
||||||
};
|
};
|
||||||
export const HuggingFaceResultItem = ({ result }: Props) => {
|
export const HuggingFaceResultItem = memo(({ result }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [installModel] = useInstallModel();
|
const [installModel] = useInstallModel();
|
||||||
@ -27,4 +27,6 @@ export const HuggingFaceResultItem = ({ result }: Props) => {
|
|||||||
<IconButton aria-label={t('modelManager.install')} icon={<PiPlusBold />} onClick={onClick} size="sm" />
|
<IconButton aria-label={t('modelManager.install')} icon={<PiPlusBold />} onClick={onClick} size="sm" />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
HuggingFaceResultItem.displayName = 'HuggingFaceResultItem';
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
||||||
import type { ChangeEventHandler } from 'react';
|
import type { ChangeEventHandler } from 'react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiXBold } from 'react-icons/pi';
|
import { PiXBold } from 'react-icons/pi';
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ type HuggingFaceResultsProps = {
|
|||||||
results: string[];
|
results: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HuggingFaceResults = ({ results }: HuggingFaceResultsProps) => {
|
export const HuggingFaceResults = memo(({ results }: HuggingFaceResultsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
@ -93,4 +93,6 @@ export const HuggingFaceResults = ({ results }: HuggingFaceResultsProps) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
HuggingFaceResults.displayName = 'HuggingFaceResults';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Button, Checkbox, Flex, FormControl, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library';
|
import { Button, Checkbox, Flex, FormControl, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library';
|
||||||
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ type SimpleImportModelConfig = {
|
|||||||
inplace: boolean;
|
inplace: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InstallModelForm = () => {
|
export const InstallModelForm = memo(() => {
|
||||||
const [installModel, { isLoading }] = useInstallModel();
|
const [installModel, { isLoading }] = useInstallModel();
|
||||||
|
|
||||||
const { register, handleSubmit, formState, reset } = useForm<SimpleImportModelConfig>({
|
const { register, handleSubmit, formState, reset } = useForm<SimpleImportModelConfig>({
|
||||||
@ -74,4 +74,6 @@ export const InstallModelForm = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
InstallModelForm.displayName = 'InstallModelForm';
|
||||||
|
@ -2,12 +2,12 @@ import { Box, Button, Flex, Heading } from '@invoke-ai/ui-library';
|
|||||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useListModelInstallsQuery, usePruneCompletedModelInstallsMutation } from 'services/api/endpoints/models';
|
import { useListModelInstallsQuery, usePruneCompletedModelInstallsMutation } from 'services/api/endpoints/models';
|
||||||
|
|
||||||
import { ModelInstallQueueItem } from './ModelInstallQueueItem';
|
import { ModelInstallQueueItem } from './ModelInstallQueueItem';
|
||||||
|
|
||||||
export const ModelInstallQueue = () => {
|
export const ModelInstallQueue = memo(() => {
|
||||||
const { data } = useListModelInstallsQuery();
|
const { data } = useListModelInstallsQuery();
|
||||||
|
|
||||||
const [_pruneCompletedModelInstalls] = usePruneCompletedModelInstallsMutation();
|
const [_pruneCompletedModelInstalls] = usePruneCompletedModelInstallsMutation();
|
||||||
@ -61,4 +61,6 @@ export const ModelInstallQueue = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ModelInstallQueue.displayName = 'ModelInstallQueue';
|
||||||
|
@ -2,7 +2,7 @@ import { Flex, IconButton, Progress, Text, Tooltip } from '@invoke-ai/ui-library
|
|||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { PiXBold } from 'react-icons/pi';
|
import { PiXBold } from 'react-icons/pi';
|
||||||
import { useCancelModelInstallMutation } from 'services/api/endpoints/models';
|
import { useCancelModelInstallMutation } from 'services/api/endpoints/models';
|
||||||
import type { ModelInstallJob } from 'services/api/types';
|
import type { ModelInstallJob } from 'services/api/types';
|
||||||
@ -25,7 +25,7 @@ const formatBytes = (bytes: number) => {
|
|||||||
return `${bytes.toFixed(2)} ${units[i]}`;
|
return `${bytes.toFixed(2)} ${units[i]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModelInstallQueueItem = (props: ModelListItemProps) => {
|
export const ModelInstallQueueItem = memo((props: ModelListItemProps) => {
|
||||||
const { installJob } = props;
|
const { installJob } = props;
|
||||||
|
|
||||||
const [deleteImportModel] = useCancelModelInstallMutation();
|
const [deleteImportModel] = useCancelModelInstallMutation();
|
||||||
@ -124,7 +124,9 @@ export const ModelInstallQueueItem = (props: ModelListItemProps) => {
|
|||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ModelInstallQueueItem.displayName = 'ModelInstallQueueItem';
|
||||||
|
|
||||||
type TooltipLabelProps = {
|
type TooltipLabelProps = {
|
||||||
installJob: ModelInstallJob;
|
installJob: ModelInstallJob;
|
||||||
@ -132,7 +134,7 @@ type TooltipLabelProps = {
|
|||||||
source: string;
|
source: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TooltipLabel = ({ name, source, installJob }: TooltipLabelProps) => {
|
const TooltipLabel = memo(({ name, source, installJob }: TooltipLabelProps) => {
|
||||||
const progressString = useMemo(() => {
|
const progressString = useMemo(() => {
|
||||||
if (installJob.status !== 'downloading' || installJob.bytes === undefined || installJob.total_bytes === undefined) {
|
if (installJob.status !== 'downloading' || installJob.bytes === undefined || installJob.total_bytes === undefined) {
|
||||||
return '';
|
return '';
|
||||||
@ -156,4 +158,6 @@ const TooltipLabel = ({ name, source, installJob }: TooltipLabelProps) => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
TooltipLabel.displayName = 'TooltipLabel';
|
||||||
|
@ -2,13 +2,13 @@ import { Button, Flex, FormControl, FormErrorMessage, FormHelperText, FormLabel,
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { setScanPath } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
import { setScanPath } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
||||||
import type { ChangeEventHandler } from 'react';
|
import type { ChangeEventHandler } from 'react';
|
||||||
import { useCallback, useState } from 'react';
|
import { memo, useCallback, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLazyScanFolderQuery } from 'services/api/endpoints/models';
|
import { useLazyScanFolderQuery } from 'services/api/endpoints/models';
|
||||||
|
|
||||||
import { ScanModelsResults } from './ScanFolderResults';
|
import { ScanModelsResults } from './ScanFolderResults';
|
||||||
|
|
||||||
export const ScanModelsForm = () => {
|
export const ScanModelsForm = memo(() => {
|
||||||
const scanPath = useAppSelector((state) => state.modelmanagerV2.scanPath);
|
const scanPath = useAppSelector((state) => state.modelmanagerV2.scanPath);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
@ -56,4 +56,6 @@ export const ScanModelsForm = () => {
|
|||||||
{data && <ScanModelsResults results={data} />}
|
{data && <ScanModelsResults results={data} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ScanModelsForm.displayName = 'ScanModelsForm';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Badge, Box, Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
import { Badge, Box, Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
||||||
import { useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
import type { ScanFolderResponse } from 'services/api/endpoints/models';
|
import type { ScanFolderResponse } from 'services/api/endpoints/models';
|
||||||
@ -8,7 +8,7 @@ type Props = {
|
|||||||
result: ScanFolderResponse[number];
|
result: ScanFolderResponse[number];
|
||||||
installModel: (source: string) => void;
|
installModel: (source: string) => void;
|
||||||
};
|
};
|
||||||
export const ScanModelResultItem = ({ result, installModel }: Props) => {
|
export const ScanModelResultItem = memo(({ result, installModel }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleInstall = useCallback(() => {
|
const handleInstall = useCallback(() => {
|
||||||
@ -30,4 +30,6 @@ export const ScanModelResultItem = ({ result, installModel }: Props) => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ScanModelResultItem.displayName = 'ScanModelResultItem';
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
||||||
import type { ChangeEvent, ChangeEventHandler } from 'react';
|
import type { ChangeEvent, ChangeEventHandler } from 'react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiXBold } from 'react-icons/pi';
|
import { PiXBold } from 'react-icons/pi';
|
||||||
import type { ScanFolderResponse } from 'services/api/endpoints/models';
|
import type { ScanFolderResponse } from 'services/api/endpoints/models';
|
||||||
@ -25,7 +25,7 @@ type ScanModelResultsProps = {
|
|||||||
results: ScanFolderResponse;
|
results: ScanFolderResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ScanModelsResults = ({ results }: ScanModelResultsProps) => {
|
export const ScanModelsResults = memo(({ results }: ScanModelResultsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [inplace, setInplace] = useState(true);
|
const [inplace, setInplace] = useState(true);
|
||||||
@ -116,4 +116,6 @@ export const ScanModelsResults = ({ results }: ScanModelResultsProps) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ScanModelsResults.displayName = 'ScanModelsResults';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Badge, Box, Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
import { Badge, Box, Flex, IconButton, Text } from '@invoke-ai/ui-library';
|
||||||
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel';
|
||||||
import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
|
import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
import type { GetStarterModelsResponse } from 'services/api/endpoints/models';
|
import type { GetStarterModelsResponse } from 'services/api/endpoints/models';
|
||||||
@ -9,20 +9,22 @@ import type { GetStarterModelsResponse } from 'services/api/endpoints/models';
|
|||||||
type Props = {
|
type Props = {
|
||||||
result: GetStarterModelsResponse[number];
|
result: GetStarterModelsResponse[number];
|
||||||
};
|
};
|
||||||
export const StarterModelsResultItem = ({ result }: Props) => {
|
export const StarterModelsResultItem = memo(({ result }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const allSources = useMemo(() => {
|
const allSources = useMemo(() => {
|
||||||
const _allSources = [result.source];
|
const _allSources = [{ source: result.source, config: { name: result.name, description: result.description } }];
|
||||||
if (result.dependencies) {
|
if (result.dependencies) {
|
||||||
_allSources.push(...result.dependencies.map((d) => d.source));
|
for (const d of result.dependencies) {
|
||||||
|
_allSources.push({ source: d.source, config: { name: d.name, description: d.description } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return _allSources;
|
return _allSources;
|
||||||
}, [result]);
|
}, [result]);
|
||||||
const [installModel] = useInstallModel();
|
const [installModel] = useInstallModel();
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
for (const source of allSources) {
|
for (const { config, source } of allSources) {
|
||||||
installModel({ source });
|
installModel({ config, source });
|
||||||
}
|
}
|
||||||
}, [allSources, installModel]);
|
}, [allSources, installModel]);
|
||||||
|
|
||||||
@ -45,4 +47,6 @@ export const StarterModelsResultItem = ({ result }: Props) => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
StarterModelsResultItem.displayName = 'StarterModelsResultItem';
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { FetchingModelsLoader } from 'features/modelManagerV2/subpanels/ModelManagerPanel/FetchingModelsLoader';
|
import { FetchingModelsLoader } from 'features/modelManagerV2/subpanels/ModelManagerPanel/FetchingModelsLoader';
|
||||||
|
import { memo } from 'react';
|
||||||
import { useGetStarterModelsQuery } from 'services/api/endpoints/models';
|
import { useGetStarterModelsQuery } from 'services/api/endpoints/models';
|
||||||
|
|
||||||
import { StarterModelsResults } from './StarterModelsResults';
|
import { StarterModelsResults } from './StarterModelsResults';
|
||||||
|
|
||||||
export const StarterModelsForm = () => {
|
export const StarterModelsForm = memo(() => {
|
||||||
const { isLoading, data } = useGetStarterModelsQuery();
|
const { isLoading, data } = useGetStarterModelsQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -13,4 +14,6 @@ export const StarterModelsForm = () => {
|
|||||||
{data && <StarterModelsResults results={data} />}
|
{data && <StarterModelsResults results={data} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
StarterModelsForm.displayName = 'StarterModelsForm';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Flex, IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
|
import { Flex, IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
|
||||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
import type { ChangeEventHandler } from 'react';
|
import type { ChangeEventHandler } from 'react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiXBold } from 'react-icons/pi';
|
import { PiXBold } from 'react-icons/pi';
|
||||||
import type { GetStarterModelsResponse } from 'services/api/endpoints/models';
|
import type { GetStarterModelsResponse } from 'services/api/endpoints/models';
|
||||||
@ -12,16 +12,23 @@ type StarterModelsResultsProps = {
|
|||||||
results: NonNullable<GetStarterModelsResponse>;
|
results: NonNullable<GetStarterModelsResponse>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StarterModelsResults = ({ results }: StarterModelsResultsProps) => {
|
export const StarterModelsResults = memo(({ results }: StarterModelsResultsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
const filteredResults = useMemo(() => {
|
const filteredResults = useMemo(() => {
|
||||||
return results.filter((result) => {
|
return results.filter((result) => {
|
||||||
const trimmedSearchTerm = searchTerm.trim().toLowerCase();
|
const trimmedSearchTerm = searchTerm.trim().toLowerCase();
|
||||||
const matchStrings = [result.name.toLowerCase(), result.type.toLowerCase(), result.description.toLowerCase()];
|
const matchStrings = [
|
||||||
|
result.name.toLowerCase(),
|
||||||
|
result.type.toLowerCase().replaceAll('_', ' '),
|
||||||
|
result.description.toLowerCase(),
|
||||||
|
];
|
||||||
if (result.type === 'spandrel_image_to_image') {
|
if (result.type === 'spandrel_image_to_image') {
|
||||||
matchStrings.push('upscale');
|
matchStrings.push('upscale');
|
||||||
|
matchStrings.push('post-processing');
|
||||||
|
matchStrings.push('postprocessing');
|
||||||
|
matchStrings.push('post processing');
|
||||||
}
|
}
|
||||||
return matchStrings.some((matchString) => matchString.includes(trimmedSearchTerm));
|
return matchStrings.some((matchString) => matchString.includes(trimmedSearchTerm));
|
||||||
});
|
});
|
||||||
@ -72,4 +79,6 @@ export const StarterModelsResults = ({ results }: StarterModelsResultsProps) =>
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
StarterModelsResults.displayName = 'StarterModelsResults';
|
||||||
|
@ -2,7 +2,7 @@ import { Box, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@in
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm';
|
import { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm';
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import { useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm';
|
import { HuggingFaceForm } from './AddModelPanel/HuggingFaceFolder/HuggingFaceForm';
|
||||||
@ -12,7 +12,7 @@ import { ScanModelsForm } from './AddModelPanel/ScanFolder/ScanFolderForm';
|
|||||||
|
|
||||||
export const $installModelsTab = atom(0);
|
export const $installModelsTab = atom(0);
|
||||||
|
|
||||||
export const InstallModels = () => {
|
export const InstallModels = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const index = useStore($installModelsTab);
|
const index = useStore($installModelsTab);
|
||||||
const onChange = useCallback((index: number) => {
|
const onChange = useCallback((index: number) => {
|
||||||
@ -49,4 +49,6 @@ export const InstallModels = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
InstallModels.displayName = 'InstallModels';
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { Button, Flex, Heading } from '@invoke-ai/ui-library';
|
import { Button, Flex, Heading } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
||||||
import { useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
|
||||||
import ModelList from './ModelManagerPanel/ModelList';
|
import ModelList from './ModelManagerPanel/ModelList';
|
||||||
import { ModelListNavigation } from './ModelManagerPanel/ModelListNavigation';
|
import { ModelListNavigation } from './ModelManagerPanel/ModelListNavigation';
|
||||||
|
|
||||||
export const ModelManager = () => {
|
export const ModelManager = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const handleClickAddModel = useCallback(() => {
|
const handleClickAddModel = useCallback(() => {
|
||||||
@ -29,4 +29,6 @@ export const ModelManager = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ModelManager.displayName = 'ModelManager';
|
||||||
|
@ -21,7 +21,8 @@ import { FetchingModelsLoader } from './FetchingModelsLoader';
|
|||||||
import { ModelListWrapper } from './ModelListWrapper';
|
import { ModelListWrapper } from './ModelListWrapper';
|
||||||
|
|
||||||
const ModelList = () => {
|
const ModelList = () => {
|
||||||
const { searchTerm, filteredModelType } = useAppSelector((s) => s.modelmanagerV2);
|
const filteredModelType = useAppSelector((s) => s.modelmanagerV2.filteredModelType);
|
||||||
|
const searchTerm = useAppSelector((s) => s.modelmanagerV2.searchTerm);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [mainModels, { isLoading: isLoadingMainModels }] = useMainModels();
|
const [mainModels, { isLoading: isLoadingMainModels }] = useMainModels();
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
import { ConfirmationAlertDialog, Flex, IconButton, Spacer, Text, useDisclosure } from '@invoke-ai/ui-library';
|
import { ConfirmationAlertDialog, Flex, IconButton, Spacer, Text, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
import { selectModelManagerV2Slice, setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
||||||
import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
|
import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
|
||||||
import ModelFormatBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge';
|
import ModelFormatBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
@ -23,15 +24,21 @@ const sx: SystemStyleObject = {
|
|||||||
"&[aria-selected='true']": { bg: 'base.700' },
|
"&[aria-selected='true']": { bg: 'base.700' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModelListItem = (props: ModelListItemProps) => {
|
const ModelListItem = ({ model }: ModelListItemProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
const selectIsSelected = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(
|
||||||
|
selectModelManagerV2Slice,
|
||||||
|
(modelManagerV2Slice) => modelManagerV2Slice.selectedModelKey === model.key
|
||||||
|
),
|
||||||
|
[model.key]
|
||||||
|
);
|
||||||
|
const isSelected = useAppSelector(selectIsSelected);
|
||||||
const [deleteModel] = useDeleteModelsMutation();
|
const [deleteModel] = useDeleteModelsMutation();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
const { model } = props;
|
|
||||||
|
|
||||||
const handleSelectModel = useCallback(() => {
|
const handleSelectModel = useCallback(() => {
|
||||||
dispatch(setSelectedModelKey(model.key));
|
dispatch(setSelectedModelKey(model.key));
|
||||||
}, [model.key, dispatch]);
|
}, [model.key, dispatch]);
|
||||||
@ -43,11 +50,6 @@ const ModelListItem = (props: ModelListItemProps) => {
|
|||||||
},
|
},
|
||||||
[onOpen]
|
[onOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSelected = useMemo(() => {
|
|
||||||
return selectedModelKey === model.key;
|
|
||||||
}, [selectedModelKey, model.key]);
|
|
||||||
|
|
||||||
const handleModelDelete = useCallback(() => {
|
const handleModelDelete = useCallback(() => {
|
||||||
deleteModel({ key: model.key })
|
deleteModel({ key: model.key })
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -3,12 +3,12 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { setSearchTerm } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
import { setSearchTerm } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import type { ChangeEventHandler } from 'react';
|
import type { ChangeEventHandler } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { PiXBold } from 'react-icons/pi';
|
import { PiXBold } from 'react-icons/pi';
|
||||||
|
|
||||||
import { ModelTypeFilter } from './ModelTypeFilter';
|
import { ModelTypeFilter } from './ModelTypeFilter';
|
||||||
|
|
||||||
export const ModelListNavigation = () => {
|
export const ModelListNavigation = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const searchTerm = useAppSelector((s) => s.modelmanagerV2.searchTerm);
|
const searchTerm = useAppSelector((s) => s.modelmanagerV2.searchTerm);
|
||||||
|
|
||||||
@ -49,4 +49,6 @@ export const ModelListNavigation = () => {
|
|||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ModelListNavigation.displayName = 'ModelListNavigation';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { StickyScrollable } from 'features/system/components/StickyScrollable';
|
import { StickyScrollable } from 'features/system/components/StickyScrollable';
|
||||||
|
import { memo } from 'react';
|
||||||
import type { AnyModelConfig } from 'services/api/types';
|
import type { AnyModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
import ModelListItem from './ModelListItem';
|
import ModelListItem from './ModelListItem';
|
||||||
@ -8,7 +9,7 @@ type ModelListWrapperProps = {
|
|||||||
modelList: AnyModelConfig[];
|
modelList: AnyModelConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModelListWrapper = (props: ModelListWrapperProps) => {
|
export const ModelListWrapper = memo((props: ModelListWrapperProps) => {
|
||||||
const { title, modelList } = props;
|
const { title, modelList } = props;
|
||||||
return (
|
return (
|
||||||
<StickyScrollable title={title} contentSx={{ gap: 1, p: 2 }}>
|
<StickyScrollable title={title} contentSx={{ gap: 1, p: 2 }}>
|
||||||
@ -17,4 +18,6 @@ export const ModelListWrapper = (props: ModelListWrapperProps) => {
|
|||||||
))}
|
))}
|
||||||
</StickyScrollable>
|
</StickyScrollable>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ModelListWrapper.displayName = 'ModelListWrapper';
|
||||||
|
@ -2,12 +2,12 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-libr
|
|||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import type { FilterableModelType } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
import type { FilterableModelType } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
||||||
import { setFilteredModelType } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
import { setFilteredModelType } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiFunnelBold } from 'react-icons/pi';
|
import { PiFunnelBold } from 'react-icons/pi';
|
||||||
import { objectKeys } from 'tsafe';
|
import { objectKeys } from 'tsafe';
|
||||||
|
|
||||||
export const ModelTypeFilter = () => {
|
export const ModelTypeFilter = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const MODEL_TYPE_LABELS: Record<FilterableModelType, string> = useMemo(
|
const MODEL_TYPE_LABELS: Record<FilterableModelType, string> = useMemo(
|
||||||
@ -57,4 +57,6 @@ export const ModelTypeFilter = () => {
|
|||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ModelTypeFilter.displayName = 'ModelTypeFilter';
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import { Box } from '@invoke-ai/ui-library';
|
import { Box } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { InstallModels } from './InstallModels';
|
import { InstallModels } from './InstallModels';
|
||||||
import { Model } from './ModelPanel/Model';
|
import { Model } from './ModelPanel/Model';
|
||||||
|
|
||||||
export const ModelPane = () => {
|
export const ModelPane = memo(() => {
|
||||||
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
||||||
return (
|
return (
|
||||||
<Box layerStyle="first" p={4} borderRadius="base" w="50%" h="full">
|
<Box layerStyle="first" p={4} borderRadius="base" w="50%" h="full">
|
||||||
{selectedModelKey ? <Model key={selectedModelKey} /> : <InstallModels />}
|
{selectedModelKey ? <Model key={selectedModelKey} /> : <InstallModels />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ModelPane.displayName = 'ModelPane';
|
||||||
|
@ -1,26 +1,28 @@
|
|||||||
import { Button, Flex, Heading, SimpleGrid, Text } from '@invoke-ai/ui-library';
|
import { Button, Flex, Heading, SimpleGrid } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { useControlNetOrT2IAdapterDefaultSettings } from 'features/modelManagerV2/hooks/useControlNetOrT2IAdapterDefaultSettings';
|
import { useControlNetOrT2IAdapterDefaultSettings } from 'features/modelManagerV2/hooks/useControlNetOrT2IAdapterDefaultSettings';
|
||||||
import { DefaultPreprocessor } from 'features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/DefaultPreprocessor';
|
import { DefaultPreprocessor } from 'features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/DefaultPreprocessor';
|
||||||
import type { FormField } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings';
|
import type { FormField } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCheckBold } from 'react-icons/pi';
|
import { PiCheckBold } from 'react-icons/pi';
|
||||||
import { useUpdateModelMutation } from 'services/api/endpoints/models';
|
import { useUpdateModelMutation } from 'services/api/endpoints/models';
|
||||||
|
import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
export type ControlNetOrT2IAdapterDefaultSettingsFormData = {
|
export type ControlNetOrT2IAdapterDefaultSettingsFormData = {
|
||||||
preprocessor: FormField<string>;
|
preprocessor: FormField<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ControlNetOrT2IAdapterDefaultSettings = () => {
|
type Props = {
|
||||||
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ControlNetOrT2IAdapterDefaultSettings = memo(({ modelConfig }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { defaultSettingsDefaults, isLoading: isLoadingDefaultSettings } =
|
const defaultSettingsDefaults = useControlNetOrT2IAdapterDefaultSettings(modelConfig);
|
||||||
useControlNetOrT2IAdapterDefaultSettings(selectedModelKey);
|
|
||||||
|
|
||||||
const [updateModel, { isLoading: isLoadingUpdateModel }] = useUpdateModelMutation();
|
const [updateModel, { isLoading: isLoadingUpdateModel }] = useUpdateModelMutation();
|
||||||
|
|
||||||
@ -30,16 +32,12 @@ export const ControlNetOrT2IAdapterDefaultSettings = () => {
|
|||||||
|
|
||||||
const onSubmit = useCallback<SubmitHandler<ControlNetOrT2IAdapterDefaultSettingsFormData>>(
|
const onSubmit = useCallback<SubmitHandler<ControlNetOrT2IAdapterDefaultSettingsFormData>>(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (!selectedModelKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
preprocessor: data.preprocessor.isEnabled ? data.preprocessor.value : null,
|
preprocessor: data.preprocessor.isEnabled ? data.preprocessor.value : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
updateModel({
|
updateModel({
|
||||||
key: selectedModelKey,
|
key: modelConfig.key,
|
||||||
body: { default_settings: body },
|
body: { default_settings: body },
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -61,13 +59,9 @@ export const ControlNetOrT2IAdapterDefaultSettings = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[selectedModelKey, reset, updateModel, t]
|
[updateModel, modelConfig.key, t, reset]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoadingDefaultSettings) {
|
|
||||||
return <Text>{t('common.loading')}</Text>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex gap="4" justifyContent="space-between" w="full" pb={4}>
|
<Flex gap="4" justifyContent="space-between" w="full" pb={4}>
|
||||||
@ -89,4 +83,6 @@ export const ControlNetOrT2IAdapterDefaultSettings = () => {
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ControlNetOrT2IAdapterDefaultSettings.displayName = 'ControlNetOrT2IAdapterDefaultSettings';
|
||||||
|
@ -4,7 +4,7 @@ import { InformationalPopover } from 'common/components/InformationalPopover/Inf
|
|||||||
import type { ControlNetOrT2IAdapterDefaultSettingsFormData } from 'features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/ControlNetOrT2IAdapterDefaultSettings';
|
import type { ControlNetOrT2IAdapterDefaultSettingsFormData } from 'features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/ControlNetOrT2IAdapterDefaultSettings';
|
||||||
import type { FormField } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings';
|
import type { FormField } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings';
|
||||||
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import type { UseControllerProps } from 'react-hook-form';
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
import { useController } from 'react-hook-form';
|
import { useController } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -28,7 +28,7 @@ const OPTIONS = [
|
|||||||
|
|
||||||
type DefaultSchedulerType = ControlNetOrT2IAdapterDefaultSettingsFormData['preprocessor'];
|
type DefaultSchedulerType = ControlNetOrT2IAdapterDefaultSettingsFormData['preprocessor'];
|
||||||
|
|
||||||
export function DefaultPreprocessor(props: UseControllerProps<ControlNetOrT2IAdapterDefaultSettingsFormData>) {
|
export const DefaultPreprocessor = memo((props: UseControllerProps<ControlNetOrT2IAdapterDefaultSettingsFormData>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { field } = useController(props);
|
const { field } = useController(props);
|
||||||
|
|
||||||
@ -63,4 +63,6 @@ export function DefaultPreprocessor(props: UseControllerProps<ControlNetOrT2IAda
|
|||||||
<Combobox isDisabled={isDisabled} value={value} options={OPTIONS} onChange={onChange} />
|
<Combobox isDisabled={isDisabled} value={value} options={OPTIONS} onChange={onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
DefaultPreprocessor.displayName = 'DefaultPreprocessor';
|
||||||
|
@ -2,7 +2,7 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } f
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import type { UseControllerProps } from 'react-hook-form';
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
import { useController } from 'react-hook-form';
|
import { useController } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -11,7 +11,7 @@ import type { MainModelDefaultSettingsFormData } from './MainModelDefaultSetting
|
|||||||
|
|
||||||
type DefaultCfgRescaleMultiplierType = MainModelDefaultSettingsFormData['cfgRescaleMultiplier'];
|
type DefaultCfgRescaleMultiplierType = MainModelDefaultSettingsFormData['cfgRescaleMultiplier'];
|
||||||
|
|
||||||
export function DefaultCfgRescaleMultiplier(props: UseControllerProps<MainModelDefaultSettingsFormData>) {
|
export const DefaultCfgRescaleMultiplier = memo((props: UseControllerProps<MainModelDefaultSettingsFormData>) => {
|
||||||
const { field } = useController(props);
|
const { field } = useController(props);
|
||||||
|
|
||||||
const sliderMin = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.sliderMin);
|
const sliderMin = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.sliderMin);
|
||||||
@ -74,4 +74,6 @@ export function DefaultCfgRescaleMultiplier(props: UseControllerProps<MainModelD
|
|||||||
</Flex>
|
</Flex>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
DefaultCfgRescaleMultiplier.displayName = 'DefaultCfgRescaleMultiplier';
|
||||||
|
@ -2,7 +2,7 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } f
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import type { UseControllerProps } from 'react-hook-form';
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
import { useController } from 'react-hook-form';
|
import { useController } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -11,7 +11,7 @@ import type { MainModelDefaultSettingsFormData } from './MainModelDefaultSetting
|
|||||||
|
|
||||||
type DefaultCfgType = MainModelDefaultSettingsFormData['cfgScale'];
|
type DefaultCfgType = MainModelDefaultSettingsFormData['cfgScale'];
|
||||||
|
|
||||||
export function DefaultCfgScale(props: UseControllerProps<MainModelDefaultSettingsFormData>) {
|
export const DefaultCfgScale = memo((props: UseControllerProps<MainModelDefaultSettingsFormData>) => {
|
||||||
const { field } = useController(props);
|
const { field } = useController(props);
|
||||||
|
|
||||||
const sliderMin = useAppSelector((s) => s.config.sd.guidance.sliderMin);
|
const sliderMin = useAppSelector((s) => s.config.sd.guidance.sliderMin);
|
||||||
@ -74,4 +74,6 @@ export function DefaultCfgScale(props: UseControllerProps<MainModelDefaultSettin
|
|||||||
</Flex>
|
</Flex>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
DefaultCfgScale.displayName = 'DefaultCfgScale';
|
||||||
|
@ -2,7 +2,7 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } f
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import type { UseControllerProps } from 'react-hook-form';
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
import { useController } from 'react-hook-form';
|
import { useController } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -16,7 +16,7 @@ type Props = {
|
|||||||
optimalDimension: number;
|
optimalDimension: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DefaultHeight({ control, optimalDimension }: Props) {
|
export const DefaultHeight = memo(({ control, optimalDimension }: Props) => {
|
||||||
const { field } = useController({ control, name: 'height' });
|
const { field } = useController({ control, name: 'height' });
|
||||||
const sliderMin = useAppSelector((s) => s.config.sd.height.sliderMin);
|
const sliderMin = useAppSelector((s) => s.config.sd.height.sliderMin);
|
||||||
const sliderMax = useAppSelector((s) => s.config.sd.height.sliderMax);
|
const sliderMax = useAppSelector((s) => s.config.sd.height.sliderMax);
|
||||||
@ -78,4 +78,6 @@ export function DefaultHeight({ control, optimalDimension }: Props) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
DefaultHeight.displayName = 'DefaultHeight';
|
||||||
|
@ -4,7 +4,7 @@ import { InformationalPopover } from 'common/components/InformationalPopover/Inf
|
|||||||
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
||||||
import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants';
|
import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants';
|
||||||
import { isParameterScheduler } from 'features/parameters/types/parameterSchemas';
|
import { isParameterScheduler } from 'features/parameters/types/parameterSchemas';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import type { UseControllerProps } from 'react-hook-form';
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
import { useController } from 'react-hook-form';
|
import { useController } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -13,7 +13,7 @@ import type { MainModelDefaultSettingsFormData } from './MainModelDefaultSetting
|
|||||||
|
|
||||||
type DefaultSchedulerType = MainModelDefaultSettingsFormData['scheduler'];
|
type DefaultSchedulerType = MainModelDefaultSettingsFormData['scheduler'];
|
||||||
|
|
||||||
export function DefaultScheduler(props: UseControllerProps<MainModelDefaultSettingsFormData>) {
|
export const DefaultScheduler = memo((props: UseControllerProps<MainModelDefaultSettingsFormData>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { field } = useController(props);
|
const { field } = useController(props);
|
||||||
|
|
||||||
@ -51,4 +51,6 @@ export function DefaultScheduler(props: UseControllerProps<MainModelDefaultSetti
|
|||||||
<Combobox isDisabled={isDisabled} value={value} options={SCHEDULER_OPTIONS} onChange={onChange} />
|
<Combobox isDisabled={isDisabled} value={value} options={SCHEDULER_OPTIONS} onChange={onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
DefaultScheduler.displayName = 'DefaultScheduler';
|
||||||
|
@ -2,7 +2,7 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } f
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import type { UseControllerProps } from 'react-hook-form';
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
import { useController } from 'react-hook-form';
|
import { useController } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -11,7 +11,7 @@ import type { MainModelDefaultSettingsFormData } from './MainModelDefaultSetting
|
|||||||
|
|
||||||
type DefaultSteps = MainModelDefaultSettingsFormData['steps'];
|
type DefaultSteps = MainModelDefaultSettingsFormData['steps'];
|
||||||
|
|
||||||
export function DefaultSteps(props: UseControllerProps<MainModelDefaultSettingsFormData>) {
|
export const DefaultSteps = memo((props: UseControllerProps<MainModelDefaultSettingsFormData>) => {
|
||||||
const { field } = useController(props);
|
const { field } = useController(props);
|
||||||
|
|
||||||
const sliderMin = useAppSelector((s) => s.config.sd.steps.sliderMin);
|
const sliderMin = useAppSelector((s) => s.config.sd.steps.sliderMin);
|
||||||
@ -74,4 +74,6 @@ export function DefaultSteps(props: UseControllerProps<MainModelDefaultSettingsF
|
|||||||
</Flex>
|
</Flex>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
DefaultSteps.displayName = 'DefaultSteps';
|
||||||
|
@ -4,7 +4,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import type { UseControllerProps } from 'react-hook-form';
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
import { useController } from 'react-hook-form';
|
import { useController } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -15,7 +15,7 @@ import type { MainModelDefaultSettingsFormData } from './MainModelDefaultSetting
|
|||||||
|
|
||||||
type DefaultVaeType = MainModelDefaultSettingsFormData['vae'];
|
type DefaultVaeType = MainModelDefaultSettingsFormData['vae'];
|
||||||
|
|
||||||
export function DefaultVae(props: UseControllerProps<MainModelDefaultSettingsFormData>) {
|
export const DefaultVae = memo((props: UseControllerProps<MainModelDefaultSettingsFormData>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { field } = useController(props);
|
const { field } = useController(props);
|
||||||
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
||||||
@ -64,4 +64,6 @@ export function DefaultVae(props: UseControllerProps<MainModelDefaultSettingsFor
|
|||||||
<Combobox isDisabled={isDisabled} value={value} options={compatibleOptions} onChange={onChange} />
|
<Combobox isDisabled={isDisabled} value={value} options={compatibleOptions} onChange={onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
DefaultVae.displayName = 'DefaultVae';
|
||||||
|
@ -3,7 +3,7 @@ import { Combobox, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
|||||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
||||||
import { isParameterPrecision } from 'features/parameters/types/parameterSchemas';
|
import { isParameterPrecision } from 'features/parameters/types/parameterSchemas';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import type { UseControllerProps } from 'react-hook-form';
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
import { useController } from 'react-hook-form';
|
import { useController } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -17,7 +17,7 @@ const options = [
|
|||||||
|
|
||||||
type DefaultVaePrecisionType = MainModelDefaultSettingsFormData['vaePrecision'];
|
type DefaultVaePrecisionType = MainModelDefaultSettingsFormData['vaePrecision'];
|
||||||
|
|
||||||
export function DefaultVaePrecision(props: UseControllerProps<MainModelDefaultSettingsFormData>) {
|
export const DefaultVaePrecision = memo((props: UseControllerProps<MainModelDefaultSettingsFormData>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { field } = useController(props);
|
const { field } = useController(props);
|
||||||
|
|
||||||
@ -52,4 +52,6 @@ export function DefaultVaePrecision(props: UseControllerProps<MainModelDefaultSe
|
|||||||
<Combobox isDisabled={isDisabled} value={value} options={options} onChange={onChange} />
|
<Combobox isDisabled={isDisabled} value={value} options={options} onChange={onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
DefaultVaePrecision.displayName = 'DefaultVaePrecision';
|
||||||
|
@ -2,7 +2,7 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } f
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import type { UseControllerProps } from 'react-hook-form';
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
import { useController } from 'react-hook-form';
|
import { useController } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -16,7 +16,7 @@ type Props = {
|
|||||||
optimalDimension: number;
|
optimalDimension: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DefaultWidth({ control, optimalDimension }: Props) {
|
export const DefaultWidth = memo(({ control, optimalDimension }: Props) => {
|
||||||
const { field } = useController({ control, name: 'width' });
|
const { field } = useController({ control, name: 'width' });
|
||||||
const sliderMin = useAppSelector((s) => s.config.sd.width.sliderMin);
|
const sliderMin = useAppSelector((s) => s.config.sd.width.sliderMin);
|
||||||
const sliderMax = useAppSelector((s) => s.config.sd.width.sliderMax);
|
const sliderMax = useAppSelector((s) => s.config.sd.width.sliderMax);
|
||||||
@ -78,4 +78,6 @@ export function DefaultWidth({ control, optimalDimension }: Props) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
DefaultWidth.displayName = 'DefaultWidth';
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import { Button, Flex, Heading, SimpleGrid, Text } from '@invoke-ai/ui-library';
|
import { Button, Flex, Heading, SimpleGrid } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useMainModelDefaultSettings } from 'features/modelManagerV2/hooks/useMainModelDefaultSettings';
|
import { useMainModelDefaultSettings } from 'features/modelManagerV2/hooks/useMainModelDefaultSettings';
|
||||||
import { DefaultHeight } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultHeight';
|
import { DefaultHeight } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultHeight';
|
||||||
import { DefaultWidth } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultWidth';
|
import { DefaultWidth } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultWidth';
|
||||||
import type { ParameterScheduler } from 'features/parameters/types/parameterSchemas';
|
import type { ParameterScheduler } from 'features/parameters/types/parameterSchemas';
|
||||||
|
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { useCallback } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCheckBold } from 'react-icons/pi';
|
import { PiCheckBold } from 'react-icons/pi';
|
||||||
import { useUpdateModelMutation } from 'services/api/endpoints/models';
|
import { useUpdateModelMutation } from 'services/api/endpoints/models';
|
||||||
|
import type { MainModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
import { DefaultCfgRescaleMultiplier } from './DefaultCfgRescaleMultiplier';
|
import { DefaultCfgRescaleMultiplier } from './DefaultCfgRescaleMultiplier';
|
||||||
import { DefaultCfgScale } from './DefaultCfgScale';
|
import { DefaultCfgScale } from './DefaultCfgScale';
|
||||||
@ -35,16 +37,16 @@ export type MainModelDefaultSettingsFormData = {
|
|||||||
height: FormField<number>;
|
height: FormField<number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MainModelDefaultSettings = () => {
|
type Props = {
|
||||||
|
modelConfig: MainModelConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MainModelDefaultSettings = memo(({ modelConfig }: Props) => {
|
||||||
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const {
|
const defaultSettingsDefaults = useMainModelDefaultSettings(modelConfig);
|
||||||
defaultSettingsDefaults,
|
const optimalDimension = useMemo(() => getOptimalDimension(modelConfig), [modelConfig]);
|
||||||
isLoading: isLoadingDefaultSettings,
|
|
||||||
optimalDimension,
|
|
||||||
} = useMainModelDefaultSettings(selectedModelKey);
|
|
||||||
|
|
||||||
const [updateModel, { isLoading: isLoadingUpdateModel }] = useUpdateModelMutation();
|
const [updateModel, { isLoading: isLoadingUpdateModel }] = useUpdateModelMutation();
|
||||||
|
|
||||||
const { handleSubmit, control, formState, reset } = useForm<MainModelDefaultSettingsFormData>({
|
const { handleSubmit, control, formState, reset } = useForm<MainModelDefaultSettingsFormData>({
|
||||||
@ -94,10 +96,6 @@ export const MainModelDefaultSettings = () => {
|
|||||||
[selectedModelKey, reset, updateModel, t]
|
[selectedModelKey, reset, updateModel, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoadingDefaultSettings) {
|
|
||||||
return <Text>{t('common.loading')}</Text>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex gap="4" justifyContent="space-between" w="full" pb={4}>
|
<Flex gap="4" justifyContent="space-between" w="full" pb={4}>
|
||||||
@ -126,4 +124,6 @@ export const MainModelDefaultSettings = () => {
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
MainModelDefaultSettings.displayName = 'MainModelDefaultSettings';
|
||||||
|
@ -1,120 +1,47 @@
|
|||||||
import { Button, Flex, Heading, Spacer, Text } from '@invoke-ai/ui-library';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { memo, useMemo } from 'react';
|
||||||
import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
|
||||||
import { ModelConvertButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton';
|
|
||||||
import { ModelEditButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelEditButton';
|
|
||||||
import { toast } from 'features/toast/toast';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCheckBold, PiXBold } from 'react-icons/pi';
|
import { PiExclamationMarkBold } from 'react-icons/pi';
|
||||||
import type { UpdateModelArg } from 'services/api/endpoints/models';
|
import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/api/endpoints/models';
|
||||||
import { useGetModelConfigQuery, useUpdateModelMutation } from 'services/api/endpoints/models';
|
|
||||||
|
|
||||||
import ModelImageUpload from './Fields/ModelImageUpload';
|
|
||||||
import { ModelEdit } from './ModelEdit';
|
import { ModelEdit } from './ModelEdit';
|
||||||
import { ModelView } from './ModelView';
|
import { ModelView } from './ModelView';
|
||||||
|
|
||||||
export const Model = () => {
|
export const Model = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const selectedModelMode = useAppSelector((s) => s.modelmanagerV2.selectedModelMode);
|
const selectedModelMode = useAppSelector((s) => s.modelmanagerV2.selectedModelMode);
|
||||||
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
||||||
const { data, isLoading } = useGetModelConfigQuery(selectedModelKey ?? skipToken);
|
const { data: modelConfigs, isLoading } = useGetModelConfigsQuery();
|
||||||
const [updateModel, { isLoading: isSubmitting }] = useUpdateModelMutation();
|
const modelConfig = useMemo(() => {
|
||||||
const dispatch = useAppDispatch();
|
if (!modelConfigs) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (selectedModelKey === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const modelConfig = modelConfigsAdapterSelectors.selectById(modelConfigs, selectedModelKey);
|
||||||
|
|
||||||
const form = useForm<UpdateModelArg['body']>({
|
if (!modelConfig) {
|
||||||
defaultValues: data,
|
return null;
|
||||||
mode: 'onChange',
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = useCallback<SubmitHandler<UpdateModelArg['body']>>(
|
|
||||||
(values) => {
|
|
||||||
if (!data?.key) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseBody: UpdateModelArg = {
|
return modelConfig;
|
||||||
key: data.key,
|
}, [modelConfigs, selectedModelKey]);
|
||||||
body: values,
|
|
||||||
};
|
|
||||||
|
|
||||||
updateModel(responseBody)
|
|
||||||
.unwrap()
|
|
||||||
.then((payload) => {
|
|
||||||
form.reset(payload, { keepDefaultValues: true });
|
|
||||||
dispatch(setSelectedModelMode('view'));
|
|
||||||
toast({
|
|
||||||
id: 'MODEL_UPDATED',
|
|
||||||
title: t('modelManager.modelUpdated'),
|
|
||||||
status: 'success',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((_) => {
|
|
||||||
form.reset();
|
|
||||||
toast({
|
|
||||||
id: 'MODEL_UPDATE_FAILED',
|
|
||||||
title: t('modelManager.modelUpdateFailed'),
|
|
||||||
status: 'error',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[dispatch, data?.key, form, t, updateModel]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClickCancel = useCallback(() => {
|
|
||||||
dispatch(setSelectedModelMode('view'));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Text>{t('common.loading')}</Text>;
|
return <IAINoContentFallbackWithSpinner label={t('common.loading')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!modelConfig) {
|
||||||
return <Text>{t('common.somethingWentWrong')}</Text>;
|
return <IAINoContentFallback label={t('common.somethingWentWrong')} icon={PiExclamationMarkBold} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (selectedModelMode === 'view') {
|
||||||
<Flex flexDir="column" gap={4}>
|
return <ModelView modelConfig={modelConfig} />;
|
||||||
<Flex alignItems="flex-start" gap={4}>
|
}
|
||||||
<ModelImageUpload model_key={selectedModelKey} model_image={data.cover_image} />
|
|
||||||
<Flex flexDir="column" gap={1} flexGrow={1} minW={0}>
|
return <ModelEdit modelConfig={modelConfig} />;
|
||||||
<Flex gap={2}>
|
});
|
||||||
<Heading as="h2" fontSize="lg" noOfLines={1} wordBreak="break-all">
|
|
||||||
{data.name}
|
Model.displayName = 'Model';
|
||||||
</Heading>
|
|
||||||
<Spacer />
|
|
||||||
{selectedModelMode === 'view' && <ModelConvertButton modelKey={selectedModelKey} />}
|
|
||||||
{selectedModelMode === 'view' && <ModelEditButton />}
|
|
||||||
{selectedModelMode === 'edit' && (
|
|
||||||
<Button size="sm" onClick={handleClickCancel} leftIcon={<PiXBold />}>
|
|
||||||
{t('common.cancel')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{selectedModelMode === 'edit' && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorScheme="invokeYellow"
|
|
||||||
leftIcon={<PiCheckBold />}
|
|
||||||
onClick={form.handleSubmit(onSubmit)}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={Boolean(Object.keys(form.formState.errors).length)}
|
|
||||||
>
|
|
||||||
{t('common.save')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
{data.source && (
|
|
||||||
<Text variant="subtext" noOfLines={1} wordBreak="break-all">
|
|
||||||
{t('modelManager.source')}: {data?.source}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
<Text noOfLines={3}>{data.description}</Text>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
{selectedModelMode === 'view' ? <ModelView /> : <ModelEdit form={form} onSubmit={onSubmit} />}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { FormControl, FormLabel, Text } from '@invoke-ai/ui-library';
|
import { FormControl, FormLabel, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | null | undefined;
|
value: string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelAttrView = ({ label, value }: Props) => {
|
export const ModelAttrView = memo(({ label, value }: Props) => {
|
||||||
return (
|
return (
|
||||||
<FormControl flexDir="column" alignItems="flex-start" gap={0}>
|
<FormControl flexDir="column" alignItems="flex-start" gap={0}>
|
||||||
<FormLabel>{label}</FormLabel>
|
<FormLabel>{label}</FormLabel>
|
||||||
@ -14,4 +15,6 @@ export const ModelAttrView = ({ label, value }: Props) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ModelAttrView.displayName = 'ModelAttrView';
|
||||||
|
@ -8,52 +8,46 @@ import {
|
|||||||
UnorderedList,
|
UnorderedList,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
|
||||||
import { toast } from 'features/toast/toast';
|
import { toast } from 'features/toast/toast';
|
||||||
import { useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useConvertModelMutation, useGetModelConfigQuery } from 'services/api/endpoints/models';
|
import { useConvertModelMutation } from 'services/api/endpoints/models';
|
||||||
|
import type { CheckpointModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
interface ModelConvertProps {
|
interface ModelConvertProps {
|
||||||
modelKey: string | null;
|
modelConfig: CheckpointModelConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelConvertButton = (props: ModelConvertProps) => {
|
export const ModelConvertButton = memo(({ modelConfig }: ModelConvertProps) => {
|
||||||
const { modelKey } = props;
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data } = useGetModelConfigQuery(modelKey ?? skipToken);
|
|
||||||
const [convertModel, { isLoading }] = useConvertModelMutation();
|
const [convertModel, { isLoading }] = useConvertModelMutation();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
const modelConvertHandler = useCallback(() => {
|
const modelConvertHandler = useCallback(() => {
|
||||||
if (!data || isLoading) {
|
if (!modelConfig || isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toastId = `CONVERTING_MODEL_${data.key}`;
|
const toastId = `CONVERTING_MODEL_${modelConfig.key}`;
|
||||||
toast({
|
toast({
|
||||||
id: toastId,
|
id: toastId,
|
||||||
title: `${t('modelManager.convertingModelBegin')}: ${data?.name}`,
|
title: `${t('modelManager.convertingModelBegin')}: ${modelConfig.name}`,
|
||||||
status: 'info',
|
status: 'info',
|
||||||
});
|
});
|
||||||
|
|
||||||
convertModel(data?.key)
|
convertModel(modelConfig.key)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast({ id: toastId, title: `${t('modelManager.modelConverted')}: ${data?.name}`, status: 'success' });
|
toast({ id: toastId, title: `${t('modelManager.modelConverted')}: ${modelConfig.name}`, status: 'success' });
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast({
|
toast({
|
||||||
id: toastId,
|
id: toastId,
|
||||||
title: `${t('modelManager.modelConversionFailed')}: ${data?.name}`,
|
title: `${t('modelManager.modelConversionFailed')}: ${modelConfig.name}`,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [data, isLoading, t, convertModel]);
|
}, [modelConfig, isLoading, t, convertModel]);
|
||||||
|
|
||||||
if (data?.format !== 'checkpoint') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -68,7 +62,7 @@ export const ModelConvertButton = (props: ModelConvertProps) => {
|
|||||||
🧨 {t('modelManager.convert')}
|
🧨 {t('modelManager.convert')}
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmationAlertDialog
|
<ConfirmationAlertDialog
|
||||||
title={`${t('modelManager.convert')} ${data?.name}`}
|
title={`${t('modelManager.convert')} ${modelConfig.name}`}
|
||||||
acceptCallback={modelConvertHandler}
|
acceptCallback={modelConvertHandler}
|
||||||
acceptButtonText={`${t('modelManager.convert')}`}
|
acceptButtonText={`${t('modelManager.convert')}`}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@ -96,4 +90,6 @@ export const ModelConvertButton = (props: ModelConvertProps) => {
|
|||||||
</ConfirmationAlertDialog>
|
</ConfirmationAlertDialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ModelConvertButton.displayName = 'ModelConvertButton';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Flex,
|
Flex,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -7,47 +8,102 @@ import {
|
|||||||
Heading,
|
Heading,
|
||||||
Input,
|
Input,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Text,
|
|
||||||
Textarea,
|
Textarea,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
||||||
import type { SubmitHandler, UseFormReturn } from 'react-hook-form';
|
import { ModelHeader } from 'features/modelManagerV2/subpanels/ModelPanel/ModelHeader';
|
||||||
|
import { toast } from 'features/toast/toast';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { type SubmitHandler, useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { UpdateModelArg } from 'services/api/endpoints/models';
|
import { PiCheckBold, PiXBold } from 'react-icons/pi';
|
||||||
import { useGetModelConfigQuery } from 'services/api/endpoints/models';
|
import { type UpdateModelArg, useUpdateModelMutation } from 'services/api/endpoints/models';
|
||||||
|
import type { AnyModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
import BaseModelSelect from './Fields/BaseModelSelect';
|
import BaseModelSelect from './Fields/BaseModelSelect';
|
||||||
import ModelVariantSelect from './Fields/ModelVariantSelect';
|
import ModelVariantSelect from './Fields/ModelVariantSelect';
|
||||||
import PredictionTypeSelect from './Fields/PredictionTypeSelect';
|
import PredictionTypeSelect from './Fields/PredictionTypeSelect';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
form: UseFormReturn<UpdateModelArg['body']>;
|
modelConfig: AnyModelConfig;
|
||||||
onSubmit: SubmitHandler<UpdateModelArg['body']>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const stringFieldOptions = {
|
const stringFieldOptions = {
|
||||||
validate: (value?: string | null) => (value && value.trim().length > 3) || 'Must be at least 3 characters',
|
validate: (value?: string | null) => (value && value.trim().length > 3) || 'Must be at least 3 characters',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModelEdit = ({ form }: Props) => {
|
export const ModelEdit = memo(({ modelConfig }: Props) => {
|
||||||
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
|
||||||
const { data, isLoading } = useGetModelConfigQuery(selectedModelKey ?? skipToken);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [updateModel, { isLoading: isSubmitting }] = useUpdateModelMutation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
if (isLoading) {
|
const form = useForm<UpdateModelArg['body']>({
|
||||||
return <Text>{t('common.loading')}</Text>;
|
defaultValues: modelConfig,
|
||||||
}
|
mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
if (!data) {
|
const onSubmit = useCallback<SubmitHandler<UpdateModelArg['body']>>(
|
||||||
return <Text>{t('common.somethingWentWrong')}</Text>;
|
(values) => {
|
||||||
}
|
const responseBody: UpdateModelArg = {
|
||||||
|
key: modelConfig.key,
|
||||||
|
body: values,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateModel(responseBody)
|
||||||
|
.unwrap()
|
||||||
|
.then((payload) => {
|
||||||
|
form.reset(payload, { keepDefaultValues: true });
|
||||||
|
dispatch(setSelectedModelMode('view'));
|
||||||
|
toast({
|
||||||
|
id: 'MODEL_UPDATED',
|
||||||
|
title: t('modelManager.modelUpdated'),
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((_) => {
|
||||||
|
form.reset();
|
||||||
|
toast({
|
||||||
|
id: 'MODEL_UPDATE_FAILED',
|
||||||
|
title: t('modelManager.modelUpdateFailed'),
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch, modelConfig.key, form, t, updateModel]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickCancel = useCallback(() => {
|
||||||
|
dispatch(setSelectedModelMode('view'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Flex flexDir="column" gap={4}>
|
||||||
|
<ModelHeader modelConfig={modelConfig}>
|
||||||
|
<Button flexShrink={0} size="sm" onClick={handleClickCancel} leftIcon={<PiXBold />}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
flexShrink={0}
|
||||||
|
size="sm"
|
||||||
|
colorScheme="invokeYellow"
|
||||||
|
leftIcon={<PiCheckBold />}
|
||||||
|
onClick={form.handleSubmit(onSubmit)}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={Boolean(Object.keys(form.formState.errors).length)}
|
||||||
|
>
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</ModelHeader>
|
||||||
<Flex flexDir="column" h="full">
|
<Flex flexDir="column" h="full">
|
||||||
<form>
|
<form>
|
||||||
<Flex w="full" justifyContent="space-between" gap={4} alignItems="center">
|
<Flex w="full" justifyContent="space-between" gap={4} alignItems="center">
|
||||||
<FormControl flexDir="column" alignItems="flex-start" gap={1} isInvalid={Boolean(form.formState.errors.name)}>
|
<FormControl
|
||||||
|
flexDir="column"
|
||||||
|
alignItems="flex-start"
|
||||||
|
gap={1}
|
||||||
|
isInvalid={Boolean(form.formState.errors.name)}
|
||||||
|
>
|
||||||
<FormLabel>{t('modelManager.modelName')}</FormLabel>
|
<FormLabel>{t('modelManager.modelName')}</FormLabel>
|
||||||
<Input {...form.register('name', stringFieldOptions)} size="md" />
|
<Input {...form.register('name', stringFieldOptions)} size="md" />
|
||||||
|
|
||||||
@ -72,13 +128,13 @@ export const ModelEdit = ({ form }: Props) => {
|
|||||||
<FormLabel>{t('modelManager.baseModel')}</FormLabel>
|
<FormLabel>{t('modelManager.baseModel')}</FormLabel>
|
||||||
<BaseModelSelect control={form.control} />
|
<BaseModelSelect control={form.control} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{data.type === 'main' && (
|
{modelConfig.type === 'main' && (
|
||||||
<FormControl flexDir="column" alignItems="flex-start" gap={1}>
|
<FormControl flexDir="column" alignItems="flex-start" gap={1}>
|
||||||
<FormLabel>{t('modelManager.variant')}</FormLabel>
|
<FormLabel>{t('modelManager.variant')}</FormLabel>
|
||||||
<ModelVariantSelect control={form.control} />
|
<ModelVariantSelect control={form.control} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
{data.type === 'main' && data.format === 'checkpoint' && (
|
{modelConfig.type === 'main' && modelConfig.format === 'checkpoint' && (
|
||||||
<>
|
<>
|
||||||
<FormControl flexDir="column" alignItems="flex-start" gap={1}>
|
<FormControl flexDir="column" alignItems="flex-start" gap={1}>
|
||||||
<FormLabel>{t('modelManager.pathToConfig')}</FormLabel>
|
<FormLabel>{t('modelManager.pathToConfig')}</FormLabel>
|
||||||
@ -98,5 +154,8 @@ export const ModelEdit = ({ form }: Props) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</form>
|
</form>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ModelEdit.displayName = 'ModelEdit';
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Button } from '@invoke-ai/ui-library';
|
import { Button } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
||||||
import { useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { IoPencil } from 'react-icons/io5';
|
import { IoPencil } from 'react-icons/io5';
|
||||||
|
|
||||||
export const ModelEditButton = () => {
|
export const ModelEditButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -18,4 +18,6 @@ export const ModelEditButton = () => {
|
|||||||
{t('modelManager.edit')}
|
{t('modelManager.edit')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ModelEditButton.displayName = 'ModelEditButton';
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Flex, Heading, Spacer, Text } from '@invoke-ai/ui-library';
|
||||||
|
import ModelImageUpload from 'features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { AnyModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
|
type Props = PropsWithChildren<{
|
||||||
|
modelConfig: AnyModelConfig;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const ModelHeader = memo(({ modelConfig, children }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Flex alignItems="flex-start" gap={4}>
|
||||||
|
<ModelImageUpload model_key={modelConfig.key} model_image={modelConfig.cover_image} />
|
||||||
|
<Flex flexDir="column" gap={1} flexGrow={1} minW={0}>
|
||||||
|
<Flex gap={2}>
|
||||||
|
<Heading as="h2" fontSize="lg" noOfLines={1} wordBreak="break-all">
|
||||||
|
{modelConfig.name}
|
||||||
|
</Heading>
|
||||||
|
<Spacer />
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
{modelConfig.source && (
|
||||||
|
<Text variant="subtext" noOfLines={1} wordBreak="break-all">
|
||||||
|
{t('modelManager.source')}: {modelConfig.source}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text noOfLines={3}>{modelConfig.description}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ModelHeader.displayName = 'ModelHeader';
|
@ -1,55 +1,67 @@
|
|||||||
import { Box, Flex, SimpleGrid, Text } from '@invoke-ai/ui-library';
|
import { Box, Flex, SimpleGrid } from '@invoke-ai/ui-library';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import { ControlNetOrT2IAdapterDefaultSettings } from 'features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/ControlNetOrT2IAdapterDefaultSettings';
|
import { ControlNetOrT2IAdapterDefaultSettings } from 'features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/ControlNetOrT2IAdapterDefaultSettings';
|
||||||
|
import { ModelConvertButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton';
|
||||||
|
import { ModelEditButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelEditButton';
|
||||||
|
import { ModelHeader } from 'features/modelManagerV2/subpanels/ModelPanel/ModelHeader';
|
||||||
import { TriggerPhrases } from 'features/modelManagerV2/subpanels/ModelPanel/TriggerPhrases';
|
import { TriggerPhrases } from 'features/modelManagerV2/subpanels/ModelPanel/TriggerPhrases';
|
||||||
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useGetModelConfigQuery } from 'services/api/endpoints/models';
|
import type { AnyModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
import { MainModelDefaultSettings } from './MainModelDefaultSettings/MainModelDefaultSettings';
|
import { MainModelDefaultSettings } from './MainModelDefaultSettings/MainModelDefaultSettings';
|
||||||
import { ModelAttrView } from './ModelAttrView';
|
import { ModelAttrView } from './ModelAttrView';
|
||||||
|
|
||||||
export const ModelView = () => {
|
type Props = {
|
||||||
|
modelConfig: AnyModelConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModelView = memo(({ modelConfig }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
|
||||||
const { data, isLoading } = useGetModelConfigQuery(selectedModelKey ?? skipToken);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Text>{t('common.loading')}</Text>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return <Text>{t('common.somethingWentWrong')}</Text>;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
|
<Flex flexDir="column" gap={4}>
|
||||||
|
<ModelHeader modelConfig={modelConfig}>
|
||||||
|
{modelConfig.format === 'checkpoint' && modelConfig.type === 'main' && (
|
||||||
|
<ModelConvertButton modelConfig={modelConfig} />
|
||||||
|
)}
|
||||||
|
<ModelEditButton />
|
||||||
|
</ModelHeader>
|
||||||
<Flex flexDir="column" h="full" gap={4}>
|
<Flex flexDir="column" h="full" gap={4}>
|
||||||
<Box layerStyle="second" borderRadius="base" p={4}>
|
<Box layerStyle="second" borderRadius="base" p={4}>
|
||||||
<SimpleGrid columns={2} gap={4}>
|
<SimpleGrid columns={2} gap={4}>
|
||||||
<ModelAttrView label={t('modelManager.baseModel')} value={data.base} />
|
<ModelAttrView label={t('modelManager.baseModel')} value={modelConfig.base} />
|
||||||
<ModelAttrView label={t('modelManager.modelType')} value={data.type} />
|
<ModelAttrView label={t('modelManager.modelType')} value={modelConfig.type} />
|
||||||
<ModelAttrView label={t('common.format')} value={data.format} />
|
<ModelAttrView label={t('common.format')} value={modelConfig.format} />
|
||||||
<ModelAttrView label={t('modelManager.path')} value={data.path} />
|
<ModelAttrView label={t('modelManager.path')} value={modelConfig.path} />
|
||||||
{data.type === 'main' && <ModelAttrView label={t('modelManager.variant')} value={data.variant} />}
|
{modelConfig.type === 'main' && (
|
||||||
{data.type === 'main' && data.format === 'diffusers' && data.repo_variant && (
|
<ModelAttrView label={t('modelManager.variant')} value={modelConfig.variant} />
|
||||||
<ModelAttrView label={t('modelManager.repoVariant')} value={data.repo_variant} />
|
|
||||||
)}
|
)}
|
||||||
{data.type === 'main' && data.format === 'checkpoint' && (
|
{modelConfig.type === 'main' && modelConfig.format === 'diffusers' && modelConfig.repo_variant && (
|
||||||
|
<ModelAttrView label={t('modelManager.repoVariant')} value={modelConfig.repo_variant} />
|
||||||
|
)}
|
||||||
|
{modelConfig.type === 'main' && modelConfig.format === 'checkpoint' && (
|
||||||
<>
|
<>
|
||||||
<ModelAttrView label={t('modelManager.pathToConfig')} value={data.config_path} />
|
<ModelAttrView label={t('modelManager.pathToConfig')} value={modelConfig.config_path} />
|
||||||
<ModelAttrView label={t('modelManager.predictionType')} value={data.prediction_type} />
|
<ModelAttrView label={t('modelManager.predictionType')} value={modelConfig.prediction_type} />
|
||||||
<ModelAttrView label={t('modelManager.upcastAttention')} value={`${data.upcast_attention}`} />
|
<ModelAttrView label={t('modelManager.upcastAttention')} value={`${modelConfig.upcast_attention}`} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{data.type === 'ip_adapter' && data.format === 'invokeai' && (
|
{modelConfig.type === 'ip_adapter' && modelConfig.format === 'invokeai' && (
|
||||||
<ModelAttrView label={t('modelManager.imageEncoderModelId')} value={data.image_encoder_model_id} />
|
<ModelAttrView label={t('modelManager.imageEncoderModelId')} value={modelConfig.image_encoder_model_id} />
|
||||||
)}
|
)}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Box>
|
</Box>
|
||||||
<Box layerStyle="second" borderRadius="base" p={4}>
|
<Box layerStyle="second" borderRadius="base" p={4}>
|
||||||
{data.type === 'main' && data.base !== 'sdxl-refiner' && <MainModelDefaultSettings />}
|
{modelConfig.type === 'main' && modelConfig.base !== 'sdxl-refiner' && (
|
||||||
{(data.type === 'controlnet' || data.type === 't2i_adapter') && <ControlNetOrT2IAdapterDefaultSettings />}
|
<MainModelDefaultSettings modelConfig={modelConfig} />
|
||||||
{(data.type === 'main' || data.type === 'lora') && <TriggerPhrases />}
|
)}
|
||||||
|
{(modelConfig.type === 'controlnet' || modelConfig.type === 't2i_adapter') && (
|
||||||
|
<ControlNetOrT2IAdapterDefaultSettings modelConfig={modelConfig} />
|
||||||
|
)}
|
||||||
|
{(modelConfig.type === 'main' || modelConfig.type === 'lora') && <TriggerPhrases modelConfig={modelConfig} />}
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ModelView.displayName = 'ModelView';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Switch } from '@invoke-ai/ui-library';
|
import { Switch, typedMemo } from '@invoke-ai/ui-library';
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import type { UseControllerProps } from 'react-hook-form';
|
import type { UseControllerProps } from 'react-hook-form';
|
||||||
@ -6,7 +6,7 @@ import { useController } from 'react-hook-form';
|
|||||||
|
|
||||||
import type { FormField } from './MainModelDefaultSettings/MainModelDefaultSettings';
|
import type { FormField } from './MainModelDefaultSettings/MainModelDefaultSettings';
|
||||||
|
|
||||||
export function SettingToggle<T, F extends Record<string, FormField<T>>>(props: UseControllerProps<F>) {
|
export const SettingToggle = typedMemo(<T, F extends Record<string, FormField<T>>>(props: UseControllerProps<F>) => {
|
||||||
const { field } = useController(props);
|
const { field } = useController(props);
|
||||||
|
|
||||||
const value = useMemo(() => {
|
const value = useMemo(() => {
|
||||||
@ -25,4 +25,6 @@ export function SettingToggle<T, F extends Record<string, FormField<T>>>(props:
|
|||||||
);
|
);
|
||||||
|
|
||||||
return <Switch size="sm" isChecked={value} onChange={onChange} />;
|
return <Switch size="sm" isChecked={value} onChange={onChange} />;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
SettingToggle.displayName = 'SettingToggle';
|
||||||
|
@ -9,19 +9,19 @@ import {
|
|||||||
TagCloseButton,
|
TagCloseButton,
|
||||||
TagLabel,
|
TagLabel,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { skipToken } from '@reduxjs/toolkit/query';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
import { useGetModelConfigQuery, useUpdateModelMutation } from 'services/api/endpoints/models';
|
import { useUpdateModelMutation } from 'services/api/endpoints/models';
|
||||||
import { isLoRAModelConfig, isNonRefinerMainModelConfig } from 'services/api/types';
|
import type { LoRAModelConfig, MainModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
export const TriggerPhrases = () => {
|
type Props = {
|
||||||
|
modelConfig: MainModelConfig | LoRAModelConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TriggerPhrases = memo(({ modelConfig }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
|
||||||
const { currentData: modelConfig } = useGetModelConfigQuery(selectedModelKey ?? skipToken);
|
|
||||||
const [phrase, setPhrase] = useState('');
|
const [phrase, setPhrase] = useState('');
|
||||||
|
|
||||||
const [updateModel, { isLoading }] = useUpdateModelMutation();
|
const [updateModel, { isLoading }] = useUpdateModelMutation();
|
||||||
@ -31,9 +31,6 @@ export const TriggerPhrases = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const triggerPhrases = useMemo(() => {
|
const triggerPhrases = useMemo(() => {
|
||||||
if (!modelConfig || (!isNonRefinerMainModelConfig(modelConfig) && !isLoRAModelConfig(modelConfig))) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return modelConfig?.trigger_phrases || [];
|
return modelConfig?.trigger_phrases || [];
|
||||||
}, [modelConfig]);
|
}, [modelConfig]);
|
||||||
|
|
||||||
@ -48,10 +45,6 @@ export const TriggerPhrases = () => {
|
|||||||
}, [phrase, triggerPhrases]);
|
}, [phrase, triggerPhrases]);
|
||||||
|
|
||||||
const addTriggerPhrase = useCallback(async () => {
|
const addTriggerPhrase = useCallback(async () => {
|
||||||
if (!selectedModelKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!phrase.length || triggerPhrases.includes(phrase)) {
|
if (!phrase.length || triggerPhrases.includes(phrase)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -59,22 +52,18 @@ export const TriggerPhrases = () => {
|
|||||||
setPhrase('');
|
setPhrase('');
|
||||||
|
|
||||||
await updateModel({
|
await updateModel({
|
||||||
key: selectedModelKey,
|
key: modelConfig.key,
|
||||||
body: { trigger_phrases: [...triggerPhrases, phrase] },
|
body: { trigger_phrases: [...triggerPhrases, phrase] },
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
}, [updateModel, selectedModelKey, phrase, triggerPhrases]);
|
}, [phrase, triggerPhrases, updateModel, modelConfig.key]);
|
||||||
|
|
||||||
const removeTriggerPhrase = useCallback(
|
const removeTriggerPhrase = useCallback(
|
||||||
async (phraseToRemove: string) => {
|
async (phraseToRemove: string) => {
|
||||||
if (!selectedModelKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredPhrases = triggerPhrases.filter((p) => p !== phraseToRemove);
|
const filteredPhrases = triggerPhrases.filter((p) => p !== phraseToRemove);
|
||||||
|
|
||||||
await updateModel({ key: selectedModelKey, body: { trigger_phrases: filteredPhrases } }).unwrap();
|
await updateModel({ key: modelConfig.key, body: { trigger_phrases: filteredPhrases } }).unwrap();
|
||||||
},
|
},
|
||||||
[updateModel, selectedModelKey, triggerPhrases]
|
[triggerPhrases, updateModel, modelConfig]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onTriggerPhraseAddFormSubmit = useCallback(
|
const onTriggerPhraseAddFormSubmit = useCallback(
|
||||||
@ -103,7 +92,9 @@ export const TriggerPhrases = () => {
|
|||||||
{t('common.add')}
|
{t('common.add')}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
{!!errors.length && errors.map((error) => <FormErrorMessage key={error}>{error}</FormErrorMessage>)}
|
{errors.map((error) => (
|
||||||
|
<FormErrorMessage key={error}>{error}</FormErrorMessage>
|
||||||
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</form>
|
</form>
|
||||||
@ -118,4 +109,6 @@ export const TriggerPhrases = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
TriggerPhrases.displayName = 'TriggerPhrases';
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user