diff --git a/Makefile b/Makefile index 7344b2e8d2..e858a89e2b 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ help: @echo "frontend-typegen Generate types for the frontend from the OpenAPI schema" @echo "installer-zip Build the installer .zip file for the current version" @echo "tag-release Tag the GitHub repository with the current version (use at release time only!)" + @echo "openapi Generate the OpenAPI schema for the app, outputting to stdout" # Runs ruff, fixing any safely-fixable errors and formatting ruff: @@ -70,3 +71,6 @@ installer-zip: tag-release: cd installer && ./tag_release.sh +# Generate the OpenAPI Schema for the app +openapi: + python scripts/generate_openapi_schema.py diff --git a/docker/README.md b/docker/README.md index d5a472da7d..9e7ac15145 100644 --- a/docker/README.md +++ b/docker/README.md @@ -64,7 +64,7 @@ GPU_DRIVER=nvidia Any environment variables supported by InvokeAI can be set here - please see the [Configuration docs](https://invoke-ai.github.io/InvokeAI/features/CONFIGURATION/) for further detail. -## Even Moar Customizing! +## Even More Customizing! See the `docker-compose.yml` file. The `command` instruction can be uncommented and used to run arbitrary startup commands. Some examples below. diff --git a/docs/contributing/frontend/WORKFLOWS.md b/docs/contributing/frontend/WORKFLOWS.md index e71d797b8a..533419e070 100644 --- a/docs/contributing/frontend/WORKFLOWS.md +++ b/docs/contributing/frontend/WORKFLOWS.md @@ -117,13 +117,13 @@ Stateless fields do not store their value in the node, so their field instances "Custom" fields will always be treated as stateless fields. -##### Collection and Scalar Fields +##### Single and Collection Fields -Field types have a name and two flags which may identify it as a **collection** or **collection or scalar** field. +Field types have a name and cardinality property which may identify it as a **SINGLE**, **COLLECTION** or **SINGLE_OR_COLLECTION** field. -If a field is annotated in python as a list, its field type is parsed and flagged as a **collection** type (e.g. `list[int]`). - -If it is annotated as a union of a type and list, the type will be flagged as a **collection or scalar** type (e.g. `Union[int, list[int]]`). Fields may not be unions of different types (e.g. `Union[int, list[str]]` and `Union[int, str]` are not allowed). +- If a field is annotated in python as a singular value or class, its field type is parsed as a **SINGLE** type (e.g. `int`, `ImageField`, `str`). +- If a field is annotated in python as a list, its field type is parsed as a **COLLECTION** type (e.g. `list[int]`). +- If it is annotated as a union of a type and list, the type will be parsed as a **SINGLE_OR_COLLECTION** type (e.g. `Union[int, list[int]]`). Fields may not be unions of different types (e.g. `Union[int, list[str]]` and `Union[int, str]` are not allowed). ## Implementation @@ -173,8 +173,7 @@ Field types are represented as structured objects: ```ts type FieldType = { name: string; - isCollection: boolean; - isCollectionOrScalar: boolean; + cardinality: 'SINGLE' | 'COLLECTION' | 'SINGLE_OR_COLLECTION'; }; ``` @@ -186,7 +185,7 @@ There are 4 general cases for field type parsing. When a field is annotated as a primitive values (e.g. `int`, `str`, `float`), the field type parsing is fairly straightforward. The field is represented by a simple OpenAPI **schema object**, which has a `type` property. -We create a field type name from this `type` string (e.g. `string` -> `StringField`). +We create a field type name from this `type` string (e.g. `string` -> `StringField`). The cardinality is `"SINGLE"`. ##### Complex Types @@ -200,13 +199,13 @@ We need to **dereference** the schema to pull these out. Dereferencing may requi When a field is annotated as a list of a single type, the schema object has an `items` property. They may be a schema object or reference object and must be parsed to determine the item type. -We use the item type for field type name, adding `isCollection: true` to the field type. +We use the item type for field type name. The cardinality is `"COLLECTION"`. -##### Collection or Scalar Types +##### Single or Collection Types When a field is annotated as a union of a type and list of that type, the schema object has an `anyOf` property, which holds a list of valid types for the union. -After verifying that the union has two members (a type and list of the same type), we use the type for field type name, adding `isCollectionOrScalar: true` to the field type. +After verifying that the union has two members (a type and list of the same type), we use the type for field type name, with cardinality `"SINGLE_OR_COLLECTION"`. ##### Optional Fields diff --git a/docs/features/CONTROLNET.md b/docs/features/CONTROLNET.md index d07353089d..718b12b0f8 100644 --- a/docs/features/CONTROLNET.md +++ b/docs/features/CONTROLNET.md @@ -165,7 +165,7 @@ Additionally, each section can be expanded with the "Show Advanced" button in o There are several ways to install IP-Adapter models with an existing InvokeAI installation: 1. Through the command line interface launched from the invoke.sh / invoke.bat scripts, option [4] to download models. -2. Through the Model Manager UI with models from the *Tools* section of [www.models.invoke.ai](https://www.models.invoke.ai). To do this, copy the repo ID from the desired model page, and paste it in the Add Model field of the model manager. **Note** Both the IP-Adapter and the Image Encoder must be installed for IP-Adapter to work. For example, the [SD 1.5 IP-Adapter](https://models.invoke.ai/InvokeAI/ip_adapter_plus_sd15) and [SD1.5 Image Encoder](https://models.invoke.ai/InvokeAI/ip_adapter_sd_image_encoder) must be installed to use IP-Adapter with SD1.5 based models. +2. Through the Model Manager UI with models from the *Tools* section of [models.invoke.ai](https://models.invoke.ai). To do this, copy the repo ID from the desired model page, and paste it in the Add Model field of the model manager. **Note** Both the IP-Adapter and the Image Encoder must be installed for IP-Adapter to work. For example, the [SD 1.5 IP-Adapter](https://models.invoke.ai/InvokeAI/ip_adapter_plus_sd15) and [SD1.5 Image Encoder](https://models.invoke.ai/InvokeAI/ip_adapter_sd_image_encoder) must be installed to use IP-Adapter with SD1.5 based models. 3. **Advanced -- Not recommended ** Manually downloading the IP-Adapter and Image Encoder files - Image Encoder folders shouid be placed in the `models\any\clip_vision` folders. IP Adapter Model folders should be placed in the relevant `ip-adapter` folder of relevant base model folder of Invoke root directory. For example, for the SDXL IP-Adapter, files should be added to the `model/sdxl/ip_adapter/` folder. #### Using IP-Adapter diff --git a/docs/help/FAQ.md b/docs/help/FAQ.md index 4c297f442a..25880f7cd2 100644 --- a/docs/help/FAQ.md +++ b/docs/help/FAQ.md @@ -154,6 +154,18 @@ This is caused by an invalid setting in the `invokeai.yaml` configuration file. Check the [configuration docs] for more detail about the settings and how to specify them. +## `ModuleNotFoundError: No module named 'controlnet_aux'` + +`controlnet_aux` is a dependency of Invoke and appears to have been packaged or distributed strangely. Sometimes, it doesn't install correctly. This is outside our control. + +If you encounter this error, the solution is to remove the package from the `pip` cache and re-run the Invoke installer so a fresh, working version of `controlnet_aux` can be downloaded and installed: + +- Run the Invoke launcher +- Choose the developer console option +- Run this command: `pip cache remove controlnet_aux` +- Close the terminal window +- Download and run the [installer](https://github.com/invoke-ai/InvokeAI/releases/latest), selecting your current install location + ## Out of Memory Issues The models are large, VRAM is expensive, and you may find yourself diff --git a/docs/help/diffusion.md b/docs/help/diffusion.md index 0dbb09f304..7182a51d67 100644 --- a/docs/help/diffusion.md +++ b/docs/help/diffusion.md @@ -20,7 +20,7 @@ When you generate an image using text-to-image, multiple steps occur in latent s 4. The VAE decodes the final latent image from latent space into image space. Image-to-image is a similar process, with only step 1 being different: -1. The input image is encoded from image space into latent space by the VAE. Noise is then added to the input latent image. Denoising Strength dictates how may noise steps are added, and the amount of noise added at each step. A Denoising Strength of 0 means there are 0 steps and no noise added, resulting in an unchanged image, while a Denoising Strength of 1 results in the image being completely replaced with noise and a full set of denoising steps are performance. The process is then the same as steps 2-4 in the text-to-image process. +1. The input image is encoded from image space into latent space by the VAE. Noise is then added to the input latent image. Denoising Strength dictates how many noise steps are added, and the amount of noise added at each step. A Denoising Strength of 0 means there are 0 steps and no noise added, resulting in an unchanged image, while a Denoising Strength of 1 results in the image being completely replaced with noise and a full set of denoising steps are performance. The process is then the same as steps 2-4 in the text-to-image process. Furthermore, a model provides the CLIP prompt tokenizer, the VAE, and a U-Net (where noise prediction occurs given a prompt and initial noise tensor). diff --git a/docs/installation/010_INSTALL_AUTOMATED.md b/docs/installation/010_INSTALL_AUTOMATED.md index 9eb8620321..3c6c90afdc 100644 --- a/docs/installation/010_INSTALL_AUTOMATED.md +++ b/docs/installation/010_INSTALL_AUTOMATED.md @@ -98,7 +98,7 @@ Updating is exactly the same as installing - download the latest installer, choo If you have installation issues, please review the [FAQ]. You can also [create an issue] or ask for help on [discord]. -[installation requirements]: INSTALLATION.md#installation-requirements +[installation requirements]: INSTALL_REQUIREMENTS.md [FAQ]: ../help/FAQ.md [install some models]: 050_INSTALLING_MODELS.md [configuration docs]: ../features/CONFIGURATION.md diff --git a/docs/installation/020_INSTALL_MANUAL.md b/docs/installation/020_INSTALL_MANUAL.md index 36859a5795..059834eb45 100644 --- a/docs/installation/020_INSTALL_MANUAL.md +++ b/docs/installation/020_INSTALL_MANUAL.md @@ -10,7 +10,7 @@ InvokeAI is distributed as a python package on PyPI, installable with `pip`. The ### Requirements -Before you start, go through the [installation requirements]. +Before you start, go through the [installation requirements](./INSTALL_REQUIREMENTS.md). ### Installation Walkthrough @@ -79,7 +79,7 @@ Before you start, go through the [installation requirements]. 1. Install the InvokeAI Package. The base command is `pip install InvokeAI --use-pep517`, but you may need to change this depending on your system and the desired features. - - You may need to provide an [extra index URL]. Select your platform configuration using [this tool on the PyTorch website]. Copy the `--extra-index-url` string from this and append it to your install command. + - You may need to provide an [extra index URL](https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-extra-index-url). Select your platform configuration using [this tool on the PyTorch website](https://pytorch.org/get-started/locally/). Copy the `--extra-index-url` string from this and append it to your install command. !!! example "Install with an extra index URL" @@ -116,4 +116,4 @@ Before you start, go through the [installation requirements]. !!! warning - If the virtual environment is _not_ inside the root directory, then you _must_ specify the path to the root directory with `--root_dir \path\to\invokeai` or the `INVOKEAI_ROOT` environment variable. + If the virtual environment is _not_ inside the root directory, then you _must_ specify the path to the root directory with `--root \path\to\invokeai` or the `INVOKEAI_ROOT` environment variable. diff --git a/docs/installation/INSTALL_REQUIREMENTS.md b/docs/installation/INSTALL_REQUIREMENTS.md index 31f60b86eb..2279e7efb8 100644 --- a/docs/installation/INSTALL_REQUIREMENTS.md +++ b/docs/installation/INSTALL_REQUIREMENTS.md @@ -37,13 +37,13 @@ Invoke runs best with a dedicated GPU, but will fall back to running on CPU, alb === "Nvidia" ``` - Any GPU with at least 8GB VRAM. Linux only. + Any GPU with at least 8GB VRAM. ``` === "AMD" ``` - Any GPU with at least 16GB VRAM. + Any GPU with at least 16GB VRAM. Linux only. ``` === "Mac" diff --git a/installer/templates/invoke.bat.in b/installer/templates/invoke.bat.in index 9c9c08d82d..c8ef19710b 100644 --- a/installer/templates/invoke.bat.in +++ b/installer/templates/invoke.bat.in @@ -10,8 +10,7 @@ set INVOKEAI_ROOT=. echo Desired action: echo 1. Generate images with the browser-based interface echo 2. Open the developer console -echo 3. Run the InvokeAI image database maintenance script -echo 4. Command-line help +echo 3. Command-line help echo Q - Quit echo. echo To update, download and run the installer from https://github.com/invoke-ai/InvokeAI/releases/latest. @@ -34,9 +33,6 @@ IF /I "%choice%" == "1" ( echo *** Type `exit` to quit this shell and deactivate the Python virtual environment *** call cmd /k ) ELSE IF /I "%choice%" == "3" ( - echo Running the db maintenance script... - python .venv\Scripts\invokeai-db-maintenance.exe -) ELSE IF /I "%choice%" == "4" ( echo Displaying command line help... python .venv\Scripts\invokeai-web.exe --help %* pause diff --git a/installer/templates/invoke.sh.in b/installer/templates/invoke.sh.in index 9c45eba5b0..b8d5a7af23 100644 --- a/installer/templates/invoke.sh.in +++ b/installer/templates/invoke.sh.in @@ -47,11 +47,6 @@ do_choice() { bash --init-file "$file_name" ;; 3) - clear - printf "Running the db maintenance script\n" - invokeai-db-maintenance --root ${INVOKEAI_ROOT} - ;; - 4) clear printf "Command-line help\n" invokeai-web --help @@ -71,8 +66,7 @@ do_line_input() { printf "What would you like to do?\n" printf "1: Generate images using the browser-based interface\n" printf "2: Open the developer console\n" - printf "3: Run the InvokeAI image database maintenance script\n" - printf "4: Command-line help\n" + printf "3: Command-line help\n" printf "Q: Quit\n\n" printf "To update, download and run the installer from https://github.com/invoke-ai/InvokeAI/releases/latest.\n\n" read -p "Please enter 1-4, Q: [1] " yn diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index f492da90f3..f2274d8f46 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -19,21 +19,22 @@ from ..services.boards.boards_default import BoardService from ..services.bulk_download.bulk_download_default import BulkDownloadService from ..services.config import InvokeAIAppConfig from ..services.download import DownloadQueueService +from ..services.events.events_fastapievents import FastAPIEventService from ..services.image_files.image_files_disk import DiskImageFileStorage from ..services.image_records.image_records_sqlite import SqliteImageRecordStorage from ..services.images.images_default import ImageService from ..services.invocation_cache.invocation_cache_memory import MemoryInvocationCache from ..services.invocation_services import InvocationServices +from ..services.invocation_stats.invocation_stats_default import InvocationStatsService from ..services.invoker import Invoker from ..services.model_images.model_images_default import ModelImageFileStorageDisk from ..services.model_manager.model_manager_default import ModelManagerService from ..services.model_records import ModelRecordServiceSQL from ..services.names.names_default import SimpleNameService -from ..services.session_processor.session_processor_default import DefaultSessionProcessor +from ..services.session_processor.session_processor_default import DefaultSessionProcessor, DefaultSessionRunner from ..services.session_queue.session_queue_sqlite import SqliteSessionQueue from ..services.urls.urls_default import LocalUrlService from ..services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage -from .events import FastAPIEventService # TODO: is there a better way to achieve this? @@ -101,11 +102,9 @@ class ApiDependencies: download_queue=download_queue_service, events=events, ) - # horrible hack - remove - invokeai.backend.util.devices.RAM_CACHE = model_manager.load.ram_cache - names = SimpleNameService() - session_processor = DefaultSessionProcessor() + performance_statistics = InvocationStatsService() + session_processor = DefaultSessionProcessor(session_runner=DefaultSessionRunner()) session_queue = SqliteSessionQueue(db=db) urls = LocalUrlService() workflow_records = SqliteWorkflowRecordsStorage(db=db) @@ -127,6 +126,7 @@ class ApiDependencies: model_manager=model_manager, download_queue=download_queue_service, names=names, + performance_statistics=performance_statistics, session_processor=session_processor, session_queue=session_queue, urls=urls, diff --git a/invokeai/app/api/events.py b/invokeai/app/api/events.py deleted file mode 100644 index 2ac07e6dfe..0000000000 --- a/invokeai/app/api/events.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) - -import asyncio -import threading -from queue import Empty, Queue -from typing import Any - -from fastapi_events.dispatcher import dispatch - -from ..services.events.events_base import EventServiceBase - - -class FastAPIEventService(EventServiceBase): - event_handler_id: int - __queue: Queue - __stop_event: threading.Event - - def __init__(self, event_handler_id: int) -> None: - self.event_handler_id = event_handler_id - self.__queue = Queue() - self.__stop_event = threading.Event() - asyncio.create_task(self.__dispatch_from_queue(stop_event=self.__stop_event)) - - super().__init__() - - def stop(self, *args, **kwargs): - self.__stop_event.set() - self.__queue.put(None) - - def dispatch(self, event_name: str, payload: Any) -> None: - self.__queue.put({"event_name": event_name, "payload": payload}) - - async def __dispatch_from_queue(self, stop_event: threading.Event): - """Get events on from the queue and dispatch them, from the correct thread""" - while not stop_event.is_set(): - try: - event = self.__queue.get(block=False) - if not event: # Probably stopping - continue - - dispatch( - event.get("event_name"), - payload=event.get("payload"), - middleware_id=self.event_handler_id, - ) - - except Empty: - await asyncio.sleep(0.1) - pass - - except asyncio.CancelledError as e: - raise e # Raise a proper error diff --git a/invokeai/app/api/routers/app_info.py b/invokeai/app/api/routers/app_info.py index 21286ac2b0..c3bc98a038 100644 --- a/invokeai/app/api/routers/app_info.py +++ b/invokeai/app/api/routers/app_info.py @@ -13,7 +13,6 @@ from pydantic import BaseModel, Field from invokeai.app.invocations.upscale import ESRGAN_MODELS from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch -from invokeai.backend.image_util.safety_checker import SafetyChecker from invokeai.backend.util.logging import logging from invokeai.version import __version__ @@ -109,9 +108,7 @@ async def get_config() -> AppConfig: upscaling_models.append(str(Path(model).stem)) upscaler = Upscaler(upscaling_method="esrgan", upscaling_models=upscaling_models) - nsfw_methods = [] - if SafetyChecker.safety_checker_available(): - nsfw_methods.append("nsfw_checker") + nsfw_methods = ["nsfw_checker"] watermarking_methods = ["invisible_watermark"] diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index dc8a04b711..a947b83abe 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -6,13 +6,12 @@ from fastapi import BackgroundTasks, Body, HTTPException, Path, Query, Request, from fastapi.responses import FileResponse from fastapi.routing import APIRouter from PIL import Image -from pydantic import BaseModel, Field, ValidationError +from pydantic import BaseModel, Field, JsonValue -from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator +from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID, WorkflowWithoutIDValidator from ..dependencies import ApiDependencies @@ -42,13 +41,17 @@ async def upload_image( board_id: Optional[str] = Query(default=None, description="The board to add this image to, if any"), session_id: Optional[str] = Query(default=None, description="The session ID associated with this upload, if any"), crop_visible: Optional[bool] = Query(default=False, description="Whether to crop the image"), + metadata: Optional[JsonValue] = Body( + default=None, description="The metadata to associate with the image", embed=True + ), ) -> ImageDTO: """Uploads an image""" if not file.content_type or not file.content_type.startswith("image"): raise HTTPException(status_code=415, detail="Not an image") - metadata = None - workflow = None + _metadata = None + _workflow = None + _graph = None contents = await file.read() try: @@ -62,22 +65,28 @@ async def upload_image( # TODO: retain non-invokeai metadata on upload? # attempt to parse metadata from image - metadata_raw = pil_image.info.get("invokeai_metadata", None) - if metadata_raw: - try: - metadata = MetadataFieldValidator.validate_json(metadata_raw) - except ValidationError: - ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image") - pass + metadata_raw = metadata if isinstance(metadata, str) else pil_image.info.get("invokeai_metadata", None) + if isinstance(metadata_raw, str): + _metadata = metadata_raw + else: + ApiDependencies.invoker.services.logger.debug("Failed to parse metadata for uploaded image") + pass # attempt to parse workflow from image workflow_raw = pil_image.info.get("invokeai_workflow", None) - if workflow_raw is not None: - try: - workflow = WorkflowWithoutIDValidator.validate_json(workflow_raw) - except ValidationError: - ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image") - pass + if isinstance(workflow_raw, str): + _workflow = workflow_raw + else: + ApiDependencies.invoker.services.logger.debug("Failed to parse workflow for uploaded image") + pass + + # attempt to extract graph from image + graph_raw = pil_image.info.get("invokeai_graph", None) + if isinstance(graph_raw, str): + _graph = graph_raw + else: + ApiDependencies.invoker.services.logger.debug("Failed to parse graph for uploaded image") + pass try: image_dto = ApiDependencies.invoker.services.images.create( @@ -86,8 +95,9 @@ async def upload_image( image_category=image_category, session_id=session_id, board_id=board_id, - metadata=metadata, - workflow=workflow, + metadata=_metadata, + workflow=_workflow, + graph=_graph, is_intermediate=is_intermediate, ) @@ -185,14 +195,21 @@ async def get_image_metadata( raise HTTPException(status_code=404) +class WorkflowAndGraphResponse(BaseModel): + workflow: Optional[str] = Field(description="The workflow used to generate the image, as stringified JSON") + graph: Optional[str] = Field(description="The graph used to generate the image, as stringified JSON") + + @images_router.get( - "/i/{image_name}/workflow", operation_id="get_image_workflow", response_model=Optional[WorkflowWithoutID] + "/i/{image_name}/workflow", operation_id="get_image_workflow", response_model=WorkflowAndGraphResponse ) async def get_image_workflow( image_name: str = Path(description="The name of image whose workflow to get"), -) -> Optional[WorkflowWithoutID]: +) -> WorkflowAndGraphResponse: try: - return ApiDependencies.invoker.services.images.get_workflow(image_name) + workflow = ApiDependencies.invoker.services.images.get_workflow(image_name) + graph = ApiDependencies.invoker.services.images.get_graph(image_name) + return WorkflowAndGraphResponse(workflow=workflow, graph=graph) except Exception: raise HTTPException(status_code=404) diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py index 7bb0f23dc8..b1221f7a34 100644 --- a/invokeai/app/api/routers/model_manager.py +++ b/invokeai/app/api/routers/model_manager.py @@ -6,7 +6,7 @@ import pathlib import shutil import traceback from copy import deepcopy -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Type from fastapi import Body, Path, Query, Response, UploadFile from fastapi.responses import FileResponse @@ -16,7 +16,8 @@ from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field from starlette.exceptions import HTTPException from typing_extensions import Annotated -from invokeai.app.services.model_install import ModelInstallJob +from invokeai.app.services.model_images.model_images_common import ModelImageFileNotFoundException +from invokeai.app.services.model_install.model_install_common import ModelInstallJob from invokeai.app.services.model_records import ( DuplicateModelException, InvalidModelException, @@ -52,6 +53,13 @@ class ModelsList(BaseModel): model_config = ConfigDict(use_enum_values=True) +def add_cover_image_to_model_config(config: AnyModelConfig, dependencies: Type[ApiDependencies]) -> AnyModelConfig: + """Add a cover image URL to a model configuration.""" + cover_image = dependencies.invoker.services.model_images.get_url(config.key) + config.cover_image = cover_image + return config + + ############################################################################## # These are example inputs and outputs that are used in places where Swagger # is unable to generate a correct example. @@ -118,8 +126,7 @@ async def list_model_records( record_store.search_by_attr(model_type=model_type, model_name=model_name, model_format=model_format) ) for model in found_models: - cover_image = ApiDependencies.invoker.services.model_images.get_url(model.key) - model.cover_image = cover_image + model = add_cover_image_to_model_config(model, ApiDependencies) return ModelsList(models=found_models) @@ -160,12 +167,9 @@ async def get_model_record( key: str = Path(description="Key of the model record to fetch."), ) -> AnyModelConfig: """Get a model record""" - record_store = ApiDependencies.invoker.services.model_manager.store try: - config: AnyModelConfig = record_store.get_model(key) - cover_image = ApiDependencies.invoker.services.model_images.get_url(key) - config.cover_image = cover_image - return config + config = ApiDependencies.invoker.services.model_manager.store.get_model(key) + return add_cover_image_to_model_config(config, ApiDependencies) except UnknownModelException as e: raise HTTPException(status_code=404, detail=str(e)) @@ -294,14 +298,15 @@ async def update_model_record( installer = ApiDependencies.invoker.services.model_manager.install try: record_store.update_model(key, changes=changes) - model_response: AnyModelConfig = installer.sync_model_path(key) + config = installer.sync_model_path(key) + config = add_cover_image_to_model_config(config, ApiDependencies) logger.info(f"Updated model: {key}") except UnknownModelException as e: raise HTTPException(status_code=404, detail=str(e)) except ValueError as e: logger.error(str(e)) raise HTTPException(status_code=409, detail=str(e)) - return model_response + return config @model_manager_router.get( @@ -648,6 +653,14 @@ async def convert_model( logger.error(str(e)) raise HTTPException(status_code=409, detail=str(e)) + # Update the model image if the model had one + try: + model_image = ApiDependencies.invoker.services.model_images.get(key) + ApiDependencies.invoker.services.model_images.save(model_image, new_key) + ApiDependencies.invoker.services.model_images.delete(key) + except ModelImageFileNotFoundException: + pass + # delete the original safetensors file installer.delete(key) @@ -655,7 +668,8 @@ async def convert_model( shutil.rmtree(cache_path) # return the config record for the new diffusers directory - new_config: AnyModelConfig = store.get_model(new_key) + new_config = store.get_model(new_key) + new_config = add_cover_image_to_model_config(new_config, ApiDependencies) return new_config diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py index 40f1f2213b..7161e54a41 100644 --- a/invokeai/app/api/routers/session_queue.py +++ b/invokeai/app/api/routers/session_queue.py @@ -203,6 +203,7 @@ async def get_batch_status( responses={ 200: {"model": SessionQueueItem}, }, + response_model_exclude_none=True, ) async def get_queue_item( queue_id: str = Path(description="The queue id to perform this operation on"), diff --git a/invokeai/app/api/sockets.py b/invokeai/app/api/sockets.py index 463545d9bc..b39922c69b 100644 --- a/invokeai/app/api/sockets.py +++ b/invokeai/app/api/sockets.py @@ -1,66 +1,125 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) +from typing import Any + from fastapi import FastAPI -from fastapi_events.handlers.local import local_handler -from fastapi_events.typing import Event +from pydantic import BaseModel from socketio import ASGIApp, AsyncServer -from ..services.events.events_base import EventServiceBase +from invokeai.app.services.events.events_common import ( + BatchEnqueuedEvent, + BulkDownloadCompleteEvent, + BulkDownloadErrorEvent, + BulkDownloadEventBase, + BulkDownloadStartedEvent, + DownloadCancelledEvent, + DownloadCompleteEvent, + DownloadErrorEvent, + DownloadEventBase, + DownloadProgressEvent, + DownloadStartedEvent, + FastAPIEvent, + InvocationCompleteEvent, + InvocationDenoiseProgressEvent, + InvocationErrorEvent, + InvocationStartedEvent, + ModelEventBase, + ModelInstallCancelledEvent, + ModelInstallCompleteEvent, + ModelInstallDownloadProgressEvent, + ModelInstallDownloadsCompleteEvent, + ModelInstallErrorEvent, + ModelInstallStartedEvent, + ModelLoadCompleteEvent, + ModelLoadStartedEvent, + QueueClearedEvent, + QueueEventBase, + QueueItemStatusChangedEvent, + register_events, +) + + +class QueueSubscriptionEvent(BaseModel): + """Event data for subscribing to the socket.io queue room. + This is a pydantic model to ensure the data is in the correct format.""" + + queue_id: str + + +class BulkDownloadSubscriptionEvent(BaseModel): + """Event data for subscribing to the socket.io bulk downloads room. + This is a pydantic model to ensure the data is in the correct format.""" + + bulk_download_id: str + + +QUEUE_EVENTS = { + InvocationStartedEvent, + InvocationDenoiseProgressEvent, + InvocationCompleteEvent, + InvocationErrorEvent, + QueueItemStatusChangedEvent, + BatchEnqueuedEvent, + QueueClearedEvent, +} + +MODEL_EVENTS = { + DownloadCancelledEvent, + DownloadCompleteEvent, + DownloadErrorEvent, + DownloadProgressEvent, + DownloadStartedEvent, + ModelLoadStartedEvent, + ModelLoadCompleteEvent, + ModelInstallDownloadProgressEvent, + ModelInstallDownloadsCompleteEvent, + ModelInstallStartedEvent, + ModelInstallCompleteEvent, + ModelInstallCancelledEvent, + ModelInstallErrorEvent, +} + +BULK_DOWNLOAD_EVENTS = {BulkDownloadStartedEvent, BulkDownloadCompleteEvent, BulkDownloadErrorEvent} class SocketIO: - __sio: AsyncServer - __app: ASGIApp + _sub_queue = "subscribe_queue" + _unsub_queue = "unsubscribe_queue" - __sub_queue: str = "subscribe_queue" - __unsub_queue: str = "unsubscribe_queue" - - __sub_bulk_download: str = "subscribe_bulk_download" - __unsub_bulk_download: str = "unsubscribe_bulk_download" + _sub_bulk_download = "subscribe_bulk_download" + _unsub_bulk_download = "unsubscribe_bulk_download" def __init__(self, app: FastAPI): - self.__sio = AsyncServer(async_mode="asgi", cors_allowed_origins="*") - self.__app = ASGIApp(socketio_server=self.__sio, socketio_path="/ws/socket.io") - app.mount("/ws", self.__app) + self._sio = AsyncServer(async_mode="asgi", cors_allowed_origins="*") + self._app = ASGIApp(socketio_server=self._sio, socketio_path="/ws/socket.io") + app.mount("/ws", self._app) - self.__sio.on(self.__sub_queue, handler=self._handle_sub_queue) - self.__sio.on(self.__unsub_queue, handler=self._handle_unsub_queue) - local_handler.register(event_name=EventServiceBase.queue_event, _func=self._handle_queue_event) - local_handler.register(event_name=EventServiceBase.model_event, _func=self._handle_model_event) + self._sio.on(self._sub_queue, handler=self._handle_sub_queue) + self._sio.on(self._unsub_queue, handler=self._handle_unsub_queue) + self._sio.on(self._sub_bulk_download, handler=self._handle_sub_bulk_download) + self._sio.on(self._unsub_bulk_download, handler=self._handle_unsub_bulk_download) - self.__sio.on(self.__sub_bulk_download, handler=self._handle_sub_bulk_download) - self.__sio.on(self.__unsub_bulk_download, handler=self._handle_unsub_bulk_download) - local_handler.register(event_name=EventServiceBase.bulk_download_event, _func=self._handle_bulk_download_event) + register_events(QUEUE_EVENTS, self._handle_queue_event) + register_events(MODEL_EVENTS, self._handle_model_event) + register_events(BULK_DOWNLOAD_EVENTS, self._handle_bulk_image_download_event) - async def _handle_queue_event(self, event: Event): - await self.__sio.emit( - event=event[1]["event"], - data=event[1]["data"], - room=event[1]["data"]["queue_id"], - ) + async def _handle_sub_queue(self, sid: str, data: Any) -> None: + await self._sio.enter_room(sid, QueueSubscriptionEvent(**data).queue_id) - async def _handle_sub_queue(self, sid, data, *args, **kwargs) -> None: - if "queue_id" in data: - await self.__sio.enter_room(sid, data["queue_id"]) + async def _handle_unsub_queue(self, sid: str, data: Any) -> None: + await self._sio.leave_room(sid, QueueSubscriptionEvent(**data).queue_id) - async def _handle_unsub_queue(self, sid, data, *args, **kwargs) -> None: - if "queue_id" in data: - await self.__sio.leave_room(sid, data["queue_id"]) + async def _handle_sub_bulk_download(self, sid: str, data: Any) -> None: + await self._sio.enter_room(sid, BulkDownloadSubscriptionEvent(**data).bulk_download_id) - async def _handle_model_event(self, event: Event) -> None: - await self.__sio.emit(event=event[1]["event"], data=event[1]["data"]) + async def _handle_unsub_bulk_download(self, sid: str, data: Any) -> None: + await self._sio.leave_room(sid, BulkDownloadSubscriptionEvent(**data).bulk_download_id) - async def _handle_bulk_download_event(self, event: Event): - await self.__sio.emit( - event=event[1]["event"], - data=event[1]["data"], - room=event[1]["data"]["bulk_download_id"], - ) + async def _handle_queue_event(self, event: FastAPIEvent[QueueEventBase]): + await self._sio.emit(event=event[0], data=event[1].model_dump(mode="json"), room=event[1].queue_id) - async def _handle_sub_bulk_download(self, sid, data, *args, **kwargs): - if "bulk_download_id" in data: - await self.__sio.enter_room(sid, data["bulk_download_id"]) + async def _handle_model_event(self, event: FastAPIEvent[ModelEventBase | DownloadEventBase]) -> None: + await self._sio.emit(event=event[0], data=event[1].model_dump(mode="json")) - async def _handle_unsub_bulk_download(self, sid, data, *args, **kwargs): - if "bulk_download_id" in data: - await self.__sio.leave_room(sid, data["bulk_download_id"]) + async def _handle_bulk_image_download_event(self, event: FastAPIEvent[BulkDownloadEventBase]) -> None: + await self._sio.emit(event=event[0], data=event[1].model_dump(mode="json"), room=event[1].bulk_download_id) diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index ceaeb95147..e69d95af71 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -3,9 +3,7 @@ import logging import mimetypes import socket from contextlib import asynccontextmanager -from inspect import signature from pathlib import Path -from typing import Any import torch import uvicorn @@ -13,11 +11,9 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html -from fastapi.openapi.utils import get_openapi from fastapi.responses import HTMLResponse from fastapi_events.handlers.local import local_handler from fastapi_events.middleware import EventHandlerASGIMiddleware -from pydantic.json_schema import models_json_schema from torch.backends.mps import is_available as is_mps_available # for PyCharm: @@ -25,9 +21,8 @@ from torch.backends.mps import is_available as is_mps_available import invokeai.backend.util.hotfixes # noqa: F401 (monkeypatching on import) import invokeai.frontend.web as web_dir from invokeai.app.api.no_cache_staticfiles import NoCacheStaticFiles -from invokeai.app.invocations.model import ModelIdentifierField from invokeai.app.services.config.config_default import get_config -from invokeai.app.services.session_processor.session_processor_common import ProgressImage +from invokeai.app.util.custom_openapi import get_openapi_func from invokeai.backend.util.devices import TorchDevice from ..backend.util.logging import InvokeAILogger @@ -44,11 +39,6 @@ from .api.routers import ( workflows, ) from .api.sockets import SocketIO -from .invocations.baseinvocation import ( - BaseInvocation, - UIConfigBase, -) -from .invocations.fields import InputFieldJSONSchemaExtra, OutputFieldJSONSchemaExtra app_config = get_config() @@ -118,85 +108,7 @@ app.include_router(app_info.app_router, prefix="/api") app.include_router(session_queue.session_queue_router, prefix="/api") app.include_router(workflows.workflows_router, prefix="/api") - -# Build a custom OpenAPI to include all outputs -# TODO: can outputs be included on metadata of invocation schemas somehow? -def custom_openapi() -> dict[str, Any]: - if app.openapi_schema: - return app.openapi_schema - openapi_schema = get_openapi( - title=app.title, - description="An API for invoking AI image operations", - version="1.0.0", - routes=app.routes, - separate_input_output_schemas=False, # https://fastapi.tiangolo.com/how-to/separate-openapi-schemas/ - ) - - # Add all outputs - all_invocations = BaseInvocation.get_invocations() - output_types = set() - output_type_titles = {} - for invoker in all_invocations: - output_type = signature(invoker.invoke).return_annotation - output_types.add(output_type) - - output_schemas = models_json_schema( - models=[(o, "serialization") for o in output_types], ref_template="#/components/schemas/{model}" - ) - for schema_key, output_schema in output_schemas[1]["$defs"].items(): - # TODO: note that we assume the schema_key here is the TYPE.__name__ - # This could break in some cases, figure out a better way to do it - output_type_titles[schema_key] = output_schema["title"] - openapi_schema["components"]["schemas"][schema_key] = output_schema - openapi_schema["components"]["schemas"][schema_key]["class"] = "output" - - # Some models don't end up in the schemas as standalone definitions - additional_schemas = models_json_schema( - [ - (UIConfigBase, "serialization"), - (InputFieldJSONSchemaExtra, "serialization"), - (OutputFieldJSONSchemaExtra, "serialization"), - (ModelIdentifierField, "serialization"), - (ProgressImage, "serialization"), - ], - ref_template="#/components/schemas/{model}", - ) - for schema_key, schema_json in additional_schemas[1]["$defs"].items(): - openapi_schema["components"]["schemas"][schema_key] = schema_json - - # Add a reference to the output type to additionalProperties of the invoker schema - for invoker in all_invocations: - invoker_name = invoker.__name__ # type: ignore [attr-defined] # this is a valid attribute - output_type = signature(obj=invoker.invoke).return_annotation - output_type_title = output_type_titles[output_type.__name__] - invoker_schema = openapi_schema["components"]["schemas"][f"{invoker_name}"] - outputs_ref = {"$ref": f"#/components/schemas/{output_type_title}"} - invoker_schema["output"] = outputs_ref - invoker_schema["class"] = "invocation" - - # This code no longer seems to be necessary? - # Leave it here just in case - # - # from invokeai.backend.model_manager import get_model_config_formats - # formats = get_model_config_formats() - # for model_config_name, enum_set in formats.items(): - - # if model_config_name in openapi_schema["components"]["schemas"]: - # # print(f"Config with name {name} already defined") - # continue - - # openapi_schema["components"]["schemas"][model_config_name] = { - # "title": model_config_name, - # "description": "An enumeration.", - # "type": "string", - # "enum": [v.value for v in enum_set], - # } - - app.openapi_schema = openapi_schema - return app.openapi_schema - - -app.openapi = custom_openapi # type: ignore [method-assign] # this is a valid assignment +app.openapi = get_openapi_func(app) @app.get("/docs", include_in_schema=False) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 40c7b41cae..1d169f0a82 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -98,11 +98,13 @@ class BaseInvocationOutput(BaseModel): _output_classes: ClassVar[set[BaseInvocationOutput]] = set() _typeadapter: ClassVar[Optional[TypeAdapter[Any]]] = None + _typeadapter_needs_update: ClassVar[bool] = False @classmethod def register_output(cls, output: BaseInvocationOutput) -> None: """Registers an invocation output.""" cls._output_classes.add(output) + cls._typeadapter_needs_update = True @classmethod def get_outputs(cls) -> Iterable[BaseInvocationOutput]: @@ -112,11 +114,12 @@ class BaseInvocationOutput(BaseModel): @classmethod def get_typeadapter(cls) -> TypeAdapter[Any]: """Gets a pydantc TypeAdapter for the union of all invocation output types.""" - if not cls._typeadapter: - InvocationOutputsUnion = TypeAliasType( - "InvocationOutputsUnion", Annotated[Union[tuple(cls._output_classes)], Field(discriminator="type")] + if not cls._typeadapter or cls._typeadapter_needs_update: + AnyInvocationOutput = TypeAliasType( + "AnyInvocationOutput", Annotated[Union[tuple(cls._output_classes)], Field(discriminator="type")] ) - cls._typeadapter = TypeAdapter(InvocationOutputsUnion) + cls._typeadapter = TypeAdapter(AnyInvocationOutput) + cls._typeadapter_needs_update = False return cls._typeadapter @classmethod @@ -125,12 +128,13 @@ class BaseInvocationOutput(BaseModel): return (i.get_type() for i in BaseInvocationOutput.get_outputs()) @staticmethod - def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseModel]) -> None: + def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseInvocationOutput]) -> None: """Adds various UI-facing attributes to the invocation output's OpenAPI schema.""" # Because we use a pydantic Literal field with default value for the invocation type, # it will be typed as optional in the OpenAPI schema. Make it required manually. if "required" not in schema or not isinstance(schema["required"], list): schema["required"] = [] + schema["class"] = "output" schema["required"].extend(["type"]) @classmethod @@ -167,6 +171,7 @@ class BaseInvocation(ABC, BaseModel): _invocation_classes: ClassVar[set[BaseInvocation]] = set() _typeadapter: ClassVar[Optional[TypeAdapter[Any]]] = None + _typeadapter_needs_update: ClassVar[bool] = False @classmethod def get_type(cls) -> str: @@ -177,15 +182,17 @@ class BaseInvocation(ABC, BaseModel): def register_invocation(cls, invocation: BaseInvocation) -> None: """Registers an invocation.""" cls._invocation_classes.add(invocation) + cls._typeadapter_needs_update = True @classmethod def get_typeadapter(cls) -> TypeAdapter[Any]: """Gets a pydantc TypeAdapter for the union of all invocation types.""" - if not cls._typeadapter: - InvocationsUnion = TypeAliasType( - "InvocationsUnion", Annotated[Union[tuple(cls._invocation_classes)], Field(discriminator="type")] + if not cls._typeadapter or cls._typeadapter_needs_update: + AnyInvocation = TypeAliasType( + "AnyInvocation", Annotated[Union[tuple(cls._invocation_classes)], Field(discriminator="type")] ) - cls._typeadapter = TypeAdapter(InvocationsUnion) + cls._typeadapter = TypeAdapter(AnyInvocation) + cls._typeadapter_needs_update = False return cls._typeadapter @classmethod @@ -221,7 +228,7 @@ class BaseInvocation(ABC, BaseModel): return signature(cls.invoke).return_annotation @staticmethod - def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseModel], *args, **kwargs) -> None: + def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseInvocation]) -> None: """Adds various UI-facing attributes to the invocation's OpenAPI schema.""" uiconfig = cast(UIConfigBase | None, getattr(model_class, "UIConfig", None)) if uiconfig is not None: @@ -237,6 +244,7 @@ class BaseInvocation(ABC, BaseModel): schema["version"] = uiconfig.version if "required" not in schema or not isinstance(schema["required"], list): schema["required"] = [] + schema["class"] = "invocation" schema["required"].extend(["type", "id"]) @abstractmethod @@ -310,7 +318,7 @@ class BaseInvocation(ABC, BaseModel): protected_namespaces=(), validate_assignment=True, json_schema_extra=json_schema_extra, - json_schema_serialization_defaults_required=True, + json_schema_serialization_defaults_required=False, coerce_numbers_to_str=True, ) diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index 158f11a58e..766b44fdc8 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -65,11 +65,7 @@ class CompelInvocation(BaseInvocation): @torch.no_grad() def invoke(self, context: InvocationContext) -> ConditioningOutput: tokenizer_info = context.models.load(self.clip.tokenizer) - tokenizer_model = tokenizer_info.model - assert isinstance(tokenizer_model, CLIPTokenizer) text_encoder_info = context.models.load(self.clip.text_encoder) - text_encoder_model = text_encoder_info.model - assert isinstance(text_encoder_model, CLIPTextModel) def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: for lora in self.clip.loras: @@ -84,19 +80,21 @@ class CompelInvocation(BaseInvocation): ti_list = generate_ti_list(self.prompt, text_encoder_info.config.base, context) with ( - ModelPatcher.apply_ti(tokenizer_model, text_encoder_model, ti_list) as ( - tokenizer, - ti_manager, - ), + # apply all patches while the model is on the target device text_encoder_info as text_encoder, - # Apply the LoRA after text_encoder has been moved to its target device for faster patching. + tokenizer_info as tokenizer, ModelPatcher.apply_lora_text_encoder(text_encoder, _lora_loader()), # Apply CLIP Skip after LoRA to prevent LoRA application from failing on skipped layers. - ModelPatcher.apply_clip_skip(text_encoder_model, self.clip.skipped_layers), + ModelPatcher.apply_clip_skip(text_encoder, self.clip.skipped_layers), + ModelPatcher.apply_ti(tokenizer, text_encoder, ti_list) as ( + patched_tokenizer, + ti_manager, + ), ): assert isinstance(text_encoder, CLIPTextModel) + assert isinstance(tokenizer, CLIPTokenizer) compel = Compel( - tokenizer=tokenizer, + tokenizer=patched_tokenizer, text_encoder=text_encoder, textual_inversion_manager=ti_manager, dtype_for_device_getter=TorchDevice.choose_torch_dtype, @@ -106,7 +104,7 @@ class CompelInvocation(BaseInvocation): conjunction = Compel.parse_prompt_string(self.prompt) if context.config.get().log_tokenization: - log_tokenization_for_conjunction(conjunction, tokenizer) + log_tokenization_for_conjunction(conjunction, patched_tokenizer) c, _options = compel.build_conditioning_tensor_for_conjunction(conjunction) @@ -136,11 +134,7 @@ class SDXLPromptInvocationBase: zero_on_empty: bool, ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]: tokenizer_info = context.models.load(clip_field.tokenizer) - tokenizer_model = tokenizer_info.model - assert isinstance(tokenizer_model, CLIPTokenizer) text_encoder_info = context.models.load(clip_field.text_encoder) - text_encoder_model = text_encoder_info.model - assert isinstance(text_encoder_model, (CLIPTextModel, CLIPTextModelWithProjection)) # return zero on empty if prompt == "" and zero_on_empty: @@ -177,20 +171,23 @@ class SDXLPromptInvocationBase: ti_list = generate_ti_list(prompt, text_encoder_info.config.base, context) with ( - ModelPatcher.apply_ti(tokenizer_model, text_encoder_model, ti_list) as ( - tokenizer, - ti_manager, - ), + # apply all patches while the model is on the target device text_encoder_info as text_encoder, - # Apply the LoRA after text_encoder has been moved to its target device for faster patching. + tokenizer_info as tokenizer, ModelPatcher.apply_lora(text_encoder, _lora_loader(), lora_prefix), # Apply CLIP Skip after LoRA to prevent LoRA application from failing on skipped layers. - ModelPatcher.apply_clip_skip(text_encoder_model, clip_field.skipped_layers), + ModelPatcher.apply_clip_skip(text_encoder, clip_field.skipped_layers), + ModelPatcher.apply_ti(tokenizer, text_encoder, ti_list) as ( + patched_tokenizer, + ti_manager, + ), ): assert isinstance(text_encoder, (CLIPTextModel, CLIPTextModelWithProjection)) + assert isinstance(tokenizer, CLIPTokenizer) + text_encoder = cast(CLIPTextModel, text_encoder) compel = Compel( - tokenizer=tokenizer, + tokenizer=patched_tokenizer, text_encoder=text_encoder, textual_inversion_manager=ti_manager, dtype_for_device_getter=TorchDevice.choose_torch_dtype, @@ -203,7 +200,7 @@ class SDXLPromptInvocationBase: if context.config.get().log_tokenization: # TODO: better logging for and syntax - log_tokenization_for_conjunction(conjunction, tokenizer) + log_tokenization_for_conjunction(conjunction, patched_tokenizer) # TODO: ask for optimizations? to not run text_encoder twice c, _options = compel.build_conditioning_tensor_for_conjunction(conjunction) diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index d2f01622b2..f5edd49874 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -24,7 +24,6 @@ from pydantic import BaseModel, Field, field_validator, model_validator from invokeai.app.invocations.fields import ( FieldDescriptions, ImageField, - Input, InputField, OutputField, UIType, @@ -80,13 +79,13 @@ class ControlOutput(BaseInvocationOutput): control: ControlField = OutputField(description=FieldDescriptions.control) -@invocation("controlnet", title="ControlNet", tags=["controlnet"], category="controlnet", version="1.1.1") +@invocation("controlnet", title="ControlNet", tags=["controlnet"], category="controlnet", version="1.1.2") class ControlNetInvocation(BaseInvocation): """Collects ControlNet info to pass to other nodes""" image: ImageField = InputField(description="The control image") control_model: ModelIdentifierField = InputField( - description=FieldDescriptions.controlnet_model, input=Input.Direct, ui_type=UIType.ControlNetModel + description=FieldDescriptions.controlnet_model, ui_type=UIType.ControlNetModel ) control_weight: Union[float, List[float]] = InputField( default=1.0, ge=-1, le=2, description="The weight given to the ControlNet" diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 05ffc0d67b..65e7ce5e06 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1,6 +1,5 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from pathlib import Path from typing import Literal, Optional import cv2 @@ -504,7 +503,7 @@ class ImageInverseLerpInvocation(BaseInvocation, WithMetadata, WithBoard): title="Blur NSFW Image", tags=["image", "nsfw"], category="image", - version="1.2.2", + version="1.2.3", ) class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithBoard): """Add blur to NSFW-flagged images""" @@ -516,23 +515,12 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithBoard): logger = context.logger logger.debug("Running NSFW checker") - if SafetyChecker.has_nsfw_concept(image): - logger.info("A potentially NSFW image has been detected. Image will be blurred.") - blurry_image = image.filter(filter=ImageFilter.GaussianBlur(radius=32)) - caution = self._get_caution_img() - blurry_image.paste(caution, (0, 0), caution) - image = blurry_image + image = SafetyChecker.blur_if_nsfw(image) image_dto = context.images.save(image=image) return ImageOutput.build(image_dto) - def _get_caution_img(self) -> Image.Image: - import invokeai.app.assets.images as image_assets - - caution = Image.open(Path(image_assets.__path__[0]) / "caution.png") - return caution.resize((caution.width // 2, caution.height // 2)) - @invocation( "img_watermark", diff --git a/invokeai/app/invocations/ip_adapter.py b/invokeai/app/invocations/ip_adapter.py index 34a30628da..de40879eef 100644 --- a/invokeai/app/invocations/ip_adapter.py +++ b/invokeai/app/invocations/ip_adapter.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Field, field_validator, model_validator from typing_extensions import Self from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output -from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, TensorField, UIType +from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField, TensorField, UIType from invokeai.app.invocations.model import ModelIdentifierField from invokeai.app.invocations.primitives import ImageField from invokeai.app.invocations.util import validate_begin_end_step, validate_weights @@ -58,7 +58,7 @@ class IPAdapterOutput(BaseInvocationOutput): CLIP_VISION_MODEL_MAP = {"ViT-H": "ip_adapter_sd_image_encoder", "ViT-G": "ip_adapter_sdxl_image_encoder"} -@invocation("ip_adapter", title="IP-Adapter", tags=["ip_adapter", "control"], category="ip_adapter", version="1.4.0") +@invocation("ip_adapter", title="IP-Adapter", tags=["ip_adapter", "control"], category="ip_adapter", version="1.4.1") class IPAdapterInvocation(BaseInvocation): """Collects IP-Adapter info to pass to other nodes.""" @@ -67,7 +67,6 @@ class IPAdapterInvocation(BaseInvocation): ip_adapter_model: ModelIdentifierField = InputField( description="The IP-Adapter model.", title="IP-Adapter Model", - input=Input.Direct, ui_order=-1, ui_type=UIType.IPAdapterModel, ) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 3d1439f7db..a88eff0fcb 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -586,13 +586,6 @@ class DenoiseLatentsInvocation(BaseInvocation): unet: UNet2DConditionModel, scheduler: Scheduler, ) -> StableDiffusionGeneratorPipeline: - # TODO: - # configure_model_padding( - # unet, - # self.seamless, - # self.seamless_axes, - # ) - class FakeVae: class FakeVaeConfig: def __init__(self) -> None: @@ -937,9 +930,9 @@ class DenoiseLatentsInvocation(BaseInvocation): assert isinstance(unet_info.model, UNet2DConditionModel) with ( ExitStack() as exit_stack, - ModelPatcher.apply_freeu(unet_info.model, self.unet.freeu_config), - set_seamless(unet_info.model, self.unet.seamless_axes), # FIXME unet_info as unet, + ModelPatcher.apply_freeu(unet, self.unet.freeu_config), + set_seamless(unet, self.unet.seamless_axes), # FIXME # Apply the LoRA after unet has been moved to its target device for faster patching. ModelPatcher.apply_lora_unet(unet, _lora_loader()), ): diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index caa8a53540..94a6136fcb 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -11,6 +11,7 @@ from invokeai.backend.model_manager.config import AnyModelConfig, BaseModelType, from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, + Classification, invocation, invocation_output, ) @@ -93,19 +94,46 @@ class ModelLoaderOutput(UNetOutput, CLIPOutput, VAEOutput): pass +@invocation_output("model_identifier_output") +class ModelIdentifierOutput(BaseInvocationOutput): + """Model identifier output""" + + model: ModelIdentifierField = OutputField(description="Model identifier", title="Model") + + +@invocation( + "model_identifier", + title="Model identifier", + tags=["model"], + category="model", + version="1.0.0", + classification=Classification.Prototype, +) +class ModelIdentifierInvocation(BaseInvocation): + """Selects any model, outputting it its identifier. Be careful with this one! The identifier will be accepted as + input for any model, even if the model types don't match. If you connect this to a mismatched input, you'll get an + error.""" + + model: ModelIdentifierField = InputField(description="The model to select", title="Model") + + def invoke(self, context: InvocationContext) -> ModelIdentifierOutput: + if not context.models.exists(self.model.key): + raise Exception(f"Unknown model {self.model.key}") + + return ModelIdentifierOutput(model=self.model) + + @invocation( "main_model_loader", title="Main Model", tags=["model"], category="model", - version="1.0.2", + version="1.0.3", ) class MainModelLoaderInvocation(BaseInvocation): """Loads a main model, outputting its submodels.""" - model: ModelIdentifierField = InputField( - description=FieldDescriptions.main_model, input=Input.Direct, ui_type=UIType.MainModel - ) + model: ModelIdentifierField = InputField(description=FieldDescriptions.main_model, ui_type=UIType.MainModel) # TODO: precision? def invoke(self, context: InvocationContext) -> ModelLoaderOutput: @@ -134,12 +162,12 @@ class LoRALoaderOutput(BaseInvocationOutput): clip: Optional[CLIPField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP") -@invocation("lora_loader", title="LoRA", tags=["model"], category="model", version="1.0.2") +@invocation("lora_loader", title="LoRA", tags=["model"], category="model", version="1.0.3") class LoRALoaderInvocation(BaseInvocation): """Apply selected lora to unet and text_encoder.""" lora: ModelIdentifierField = InputField( - description=FieldDescriptions.lora_model, input=Input.Direct, title="LoRA", ui_type=UIType.LoRAModel + description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel ) weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight) unet: Optional[UNetField] = InputField( @@ -190,6 +218,75 @@ class LoRALoaderInvocation(BaseInvocation): return output +@invocation_output("lora_selector_output") +class LoRASelectorOutput(BaseInvocationOutput): + """Model loader output""" + + lora: LoRAField = OutputField(description="LoRA model and weight", title="LoRA") + + +@invocation("lora_selector", title="LoRA Selector", tags=["model"], category="model", version="1.0.1") +class LoRASelectorInvocation(BaseInvocation): + """Selects a LoRA model and weight.""" + + lora: ModelIdentifierField = InputField( + description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel + ) + weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight) + + def invoke(self, context: InvocationContext) -> LoRASelectorOutput: + return LoRASelectorOutput(lora=LoRAField(lora=self.lora, weight=self.weight)) + + +@invocation("lora_collection_loader", title="LoRA Collection Loader", tags=["model"], category="model", version="1.0.0") +class LoRACollectionLoader(BaseInvocation): + """Applies a collection of LoRAs to the provided UNet and CLIP models.""" + + loras: LoRAField | list[LoRAField] = InputField( + description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs" + ) + unet: Optional[UNetField] = InputField( + default=None, + description=FieldDescriptions.unet, + input=Input.Connection, + title="UNet", + ) + clip: Optional[CLIPField] = InputField( + default=None, + description=FieldDescriptions.clip, + input=Input.Connection, + title="CLIP", + ) + + def invoke(self, context: InvocationContext) -> LoRALoaderOutput: + output = LoRALoaderOutput() + loras = self.loras if isinstance(self.loras, list) else [self.loras] + added_loras: list[str] = [] + + for lora in loras: + if lora.lora.key in added_loras: + continue + + if not context.models.exists(lora.lora.key): + raise Exception(f"Unknown lora: {lora.lora.key}!") + + assert lora.lora.base in (BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2) + + added_loras.append(lora.lora.key) + + if self.unet is not None: + if output.unet is None: + output.unet = self.unet.model_copy(deep=True) + output.unet.loras.append(lora) + + if self.clip is not None: + if output.clip is None: + output.clip = self.clip.model_copy(deep=True) + output.clip.loras.append(lora) + + return output + + @invocation_output("sdxl_lora_loader_output") class SDXLLoRALoaderOutput(BaseInvocationOutput): """SDXL LoRA Loader Output""" @@ -204,13 +301,13 @@ class SDXLLoRALoaderOutput(BaseInvocationOutput): title="SDXL LoRA", tags=["lora", "model"], category="model", - version="1.0.2", + version="1.0.3", ) class SDXLLoRALoaderInvocation(BaseInvocation): """Apply selected lora to unet and text_encoder.""" lora: ModelIdentifierField = InputField( - description=FieldDescriptions.lora_model, input=Input.Direct, title="LoRA", ui_type=UIType.LoRAModel + description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel ) weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight) unet: Optional[UNetField] = InputField( @@ -279,12 +376,78 @@ class SDXLLoRALoaderInvocation(BaseInvocation): return output -@invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.2") +@invocation( + "sdxl_lora_collection_loader", + title="SDXL LoRA Collection Loader", + tags=["model"], + category="model", + version="1.0.0", +) +class SDXLLoRACollectionLoader(BaseInvocation): + """Applies a collection of SDXL LoRAs to the provided UNet and CLIP models.""" + + loras: LoRAField | list[LoRAField] = InputField( + description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs" + ) + unet: Optional[UNetField] = InputField( + default=None, + description=FieldDescriptions.unet, + input=Input.Connection, + title="UNet", + ) + clip: Optional[CLIPField] = InputField( + default=None, + description=FieldDescriptions.clip, + input=Input.Connection, + title="CLIP", + ) + clip2: Optional[CLIPField] = InputField( + default=None, + description=FieldDescriptions.clip, + input=Input.Connection, + title="CLIP 2", + ) + + def invoke(self, context: InvocationContext) -> SDXLLoRALoaderOutput: + output = SDXLLoRALoaderOutput() + loras = self.loras if isinstance(self.loras, list) else [self.loras] + added_loras: list[str] = [] + + for lora in loras: + if lora.lora.key in added_loras: + continue + + if not context.models.exists(lora.lora.key): + raise Exception(f"Unknown lora: {lora.lora.key}!") + + assert lora.lora.base is BaseModelType.StableDiffusionXL + + added_loras.append(lora.lora.key) + + if self.unet is not None: + if output.unet is None: + output.unet = self.unet.model_copy(deep=True) + output.unet.loras.append(lora) + + if self.clip is not None: + if output.clip is None: + output.clip = self.clip.model_copy(deep=True) + output.clip.loras.append(lora) + + if self.clip2 is not None: + if output.clip2 is None: + output.clip2 = self.clip2.model_copy(deep=True) + output.clip2.loras.append(lora) + + return output + + +@invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.3") class VAELoaderInvocation(BaseInvocation): """Loads a VAE model, outputting a VaeLoaderOutput""" vae_model: ModelIdentifierField = InputField( - description=FieldDescriptions.vae_model, input=Input.Direct, title="VAE", ui_type=UIType.VAEModel + description=FieldDescriptions.vae_model, title="VAE", ui_type=UIType.VAEModel ) def invoke(self, context: InvocationContext) -> VAEOutput: diff --git a/invokeai/app/invocations/sdxl.py b/invokeai/app/invocations/sdxl.py index 9b1ee90350..1c0817cb92 100644 --- a/invokeai/app/invocations/sdxl.py +++ b/invokeai/app/invocations/sdxl.py @@ -1,4 +1,4 @@ -from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType +from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField, UIType from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.model_manager import SubModelType @@ -30,12 +30,12 @@ class SDXLRefinerModelLoaderOutput(BaseInvocationOutput): vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE") -@invocation("sdxl_model_loader", title="SDXL Main Model", tags=["model", "sdxl"], category="model", version="1.0.2") +@invocation("sdxl_model_loader", title="SDXL Main Model", tags=["model", "sdxl"], category="model", version="1.0.3") class SDXLModelLoaderInvocation(BaseInvocation): """Loads an sdxl base model, outputting its submodels.""" model: ModelIdentifierField = InputField( - description=FieldDescriptions.sdxl_main_model, input=Input.Direct, ui_type=UIType.SDXLMainModel + description=FieldDescriptions.sdxl_main_model, ui_type=UIType.SDXLMainModel ) # TODO: precision? @@ -67,13 +67,13 @@ class SDXLModelLoaderInvocation(BaseInvocation): title="SDXL Refiner Model", tags=["model", "sdxl", "refiner"], category="model", - version="1.0.2", + version="1.0.3", ) class SDXLRefinerModelLoaderInvocation(BaseInvocation): """Loads an sdxl refiner model, outputting its submodels.""" model: ModelIdentifierField = InputField( - description=FieldDescriptions.sdxl_refiner_model, input=Input.Direct, ui_type=UIType.SDXLRefinerModel + description=FieldDescriptions.sdxl_refiner_model, ui_type=UIType.SDXLRefinerModel ) # TODO: precision? diff --git a/invokeai/app/invocations/t2i_adapter.py b/invokeai/app/invocations/t2i_adapter.py index b22a089d3f..04f9a6c695 100644 --- a/invokeai/app/invocations/t2i_adapter.py +++ b/invokeai/app/invocations/t2i_adapter.py @@ -8,7 +8,7 @@ from invokeai.app.invocations.baseinvocation import ( invocation, invocation_output, ) -from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField, UIType +from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField, UIType from invokeai.app.invocations.model import ModelIdentifierField from invokeai.app.invocations.util import validate_begin_end_step, validate_weights from invokeai.app.services.shared.invocation_context import InvocationContext @@ -45,7 +45,7 @@ class T2IAdapterOutput(BaseInvocationOutput): @invocation( - "t2i_adapter", title="T2I-Adapter", tags=["t2i_adapter", "control"], category="t2i_adapter", version="1.0.2" + "t2i_adapter", title="T2I-Adapter", tags=["t2i_adapter", "control"], category="t2i_adapter", version="1.0.3" ) class T2IAdapterInvocation(BaseInvocation): """Collects T2I-Adapter info to pass to other nodes.""" @@ -55,7 +55,6 @@ class T2IAdapterInvocation(BaseInvocation): t2i_adapter_model: ModelIdentifierField = InputField( description="The T2I-Adapter model.", title="T2I-Adapter Model", - input=Input.Direct, ui_order=-1, ui_type=UIType.T2IAdapterModel, ) diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index 04cec928f4..d4bf059b8f 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -106,9 +106,7 @@ class BulkDownloadService(BulkDownloadBase): if self._invoker: assert bulk_download_id is not None self._invoker.services.events.emit_bulk_download_started( - bulk_download_id=bulk_download_id, - bulk_download_item_id=bulk_download_item_id, - bulk_download_item_name=bulk_download_item_name, + bulk_download_id, bulk_download_item_id, bulk_download_item_name ) def _signal_job_completed( @@ -118,10 +116,8 @@ class BulkDownloadService(BulkDownloadBase): if self._invoker: assert bulk_download_id is not None assert bulk_download_item_name is not None - self._invoker.services.events.emit_bulk_download_completed( - bulk_download_id=bulk_download_id, - bulk_download_item_id=bulk_download_item_id, - bulk_download_item_name=bulk_download_item_name, + self._invoker.services.events.emit_bulk_download_complete( + bulk_download_id, bulk_download_item_id, bulk_download_item_name ) def _signal_job_failed( @@ -131,11 +127,8 @@ class BulkDownloadService(BulkDownloadBase): if self._invoker: assert bulk_download_id is not None assert exception is not None - self._invoker.services.events.emit_bulk_download_failed( - bulk_download_id=bulk_download_id, - bulk_download_item_id=bulk_download_item_id, - bulk_download_item_name=bulk_download_item_name, - error=str(exception), + self._invoker.services.events.emit_bulk_download_error( + bulk_download_id, bulk_download_item_id, bulk_download_item_name, str(exception) ) def stop(self, *args, **kwargs): diff --git a/invokeai/app/services/download/download_default.py b/invokeai/app/services/download/download_default.py index 7d8229fba1..180f0f1a8c 100644 --- a/invokeai/app/services/download/download_default.py +++ b/invokeai/app/services/download/download_default.py @@ -8,14 +8,13 @@ import time import traceback from pathlib import Path from queue import Empty, PriorityQueue -from typing import Any, Dict, List, Optional, Set +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set import requests from pydantic.networks import AnyHttpUrl from requests import HTTPError from tqdm import tqdm -from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.util.misc import get_iso_timestamp from invokeai.backend.util.logging import InvokeAILogger @@ -30,6 +29,9 @@ from .download_base import ( UnknownJobIDException, ) +if TYPE_CHECKING: + from invokeai.app.services.events.events_base import EventServiceBase + # Maximum number of bytes to download during each call to requests.iter_content() DOWNLOAD_CHUNK_SIZE = 100000 @@ -40,7 +42,7 @@ class DownloadQueueService(DownloadQueueServiceBase): def __init__( self, max_parallel_dl: int = 5, - event_bus: Optional[EventServiceBase] = None, + event_bus: Optional["EventServiceBase"] = None, requests_session: Optional[requests.sessions.Session] = None, ): """ @@ -343,8 +345,7 @@ class DownloadQueueService(DownloadQueueServiceBase): f"An error occurred while processing the on_start callback: {traceback.format_exception(e)}" ) if self._event_bus: - assert job.download_path - self._event_bus.emit_download_started(str(job.source), job.download_path.as_posix()) + self._event_bus.emit_download_started(job) def _signal_job_progress(self, job: DownloadJob) -> None: if job.on_progress: @@ -355,13 +356,7 @@ class DownloadQueueService(DownloadQueueServiceBase): f"An error occurred while processing the on_progress callback: {traceback.format_exception(e)}" ) if self._event_bus: - assert job.download_path - self._event_bus.emit_download_progress( - str(job.source), - download_path=job.download_path.as_posix(), - current_bytes=job.bytes, - total_bytes=job.total_bytes, - ) + self._event_bus.emit_download_progress(job) def _signal_job_complete(self, job: DownloadJob) -> None: job.status = DownloadJobStatus.COMPLETED @@ -373,10 +368,7 @@ class DownloadQueueService(DownloadQueueServiceBase): f"An error occurred while processing the on_complete callback: {traceback.format_exception(e)}" ) if self._event_bus: - assert job.download_path - self._event_bus.emit_download_complete( - str(job.source), download_path=job.download_path.as_posix(), total_bytes=job.total_bytes - ) + self._event_bus.emit_download_complete(job) def _signal_job_cancelled(self, job: DownloadJob) -> None: if job.status not in [DownloadJobStatus.RUNNING, DownloadJobStatus.WAITING]: @@ -390,7 +382,7 @@ class DownloadQueueService(DownloadQueueServiceBase): f"An error occurred while processing the on_cancelled callback: {traceback.format_exception(e)}" ) if self._event_bus: - self._event_bus.emit_download_cancelled(str(job.source)) + self._event_bus.emit_download_cancelled(job) def _signal_job_error(self, job: DownloadJob, excp: Optional[Exception] = None) -> None: job.status = DownloadJobStatus.ERROR @@ -403,9 +395,7 @@ class DownloadQueueService(DownloadQueueServiceBase): f"An error occurred while processing the on_error callback: {traceback.format_exception(e)}" ) if self._event_bus: - assert job.error_type - assert job.error - self._event_bus.emit_download_error(str(job.source), error_type=job.error_type, error=job.error) + self._event_bus.emit_download_error(job) def _cleanup_cancelled_job(self, job: DownloadJob) -> None: self._logger.debug(f"Cleaning up leftover files from cancelled download job {job.download_path}") diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index 6373ec1f78..3c0fb0a30b 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -1,486 +1,195 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from typing import Any, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Optional -from invokeai.app.services.session_processor.session_processor_common import ProgressImage -from invokeai.app.services.session_queue.session_queue_common import ( - BatchStatus, - EnqueueBatchResult, - SessionQueueItem, - SessionQueueStatus, +from invokeai.app.services.events.events_common import ( + BatchEnqueuedEvent, + BulkDownloadCompleteEvent, + BulkDownloadErrorEvent, + BulkDownloadStartedEvent, + DownloadCancelledEvent, + DownloadCompleteEvent, + DownloadErrorEvent, + DownloadProgressEvent, + DownloadStartedEvent, + EventBase, + InvocationCompleteEvent, + InvocationDenoiseProgressEvent, + InvocationErrorEvent, + InvocationStartedEvent, + ModelInstallCancelledEvent, + ModelInstallCompleteEvent, + ModelInstallDownloadProgressEvent, + ModelInstallDownloadsCompleteEvent, + ModelInstallErrorEvent, + ModelInstallStartedEvent, + ModelLoadCompleteEvent, + ModelLoadStartedEvent, + QueueClearedEvent, + QueueItemStatusChangedEvent, ) -from invokeai.app.util.misc import get_timestamp -from invokeai.backend.model_manager import AnyModelConfig -from invokeai.backend.model_manager.config import SubModelType +from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState + +if TYPE_CHECKING: + from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput + from invokeai.app.services.download.download_base import DownloadJob + from invokeai.app.services.events.events_common import EventBase + from invokeai.app.services.model_install.model_install_common import ModelInstallJob + from invokeai.app.services.session_processor.session_processor_common import ProgressImage + from invokeai.app.services.session_queue.session_queue_common import ( + BatchStatus, + EnqueueBatchResult, + SessionQueueItem, + SessionQueueStatus, + ) + from invokeai.backend.model_manager.config import AnyModelConfig, SubModelType class EventServiceBase: - queue_event: str = "queue_event" - bulk_download_event: str = "bulk_download_event" - download_event: str = "download_event" - model_event: str = "model_event" - """Basic event bus, to have an empty stand-in when not needed""" - def dispatch(self, event_name: str, payload: Any) -> None: + def dispatch(self, event: "EventBase") -> None: pass - def _emit_bulk_download_event(self, event_name: str, payload: dict) -> None: - """Bulk download events are emitted to a room with queue_id as the room name""" - payload["timestamp"] = get_timestamp() - self.dispatch( - event_name=EventServiceBase.bulk_download_event, - payload={"event": event_name, "data": payload}, - ) + # region: Invocation - def __emit_queue_event(self, event_name: str, payload: dict) -> None: - """Queue events are emitted to a room with queue_id as the room name""" - payload["timestamp"] = get_timestamp() - self.dispatch( - event_name=EventServiceBase.queue_event, - payload={"event": event_name, "data": payload}, - ) + def emit_invocation_started(self, queue_item: "SessionQueueItem", invocation: "BaseInvocation") -> None: + """Emitted when an invocation is started""" + self.dispatch(InvocationStartedEvent.build(queue_item, invocation)) - def __emit_download_event(self, event_name: str, payload: dict) -> None: - payload["timestamp"] = get_timestamp() - self.dispatch( - event_name=EventServiceBase.download_event, - payload={"event": event_name, "data": payload}, - ) - - def __emit_model_event(self, event_name: str, payload: dict) -> None: - payload["timestamp"] = get_timestamp() - self.dispatch( - event_name=EventServiceBase.model_event, - payload={"event": event_name, "data": payload}, - ) - - # Define events here for every event in the system. - # This will make them easier to integrate until we find a schema generator. - def emit_generator_progress( + def emit_invocation_denoise_progress( self, - queue_id: str, - queue_item_id: int, - queue_batch_id: str, - graph_execution_state_id: str, - node_id: str, - source_node_id: str, - progress_image: Optional[ProgressImage], - step: int, - order: int, - total_steps: int, + queue_item: "SessionQueueItem", + invocation: "BaseInvocation", + intermediate_state: PipelineIntermediateState, + progress_image: "ProgressImage", ) -> None: - """Emitted when there is generation progress""" - self.__emit_queue_event( - event_name="generator_progress", - payload={ - "queue_id": queue_id, - "queue_item_id": queue_item_id, - "queue_batch_id": queue_batch_id, - "graph_execution_state_id": graph_execution_state_id, - "node_id": node_id, - "source_node_id": source_node_id, - "progress_image": progress_image.model_dump(mode="json") if progress_image is not None else None, - "step": step, - "order": order, - "total_steps": total_steps, - }, - ) + """Emitted at each step during denoising of an invocation.""" + self.dispatch(InvocationDenoiseProgressEvent.build(queue_item, invocation, intermediate_state, progress_image)) def emit_invocation_complete( - self, - queue_id: str, - queue_item_id: int, - queue_batch_id: str, - graph_execution_state_id: str, - result: dict, - node: dict, - source_node_id: str, + self, queue_item: "SessionQueueItem", invocation: "BaseInvocation", output: "BaseInvocationOutput" ) -> None: - """Emitted when an invocation has completed""" - self.__emit_queue_event( - event_name="invocation_complete", - payload={ - "queue_id": queue_id, - "queue_item_id": queue_item_id, - "queue_batch_id": queue_batch_id, - "graph_execution_state_id": graph_execution_state_id, - "node": node, - "source_node_id": source_node_id, - "result": result, - }, - ) + """Emitted when an invocation is complete""" + self.dispatch(InvocationCompleteEvent.build(queue_item, invocation, output)) def emit_invocation_error( self, - queue_id: str, - queue_item_id: int, - queue_batch_id: str, - graph_execution_state_id: str, - node: dict, - source_node_id: str, + queue_item: "SessionQueueItem", + invocation: "BaseInvocation", error_type: str, - error: str, + error_message: str, + error_traceback: str, ) -> None: - """Emitted when an invocation has completed""" - self.__emit_queue_event( - event_name="invocation_error", - payload={ - "queue_id": queue_id, - "queue_item_id": queue_item_id, - "queue_batch_id": queue_batch_id, - "graph_execution_state_id": graph_execution_state_id, - "node": node, - "source_node_id": source_node_id, - "error_type": error_type, - "error": error, - }, - ) + """Emitted when an invocation encounters an error""" + self.dispatch(InvocationErrorEvent.build(queue_item, invocation, error_type, error_message, error_traceback)) - def emit_invocation_started( - self, - queue_id: str, - queue_item_id: int, - queue_batch_id: str, - graph_execution_state_id: str, - node: dict, - source_node_id: str, - ) -> None: - """Emitted when an invocation has started""" - self.__emit_queue_event( - event_name="invocation_started", - payload={ - "queue_id": queue_id, - "queue_item_id": queue_item_id, - "queue_batch_id": queue_batch_id, - "graph_execution_state_id": graph_execution_state_id, - "node": node, - "source_node_id": source_node_id, - }, - ) + # endregion - def emit_graph_execution_complete( - self, queue_id: str, queue_item_id: int, queue_batch_id: str, graph_execution_state_id: str - ) -> None: - """Emitted when a session has completed all invocations""" - self.__emit_queue_event( - event_name="graph_execution_state_complete", - payload={ - "queue_id": queue_id, - "queue_item_id": queue_item_id, - "queue_batch_id": queue_batch_id, - "graph_execution_state_id": graph_execution_state_id, - }, - ) - - def emit_model_load_started( - self, - queue_id: str, - queue_item_id: int, - queue_batch_id: str, - graph_execution_state_id: str, - model_config: AnyModelConfig, - submodel_type: Optional[SubModelType] = None, - ) -> None: - """Emitted when a model is requested""" - self.__emit_queue_event( - event_name="model_load_started", - payload={ - "queue_id": queue_id, - "queue_item_id": queue_item_id, - "queue_batch_id": queue_batch_id, - "graph_execution_state_id": graph_execution_state_id, - "model_config": model_config.model_dump(mode="json"), - "submodel_type": submodel_type, - }, - ) - - def emit_model_load_completed( - self, - queue_id: str, - queue_item_id: int, - queue_batch_id: str, - graph_execution_state_id: str, - model_config: AnyModelConfig, - submodel_type: Optional[SubModelType] = None, - ) -> None: - """Emitted when a model is correctly loaded (returns model info)""" - self.__emit_queue_event( - event_name="model_load_completed", - payload={ - "queue_id": queue_id, - "queue_item_id": queue_item_id, - "queue_batch_id": queue_batch_id, - "graph_execution_state_id": graph_execution_state_id, - "model_config": model_config.model_dump(mode="json"), - "submodel_type": submodel_type, - }, - ) - - def emit_session_canceled( - self, - queue_id: str, - queue_item_id: int, - queue_batch_id: str, - graph_execution_state_id: str, - ) -> None: - """Emitted when a session is canceled""" - self.__emit_queue_event( - event_name="session_canceled", - payload={ - "queue_id": queue_id, - "queue_item_id": queue_item_id, - "queue_batch_id": queue_batch_id, - "graph_execution_state_id": graph_execution_state_id, - }, - ) + # region Queue def emit_queue_item_status_changed( - self, - session_queue_item: SessionQueueItem, - batch_status: BatchStatus, - queue_status: SessionQueueStatus, + self, queue_item: "SessionQueueItem", batch_status: "BatchStatus", queue_status: "SessionQueueStatus" ) -> None: """Emitted when a queue item's status changes""" - self.__emit_queue_event( - event_name="queue_item_status_changed", - payload={ - "queue_id": queue_status.queue_id, - "queue_item": { - "queue_id": session_queue_item.queue_id, - "item_id": session_queue_item.item_id, - "status": session_queue_item.status, - "batch_id": session_queue_item.batch_id, - "session_id": session_queue_item.session_id, - "error": session_queue_item.error, - "created_at": str(session_queue_item.created_at) if session_queue_item.created_at else None, - "updated_at": str(session_queue_item.updated_at) if session_queue_item.updated_at else None, - "started_at": str(session_queue_item.started_at) if session_queue_item.started_at else None, - "completed_at": str(session_queue_item.completed_at) if session_queue_item.completed_at else None, - }, - "batch_status": batch_status.model_dump(mode="json"), - "queue_status": queue_status.model_dump(mode="json"), - }, - ) + self.dispatch(QueueItemStatusChangedEvent.build(queue_item, batch_status, queue_status)) - def emit_batch_enqueued(self, enqueue_result: EnqueueBatchResult) -> None: + def emit_batch_enqueued(self, enqueue_result: "EnqueueBatchResult") -> None: """Emitted when a batch is enqueued""" - self.__emit_queue_event( - event_name="batch_enqueued", - payload={ - "queue_id": enqueue_result.queue_id, - "batch_id": enqueue_result.batch.batch_id, - "enqueued": enqueue_result.enqueued, - }, - ) + self.dispatch(BatchEnqueuedEvent.build(enqueue_result)) def emit_queue_cleared(self, queue_id: str) -> None: - """Emitted when the queue is cleared""" - self.__emit_queue_event( - event_name="queue_cleared", - payload={"queue_id": queue_id}, - ) + """Emitted when a queue is cleared""" + self.dispatch(QueueClearedEvent.build(queue_id)) - def emit_download_started(self, source: str, download_path: str) -> None: - """ - Emit when a download job is started. + # endregion - :param url: The downloaded url - """ - self.__emit_download_event( - event_name="download_started", - payload={"source": source, "download_path": download_path}, - ) + # region Download - def emit_download_progress(self, source: str, download_path: str, current_bytes: int, total_bytes: int) -> None: - """ - Emit "download_progress" events at regular intervals during a download job. + def emit_download_started(self, job: "DownloadJob") -> None: + """Emitted when a download is started""" + self.dispatch(DownloadStartedEvent.build(job)) - :param source: The downloaded source - :param download_path: The local downloaded file - :param current_bytes: Number of bytes downloaded so far - :param total_bytes: The size of the file being downloaded (if known) - """ - self.__emit_download_event( - event_name="download_progress", - payload={ - "source": source, - "download_path": download_path, - "current_bytes": current_bytes, - "total_bytes": total_bytes, - }, - ) + def emit_download_progress(self, job: "DownloadJob") -> None: + """Emitted at intervals during a download""" + self.dispatch(DownloadProgressEvent.build(job)) - def emit_download_complete(self, source: str, download_path: str, total_bytes: int) -> None: - """ - Emit a "download_complete" event at the end of a successful download. + def emit_download_complete(self, job: "DownloadJob") -> None: + """Emitted when a download is completed""" + self.dispatch(DownloadCompleteEvent.build(job)) - :param source: Source URL - :param download_path: Path to the locally downloaded file - :param total_bytes: The size of the downloaded file - """ - self.__emit_download_event( - event_name="download_complete", - payload={ - "source": source, - "download_path": download_path, - "total_bytes": total_bytes, - }, - ) + def emit_download_cancelled(self, job: "DownloadJob") -> None: + """Emitted when a download is cancelled""" + self.dispatch(DownloadCancelledEvent.build(job)) - def emit_download_cancelled(self, source: str) -> None: - """Emit a "download_cancelled" event in the event that the download was cancelled by user.""" - self.__emit_download_event( - event_name="download_cancelled", - payload={ - "source": source, - }, - ) + def emit_download_error(self, job: "DownloadJob") -> None: + """Emitted when a download encounters an error""" + self.dispatch(DownloadErrorEvent.build(job)) - def emit_download_error(self, source: str, error_type: str, error: str) -> None: - """ - Emit a "download_error" event when an download job encounters an exception. + # endregion - :param source: Source URL - :param error_type: The name of the exception that raised the error - :param error: The traceback from this error - """ - self.__emit_download_event( - event_name="download_error", - payload={ - "source": source, - "error_type": error_type, - "error": error, - }, - ) + # region Model loading - def emit_model_install_downloading( - self, - source: str, - local_path: str, - bytes: int, - total_bytes: int, - parts: List[Dict[str, Union[str, int]]], - id: int, + def emit_model_load_started(self, config: "AnyModelConfig", submodel_type: Optional["SubModelType"] = None) -> None: + """Emitted when a model load is started.""" + self.dispatch(ModelLoadStartedEvent.build(config, submodel_type)) + + def emit_model_load_complete( + self, config: "AnyModelConfig", submodel_type: Optional["SubModelType"] = None ) -> None: - """ - Emit at intervals while the install job is in progress (remote models only). + """Emitted when a model load is complete.""" + self.dispatch(ModelLoadCompleteEvent.build(config, submodel_type)) - :param source: Source of the model - :param local_path: Where model is downloading to - :param parts: Progress of downloading URLs that comprise the model, if any. - :param bytes: Number of bytes downloaded so far. - :param total_bytes: Total size of download, including all files. - This emits a Dict with keys "source", "local_path", "bytes" and "total_bytes". - """ - self.__emit_model_event( - event_name="model_install_downloading", - payload={ - "source": source, - "local_path": local_path, - "bytes": bytes, - "total_bytes": total_bytes, - "parts": parts, - "id": id, - }, - ) + # endregion - def emit_model_install_downloads_done(self, source: str) -> None: - """ - Emit once when all parts are downloaded, but before the probing and registration start. + # region Model install - :param source: Source of the model; local path, repo_id or url - """ - self.__emit_model_event( - event_name="model_install_downloads_done", - payload={"source": source}, - ) + def emit_model_install_download_progress(self, job: "ModelInstallJob") -> None: + """Emitted at intervals while the install job is in progress (remote models only).""" + self.dispatch(ModelInstallDownloadProgressEvent.build(job)) - def emit_model_install_running(self, source: str) -> None: - """ - Emit once when an install job becomes active. + def emit_model_install_downloads_complete(self, job: "ModelInstallJob") -> None: + self.dispatch(ModelInstallDownloadsCompleteEvent.build(job)) - :param source: Source of the model; local path, repo_id or url - """ - self.__emit_model_event( - event_name="model_install_running", - payload={"source": source}, - ) + def emit_model_install_started(self, job: "ModelInstallJob") -> None: + """Emitted once when an install job is started (after any download).""" + self.dispatch(ModelInstallStartedEvent.build(job)) - def emit_model_install_completed(self, source: str, key: str, id: int, total_bytes: Optional[int] = None) -> None: - """ - Emit when an install job is completed successfully. + def emit_model_install_complete(self, job: "ModelInstallJob") -> None: + """Emitted when an install job is completed successfully.""" + self.dispatch(ModelInstallCompleteEvent.build(job)) - :param source: Source of the model; local path, repo_id or url - :param key: Model config record key - :param total_bytes: Size of the model (may be None for installation of a local path) - """ - self.__emit_model_event( - event_name="model_install_completed", - payload={"source": source, "total_bytes": total_bytes, "key": key, "id": id}, - ) + def emit_model_install_cancelled(self, job: "ModelInstallJob") -> None: + """Emitted when an install job is cancelled.""" + self.dispatch(ModelInstallCancelledEvent.build(job)) - def emit_model_install_cancelled(self, source: str, id: int) -> None: - """ - Emit when an install job is cancelled. + def emit_model_install_error(self, job: "ModelInstallJob") -> None: + """Emitted when an install job encounters an exception.""" + self.dispatch(ModelInstallErrorEvent.build(job)) - :param source: Source of the model; local path, repo_id or url - """ - self.__emit_model_event( - event_name="model_install_cancelled", - payload={"source": source, "id": id}, - ) + # endregion - def emit_model_install_error(self, source: str, error_type: str, error: str, id: int) -> None: - """ - Emit when an install job encounters an exception. - - :param source: Source of the model - :param error_type: The name of the exception - :param error: A text description of the exception - """ - self.__emit_model_event( - event_name="model_install_error", - payload={"source": source, "error_type": error_type, "error": error, "id": id}, - ) + # region Bulk image download def emit_bulk_download_started( self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str ) -> None: - """Emitted when a bulk download starts""" - self._emit_bulk_download_event( - event_name="bulk_download_started", - payload={ - "bulk_download_id": bulk_download_id, - "bulk_download_item_id": bulk_download_item_id, - "bulk_download_item_name": bulk_download_item_name, - }, - ) + """Emitted when a bulk image download is started""" + self.dispatch(BulkDownloadStartedEvent.build(bulk_download_id, bulk_download_item_id, bulk_download_item_name)) - def emit_bulk_download_completed( + def emit_bulk_download_complete( self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str ) -> None: - """Emitted when a bulk download completes""" - self._emit_bulk_download_event( - event_name="bulk_download_completed", - payload={ - "bulk_download_id": bulk_download_id, - "bulk_download_item_id": bulk_download_item_id, - "bulk_download_item_name": bulk_download_item_name, - }, - ) + """Emitted when a bulk image download is complete""" + self.dispatch(BulkDownloadCompleteEvent.build(bulk_download_id, bulk_download_item_id, bulk_download_item_name)) - def emit_bulk_download_failed( + def emit_bulk_download_error( self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str, error: str ) -> None: - """Emitted when a bulk download fails""" - self._emit_bulk_download_event( - event_name="bulk_download_failed", - payload={ - "bulk_download_id": bulk_download_id, - "bulk_download_item_id": bulk_download_item_id, - "bulk_download_item_name": bulk_download_item_name, - "error": error, - }, + """Emitted when a bulk image download has an error""" + self.dispatch( + BulkDownloadErrorEvent.build(bulk_download_id, bulk_download_item_id, bulk_download_item_name, error) ) + + # endregion diff --git a/invokeai/app/services/events/events_common.py b/invokeai/app/services/events/events_common.py new file mode 100644 index 0000000000..0adcaa2ab1 --- /dev/null +++ b/invokeai/app/services/events/events_common.py @@ -0,0 +1,592 @@ +from math import floor +from typing import TYPE_CHECKING, Any, ClassVar, Coroutine, Generic, Optional, Protocol, TypeAlias, TypeVar + +from fastapi_events.handlers.local import local_handler +from fastapi_events.registry.payload_schema import registry as payload_schema +from pydantic import BaseModel, ConfigDict, Field + +from invokeai.app.services.session_processor.session_processor_common import ProgressImage +from invokeai.app.services.session_queue.session_queue_common import ( + QUEUE_ITEM_STATUS, + BatchStatus, + EnqueueBatchResult, + SessionQueueItem, + SessionQueueStatus, +) +from invokeai.app.services.shared.graph import AnyInvocation, AnyInvocationOutput +from invokeai.app.util.misc import get_timestamp +from invokeai.backend.model_manager.config import AnyModelConfig, SubModelType +from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState + +if TYPE_CHECKING: + from invokeai.app.services.download.download_base import DownloadJob + from invokeai.app.services.model_install.model_install_common import ModelInstallJob + + +class EventBase(BaseModel): + """Base class for all events. All events must inherit from this class. + + Events must define a class attribute `__event_name__` to identify the event. + + All other attributes should be defined as normal for a pydantic model. + + A timestamp is automatically added to the event when it is created. + """ + + __event_name__: ClassVar[str] + timestamp: int = Field(description="The timestamp of the event", default_factory=get_timestamp) + + model_config = ConfigDict(json_schema_serialization_defaults_required=True) + + @classmethod + def get_events(cls) -> set[type["EventBase"]]: + """Get a set of all event models.""" + + event_subclasses: set[type["EventBase"]] = set() + for subclass in cls.__subclasses__(): + # We only want to include subclasses that are event models, not intermediary classes + if hasattr(subclass, "__event_name__"): + event_subclasses.add(subclass) + event_subclasses.update(subclass.get_events()) + + return event_subclasses + + +TEvent = TypeVar("TEvent", bound=EventBase, contravariant=True) + +FastAPIEvent: TypeAlias = tuple[str, TEvent] +""" +A tuple representing a `fastapi-events` event, with the event name and payload. +Provide a generic type to `TEvent` to specify the payload type. +""" + + +class FastAPIEventFunc(Protocol, Generic[TEvent]): + def __call__(self, event: FastAPIEvent[TEvent]) -> Optional[Coroutine[Any, Any, None]]: ... + + +def register_events(events: set[type[TEvent]] | type[TEvent], func: FastAPIEventFunc[TEvent]) -> None: + """Register a function to handle specific events. + + :param events: An event or set of events to handle + :param func: The function to handle the events + """ + events = events if isinstance(events, set) else {events} + for event in events: + assert hasattr(event, "__event_name__") + local_handler.register(event_name=event.__event_name__, _func=func) # pyright: ignore [reportUnknownMemberType, reportUnknownArgumentType, reportAttributeAccessIssue] + + +class QueueEventBase(EventBase): + """Base class for queue events""" + + queue_id: str = Field(description="The ID of the queue") + + +class QueueItemEventBase(QueueEventBase): + """Base class for queue item events""" + + item_id: int = Field(description="The ID of the queue item") + batch_id: str = Field(description="The ID of the queue batch") + + +class InvocationEventBase(QueueItemEventBase): + """Base class for invocation events""" + + session_id: str = Field(description="The ID of the session (aka graph execution state)") + queue_id: str = Field(description="The ID of the queue") + item_id: int = Field(description="The ID of the queue item") + batch_id: str = Field(description="The ID of the queue batch") + session_id: str = Field(description="The ID of the session (aka graph execution state)") + invocation: AnyInvocation = Field(description="The ID of the invocation") + invocation_source_id: str = Field(description="The ID of the prepared invocation's source node") + + +@payload_schema.register +class InvocationStartedEvent(InvocationEventBase): + """Event model for invocation_started""" + + __event_name__ = "invocation_started" + + @classmethod + def build(cls, queue_item: SessionQueueItem, invocation: AnyInvocation) -> "InvocationStartedEvent": + return cls( + queue_id=queue_item.queue_id, + item_id=queue_item.item_id, + batch_id=queue_item.batch_id, + session_id=queue_item.session_id, + invocation=invocation, + invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id], + ) + + +@payload_schema.register +class InvocationDenoiseProgressEvent(InvocationEventBase): + """Event model for invocation_denoise_progress""" + + __event_name__ = "invocation_denoise_progress" + + progress_image: ProgressImage = Field(description="The progress image sent at each step during processing") + step: int = Field(description="The current step of the invocation") + total_steps: int = Field(description="The total number of steps in the invocation") + order: int = Field(description="The order of the invocation in the session") + percentage: float = Field(description="The percentage of completion of the invocation") + + @classmethod + def build( + cls, + queue_item: SessionQueueItem, + invocation: AnyInvocation, + intermediate_state: PipelineIntermediateState, + progress_image: ProgressImage, + ) -> "InvocationDenoiseProgressEvent": + step = intermediate_state.step + total_steps = intermediate_state.total_steps + order = intermediate_state.order + return cls( + queue_id=queue_item.queue_id, + item_id=queue_item.item_id, + batch_id=queue_item.batch_id, + session_id=queue_item.session_id, + invocation=invocation, + invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id], + progress_image=progress_image, + step=step, + total_steps=total_steps, + order=order, + percentage=cls.calc_percentage(step, total_steps, order), + ) + + @staticmethod + def calc_percentage(step: int, total_steps: int, scheduler_order: float) -> float: + """Calculate the percentage of completion of denoising.""" + if total_steps == 0: + return 0.0 + if scheduler_order == 2: + return floor((step + 1 + 1) / 2) / floor((total_steps + 1) / 2) + # order == 1 + return (step + 1 + 1) / (total_steps + 1) + + +@payload_schema.register +class InvocationCompleteEvent(InvocationEventBase): + """Event model for invocation_complete""" + + __event_name__ = "invocation_complete" + + result: AnyInvocationOutput = Field(description="The result of the invocation") + + @classmethod + def build( + cls, queue_item: SessionQueueItem, invocation: AnyInvocation, result: AnyInvocationOutput + ) -> "InvocationCompleteEvent": + return cls( + queue_id=queue_item.queue_id, + item_id=queue_item.item_id, + batch_id=queue_item.batch_id, + session_id=queue_item.session_id, + invocation=invocation, + invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id], + result=result, + ) + + +@payload_schema.register +class InvocationErrorEvent(InvocationEventBase): + """Event model for invocation_error""" + + __event_name__ = "invocation_error" + + error_type: str = Field(description="The error type") + error_message: str = Field(description="The error message") + error_traceback: str = Field(description="The error traceback") + user_id: Optional[str] = Field(default=None, description="The ID of the user who created the invocation") + project_id: Optional[str] = Field(default=None, description="The ID of the user who created the invocation") + + @classmethod + def build( + cls, + queue_item: SessionQueueItem, + invocation: AnyInvocation, + error_type: str, + error_message: str, + error_traceback: str, + ) -> "InvocationErrorEvent": + return cls( + queue_id=queue_item.queue_id, + item_id=queue_item.item_id, + batch_id=queue_item.batch_id, + session_id=queue_item.session_id, + invocation=invocation, + invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id], + error_type=error_type, + error_message=error_message, + error_traceback=error_traceback, + user_id=getattr(queue_item, "user_id", None), + project_id=getattr(queue_item, "project_id", None), + ) + + +@payload_schema.register +class QueueItemStatusChangedEvent(QueueItemEventBase): + """Event model for queue_item_status_changed""" + + __event_name__ = "queue_item_status_changed" + + status: QUEUE_ITEM_STATUS = Field(description="The new status of the queue item") + error_type: Optional[str] = Field(default=None, description="The error type, if any") + error_message: Optional[str] = Field(default=None, description="The error message, if any") + error_traceback: Optional[str] = Field(default=None, description="The error traceback, if any") + created_at: Optional[str] = Field(default=None, description="The timestamp when the queue item was created") + updated_at: Optional[str] = Field(default=None, description="The timestamp when the queue item was last updated") + started_at: Optional[str] = Field(default=None, description="The timestamp when the queue item was started") + completed_at: Optional[str] = Field(default=None, description="The timestamp when the queue item was completed") + batch_status: BatchStatus = Field(description="The status of the batch") + queue_status: SessionQueueStatus = Field(description="The status of the queue") + session_id: str = Field(description="The ID of the session (aka graph execution state)") + + @classmethod + def build( + cls, queue_item: SessionQueueItem, batch_status: BatchStatus, queue_status: SessionQueueStatus + ) -> "QueueItemStatusChangedEvent": + return cls( + queue_id=queue_item.queue_id, + item_id=queue_item.item_id, + batch_id=queue_item.batch_id, + session_id=queue_item.session_id, + status=queue_item.status, + error_type=queue_item.error_type, + error_message=queue_item.error_message, + error_traceback=queue_item.error_traceback, + created_at=str(queue_item.created_at) if queue_item.created_at else None, + updated_at=str(queue_item.updated_at) if queue_item.updated_at else None, + started_at=str(queue_item.started_at) if queue_item.started_at else None, + completed_at=str(queue_item.completed_at) if queue_item.completed_at else None, + batch_status=batch_status, + queue_status=queue_status, + ) + + +@payload_schema.register +class BatchEnqueuedEvent(QueueEventBase): + """Event model for batch_enqueued""" + + __event_name__ = "batch_enqueued" + + batch_id: str = Field(description="The ID of the batch") + enqueued: int = Field(description="The number of invocations enqueued") + requested: int = Field( + description="The number of invocations initially requested to be enqueued (may be less than enqueued if queue was full)" + ) + priority: int = Field(description="The priority of the batch") + + @classmethod + def build(cls, enqueue_result: EnqueueBatchResult) -> "BatchEnqueuedEvent": + return cls( + queue_id=enqueue_result.queue_id, + batch_id=enqueue_result.batch.batch_id, + enqueued=enqueue_result.enqueued, + requested=enqueue_result.requested, + priority=enqueue_result.priority, + ) + + +@payload_schema.register +class QueueClearedEvent(QueueEventBase): + """Event model for queue_cleared""" + + __event_name__ = "queue_cleared" + + @classmethod + def build(cls, queue_id: str) -> "QueueClearedEvent": + return cls(queue_id=queue_id) + + +class DownloadEventBase(EventBase): + """Base class for events associated with a download""" + + source: str = Field(description="The source of the download") + + +@payload_schema.register +class DownloadStartedEvent(DownloadEventBase): + """Event model for download_started""" + + __event_name__ = "download_started" + + download_path: str = Field(description="The local path where the download is saved") + + @classmethod + def build(cls, job: "DownloadJob") -> "DownloadStartedEvent": + assert job.download_path + return cls(source=str(job.source), download_path=job.download_path.as_posix()) + + +@payload_schema.register +class DownloadProgressEvent(DownloadEventBase): + """Event model for download_progress""" + + __event_name__ = "download_progress" + + download_path: str = Field(description="The local path where the download is saved") + current_bytes: int = Field(description="The number of bytes downloaded so far") + total_bytes: int = Field(description="The total number of bytes to be downloaded") + + @classmethod + def build(cls, job: "DownloadJob") -> "DownloadProgressEvent": + assert job.download_path + return cls( + source=str(job.source), + download_path=job.download_path.as_posix(), + current_bytes=job.bytes, + total_bytes=job.total_bytes, + ) + + +@payload_schema.register +class DownloadCompleteEvent(DownloadEventBase): + """Event model for download_complete""" + + __event_name__ = "download_complete" + + download_path: str = Field(description="The local path where the download is saved") + total_bytes: int = Field(description="The total number of bytes downloaded") + + @classmethod + def build(cls, job: "DownloadJob") -> "DownloadCompleteEvent": + assert job.download_path + return cls(source=str(job.source), download_path=job.download_path.as_posix(), total_bytes=job.total_bytes) + + +@payload_schema.register +class DownloadCancelledEvent(DownloadEventBase): + """Event model for download_cancelled""" + + __event_name__ = "download_cancelled" + + @classmethod + def build(cls, job: "DownloadJob") -> "DownloadCancelledEvent": + return cls(source=str(job.source)) + + +@payload_schema.register +class DownloadErrorEvent(DownloadEventBase): + """Event model for download_error""" + + __event_name__ = "download_error" + + error_type: str = Field(description="The type of error") + error: str = Field(description="The error message") + + @classmethod + def build(cls, job: "DownloadJob") -> "DownloadErrorEvent": + assert job.error_type + assert job.error + return cls(source=str(job.source), error_type=job.error_type, error=job.error) + + +class ModelEventBase(EventBase): + """Base class for events associated with a model""" + + +@payload_schema.register +class ModelLoadStartedEvent(ModelEventBase): + """Event model for model_load_started""" + + __event_name__ = "model_load_started" + + config: AnyModelConfig = Field(description="The model's config") + submodel_type: Optional[SubModelType] = Field(default=None, description="The submodel type, if any") + + @classmethod + def build(cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> "ModelLoadStartedEvent": + return cls(config=config, submodel_type=submodel_type) + + +@payload_schema.register +class ModelLoadCompleteEvent(ModelEventBase): + """Event model for model_load_complete""" + + __event_name__ = "model_load_complete" + + config: AnyModelConfig = Field(description="The model's config") + submodel_type: Optional[SubModelType] = Field(default=None, description="The submodel type, if any") + + @classmethod + def build(cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> "ModelLoadCompleteEvent": + return cls(config=config, submodel_type=submodel_type) + + +@payload_schema.register +class ModelInstallDownloadProgressEvent(ModelEventBase): + """Event model for model_install_download_progress""" + + __event_name__ = "model_install_download_progress" + + id: int = Field(description="The ID of the install job") + source: str = Field(description="Source of the model; local path, repo_id or url") + local_path: str = Field(description="Where model is downloading to") + bytes: int = Field(description="Number of bytes downloaded so far") + total_bytes: int = Field(description="Total size of download, including all files") + parts: list[dict[str, int | str]] = Field( + description="Progress of downloading URLs that comprise the model, if any" + ) + + @classmethod + def build(cls, job: "ModelInstallJob") -> "ModelInstallDownloadProgressEvent": + parts: list[dict[str, str | int]] = [ + { + "url": str(x.source), + "local_path": str(x.download_path), + "bytes": x.bytes, + "total_bytes": x.total_bytes, + } + for x in job.download_parts + ] + return cls( + id=job.id, + source=str(job.source), + local_path=job.local_path.as_posix(), + parts=parts, + bytes=job.bytes, + total_bytes=job.total_bytes, + ) + + +@payload_schema.register +class ModelInstallDownloadsCompleteEvent(ModelEventBase): + """Emitted once when an install job becomes active.""" + + __event_name__ = "model_install_downloads_complete" + + id: int = Field(description="The ID of the install job") + source: str = Field(description="Source of the model; local path, repo_id or url") + + @classmethod + def build(cls, job: "ModelInstallJob") -> "ModelInstallDownloadsCompleteEvent": + return cls(id=job.id, source=str(job.source)) + + +@payload_schema.register +class ModelInstallStartedEvent(ModelEventBase): + """Event model for model_install_started""" + + __event_name__ = "model_install_started" + + id: int = Field(description="The ID of the install job") + source: str = Field(description="Source of the model; local path, repo_id or url") + + @classmethod + def build(cls, job: "ModelInstallJob") -> "ModelInstallStartedEvent": + return cls(id=job.id, source=str(job.source)) + + +@payload_schema.register +class ModelInstallCompleteEvent(ModelEventBase): + """Event model for model_install_complete""" + + __event_name__ = "model_install_complete" + + id: int = Field(description="The ID of the install job") + source: str = Field(description="Source of the model; local path, repo_id or url") + key: str = Field(description="Model config record key") + total_bytes: Optional[int] = Field(description="Size of the model (may be None for installation of a local path)") + + @classmethod + def build(cls, job: "ModelInstallJob") -> "ModelInstallCompleteEvent": + assert job.config_out is not None + return cls(id=job.id, source=str(job.source), key=(job.config_out.key), total_bytes=job.total_bytes) + + +@payload_schema.register +class ModelInstallCancelledEvent(ModelEventBase): + """Event model for model_install_cancelled""" + + __event_name__ = "model_install_cancelled" + + id: int = Field(description="The ID of the install job") + source: str = Field(description="Source of the model; local path, repo_id or url") + + @classmethod + def build(cls, job: "ModelInstallJob") -> "ModelInstallCancelledEvent": + return cls(id=job.id, source=str(job.source)) + + +@payload_schema.register +class ModelInstallErrorEvent(ModelEventBase): + """Event model for model_install_error""" + + __event_name__ = "model_install_error" + + id: int = Field(description="The ID of the install job") + source: str = Field(description="Source of the model; local path, repo_id or url") + error_type: str = Field(description="The name of the exception") + error: str = Field(description="A text description of the exception") + + @classmethod + def build(cls, job: "ModelInstallJob") -> "ModelInstallErrorEvent": + assert job.error_type is not None + assert job.error is not None + return cls(id=job.id, source=str(job.source), error_type=job.error_type, error=job.error) + + +class BulkDownloadEventBase(EventBase): + """Base class for events associated with a bulk image download""" + + bulk_download_id: str = Field(description="The ID of the bulk image download") + bulk_download_item_id: str = Field(description="The ID of the bulk image download item") + bulk_download_item_name: str = Field(description="The name of the bulk image download item") + + +@payload_schema.register +class BulkDownloadStartedEvent(BulkDownloadEventBase): + """Event model for bulk_download_started""" + + __event_name__ = "bulk_download_started" + + @classmethod + def build( + cls, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str + ) -> "BulkDownloadStartedEvent": + return cls( + bulk_download_id=bulk_download_id, + bulk_download_item_id=bulk_download_item_id, + bulk_download_item_name=bulk_download_item_name, + ) + + +@payload_schema.register +class BulkDownloadCompleteEvent(BulkDownloadEventBase): + """Event model for bulk_download_complete""" + + __event_name__ = "bulk_download_complete" + + @classmethod + def build( + cls, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str + ) -> "BulkDownloadCompleteEvent": + return cls( + bulk_download_id=bulk_download_id, + bulk_download_item_id=bulk_download_item_id, + bulk_download_item_name=bulk_download_item_name, + ) + + +@payload_schema.register +class BulkDownloadErrorEvent(BulkDownloadEventBase): + """Event model for bulk_download_error""" + + __event_name__ = "bulk_download_error" + + error: str = Field(description="The error message") + + @classmethod + def build( + cls, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str, error: str + ) -> "BulkDownloadErrorEvent": + return cls( + bulk_download_id=bulk_download_id, + bulk_download_item_id=bulk_download_item_id, + bulk_download_item_name=bulk_download_item_name, + error=error, + ) diff --git a/invokeai/app/services/events/events_fastapievents.py b/invokeai/app/services/events/events_fastapievents.py new file mode 100644 index 0000000000..8279d3bb34 --- /dev/null +++ b/invokeai/app/services/events/events_fastapievents.py @@ -0,0 +1,47 @@ +# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) + +import asyncio +import threading +from queue import Empty, Queue + +from fastapi_events.dispatcher import dispatch + +from invokeai.app.services.events.events_common import ( + EventBase, +) + +from .events_base import EventServiceBase + + +class FastAPIEventService(EventServiceBase): + def __init__(self, event_handler_id: int) -> None: + self.event_handler_id = event_handler_id + self._queue = Queue[EventBase | None]() + self._stop_event = threading.Event() + asyncio.create_task(self._dispatch_from_queue(stop_event=self._stop_event)) + + super().__init__() + + def stop(self, *args, **kwargs): + self._stop_event.set() + self._queue.put(None) + + def dispatch(self, event: EventBase) -> None: + self._queue.put(event) + + async def _dispatch_from_queue(self, stop_event: threading.Event): + """Get events on from the queue and dispatch them, from the correct thread""" + while not stop_event.is_set(): + try: + event = self._queue.get(block=False) + if not event: # Probably stopping + continue + # Leave the payloads as live pydantic models + dispatch(event, middleware_id=self.event_handler_id, payload_schema_dump=False) + + except Empty: + await asyncio.sleep(0.1) + pass + + except asyncio.CancelledError as e: + raise e # Raise a proper error diff --git a/invokeai/app/services/image_files/image_files_base.py b/invokeai/app/services/image_files/image_files_base.py index f4036277b7..dc6609aa48 100644 --- a/invokeai/app/services/image_files/image_files_base.py +++ b/invokeai/app/services/image_files/image_files_base.py @@ -4,9 +4,6 @@ from typing import Optional from PIL.Image import Image as PILImageType -from invokeai.app.invocations.fields import MetadataField -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID - class ImageFileStorageBase(ABC): """Low-level service responsible for storing and retrieving image files.""" @@ -33,8 +30,9 @@ class ImageFileStorageBase(ABC): self, image: PILImageType, image_name: str, - metadata: Optional[MetadataField] = None, - workflow: Optional[WorkflowWithoutID] = None, + metadata: Optional[str] = None, + workflow: Optional[str] = None, + graph: Optional[str] = None, thumbnail_size: int = 256, ) -> None: """Saves an image and a 256x256 WEBP thumbnail. Returns a tuple of the image name, thumbnail name, and created timestamp.""" @@ -46,6 +44,11 @@ class ImageFileStorageBase(ABC): pass @abstractmethod - def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]: + def get_workflow(self, image_name: str) -> Optional[str]: """Gets the workflow of an image.""" pass + + @abstractmethod + def get_graph(self, image_name: str) -> Optional[str]: + """Gets the graph of an image.""" + pass diff --git a/invokeai/app/services/image_files/image_files_disk.py b/invokeai/app/services/image_files/image_files_disk.py index 35fa93f81c..15d0be31f8 100644 --- a/invokeai/app/services/image_files/image_files_disk.py +++ b/invokeai/app/services/image_files/image_files_disk.py @@ -7,9 +7,7 @@ from PIL import Image, PngImagePlugin from PIL.Image import Image as PILImageType from send2trash import send2trash -from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.invoker import Invoker -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail from .image_files_base import ImageFileStorageBase @@ -56,8 +54,9 @@ class DiskImageFileStorage(ImageFileStorageBase): self, image: PILImageType, image_name: str, - metadata: Optional[MetadataField] = None, - workflow: Optional[WorkflowWithoutID] = None, + metadata: Optional[str] = None, + workflow: Optional[str] = None, + graph: Optional[str] = None, thumbnail_size: int = 256, ) -> None: try: @@ -68,13 +67,14 @@ class DiskImageFileStorage(ImageFileStorageBase): info_dict = {} if metadata is not None: - metadata_json = metadata.model_dump_json() - info_dict["invokeai_metadata"] = metadata_json - pnginfo.add_text("invokeai_metadata", metadata_json) + info_dict["invokeai_metadata"] = metadata + pnginfo.add_text("invokeai_metadata", metadata) if workflow is not None: - workflow_json = workflow.model_dump_json() - info_dict["invokeai_workflow"] = workflow_json - pnginfo.add_text("invokeai_workflow", workflow_json) + info_dict["invokeai_workflow"] = workflow + pnginfo.add_text("invokeai_workflow", workflow) + if graph is not None: + info_dict["invokeai_graph"] = graph + pnginfo.add_text("invokeai_graph", graph) # When saving the image, the image object's info field is not populated. We need to set it image.info = info_dict @@ -129,11 +129,18 @@ class DiskImageFileStorage(ImageFileStorageBase): path = path if isinstance(path, Path) else Path(path) return path.exists() - def get_workflow(self, image_name: str) -> WorkflowWithoutID | None: + def get_workflow(self, image_name: str) -> str | None: image = self.get(image_name) workflow = image.info.get("invokeai_workflow", None) - if workflow is not None: - return WorkflowWithoutID.model_validate_json(workflow) + if isinstance(workflow, str): + return workflow + return None + + def get_graph(self, image_name: str) -> str | None: + image = self.get(image_name) + graph = image.info.get("invokeai_graph", None) + if isinstance(graph, str): + return graph return None def __validate_storage_folders(self) -> None: diff --git a/invokeai/app/services/image_records/image_records_base.py b/invokeai/app/services/image_records/image_records_base.py index 7b7b261eca..45c0705090 100644 --- a/invokeai/app/services/image_records/image_records_base.py +++ b/invokeai/app/services/image_records/image_records_base.py @@ -80,7 +80,7 @@ class ImageRecordStorageBase(ABC): starred: Optional[bool] = False, session_id: Optional[str] = None, node_id: Optional[str] = None, - metadata: Optional[MetadataField] = None, + metadata: Optional[str] = None, ) -> datetime: """Saves an image record.""" pass diff --git a/invokeai/app/services/image_records/image_records_sqlite.py b/invokeai/app/services/image_records/image_records_sqlite.py index 5b37913c8f..ef73e79fa1 100644 --- a/invokeai/app/services/image_records/image_records_sqlite.py +++ b/invokeai/app/services/image_records/image_records_sqlite.py @@ -328,10 +328,9 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): starred: Optional[bool] = False, session_id: Optional[str] = None, node_id: Optional[str] = None, - metadata: Optional[MetadataField] = None, + metadata: Optional[str] = None, ) -> datetime: try: - metadata_json = metadata.model_dump_json() if metadata is not None else None self._lock.acquire() self._cursor.execute( """--sql @@ -358,7 +357,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): height, node_id, session_id, - metadata_json, + metadata, is_intermediate, starred, has_workflow, diff --git a/invokeai/app/services/images/images_base.py b/invokeai/app/services/images/images_base.py index 42c4266774..9175fc4809 100644 --- a/invokeai/app/services/images/images_base.py +++ b/invokeai/app/services/images/images_base.py @@ -12,7 +12,6 @@ from invokeai.app.services.image_records.image_records_common import ( ) from invokeai.app.services.images.images_common import ImageDTO from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID class ImageServiceABC(ABC): @@ -51,8 +50,9 @@ class ImageServiceABC(ABC): session_id: Optional[str] = None, board_id: Optional[str] = None, is_intermediate: Optional[bool] = False, - metadata: Optional[MetadataField] = None, - workflow: Optional[WorkflowWithoutID] = None, + metadata: Optional[str] = None, + workflow: Optional[str] = None, + graph: Optional[str] = None, ) -> ImageDTO: """Creates an image, storing the file and its metadata.""" pass @@ -87,7 +87,12 @@ class ImageServiceABC(ABC): pass @abstractmethod - def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]: + def get_workflow(self, image_name: str) -> Optional[str]: + """Gets an image's workflow.""" + pass + + @abstractmethod + def get_graph(self, image_name: str) -> Optional[str]: """Gets an image's workflow.""" pass diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py index adeed73811..1206526bd5 100644 --- a/invokeai/app/services/images/images_default.py +++ b/invokeai/app/services/images/images_default.py @@ -5,7 +5,6 @@ from PIL.Image import Image as PILImageType from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.invoker import Invoker from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from ..image_files.image_files_common import ( ImageFileDeleteException, @@ -42,8 +41,9 @@ class ImageService(ImageServiceABC): session_id: Optional[str] = None, board_id: Optional[str] = None, is_intermediate: Optional[bool] = False, - metadata: Optional[MetadataField] = None, - workflow: Optional[WorkflowWithoutID] = None, + metadata: Optional[str] = None, + workflow: Optional[str] = None, + graph: Optional[str] = None, ) -> ImageDTO: if image_origin not in ResourceOrigin: raise InvalidOriginException @@ -64,7 +64,7 @@ class ImageService(ImageServiceABC): image_category=image_category, width=width, height=height, - has_workflow=workflow is not None, + has_workflow=workflow is not None or graph is not None, # Meta fields is_intermediate=is_intermediate, # Nullable fields @@ -75,7 +75,7 @@ class ImageService(ImageServiceABC): if board_id is not None: self.__invoker.services.board_image_records.add_image_to_board(board_id=board_id, image_name=image_name) self.__invoker.services.image_files.save( - image_name=image_name, image=image, metadata=metadata, workflow=workflow + image_name=image_name, image=image, metadata=metadata, workflow=workflow, graph=graph ) image_dto = self.get_dto(image_name) @@ -157,7 +157,7 @@ class ImageService(ImageServiceABC): self.__invoker.services.logger.error("Problem getting image metadata") raise e - def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]: + def get_workflow(self, image_name: str) -> Optional[str]: try: return self.__invoker.services.image_files.get_workflow(image_name) except ImageFileNotFoundException: @@ -167,6 +167,16 @@ class ImageService(ImageServiceABC): self.__invoker.services.logger.error("Problem getting image workflow") raise + def get_graph(self, image_name: str) -> Optional[str]: + try: + return self.__invoker.services.image_files.get_graph(image_name) + except ImageFileNotFoundException: + self.__invoker.services.logger.error("Image file not found") + raise + except Exception: + self.__invoker.services.logger.error("Problem getting image graph") + raise + def get_path(self, image_name: str, thumbnail: bool = False) -> str: try: return str(self.__invoker.services.image_files.get_path(image_name, thumbnail)) diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 0e1ec123ca..b23a3bb327 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -24,6 +24,7 @@ if TYPE_CHECKING: from .image_records.image_records_base import ImageRecordStorageBase from .images.images_base import ImageServiceABC from .invocation_cache.invocation_cache_base import InvocationCacheBase + from .invocation_stats.invocation_stats_base import InvocationStatsServiceBase from .model_images.model_images_base import ModelImageFileStorageBase from .model_manager.model_manager_base import ModelManagerServiceBase from .names.names_base import NameServiceBase @@ -56,6 +57,7 @@ class InvocationServices: session_processor: "SessionProcessorBase", invocation_cache: "InvocationCacheBase", names: "NameServiceBase", + performance_statistics: "InvocationStatsServiceBase", urls: "UrlServiceBase", workflow_records: "WorkflowRecordsStorageBase", tensors: "ObjectSerializerBase[torch.Tensor]", @@ -79,6 +81,7 @@ class InvocationServices: self.session_processor = session_processor self.invocation_cache = invocation_cache self.names = names + self.performance_statistics = performance_statistics self.urls = urls self.workflow_records = workflow_records self.tensors = tensors diff --git a/invokeai/app/services/model_install/__init__.py b/invokeai/app/services/model_install/__init__.py index 00a33c203e..941485a134 100644 --- a/invokeai/app/services/model_install/__init__.py +++ b/invokeai/app/services/model_install/__init__.py @@ -1,11 +1,13 @@ """Initialization file for model install service package.""" from .model_install_base import ( + ModelInstallServiceBase, +) +from .model_install_common import ( HFModelSource, InstallStatus, LocalModelSource, ModelInstallJob, - ModelInstallServiceBase, ModelSource, UnknownInstallJobException, URLModelSource, diff --git a/invokeai/app/services/model_install/model_install_base.py b/invokeai/app/services/model_install/model_install_base.py index 0ea901fb46..6ee671062d 100644 --- a/invokeai/app/services/model_install/model_install_base.py +++ b/invokeai/app/services/model_install/model_install_base.py @@ -1,244 +1,19 @@ # Copyright 2023 Lincoln D. Stein and the InvokeAI development team """Baseclass definitions for the model installer.""" -import re -import traceback from abc import ABC, abstractmethod -from enum import Enum from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Set, Union +from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel, Field, PrivateAttr, field_validator from pydantic.networks import AnyHttpUrl -from typing_extensions import Annotated from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.download import DownloadJob, DownloadQueueServiceBase +from invokeai.app.services.download import DownloadQueueServiceBase from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.invoker import Invoker +from invokeai.app.services.model_install.model_install_common import ModelInstallJob, ModelSource from invokeai.app.services.model_records import ModelRecordServiceBase -from invokeai.backend.model_manager import AnyModelConfig, ModelRepoVariant -from invokeai.backend.model_manager.config import ModelSourceType -from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata - - -class InstallStatus(str, Enum): - """State of an install job running in the background.""" - - WAITING = "waiting" # waiting to be dequeued - DOWNLOADING = "downloading" # downloading of model files in process - DOWNLOADS_DONE = "downloads_done" # downloading done, waiting to run - RUNNING = "running" # being processed - COMPLETED = "completed" # finished running - ERROR = "error" # terminated with an error message - CANCELLED = "cancelled" # terminated with an error message - - -class ModelInstallPart(BaseModel): - url: AnyHttpUrl - path: Path - bytes: int = 0 - total_bytes: int = 0 - - -class UnknownInstallJobException(Exception): - """Raised when the status of an unknown job is requested.""" - - -class StringLikeSource(BaseModel): - """ - Base class for model sources, implements functions that lets the source be sorted and indexed. - - These shenanigans let this stuff work: - - source1 = LocalModelSource(path='C:/users/mort/foo.safetensors') - mydict = {source1: 'model 1'} - assert mydict['C:/users/mort/foo.safetensors'] == 'model 1' - assert mydict[LocalModelSource(path='C:/users/mort/foo.safetensors')] == 'model 1' - - source2 = LocalModelSource(path=Path('C:/users/mort/foo.safetensors')) - assert source1 == source2 - assert source1 == 'C:/users/mort/foo.safetensors' - """ - - def __hash__(self) -> int: - """Return hash of the path field, for indexing.""" - return hash(str(self)) - - def __lt__(self, other: object) -> int: - """Return comparison of the stringified version, for sorting.""" - return str(self) < str(other) - - def __eq__(self, other: object) -> bool: - """Return equality on the stringified version.""" - if isinstance(other, Path): - return str(self) == other.as_posix() - else: - return str(self) == str(other) - - -class LocalModelSource(StringLikeSource): - """A local file or directory path.""" - - path: str | Path - inplace: Optional[bool] = False - type: Literal["local"] = "local" - - # these methods allow the source to be used in a string-like way, - # for example as an index into a dict - def __str__(self) -> str: - """Return string version of path when string rep needed.""" - return Path(self.path).as_posix() - - -class HFModelSource(StringLikeSource): - """ - A HuggingFace repo_id with optional variant, sub-folder and access token. - Note that the variant option, if not provided to the constructor, will default to fp16, which is - what people (almost) always want. - """ - - repo_id: str - variant: Optional[ModelRepoVariant] = ModelRepoVariant.FP16 - subfolder: Optional[Path] = None - access_token: Optional[str] = None - type: Literal["hf"] = "hf" - - @field_validator("repo_id") - @classmethod - def proper_repo_id(cls, v: str) -> str: # noqa D102 - if not re.match(r"^([.\w-]+/[.\w-]+)$", v): - raise ValueError(f"{v}: invalid repo_id format") - return v - - def __str__(self) -> str: - """Return string version of repoid when string rep needed.""" - base: str = self.repo_id - if self.variant: - base += f":{self.variant or ''}" - if self.subfolder: - base += f":{self.subfolder}" - return base - - -class URLModelSource(StringLikeSource): - """A generic URL point to a checkpoint file.""" - - url: AnyHttpUrl - access_token: Optional[str] = None - type: Literal["url"] = "url" - - def __str__(self) -> str: - """Return string version of the url when string rep needed.""" - return str(self.url) - - -ModelSource = Annotated[Union[LocalModelSource, HFModelSource, URLModelSource], Field(discriminator="type")] - -MODEL_SOURCE_TO_TYPE_MAP = { - URLModelSource: ModelSourceType.Url, - HFModelSource: ModelSourceType.HFRepoID, - LocalModelSource: ModelSourceType.Path, -} - - -class ModelInstallJob(BaseModel): - """Object that tracks the current status of an install request.""" - - id: int = Field(description="Unique ID for this job") - 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") - config_in: Dict[str, Any] = Field( - default_factory=dict, description="Configuration information (e.g. 'description') to apply to model." - ) - config_out: Optional[AnyModelConfig] = Field( - default=None, description="After successful installation, this will hold the configuration object." - ) - inplace: bool = Field( - default=False, description="Leave model in its current location; otherwise install under models directory" - ) - source: ModelSource = Field(description="Source (URL, repo_id, or local path) of model") - local_path: Path = Field(description="Path to locally-downloaded model; may be the same as the source") - bytes: int = Field( - default=0, description="For a remote model, the number of bytes downloaded so far (may not be available)" - ) - total_bytes: int = Field(default=0, description="Total size of the model to be installed") - source_metadata: Optional[AnyModelRepoMetadata] = Field( - default=None, description="Metadata provided by the model source" - ) - download_parts: Set[DownloadJob] = Field( - default_factory=set, description="Download jobs contributing to this install" - ) - error: Optional[str] = Field( - default=None, description="On an error condition, this field will contain the text of the exception" - ) - error_traceback: Optional[str] = Field( - default=None, description="On an error condition, this field will contain the exception traceback" - ) - # internal flags and transitory settings - _install_tmpdir: Optional[Path] = PrivateAttr(default=None) - _exception: Optional[Exception] = PrivateAttr(default=None) - - def set_error(self, e: Exception) -> None: - """Record the error and traceback from an exception.""" - self._exception = e - self.error = str(e) - self.error_traceback = self._format_error(e) - self.status = InstallStatus.ERROR - self.error_reason = self._exception.__class__.__name__ if self._exception else None - - def cancel(self) -> None: - """Call to cancel the job.""" - self.status = InstallStatus.CANCELLED - - @property - def error_type(self) -> Optional[str]: - """Class name of the exception that led to status==ERROR.""" - return self._exception.__class__.__name__ if self._exception else None - - def _format_error(self, exception: Exception) -> str: - """Error traceback.""" - return "".join(traceback.format_exception(exception)) - - @property - def cancelled(self) -> bool: - """Set status to CANCELLED.""" - return self.status == InstallStatus.CANCELLED - - @property - def errored(self) -> bool: - """Return true if job has errored.""" - return self.status == InstallStatus.ERROR - - @property - def waiting(self) -> bool: - """Return true if job is waiting to run.""" - return self.status == InstallStatus.WAITING - - @property - def downloading(self) -> bool: - """Return true if job is downloading.""" - return self.status == InstallStatus.DOWNLOADING - - @property - def downloads_done(self) -> bool: - """Return true if job's downloads ae done.""" - return self.status == InstallStatus.DOWNLOADS_DONE - - @property - def running(self) -> bool: - """Return true if job is running.""" - return self.status == InstallStatus.RUNNING - - @property - def complete(self) -> bool: - """Return true if job completed without errors.""" - return self.status == InstallStatus.COMPLETED - - @property - def in_terminal_state(self) -> bool: - """Return true if job is in a terminal state.""" - return self.status in [InstallStatus.COMPLETED, InstallStatus.ERROR, InstallStatus.CANCELLED] +from invokeai.backend.model_manager.config import AnyModelConfig class ModelInstallServiceBase(ABC): @@ -282,7 +57,7 @@ class ModelInstallServiceBase(ABC): @property @abstractmethod - def event_bus(self) -> Optional[EventServiceBase]: + def event_bus(self) -> Optional["EventServiceBase"]: """Return the event service base object associated with the installer.""" @abstractmethod diff --git a/invokeai/app/services/model_install/model_install_common.py b/invokeai/app/services/model_install/model_install_common.py new file mode 100644 index 0000000000..d42e7632f3 --- /dev/null +++ b/invokeai/app/services/model_install/model_install_common.py @@ -0,0 +1,233 @@ +import re +import traceback +from enum import Enum +from pathlib import Path +from typing import Any, Dict, Literal, Optional, Set, Union + +from pydantic import BaseModel, Field, PrivateAttr, field_validator +from pydantic.networks import AnyHttpUrl +from typing_extensions import Annotated + +from invokeai.app.services.download import DownloadJob +from invokeai.backend.model_manager import AnyModelConfig, ModelRepoVariant +from invokeai.backend.model_manager.config import ModelSourceType +from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata + + +class InstallStatus(str, Enum): + """State of an install job running in the background.""" + + WAITING = "waiting" # waiting to be dequeued + DOWNLOADING = "downloading" # downloading of model files in process + DOWNLOADS_DONE = "downloads_done" # downloading done, waiting to run + RUNNING = "running" # being processed + COMPLETED = "completed" # finished running + ERROR = "error" # terminated with an error message + CANCELLED = "cancelled" # terminated with an error message + + +class ModelInstallPart(BaseModel): + url: AnyHttpUrl + path: Path + bytes: int = 0 + total_bytes: int = 0 + + +class UnknownInstallJobException(Exception): + """Raised when the status of an unknown job is requested.""" + + +class StringLikeSource(BaseModel): + """ + Base class for model sources, implements functions that lets the source be sorted and indexed. + + These shenanigans let this stuff work: + + source1 = LocalModelSource(path='C:/users/mort/foo.safetensors') + mydict = {source1: 'model 1'} + assert mydict['C:/users/mort/foo.safetensors'] == 'model 1' + assert mydict[LocalModelSource(path='C:/users/mort/foo.safetensors')] == 'model 1' + + source2 = LocalModelSource(path=Path('C:/users/mort/foo.safetensors')) + assert source1 == source2 + assert source1 == 'C:/users/mort/foo.safetensors' + """ + + def __hash__(self) -> int: + """Return hash of the path field, for indexing.""" + return hash(str(self)) + + def __lt__(self, other: object) -> int: + """Return comparison of the stringified version, for sorting.""" + return str(self) < str(other) + + def __eq__(self, other: object) -> bool: + """Return equality on the stringified version.""" + if isinstance(other, Path): + return str(self) == other.as_posix() + else: + return str(self) == str(other) + + +class LocalModelSource(StringLikeSource): + """A local file or directory path.""" + + path: str | Path + inplace: Optional[bool] = False + type: Literal["local"] = "local" + + # these methods allow the source to be used in a string-like way, + # for example as an index into a dict + def __str__(self) -> str: + """Return string version of path when string rep needed.""" + return Path(self.path).as_posix() + + +class HFModelSource(StringLikeSource): + """ + A HuggingFace repo_id with optional variant, sub-folder and access token. + Note that the variant option, if not provided to the constructor, will default to fp16, which is + what people (almost) always want. + """ + + repo_id: str + variant: Optional[ModelRepoVariant] = ModelRepoVariant.FP16 + subfolder: Optional[Path] = None + access_token: Optional[str] = None + type: Literal["hf"] = "hf" + + @field_validator("repo_id") + @classmethod + def proper_repo_id(cls, v: str) -> str: # noqa D102 + if not re.match(r"^([.\w-]+/[.\w-]+)$", v): + raise ValueError(f"{v}: invalid repo_id format") + return v + + def __str__(self) -> str: + """Return string version of repoid when string rep needed.""" + base: str = self.repo_id + if self.variant: + base += f":{self.variant or ''}" + if self.subfolder: + base += f":{self.subfolder}" + return base + + +class URLModelSource(StringLikeSource): + """A generic URL point to a checkpoint file.""" + + url: AnyHttpUrl + access_token: Optional[str] = None + type: Literal["url"] = "url" + + def __str__(self) -> str: + """Return string version of the url when string rep needed.""" + return str(self.url) + + +ModelSource = Annotated[Union[LocalModelSource, HFModelSource, URLModelSource], Field(discriminator="type")] + +MODEL_SOURCE_TO_TYPE_MAP = { + URLModelSource: ModelSourceType.Url, + HFModelSource: ModelSourceType.HFRepoID, + LocalModelSource: ModelSourceType.Path, +} + + +class ModelInstallJob(BaseModel): + """Object that tracks the current status of an install request.""" + + id: int = Field(description="Unique ID for this job") + 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") + config_in: Dict[str, Any] = Field( + default_factory=dict, description="Configuration information (e.g. 'description') to apply to model." + ) + config_out: Optional[AnyModelConfig] = Field( + default=None, description="After successful installation, this will hold the configuration object." + ) + inplace: bool = Field( + default=False, description="Leave model in its current location; otherwise install under models directory" + ) + source: ModelSource = Field(description="Source (URL, repo_id, or local path) of model") + local_path: Path = Field(description="Path to locally-downloaded model; may be the same as the source") + bytes: int = Field( + default=0, description="For a remote model, the number of bytes downloaded so far (may not be available)" + ) + total_bytes: int = Field(default=0, description="Total size of the model to be installed") + source_metadata: Optional[AnyModelRepoMetadata] = Field( + default=None, description="Metadata provided by the model source" + ) + download_parts: Set[DownloadJob] = Field( + default_factory=set, description="Download jobs contributing to this install" + ) + error: Optional[str] = Field( + default=None, description="On an error condition, this field will contain the text of the exception" + ) + error_traceback: Optional[str] = Field( + default=None, description="On an error condition, this field will contain the exception traceback" + ) + # internal flags and transitory settings + _install_tmpdir: Optional[Path] = PrivateAttr(default=None) + _exception: Optional[Exception] = PrivateAttr(default=None) + + def set_error(self, e: Exception) -> None: + """Record the error and traceback from an exception.""" + self._exception = e + self.error = str(e) + self.error_traceback = self._format_error(e) + self.status = InstallStatus.ERROR + self.error_reason = self._exception.__class__.__name__ if self._exception else None + + def cancel(self) -> None: + """Call to cancel the job.""" + self.status = InstallStatus.CANCELLED + + @property + def error_type(self) -> Optional[str]: + """Class name of the exception that led to status==ERROR.""" + return self._exception.__class__.__name__ if self._exception else None + + def _format_error(self, exception: Exception) -> str: + """Error traceback.""" + return "".join(traceback.format_exception(exception)) + + @property + def cancelled(self) -> bool: + """Set status to CANCELLED.""" + return self.status == InstallStatus.CANCELLED + + @property + def errored(self) -> bool: + """Return true if job has errored.""" + return self.status == InstallStatus.ERROR + + @property + def waiting(self) -> bool: + """Return true if job is waiting to run.""" + return self.status == InstallStatus.WAITING + + @property + def downloading(self) -> bool: + """Return true if job is downloading.""" + return self.status == InstallStatus.DOWNLOADING + + @property + def downloads_done(self) -> bool: + """Return true if job's downloads ae done.""" + return self.status == InstallStatus.DOWNLOADS_DONE + + @property + def running(self) -> bool: + """Return true if job is running.""" + return self.status == InstallStatus.RUNNING + + @property + def complete(self) -> bool: + """Return true if job completed without errors.""" + return self.status == InstallStatus.COMPLETED + + @property + def in_terminal_state(self) -> bool: + """Return true if job is in a terminal state.""" + return self.status in [InstallStatus.COMPLETED, InstallStatus.ERROR, InstallStatus.CANCELLED] diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index 6eb9549ef0..df060caff3 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -10,7 +10,7 @@ from pathlib import Path from queue import Empty, Queue from shutil import copyfile, copytree, move, rmtree from tempfile import mkdtemp -from typing import Any, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union import torch import yaml @@ -20,8 +20,8 @@ from requests import Session from invokeai.app.services.config import InvokeAIAppConfig from invokeai.app.services.download import DownloadJob, DownloadQueueServiceBase, TqdmProgress -from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.invoker import Invoker +from invokeai.app.services.model_install.model_install_base import ModelInstallServiceBase from invokeai.app.services.model_records import DuplicateModelException, ModelRecordServiceBase from invokeai.app.services.model_records.model_records_base import ModelRecordChanges from invokeai.backend.model_manager.config import ( @@ -45,13 +45,12 @@ from invokeai.backend.util import InvokeAILogger from invokeai.backend.util.catch_sigint import catch_sigint from invokeai.backend.util.devices import TorchDevice -from .model_install_base import ( +from .model_install_common import ( MODEL_SOURCE_TO_TYPE_MAP, HFModelSource, InstallStatus, LocalModelSource, ModelInstallJob, - ModelInstallServiceBase, ModelSource, StringLikeSource, URLModelSource, @@ -59,6 +58,9 @@ from .model_install_base import ( TMPDIR_PREFIX = "tmpinstall_" +if TYPE_CHECKING: + from invokeai.app.services.events.events_base import EventServiceBase + class ModelInstallService(ModelInstallServiceBase): """class for InvokeAI model installation.""" @@ -68,7 +70,7 @@ class ModelInstallService(ModelInstallServiceBase): app_config: InvokeAIAppConfig, record_store: ModelRecordServiceBase, download_queue: DownloadQueueServiceBase, - event_bus: Optional[EventServiceBase] = None, + event_bus: Optional["EventServiceBase"] = None, session: Optional[Session] = None, ): """ @@ -104,7 +106,7 @@ class ModelInstallService(ModelInstallServiceBase): return self._record_store @property - def event_bus(self) -> Optional[EventServiceBase]: # noqa D102 + def event_bus(self) -> Optional["EventServiceBase"]: # noqa D102 return self._event_bus # make the invoker optional here because we don't need it and it @@ -855,35 +857,17 @@ class ModelInstallService(ModelInstallServiceBase): job.status = InstallStatus.RUNNING self._logger.info(f"Model install started: {job.source}") if self._event_bus: - self._event_bus.emit_model_install_running(str(job.source)) + self._event_bus.emit_model_install_started(job) def _signal_job_downloading(self, job: ModelInstallJob) -> None: if self._event_bus: - parts: List[Dict[str, str | int]] = [ - { - "url": str(x.source), - "local_path": str(x.download_path), - "bytes": x.bytes, - "total_bytes": x.total_bytes, - } - for x in job.download_parts - ] - assert job.bytes is not None - assert job.total_bytes is not None - self._event_bus.emit_model_install_downloading( - str(job.source), - local_path=job.local_path.as_posix(), - parts=parts, - bytes=job.bytes, - total_bytes=job.total_bytes, - id=job.id, - ) + self._event_bus.emit_model_install_download_progress(job) def _signal_job_downloads_done(self, job: ModelInstallJob) -> None: job.status = InstallStatus.DOWNLOADS_DONE self._logger.info(f"Model download complete: {job.source}") if self._event_bus: - self._event_bus.emit_model_install_downloads_done(str(job.source)) + self._event_bus.emit_model_install_downloads_complete(job) def _signal_job_completed(self, job: ModelInstallJob) -> None: job.status = InstallStatus.COMPLETED @@ -891,24 +875,19 @@ class ModelInstallService(ModelInstallServiceBase): self._logger.info(f"Model install complete: {job.source}") self._logger.debug(f"{job.local_path} registered key {job.config_out.key}") if self._event_bus: - assert job.local_path is not None - assert job.config_out is not None - key = job.config_out.key - self._event_bus.emit_model_install_completed(str(job.source), key, id=job.id) + self._event_bus.emit_model_install_complete(job) def _signal_job_errored(self, job: ModelInstallJob) -> None: self._logger.error(f"Model install error: {job.source}\n{job.error_type}: {job.error}") if self._event_bus: - error_type = job.error_type - error = job.error - assert error_type is not None - assert error is not None - self._event_bus.emit_model_install_error(str(job.source), error_type, error, id=job.id) + assert job.error_type is not None + assert job.error is not None + self._event_bus.emit_model_install_error(job) def _signal_job_cancelled(self, job: ModelInstallJob) -> None: self._logger.info(f"Model install canceled: {job.source}") if self._event_bus: - self._event_bus.emit_model_install_cancelled(str(job.source), id=job.id) + self._event_bus.emit_model_install_cancelled(job) @staticmethod def get_fetcher_from_url(url: str) -> ModelMetadataFetchBase: diff --git a/invokeai/app/services/model_load/model_load_base.py b/invokeai/app/services/model_load/model_load_base.py index cdd59f4e74..f0bb3de08a 100644 --- a/invokeai/app/services/model_load/model_load_base.py +++ b/invokeai/app/services/model_load/model_load_base.py @@ -4,7 +4,6 @@ from abc import ABC, abstractmethod from typing import Optional -from invokeai.app.services.shared.invocation_context import InvocationContextData from invokeai.backend.model_manager import AnyModel, AnyModelConfig, SubModelType from invokeai.backend.model_manager.load import LoadedModel from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase @@ -15,18 +14,12 @@ class ModelLoadServiceBase(ABC): """Wrapper around AnyModelLoader.""" @abstractmethod - def load_model( - self, - model_config: AnyModelConfig, - submodel_type: Optional[SubModelType] = None, - context_data: Optional[InvocationContextData] = None, - ) -> LoadedModel: + def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: """ Given a model's configuration, load it and return the LoadedModel object. :param model_config: Model configuration record (as returned by ModelRecordBase.get_model()) :param submodel: For main (pipeline models), the submodel to fetch. - :param context_data: Invocation context data used for event reporting """ @property diff --git a/invokeai/app/services/model_load/model_load_default.py b/invokeai/app/services/model_load/model_load_default.py index 42d1e74543..0e7e756b24 100644 --- a/invokeai/app/services/model_load/model_load_default.py +++ b/invokeai/app/services/model_load/model_load_default.py @@ -5,7 +5,6 @@ from typing import Optional, Type from invokeai.app.services.config import InvokeAIAppConfig from invokeai.app.services.invoker import Invoker -from invokeai.app.services.shared.invocation_context import InvocationContextData from invokeai.backend.model_manager import AnyModel, AnyModelConfig, SubModelType from invokeai.backend.model_manager.load import ( LoadedModel, @@ -57,25 +56,18 @@ class ModelLoadService(ModelLoadServiceBase): """Return the checkpoint convert cache used by this loader.""" return self._convert_cache - def load_model( - self, - model_config: AnyModelConfig, - submodel_type: Optional[SubModelType] = None, - context_data: Optional[InvocationContextData] = None, - ) -> LoadedModel: + def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: """ Given a model's configuration, load it and return the LoadedModel object. :param model_config: Model configuration record (as returned by ModelRecordBase.get_model()) :param submodel: For main (pipeline models), the submodel to fetch. - :param context: Invocation context used for event reporting """ - if context_data: - self._emit_load_event( - context_data=context_data, - model_config=model_config, - submodel_type=submodel_type, - ) + + # We don't have an invoker during testing + # TODO(psyche): Mock this method on the invoker in the tests + if hasattr(self, "_invoker"): + self._invoker.services.events.emit_model_load_started(model_config, submodel_type) implementation, model_config, submodel_type = self._registry.get_implementation(model_config, submodel_type) # type: ignore loaded_model: LoadedModel = implementation( @@ -85,40 +77,7 @@ class ModelLoadService(ModelLoadServiceBase): convert_cache=self._convert_cache, ).load_model(model_config, submodel_type) - if context_data: - self._emit_load_event( - context_data=context_data, - model_config=model_config, - submodel_type=submodel_type, - loaded=True, - ) + if hasattr(self, "_invoker"): + self._invoker.services.events.emit_model_load_complete(model_config, submodel_type) + return loaded_model - - def _emit_load_event( - self, - context_data: InvocationContextData, - model_config: AnyModelConfig, - loaded: Optional[bool] = False, - submodel_type: Optional[SubModelType] = None, - ) -> None: - if not self._invoker: - return - - if not loaded: - self._invoker.services.events.emit_model_load_started( - queue_id=context_data.queue_item.queue_id, - queue_item_id=context_data.queue_item.item_id, - queue_batch_id=context_data.queue_item.batch_id, - graph_execution_state_id=context_data.queue_item.session_id, - model_config=model_config, - submodel_type=submodel_type, - ) - else: - self._invoker.services.events.emit_model_load_completed( - queue_id=context_data.queue_item.queue_id, - queue_item_id=context_data.queue_item.item_id, - queue_batch_id=context_data.queue_item.batch_id, - graph_execution_state_id=context_data.queue_item.session_id, - model_config=model_config, - submodel_type=submodel_type, - ) diff --git a/invokeai/app/services/session_processor/session_processor_base.py b/invokeai/app/services/session_processor/session_processor_base.py index 485ef2f8c3..15611bb5f8 100644 --- a/invokeai/app/services/session_processor/session_processor_base.py +++ b/invokeai/app/services/session_processor/session_processor_base.py @@ -1,6 +1,49 @@ from abc import ABC, abstractmethod +from threading import Event +from typing import Optional, Protocol +from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput +from invokeai.app.services.invocation_services import InvocationServices from invokeai.app.services.session_processor.session_processor_common import SessionProcessorStatus +from invokeai.app.services.session_queue.session_queue_common import SessionQueueItem +from invokeai.app.util.profiler import Profiler + + +class SessionRunnerBase(ABC): + """ + Base class for session runner. + """ + + @abstractmethod + def start(self, services: InvocationServices, cancel_event: Event, profiler: Optional[Profiler] = None) -> None: + """Starts the session runner. + + Args: + services: The invocation services. + cancel_event: The cancel event. + profiler: The profiler to use for session profiling via cProfile. Omit to disable profiling. Basic session + stats will be still be recorded and logged when profiling is disabled. + """ + pass + + @abstractmethod + def run(self, queue_item: SessionQueueItem) -> None: + """Runs a session. + + Args: + queue_item: The session to run. + """ + pass + + @abstractmethod + def run_node(self, invocation: BaseInvocation, queue_item: SessionQueueItem) -> None: + """Run a single node in the graph. + + Args: + invocation: The invocation to run. + queue_item: The session queue item. + """ + pass class SessionProcessorBase(ABC): @@ -26,3 +69,85 @@ class SessionProcessorBase(ABC): def get_status(self) -> SessionProcessorStatus: """Gets the status of the session processor""" pass + + +class OnBeforeRunNode(Protocol): + def __call__(self, invocation: BaseInvocation, queue_item: SessionQueueItem) -> None: + """Callback to run before executing a node. + + Args: + invocation: The invocation that will be executed. + queue_item: The session queue item. + """ + ... + + +class OnAfterRunNode(Protocol): + def __call__(self, invocation: BaseInvocation, queue_item: SessionQueueItem, output: BaseInvocationOutput) -> None: + """Callback to run before executing a node. + + Args: + invocation: The invocation that was executed. + queue_item: The session queue item. + """ + ... + + +class OnNodeError(Protocol): + def __call__( + self, + invocation: BaseInvocation, + queue_item: SessionQueueItem, + error_type: str, + error_message: str, + error_traceback: str, + ) -> None: + """Callback to run when a node has an error. + + Args: + invocation: The invocation that errored. + queue_item: The session queue item. + error_type: The type of error, e.g. "ValueError". + error_message: The error message, e.g. "Invalid value". + error_traceback: The stringified error traceback. + """ + ... + + +class OnBeforeRunSession(Protocol): + def __call__(self, queue_item: SessionQueueItem) -> None: + """Callback to run before executing a session. + + Args: + queue_item: The session queue item. + """ + ... + + +class OnAfterRunSession(Protocol): + def __call__(self, queue_item: SessionQueueItem) -> None: + """Callback to run after executing a session. + + Args: + queue_item: The session queue item. + """ + ... + + +class OnNonFatalProcessorError(Protocol): + def __call__( + self, + queue_item: Optional[SessionQueueItem], + error_type: str, + error_message: str, + error_traceback: str, + ) -> None: + """Callback to run when a non-fatal error occurs in the processor. + + Args: + queue_item: The session queue item, if one was being executed when the error occurred. + error_type: The type of error, e.g. "ValueError". + error_message: The error message, e.g. "Invalid value". + error_traceback: The stringified error traceback. + """ + ... diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index 02860f46f1..916df83261 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -1,33 +1,334 @@ import traceback from contextlib import suppress from queue import Queue -from threading import BoundedSemaphore, Lock, Thread +from threading import BoundedSemaphore, Thread, Lock from threading import Event as ThreadEvent from typing import Optional, Set -from fastapi_events.handlers.local import local_handler -from fastapi_events.typing import Event as FastAPIEvent - -from invokeai.app.invocations.baseinvocation import BaseInvocation -from invokeai.app.services.events.events_base import EventServiceBase +from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput +from invokeai.app.services.events.events_common import ( + BatchEnqueuedEvent, + FastAPIEvent, + QueueClearedEvent, + QueueItemStatusChangedEvent, + register_events, +) from invokeai.app.services.invocation_stats.invocation_stats_common import GESStatsNotFoundError -from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService +from invokeai.app.services.session_processor.session_processor_base import ( + OnAfterRunNode, + OnAfterRunSession, + OnBeforeRunNode, + OnBeforeRunSession, + OnNodeError, + OnNonFatalProcessorError, +) from invokeai.app.services.session_processor.session_processor_common import CanceledException -from invokeai.app.services.session_queue.session_queue_common import SessionQueueItem +from invokeai.app.services.session_queue.session_queue_common import SessionQueueItem, SessionQueueItemNotFoundError +from invokeai.app.services.shared.graph import NodeInputError from invokeai.app.services.shared.invocation_context import InvocationContextData, build_invocation_context from invokeai.app.util.profiler import Profiler from invokeai.backend.util.devices import TorchDevice from ..invoker import Invoker -from .session_processor_base import SessionProcessorBase +from .session_processor_base import InvocationServices, SessionProcessorBase, SessionRunnerBase from .session_processor_common import SessionProcessorStatus +class DefaultSessionRunner(SessionRunnerBase): + """Processes a single session's invocations.""" + + def __init__( + self, + on_before_run_session_callbacks: Optional[list[OnBeforeRunSession]] = None, + on_before_run_node_callbacks: Optional[list[OnBeforeRunNode]] = None, + on_after_run_node_callbacks: Optional[list[OnAfterRunNode]] = None, + on_node_error_callbacks: Optional[list[OnNodeError]] = None, + on_after_run_session_callbacks: Optional[list[OnAfterRunSession]] = None, + ): + """ + Args: + on_before_run_session_callbacks: Callbacks to run before the session starts. + on_before_run_node_callbacks: Callbacks to run before each node starts. + on_after_run_node_callbacks: Callbacks to run after each node completes. + on_node_error_callbacks: Callbacks to run when a node errors. + on_after_run_session_callbacks: Callbacks to run after the session completes. + """ + + self._on_before_run_session_callbacks = on_before_run_session_callbacks or [] + self._on_before_run_node_callbacks = on_before_run_node_callbacks or [] + self._on_after_run_node_callbacks = on_after_run_node_callbacks or [] + self._on_node_error_callbacks = on_node_error_callbacks or [] + self._on_after_run_session_callbacks = on_after_run_session_callbacks or [] + self._process_lock = Lock() + + def start(self, services: InvocationServices, cancel_event: ThreadEvent, profiler: Optional[Profiler] = None) -> None: + self._services = services + self._cancel_event = cancel_event + self._profiler = profiler + + def _is_canceled(self) -> bool: + """Check if the cancel event is set. This is also passed to the invocation context builder and called during + denoising to check if the session has been canceled.""" + return self._cancel_event.is_set() + + def run(self, queue_item: SessionQueueItem): + # Exceptions raised outside `run_node` are handled by the processor. There is no need to catch them here. + + self._on_before_run_session(queue_item=queue_item) + + # Loop over invocations until the session is complete or canceled + while True: + try: + with self._process_lock: + invocation = queue_item.session.next() + # Anything other than a `NodeInputError` is handled as a processor error + except NodeInputError as e: + error_type = e.__class__.__name__ + error_message = str(e) + error_traceback = traceback.format_exc() + self._on_node_error( + invocation=e.node, + queue_item=queue_item, + error_type=error_type, + error_message=error_message, + error_traceback=error_traceback, + ) + break + + if invocation is None or self._is_canceled(): + break + + self.run_node(invocation, queue_item) + + # The session is complete if all invocations have been run or there is an error on the session. + # At this time, the queue item may be canceled, but the object itself here won't be updated yet. We must + # use the cancel event to check if the session is canceled. + if ( + queue_item.session.is_complete() + or self._is_canceled() + or queue_item.status in ["failed", "canceled", "completed"] + ): + break + + self._on_after_run_session(queue_item=queue_item) + + def run_node(self, invocation: BaseInvocation, queue_item: SessionQueueItem) -> None: + try: + # Any unhandled exception in this scope is an invocation error & will fail the graph + with self._services.performance_statistics.collect_stats(invocation, queue_item.session_id): + self._on_before_run_node(invocation, queue_item) + + data = InvocationContextData( + invocation=invocation, + source_invocation_id=queue_item.session.prepared_source_mapping[invocation.id], + queue_item=queue_item, + ) + context = build_invocation_context( + data=data, + services=self._services, + is_canceled=self._is_canceled, + ) + + # Invoke the node + output = invocation.invoke_internal(context=context, services=self._services) + # Save output and history + queue_item.session.complete(invocation.id, output) + + self._on_after_run_node(invocation, queue_item, output) + + except KeyboardInterrupt: + # TODO(psyche): This is expected to be caught in the main thread. Do we need to catch this here? + pass + except CanceledException: + # A CanceledException is raised during the denoising step callback if the cancel event is set. We don't need + # to do any handling here, and no error should be set - just pass and the cancellation will be handled + # correctly in the next iteration of the session runner loop. + # + # See the comment in the processor's `_on_queue_item_status_changed()` method for more details on how we + # handle cancellation. + pass + except Exception as e: + error_type = e.__class__.__name__ + error_message = str(e) + error_traceback = traceback.format_exc() + self._on_node_error( + invocation=invocation, + queue_item=queue_item, + error_type=error_type, + error_message=error_message, + error_traceback=error_traceback, + ) + + def _on_before_run_session(self, queue_item: SessionQueueItem) -> None: + """Called before a session is run. + + - Start the profiler if profiling is enabled. + - Run any callbacks registered for this event. + """ + + self._services.logger.debug( + f"On before run session: queue item {queue_item.item_id}, session {queue_item.session_id}" + ) + + # If profiling is enabled, start the profiler + if self._profiler is not None: + self._profiler.start(profile_id=queue_item.session_id) + + for callback in self._on_before_run_session_callbacks: + callback(queue_item=queue_item) + + def _on_after_run_session(self, queue_item: SessionQueueItem) -> None: + """Called after a session is run. + + - Stop the profiler if profiling is enabled. + - Update the queue item's session object in the database. + - If not already canceled or failed, complete the queue item. + - Log and reset performance statistics. + - Run any callbacks registered for this event. + """ + + self._services.logger.debug( + f"On after run session: queue item {queue_item.item_id}, session {queue_item.session_id}" + ) + + # If we are profiling, stop the profiler and dump the profile & stats + if self._profiler is not None: + profile_path = self._profiler.stop() + stats_path = profile_path.with_suffix(".json") + self._services.performance_statistics.dump_stats( + graph_execution_state_id=queue_item.session.id, output_path=stats_path + ) + + try: + # Update the queue item with the completed session. If the queue item has been removed from the queue, + # we'll get a SessionQueueItemNotFoundError and we can ignore it. This can happen if the queue is cleared + # while the session is running. + queue_item = self._services.session_queue.set_queue_item_session(queue_item.item_id, queue_item.session) + + # The queue item may have been canceled or failed while the session was running. We should only complete it + # if it is not already canceled or failed. + if queue_item.status not in ["canceled", "failed"]: + queue_item = self._services.session_queue.complete_queue_item(queue_item.item_id) + + # We'll get a GESStatsNotFoundError if we try to log stats for an untracked graph, but in the processor + # we don't care about that - suppress the error. + with suppress(GESStatsNotFoundError): + self._services.performance_statistics.log_stats(queue_item.session.id) + self._services.performance_statistics.reset_stats() + + for callback in self._on_after_run_session_callbacks: + callback(queue_item=queue_item) + except SessionQueueItemNotFoundError: + pass + + def _on_before_run_node(self, invocation: BaseInvocation, queue_item: SessionQueueItem): + """Called before a node is run. + + - Emits an invocation started event. + - Run any callbacks registered for this event. + """ + + self._services.logger.debug( + f"On before run node: queue item {queue_item.item_id}, session {queue_item.session_id}, node {invocation.id} ({invocation.get_type()})" + ) + + # Send starting event + self._services.events.emit_invocation_started(queue_item=queue_item, invocation=invocation) + + for callback in self._on_before_run_node_callbacks: + callback(invocation=invocation, queue_item=queue_item) + + def _on_after_run_node( + self, invocation: BaseInvocation, queue_item: SessionQueueItem, output: BaseInvocationOutput + ): + """Called after a node is run. + + - Emits an invocation complete event. + - Run any callbacks registered for this event. + """ + + self._services.logger.debug( + f"On after run node: queue item {queue_item.item_id}, session {queue_item.session_id}, node {invocation.id} ({invocation.get_type()})" + ) + + # Send complete event on successful runs + self._services.events.emit_invocation_complete(invocation=invocation, queue_item=queue_item, output=output) + + for callback in self._on_after_run_node_callbacks: + callback(invocation=invocation, queue_item=queue_item, output=output) + + def _on_node_error( + self, + invocation: BaseInvocation, + queue_item: SessionQueueItem, + error_type: str, + error_message: str, + error_traceback: str, + ): + """Called when a node errors. Node errors may occur when running or preparing the node.. + + - Set the node error on the session object. + - Log the error. + - Fail the queue item. + - Emits an invocation error event. + - Run any callbacks registered for this event. + """ + + self._services.logger.debug( + f"On node error: queue item {queue_item.item_id}, session {queue_item.session_id}, node {invocation.id} ({invocation.get_type()})" + ) + + # Node errors do not get the full traceback. Only the queue item gets the full traceback. + node_error = f"{error_type}: {error_message}" + queue_item.session.set_node_error(invocation.id, node_error) + self._services.logger.error( + f"Error while invoking session {queue_item.session_id}, invocation {invocation.id} ({invocation.get_type()}): {error_message}" + ) + self._services.logger.error(error_traceback) + + # Fail the queue item + queue_item = self._services.session_queue.set_queue_item_session(queue_item.item_id, queue_item.session) + queue_item = self._services.session_queue.fail_queue_item( + queue_item.item_id, error_type, error_message, error_traceback + ) + + # Send error event + self._services.events.emit_invocation_error( + queue_item=queue_item, + invocation=invocation, + error_type=error_type, + error_message=error_message, + error_traceback=error_traceback, + ) + + for callback in self._on_node_error_callbacks: + callback( + invocation=invocation, + queue_item=queue_item, + error_type=error_type, + error_message=error_message, + error_traceback=error_traceback, + ) + + class DefaultSessionProcessor(SessionProcessorBase): - def start(self, invoker: Invoker, polling_interval: int = 1) -> None: + def __init__( + self, + session_runner: Optional[SessionRunnerBase] = None, + on_non_fatal_processor_error_callbacks: Optional[list[OnNonFatalProcessorError]] = None, + thread_limit: int = 1, + polling_interval: int = 1, + ) -> None: + super().__init__() + + self.session_runner = session_runner if session_runner else DefaultSessionRunner() + self._on_non_fatal_processor_error_callbacks = on_non_fatal_processor_error_callbacks or [] + self._thread_limit = thread_limit + self._polling_interval = polling_interval + + def start(self, invoker: Invoker) -> None: self._invoker: Invoker = invoker - self._queue_items: Set[int] = set() - self._sessions_to_cancel: Set[int] = set() + self._active_queue_items: Set[SessionQueueItem] = set() self._invocation: Optional[BaseInvocation] = None self._resume_event = ThreadEvent() @@ -35,17 +336,11 @@ class DefaultSessionProcessor(SessionProcessorBase): self._poll_now_event = ThreadEvent() self._cancel_event = ThreadEvent() - local_handler.register(event_name=EventServiceBase.queue_event, _func=self._on_queue_event) + register_events(QueueClearedEvent, self._on_queue_cleared) + register_events(BatchEnqueuedEvent, self._on_batch_enqueued) + register_events(QueueItemStatusChangedEvent, self._on_queue_item_status_changed) - self._thread_limit = 1 self._thread_semaphore = BoundedSemaphore(self._thread_limit) - self._polling_interval = polling_interval - - self._worker_thread_count = self._invoker.services.configuration.max_threads or len( - TorchDevice.execution_devices() - ) - self._session_worker_queue: Queue[SessionQueueItem] = Queue() - self._process_lock = Lock() # If profiling is enabled, create a profiler. The same profiler will be used for all sessions. Internally, # the profiler will create a new profile for each session. @@ -59,7 +354,14 @@ class DefaultSessionProcessor(SessionProcessorBase): else None ) - # main session processor loop - single thread + self._worker_thread_count = self._invoker.services.configuration.max_threads or len( + TorchDevice.execution_devices() + ) + + self._session_worker_queue: Queue[SessionQueueItem] = Queue() + + self.session_runner.start(services=invoker.services, cancel_event=self._cancel_event, profiler=self._profiler) + # Session processor - singlethreaded self._thread = Thread( name="session_processor", target=self._process, @@ -82,30 +384,32 @@ class DefaultSessionProcessor(SessionProcessorBase): ) worker.start() + def stop(self, *args, **kwargs) -> None: self._stop_event.set() def _poll_now(self) -> None: self._poll_now_event.set() - async def _on_queue_event(self, event: FastAPIEvent) -> None: - event_name = event[1]["event"] + async def _on_queue_cleared(self, event: FastAPIEvent[QueueClearedEvent]) -> None: + if any(item.queue_id == event[1].queue_id for item in self._active_queue_items): + self._cancel_event.set() + self._poll_now() - if event_name == "session_canceled" and event[1]["data"]["queue_item_id"] in self._queue_items: - self._sessions_to_cancel.add(event[1]["data"]["queue_item_id"]) - self._cancel_event.set() - self._poll_now() - elif event_name == "queue_cleared" and event[1]["data"]["queue_id"] in self._queue_items: - self._sessions_to_cancel.add(event[1]["data"]["queue_item_id"]) - self._cancel_event.set() - self._poll_now() - elif event_name == "batch_enqueued": - self._poll_now() - elif event_name == "queue_item_status_changed" and event[1]["data"]["queue_item"]["status"] in [ - "completed", - "failed", - "canceled", - ]: + async def _on_batch_enqueued(self, event: FastAPIEvent[BatchEnqueuedEvent]) -> None: + self._poll_now() + + async def _on_queue_item_status_changed(self, event: FastAPIEvent[QueueItemStatusChangedEvent]) -> None: + if self._active_queue_items and event[1].status in ["completed", "failed", "canceled"]: + # When the queue item is canceled via HTTP, the queue item status is set to `"canceled"` and this event is + # emitted. We need to respond to this event and stop graph execution. This is done by setting the cancel + # event, which the session runner checks between invocations. If set, the session runner loop is broken. + # + # Long-running nodes that cannot be interrupted easily present a challenge. `denoise_latents` is one such + # node, but it gets a step callback, called on each step of denoising. This callback checks if the queue item + # is canceled, and if it is, raises a `CanceledException` to stop execution immediately. + if event[1].status == "canceled": + self._cancel_event.set() self._poll_now() def resume(self) -> SessionProcessorStatus: @@ -121,7 +425,7 @@ class DefaultSessionProcessor(SessionProcessorBase): def get_status(self) -> SessionProcessorStatus: return SessionProcessorStatus( is_started=self._resume_event.is_set(), - is_processing=len(self._queue_items) > 0, + is_processing=len(self._active_queue_items) > 0, ) def _process( @@ -130,9 +434,9 @@ class DefaultSessionProcessor(SessionProcessorBase): poll_now_event: ThreadEvent, resume_event: ThreadEvent, cancel_event: ThreadEvent, - ) -> None: - # Outermost processor try block; any unhandled exception is a fatal processor error + ): try: + # Any unhandled exception in this block is a fatal processor error and will stop the processor. self._thread_semaphore.acquire() stop_event.clear() resume_event.set() @@ -140,198 +444,94 @@ class DefaultSessionProcessor(SessionProcessorBase): while not stop_event.is_set(): poll_now_event.clear() - resume_event.wait() + try: + # Any unhandled exception in this block is a nonfatal processor error and will be handled. + # If we are paused, wait for resume event + resume_event.wait() - # Get the next session to process - session = self._invoker.services.session_queue.dequeue() + # Get the next session to process + queue_item = self._invoker.services.session_queue.dequeue() - if session is None: - # The queue was empty, wait for next polling interval or event to try again - self._invoker.services.logger.debug("Waiting for next polling interval or event") + if queue_item is None: + # The queue was empty, wait for next polling interval or event to try again + self._invoker.services.logger.debug("Waiting for next polling interval or event") + poll_now_event.wait(self._polling_interval) + continue + + self._session_worker_queue.put(queue_item) + self._invoker.services.logger.debug(f"Scheduling queue item {queue_item.item_id} to run") + cancel_event.clear() + + # Run the graph + # self.session_runner.run(queue_item=self._queue_item) + + except Exception as e: + # Wait for next polling interval or event to try again poll_now_event.wait(self._polling_interval) continue - - self._queue_items.add(session.item_id) - self._session_worker_queue.put(session) - self._invoker.services.logger.debug(f"Executing queue item {session.item_id}") - cancel_event.clear() - except Exception: + except Exception as e: # Fatal error in processor, log and pass - we're done here - self._invoker.services.logger.error(f"Fatal Error in session processor:\n{traceback.format_exc()}") + error_type = e.__class__.__name__ + error_message = str(e) + error_traceback = traceback.format_exc() + self._invoker.services.logger.error(f"Fatal Error in session processor {error_type}: {error_message}") + self._invoker.services.logger.error(error_traceback) pass finally: stop_event.clear() poll_now_event.clear() - self._queue_items.clear() self._thread_semaphore.release() def _process_next_session(self) -> None: - profiler = ( - Profiler( - logger=self._invoker.services.logger, - output_dir=self._invoker.services.configuration.profiles_path, - prefix=self._invoker.services.configuration.profile_prefix, - ) - if self._invoker.services.configuration.profile_graphs - else None - ) - stats_service = InvocationStatsService() - stats_service.start(self._invoker) - while True: - # Outer try block. Any error here is a fatal processor error + self._resume_event.wait() + queue_item = self._session_worker_queue.get() + if queue_item.status == "canceled": + continue try: - self._resume_event.wait() - session = self._session_worker_queue.get() - - if self._cancel_event.is_set(): - if session.item_id in self._sessions_to_cancel: - continue - - if profiler is not None: - profiler.start(profile_id=session.session_id) - + self._active_queue_items.add(queue_item) # reserve a GPU for this session - may block with self._invoker.services.model_manager.load.ram_cache.reserve_execution_device(): - # Prepare invocations and take the first - with self._process_lock: - invocation = session.session.next() - - # Loop over invocations until the session is complete or canceled - while invocation is not None and not self._cancel_event.is_set(): - self._process_next_invocation(session, invocation, stats_service) - - # The session is complete if all invocations are complete or there was an error - if session.session.is_complete(): - # Send complete event - self._invoker.services.events.emit_graph_execution_complete( - queue_batch_id=session.batch_id, - queue_item_id=session.item_id, - queue_id=session.queue_id, - graph_execution_state_id=session.session.id, - ) - # Log stats - # We'll get a GESStatsNotFoundError if we try to log stats for an untracked graph, but in the processor - # we don't care about that - suppress the error. - with suppress(GESStatsNotFoundError): - stats_service.log_stats(session.session.id) - stats_service.reset_stats() - - # If we are profiling, stop the profiler and dump the profile & stats - if self._profiler: - profile_path = self._profiler.stop() - stats_path = profile_path.with_suffix(".json") - stats_service.dump_stats( - graph_execution_state_id=session.session.id, output_path=stats_path - ) - self._queue_items.remove(session.item_id) - invocation = None - else: - # Prepare the next invocation - with self._process_lock: - invocation = session.session.next() - - except Exception: - # Non-fatal error in processor - self._invoker.services.logger.error(f"Non-fatal error in session processor:\n{traceback.format_exc()}") - - # Cancel the queue item - if session is not None: - self._invoker.services.session_queue.cancel_queue_item( - session.item_id, error=traceback.format_exc() - ) + # Run the session on the reserved GPU + self.session_runner.run(queue_item=queue_item) + except Exception as e: + continue finally: - self._session_worker_queue.task_done() + self._active_queue_items.remove(queue_item) - def _process_next_invocation( + def _on_non_fatal_processor_error( self, - session: SessionQueueItem, - invocation: BaseInvocation, - stats_service: InvocationStatsService, + queue_item: Optional[SessionQueueItem], + error_type: str, + error_message: str, + error_traceback: str, ) -> None: - # get the source node id to provide to clients (the prepared node id is not as useful) - source_invocation_id = session.session.prepared_source_mapping[invocation.id] + """Called when a non-fatal error occurs in the processor. - self._invoker.services.logger.debug(f"Executing invocation {session.session.id}:{source_invocation_id}") + - Log the error. + - If a queue item is provided, update the queue item with the completed session & fail it. + - Run any callbacks registered for this event. + """ - # Send starting event - self._invoker.services.events.emit_invocation_started( - queue_batch_id=session.batch_id, - queue_item_id=session.item_id, - queue_id=session.queue_id, - graph_execution_state_id=session.session_id, - node=invocation.model_dump(), - source_node_id=source_invocation_id, - ) + self._invoker.services.logger.error(f"Non-fatal error in session processor {error_type}: {error_message}") + self._invoker.services.logger.error(error_traceback) - # Innermost processor try block; any unhandled exception is an invocation error & will fail the graph - try: - # Build invocation context (the node-facing API) - data = InvocationContextData( - invocation=invocation, - source_invocation_id=source_invocation_id, - queue_item=session, + if queue_item is not None: + # Update the queue item with the completed session & fail it + queue_item = self._invoker.services.session_queue.set_queue_item_session( + queue_item.item_id, queue_item.session ) - context = build_invocation_context( - data=data, - services=self._invoker.services, - cancel_event=self._cancel_event, + queue_item = self._invoker.services.session_queue.fail_queue_item( + item_id=queue_item.item_id, + error_type=error_type, + error_message=error_message, + error_traceback=error_traceback, ) - # Invoke the node - # title = invocation.UIConfig.title - with stats_service.collect_stats(invocation, session.session.id): - outputs = invocation.invoke_internal(context=context, services=self._invoker.services) - - # Save outputs and history - session.session.complete(invocation.id, outputs) - - # Send complete event - self._invoker.services.events.emit_invocation_complete( - queue_batch_id=session.batch_id, - queue_item_id=session.item_id, - queue_id=session.queue_id, - graph_execution_state_id=session.session.id, - node=invocation.model_dump(), - source_node_id=source_invocation_id, - result=outputs.model_dump(), - ) - - except KeyboardInterrupt: - # TODO(MM2): Create an event for this - pass - - except CanceledException: - # When the user cancels the graph, we first set the cancel event. The event is checked - # between invocations, in this loop. Some invocations are long-running, and we need to - # be able to cancel them mid-execution. - # - # For example, denoising is a long-running invocation with many steps. A step callback - # is executed after each step. This step callback checks if the canceled event is set, - # then raises a CanceledException to stop execution immediately. - # - # When we get a CanceledException, we don't need to do anything - just pass and let the - # loop go to its next iteration, and the cancel event will be handled correctly. - pass - - except Exception as e: - error = traceback.format_exc() - - # Save error - session.session.set_node_error(invocation.id, error) - self._invoker.services.logger.error( - f"Error while invoking session {session.session_id}, invocation {invocation.id} ({invocation.get_type()}):\n{e}" - ) - self._invoker.services.logger.error(error) - - # Send error event - self._invoker.services.events.emit_invocation_error( - queue_batch_id=session.session_id, - queue_item_id=session.item_id, - queue_id=session.queue_id, - graph_execution_state_id=session.session.id, - node=invocation.model_dump(), - source_node_id=source_invocation_id, - error_type=e.__class__.__name__, - error=error, + for callback in self._on_non_fatal_processor_error_callbacks: + callback( + queue_item=queue_item, + error_type=error_type, + error_message=error_message, + error_traceback=error_traceback, ) diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py index e0b6e4f528..341e034487 100644 --- a/invokeai/app/services/session_queue/session_queue_base.py +++ b/invokeai/app/services/session_queue/session_queue_base.py @@ -16,6 +16,7 @@ from invokeai.app.services.session_queue.session_queue_common import ( SessionQueueItemDTO, SessionQueueStatus, ) +from invokeai.app.services.shared.graph import GraphExecutionState from invokeai.app.services.shared.pagination import CursorPaginatedResults @@ -73,10 +74,22 @@ class SessionQueueBase(ABC): pass @abstractmethod - def cancel_queue_item(self, item_id: int, error: Optional[str] = None) -> SessionQueueItem: + def complete_queue_item(self, item_id: int) -> SessionQueueItem: + """Completes a session queue item""" + pass + + @abstractmethod + def cancel_queue_item(self, item_id: int) -> SessionQueueItem: """Cancels a session queue item""" pass + @abstractmethod + def fail_queue_item( + self, item_id: int, error_type: str, error_message: str, error_traceback: str + ) -> SessionQueueItem: + """Fails a session queue item""" + pass + @abstractmethod def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBatchIDsResult: """Cancels all queue items with matching batch IDs""" @@ -103,3 +116,8 @@ class SessionQueueBase(ABC): def get_queue_item(self, item_id: int) -> SessionQueueItem: """Gets a session queue item by ID""" pass + + @abstractmethod + def set_queue_item_session(self, item_id: int, session: GraphExecutionState) -> SessionQueueItem: + """Sets the session for a session queue item. Use this to update the session state.""" + pass diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py index 94db6999c2..f043248d27 100644 --- a/invokeai/app/services/session_queue/session_queue_common.py +++ b/invokeai/app/services/session_queue/session_queue_common.py @@ -3,7 +3,16 @@ import json from itertools import chain, product from typing import Generator, Iterable, Literal, NamedTuple, Optional, TypeAlias, Union, cast -from pydantic import BaseModel, ConfigDict, Field, StrictStr, TypeAdapter, field_validator, model_validator +from pydantic import ( + AliasChoices, + BaseModel, + ConfigDict, + Field, + StrictStr, + TypeAdapter, + field_validator, + model_validator, +) from pydantic_core import to_jsonable_python from invokeai.app.invocations.baseinvocation import BaseInvocation @@ -189,7 +198,13 @@ class SessionQueueItemWithoutGraph(BaseModel): session_id: str = Field( description="The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed." ) - error: Optional[str] = Field(default=None, description="The error message if this queue item errored") + error_type: Optional[str] = Field(default=None, description="The error type if this queue item errored") + error_message: Optional[str] = Field(default=None, description="The error message if this queue item errored") + error_traceback: Optional[str] = Field( + default=None, + description="The error traceback if this queue item errored", + validation_alias=AliasChoices("error_traceback", "error"), + ) created_at: Union[datetime.datetime, str] = Field(description="When this queue item was created") updated_at: Union[datetime.datetime, str] = Field(description="When this queue item was updated") started_at: Optional[Union[datetime.datetime, str]] = Field(description="When this queue item was started") @@ -221,6 +236,8 @@ class SessionQueueItemWithoutGraph(BaseModel): } ) + def __hash__(self) -> int: + return self.item_id class SessionQueueItemDTO(SessionQueueItemWithoutGraph): pass diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py index ffcd7c40ca..467853aae4 100644 --- a/invokeai/app/services/session_queue/session_queue_sqlite.py +++ b/invokeai/app/services/session_queue/session_queue_sqlite.py @@ -2,10 +2,6 @@ import sqlite3 import threading from typing import Optional, Union, cast -from fastapi_events.handlers.local import local_handler -from fastapi_events.typing import Event as FastAPIEvent - -from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.invoker import Invoker from invokeai.app.services.session_queue.session_queue_base import SessionQueueBase from invokeai.app.services.session_queue.session_queue_common import ( @@ -27,6 +23,7 @@ from invokeai.app.services.session_queue.session_queue_common import ( calc_session_count, prepare_values_to_insert, ) +from invokeai.app.services.shared.graph import GraphExecutionState from invokeai.app.services.shared.pagination import CursorPaginatedResults from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase @@ -41,7 +38,7 @@ class SqliteSessionQueue(SessionQueueBase): self.__invoker = invoker self._set_in_progress_to_canceled() prune_result = self.prune(DEFAULT_QUEUE_ID) - local_handler.register(event_name=EventServiceBase.queue_event, _func=self._on_session_event) + if prune_result.deleted > 0: self.__invoker.services.logger.info(f"Pruned {prune_result.deleted} finished queue items") @@ -51,52 +48,6 @@ class SqliteSessionQueue(SessionQueueBase): self.__conn = db.conn self.__cursor = self.__conn.cursor() - def _match_event_name(self, event: FastAPIEvent, match_in: list[str]) -> bool: - return event[1]["event"] in match_in - - async def _on_session_event(self, event: FastAPIEvent) -> FastAPIEvent: - event_name = event[1]["event"] - - # This was a match statement, but match is not supported on python 3.9 - if event_name == "graph_execution_state_complete": - await self._handle_complete_event(event) - elif event_name == "invocation_error": - await self._handle_error_event(event) - elif event_name == "session_canceled": - await self._handle_cancel_event(event) - return event - - async def _handle_complete_event(self, event: FastAPIEvent) -> None: - try: - item_id = event[1]["data"]["queue_item_id"] - # When a queue item has an error, we get an error event, then a completed event. - # Mark the queue item completed only if it isn't already marked completed, e.g. - # by a previously-handled error event. - queue_item = self.get_queue_item(item_id) - if queue_item.status not in ["completed", "failed", "canceled"]: - queue_item = self._set_queue_item_status(item_id=queue_item.item_id, status="completed") - except SessionQueueItemNotFoundError: - return - - async def _handle_error_event(self, event: FastAPIEvent) -> None: - try: - item_id = event[1]["data"]["queue_item_id"] - error = event[1]["data"]["error"] - queue_item = self.get_queue_item(item_id) - # always set to failed if have an error, even if previously the item was marked completed or canceled - queue_item = self._set_queue_item_status(item_id=queue_item.item_id, status="failed", error=error) - except SessionQueueItemNotFoundError: - return - - async def _handle_cancel_event(self, event: FastAPIEvent) -> None: - try: - item_id = event[1]["data"]["queue_item_id"] - queue_item = self.get_queue_item(item_id) - if queue_item.status not in ["completed", "failed", "canceled"]: - queue_item = self._set_queue_item_status(item_id=queue_item.item_id, status="canceled") - except SessionQueueItemNotFoundError: - return - def _set_in_progress_to_canceled(self) -> None: """ Sets all in_progress queue items to canceled. Run on app startup, not associated with any queue. @@ -271,17 +222,22 @@ class SqliteSessionQueue(SessionQueueBase): return SessionQueueItem.queue_item_from_dict(dict(result)) def _set_queue_item_status( - self, item_id: int, status: QUEUE_ITEM_STATUS, error: Optional[str] = None + self, + item_id: int, + status: QUEUE_ITEM_STATUS, + error_type: Optional[str] = None, + error_message: Optional[str] = None, + error_traceback: Optional[str] = None, ) -> SessionQueueItem: try: self.__lock.acquire() self.__cursor.execute( """--sql UPDATE session_queue - SET status = ?, error = ? + SET status = ?, error_type = ?, error_message = ?, error_traceback = ? WHERE item_id = ? """, - (status, error, item_id), + (status, error_type, error_message, error_traceback, item_id), ) self.__conn.commit() except Exception: @@ -292,11 +248,7 @@ class SqliteSessionQueue(SessionQueueBase): queue_item = self.get_queue_item(item_id) batch_status = self.get_batch_status(queue_id=queue_item.queue_id, batch_id=queue_item.batch_id) queue_status = self.get_queue_status(queue_id=queue_item.queue_id) - self.__invoker.services.events.emit_queue_item_status_changed( - session_queue_item=queue_item, - batch_status=batch_status, - queue_status=queue_status, - ) + self.__invoker.services.events.emit_queue_item_status_changed(queue_item, batch_status, queue_status) return queue_item def is_empty(self, queue_id: str) -> IsEmptyResult: @@ -338,26 +290,6 @@ class SqliteSessionQueue(SessionQueueBase): self.__lock.release() return IsFullResult(is_full=is_full) - def delete_queue_item(self, item_id: int) -> SessionQueueItem: - queue_item = self.get_queue_item(item_id=item_id) - try: - self.__lock.acquire() - self.__cursor.execute( - """--sql - DELETE FROM session_queue - WHERE - item_id = ? - """, - (item_id,), - ) - self.__conn.commit() - except Exception: - self.__conn.rollback() - raise - finally: - self.__lock.release() - return queue_item - def clear(self, queue_id: str) -> ClearResult: try: self.__lock.acquire() @@ -424,17 +356,28 @@ class SqliteSessionQueue(SessionQueueBase): self.__lock.release() return PruneResult(deleted=count) - def cancel_queue_item(self, item_id: int, error: Optional[str] = None) -> SessionQueueItem: - queue_item = self.get_queue_item(item_id) - if queue_item.status not in ["canceled", "failed", "completed"]: - status = "failed" if error is not None else "canceled" - queue_item = self._set_queue_item_status(item_id=item_id, status=status, error=error) # type: ignore [arg-type] # mypy seems to not narrow the Literals here - self.__invoker.services.events.emit_session_canceled( - queue_item_id=queue_item.item_id, - queue_id=queue_item.queue_id, - queue_batch_id=queue_item.batch_id, - graph_execution_state_id=queue_item.session_id, - ) + def cancel_queue_item(self, item_id: int) -> SessionQueueItem: + queue_item = self._set_queue_item_status(item_id=item_id, status="canceled") + return queue_item + + def complete_queue_item(self, item_id: int) -> SessionQueueItem: + queue_item = self._set_queue_item_status(item_id=item_id, status="completed") + return queue_item + + def fail_queue_item( + self, + item_id: int, + error_type: str, + error_message: str, + error_traceback: str, + ) -> SessionQueueItem: + queue_item = self._set_queue_item_status( + item_id=item_id, + status="failed", + error_type=error_type, + error_message=error_message, + error_traceback=error_traceback, + ) return queue_item def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBatchIDsResult: @@ -470,18 +413,10 @@ class SqliteSessionQueue(SessionQueueBase): ) self.__conn.commit() if current_queue_item is not None and current_queue_item.batch_id in batch_ids: - self.__invoker.services.events.emit_session_canceled( - queue_item_id=current_queue_item.item_id, - queue_id=current_queue_item.queue_id, - queue_batch_id=current_queue_item.batch_id, - graph_execution_state_id=current_queue_item.session_id, - ) batch_status = self.get_batch_status(queue_id=queue_id, batch_id=current_queue_item.batch_id) queue_status = self.get_queue_status(queue_id=queue_id) self.__invoker.services.events.emit_queue_item_status_changed( - session_queue_item=current_queue_item, - batch_status=batch_status, - queue_status=queue_status, + current_queue_item, batch_status, queue_status ) except Exception: self.__conn.rollback() @@ -521,18 +456,10 @@ class SqliteSessionQueue(SessionQueueBase): ) self.__conn.commit() if current_queue_item is not None and current_queue_item.queue_id == queue_id: - self.__invoker.services.events.emit_session_canceled( - queue_item_id=current_queue_item.item_id, - queue_id=current_queue_item.queue_id, - queue_batch_id=current_queue_item.batch_id, - graph_execution_state_id=current_queue_item.session_id, - ) batch_status = self.get_batch_status(queue_id=queue_id, batch_id=current_queue_item.batch_id) queue_status = self.get_queue_status(queue_id=queue_id) self.__invoker.services.events.emit_queue_item_status_changed( - session_queue_item=current_queue_item, - batch_status=batch_status, - queue_status=queue_status, + current_queue_item, batch_status, queue_status ) except Exception: self.__conn.rollback() @@ -562,6 +489,29 @@ class SqliteSessionQueue(SessionQueueBase): raise SessionQueueItemNotFoundError(f"No queue item with id {item_id}") return SessionQueueItem.queue_item_from_dict(dict(result)) + def set_queue_item_session(self, item_id: int, session: GraphExecutionState) -> SessionQueueItem: + try: + # Use exclude_none so we don't end up with a bunch of nulls in the graph - this can cause validation errors + # when the graph is loaded. Graph execution occurs purely in memory - the session saved here is not referenced + # during execution. + session_json = session.model_dump_json(warnings=False, exclude_none=True) + self.__lock.acquire() + self.__cursor.execute( + """--sql + UPDATE session_queue + SET session = ? + WHERE item_id = ? + """, + (session_json, item_id), + ) + self.__conn.commit() + except Exception: + self.__conn.rollback() + raise + finally: + self.__lock.release() + return self.get_queue_item(item_id) + def list_queue_items( self, queue_id: str, @@ -578,7 +528,9 @@ class SqliteSessionQueue(SessionQueueBase): status, priority, field_values, - error, + error_type, + error_message, + error_traceback, created_at, updated_at, completed_at, diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index cc2ea5cedb..d745e73823 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -2,17 +2,19 @@ import copy import itertools -from typing import Annotated, Any, Optional, TypeVar, Union, get_args, get_origin, get_type_hints +from typing import Any, Optional, TypeVar, Union, get_args, get_origin, get_type_hints import networkx as nx from pydantic import ( BaseModel, + GetCoreSchemaHandler, GetJsonSchemaHandler, + ValidationError, field_validator, ) from pydantic.fields import Field from pydantic.json_schema import JsonSchemaValue -from pydantic_core import CoreSchema +from pydantic_core import core_schema # Importing * is bad karma but needed here for node detection from invokeai.app.invocations import * # noqa: F401 F403 @@ -190,6 +192,39 @@ class UnknownGraphValidationError(ValueError): pass +class NodeInputError(ValueError): + """Raised when a node fails preparation. This occurs when a node's inputs are being set from its incomers, but an + input fails validation. + + Attributes: + node: The node that failed preparation. Note: only successfully set fields will be accurate. Review the error to + determine which field caused the failure. + """ + + def __init__(self, node: BaseInvocation, e: ValidationError): + self.original_error = e + self.node = node + # When preparing a node, we set each input one-at-a-time. We may thus safely assume that the first error + # represents the first input that failed. + self.failed_input = loc_to_dot_sep(e.errors()[0]["loc"]) + super().__init__(f"Node {node.id} has invalid incoming input for {self.failed_input}") + + +def loc_to_dot_sep(loc: tuple[Union[str, int], ...]) -> str: + """Helper to pretty-print pydantic error locations as dot-separated strings. + Taken from https://docs.pydantic.dev/latest/errors/errors/#customize-error-messages + """ + path = "" + for i, x in enumerate(loc): + if isinstance(x, str): + if i > 0: + path += "." + path += x + else: + path += f"[{x}]" + return path + + @invocation_output("iterate_output") class IterateInvocationOutput(BaseInvocationOutput): """Used to connect iteration outputs. Will be expanded to a specific output.""" @@ -243,73 +278,58 @@ class CollectInvocation(BaseInvocation): return CollectInvocationOutput(collection=copy.copy(self.collection)) +class AnyInvocation(BaseInvocation): + @classmethod + def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + def validate_invocation(v: Any) -> "AnyInvocation": + return BaseInvocation.get_typeadapter().validate_python(v) + + return core_schema.no_info_plain_validator_function(validate_invocation) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + # Nodes are too powerful, we have to make our own OpenAPI schema manually + # No but really, because the schema is dynamic depending on loaded nodes, we need to generate it manually + oneOf: list[dict[str, str]] = [] + names = [i.__name__ for i in BaseInvocation.get_invocations()] + for name in sorted(names): + oneOf.append({"$ref": f"#/components/schemas/{name}"}) + return {"oneOf": oneOf} + + +class AnyInvocationOutput(BaseInvocationOutput): + @classmethod + def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler): + def validate_invocation_output(v: Any) -> "AnyInvocationOutput": + return BaseInvocationOutput.get_typeadapter().validate_python(v) + + return core_schema.no_info_plain_validator_function(validate_invocation_output) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + # Nodes are too powerful, we have to make our own OpenAPI schema manually + # No but really, because the schema is dynamic depending on loaded nodes, we need to generate it manually + + oneOf: list[dict[str, str]] = [] + names = [i.__name__ for i in BaseInvocationOutput.get_outputs()] + for name in sorted(names): + oneOf.append({"$ref": f"#/components/schemas/{name}"}) + return {"oneOf": oneOf} + + class Graph(BaseModel): id: str = Field(description="The id of this graph", default_factory=uuid_string) # TODO: use a list (and never use dict in a BaseModel) because pydantic/fastapi hates me - nodes: dict[str, BaseInvocation] = Field(description="The nodes in this graph", default_factory=dict) + nodes: dict[str, AnyInvocation] = Field(description="The nodes in this graph", default_factory=dict) edges: list[Edge] = Field( description="The connections between nodes and their fields in this graph", default_factory=list, ) - @field_validator("nodes", mode="plain") - @classmethod - def validate_nodes(cls, v: dict[str, Any]): - """Validates the nodes in the graph by retrieving a union of all node types and validating each node.""" - - # Invocations register themselves as their python modules are executed. The union of all invocations is - # constructed at runtime. We use pydantic to validate `Graph.nodes` using that union. - # - # It's possible that when `graph.py` is executed, not all invocation-containing modules will have executed. If - # we construct the invocation union as `graph.py` is executed, we may miss some invocations. Those missing - # invocations will cause a graph to fail if they are used. - # - # We can get around this by validating the nodes in the graph using a "plain" validator, which overrides the - # pydantic validation entirely. This allows us to validate the nodes using the union of invocations at runtime. - # - # This same pattern is used in `GraphExecutionState`. - - nodes: dict[str, BaseInvocation] = {} - typeadapter = BaseInvocation.get_typeadapter() - for node_id, node in v.items(): - nodes[node_id] = typeadapter.validate_python(node) - return nodes - - @classmethod - def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> JsonSchemaValue: - # We use a "plain" validator to validate the nodes in the graph. Pydantic is unable to create a JSON Schema for - # fields that use "plain" validators, so we have to hack around this. Also, we need to add all invocations to - # the generated schema as options for the `nodes` field. - # - # The workaround is to create a new BaseModel that has the same fields as `Graph` but without the validator and - # with the invocation union as the type for the `nodes` field. Pydantic then generates the JSON Schema as - # expected. - # - # You might be tempted to do something like this: - # - # ```py - # cloned_model = create_model(cls.__name__, __base__=cls, nodes=...) - # delattr(cloned_model, "validate_nodes") - # cloned_model.model_rebuild(force=True) - # json_schema = handler(cloned_model.__pydantic_core_schema__) - # ``` - # - # Unfortunately, this does not work. Calling `handler` here results in infinite recursion as pydantic attempts - # to build the JSON Schema for the cloned model. Instead, we have to manually clone the model. - # - # This same pattern is used in `GraphExecutionState`. - - class Graph(BaseModel): - id: Optional[str] = Field(default=None, description="The id of this graph") - nodes: dict[ - str, Annotated[Union[tuple(BaseInvocation._invocation_classes)], Field(discriminator="type")] - ] = Field(description="The nodes in this graph") - edges: list[Edge] = Field(description="The connections between nodes and their fields in this graph") - - json_schema = handler(Graph.__pydantic_core_schema__) - json_schema = handler.resolve_ref_schema(json_schema) - return json_schema - def add_node(self, node: BaseInvocation) -> None: """Adds a node to a graph @@ -740,7 +760,7 @@ class GraphExecutionState(BaseModel): ) # The results of executed nodes - results: dict[str, BaseInvocationOutput] = Field(description="The results of node executions", default_factory=dict) + results: dict[str, AnyInvocationOutput] = Field(description="The results of node executions", default_factory=dict) # Errors raised when executing nodes errors: dict[str, str] = Field(description="Errors raised when executing nodes", default_factory=dict) @@ -757,52 +777,12 @@ class GraphExecutionState(BaseModel): default_factory=dict, ) - @field_validator("results", mode="plain") - @classmethod - def validate_results(cls, v: dict[str, BaseInvocationOutput]): - """Validates the results in the GES by retrieving a union of all output types and validating each result.""" - - # See the comment in `Graph.validate_nodes` for an explanation of this logic. - results: dict[str, BaseInvocationOutput] = {} - typeadapter = BaseInvocationOutput.get_typeadapter() - for result_id, result in v.items(): - results[result_id] = typeadapter.validate_python(result) - return results - @field_validator("graph") def graph_is_valid(cls, v: Graph): """Validates that the graph is valid""" v.validate_self() return v - @classmethod - def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> JsonSchemaValue: - # See the comment in `Graph.__get_pydantic_json_schema__` for an explanation of this logic. - class GraphExecutionState(BaseModel): - """Tracks the state of a graph execution""" - - id: str = Field(description="The id of the execution state") - graph: Graph = Field(description="The graph being executed") - execution_graph: Graph = Field(description="The expanded graph of activated and executed nodes") - executed: set[str] = Field(description="The set of node ids that have been executed") - executed_history: list[str] = Field( - description="The list of node ids that have been executed, in order of execution" - ) - results: dict[ - str, Annotated[Union[tuple(BaseInvocationOutput._output_classes)], Field(discriminator="type")] - ] = Field(description="The results of node executions") - errors: dict[str, str] = Field(description="Errors raised when executing nodes") - prepared_source_mapping: dict[str, str] = Field( - description="The map of prepared nodes to original graph nodes" - ) - source_prepared_mapping: dict[str, set[str]] = Field( - description="The map of original graph nodes to prepared nodes" - ) - - json_schema = handler(GraphExecutionState.__pydantic_core_schema__) - json_schema = handler.resolve_ref_schema(json_schema) - return json_schema - def next(self) -> Optional[BaseInvocation]: """Gets the next node ready to execute.""" @@ -821,7 +801,10 @@ class GraphExecutionState(BaseModel): # Get values from edges if next_node is not None: - self._prepare_inputs(next_node) + try: + self._prepare_inputs(next_node) + except ValidationError as e: + raise NodeInputError(next_node, e) # If next is still none, there's no next node, return None return next_node diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index c2c37de78d..e3be2035c6 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -1,7 +1,6 @@ -import threading from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Callable, Optional, Union import torch from PIL.Image import Image @@ -190,9 +189,9 @@ class ImagesInterface(InvocationContextInterface): # If `metadata` is provided directly, use that. Else, use the metadata provided by `WithMetadata`, falling back to None. metadata_ = None if metadata: - metadata_ = metadata - elif isinstance(self._data.invocation, WithMetadata): - metadata_ = self._data.invocation.metadata + metadata_ = metadata.model_dump_json() + elif isinstance(self._data.invocation, WithMetadata) and self._data.invocation.metadata: + metadata_ = self._data.invocation.metadata.model_dump_json() # If `board_id` is provided directly, use that. Else, use the board provided by `WithBoard`, falling back to None. board_id_ = None @@ -201,6 +200,14 @@ class ImagesInterface(InvocationContextInterface): elif isinstance(self._data.invocation, WithBoard) and self._data.invocation.board: board_id_ = self._data.invocation.board.board_id + workflow_ = None + if self._data.queue_item.workflow: + workflow_ = self._data.queue_item.workflow.model_dump_json() + + graph_ = None + if self._data.queue_item.session.graph: + graph_ = self._data.queue_item.session.graph.model_dump_json() + return self._services.images.create( image=image, is_intermediate=self._data.invocation.is_intermediate, @@ -208,7 +215,8 @@ class ImagesInterface(InvocationContextInterface): board_id=board_id_, metadata=metadata_, image_origin=ResourceOrigin.INTERNAL, - workflow=self._data.queue_item.workflow, + workflow=workflow_, + graph=graph_, session_id=self._data.queue_item.session_id, node_id=self._data.invocation.id, ) @@ -354,11 +362,11 @@ class ModelsInterface(InvocationContextInterface): if isinstance(identifier, str): model = self._services.model_manager.store.get_model(identifier) - return self._services.model_manager.load.load_model(model, submodel_type, self._data) + return self._services.model_manager.load.load_model(model, submodel_type) else: _submodel_type = submodel_type or identifier.submodel_type model = self._services.model_manager.store.get_model(identifier.key) - return self._services.model_manager.load.load_model(model, _submodel_type, self._data) + return self._services.model_manager.load.load_model(model, _submodel_type) def load_by_attrs( self, name: str, base: BaseModelType, type: ModelType, submodel_type: Optional[SubModelType] = None @@ -383,7 +391,7 @@ class ModelsInterface(InvocationContextInterface): if len(configs) > 1: raise ValueError(f"More than one model found with name {name}, base {base}, and type {type}") - return self._services.model_manager.load.load_model(configs[0], submodel_type, self._data) + return self._services.model_manager.load.load_model(configs[0], submodel_type) def get_config(self, identifier: Union[str, "ModelIdentifierField"]) -> AnyModelConfig: """Gets a model's config. @@ -450,10 +458,10 @@ class ConfigInterface(InvocationContextInterface): class UtilInterface(InvocationContextInterface): def __init__( - self, services: InvocationServices, data: InvocationContextData, cancel_event: threading.Event + self, services: InvocationServices, data: InvocationContextData, is_canceled: Callable[[], bool] ) -> None: super().__init__(services, data) - self._cancel_event = cancel_event + self._is_canceled = is_canceled def is_canceled(self) -> bool: """Checks if the current session has been canceled. @@ -461,7 +469,7 @@ class UtilInterface(InvocationContextInterface): Returns: True if the current session has been canceled, False if not. """ - return self._cancel_event.is_set() + return self._is_canceled() def sd_step_callback(self, intermediate_state: PipelineIntermediateState, base_model: BaseModelType) -> None: """ @@ -558,7 +566,7 @@ class InvocationContext: def build_invocation_context( services: InvocationServices, data: InvocationContextData, - cancel_event: threading.Event, + is_canceled: Callable[[], bool], ) -> InvocationContext: """Builds the invocation context for a specific invocation execution. @@ -575,7 +583,7 @@ def build_invocation_context( tensors = TensorsInterface(services=services, data=data) models = ModelsInterface(services=services, data=data) config = ConfigInterface(services=services, data=data) - util = UtilInterface(services=services, data=data, cancel_event=cancel_event) + util = UtilInterface(services=services, data=data, is_canceled=is_canceled) conditioning = ConditioningInterface(services=services, data=data) boards = BoardsInterface(services=services, data=data) diff --git a/invokeai/app/services/shared/sqlite/sqlite_util.py b/invokeai/app/services/shared/sqlite/sqlite_util.py index 1eed0b4409..cadf09f457 100644 --- a/invokeai/app/services/shared/sqlite/sqlite_util.py +++ b/invokeai/app/services/shared/sqlite/sqlite_util.py @@ -12,6 +12,7 @@ from invokeai.app.services.shared.sqlite_migrator.migrations.migration_6 import from invokeai.app.services.shared.sqlite_migrator.migrations.migration_7 import build_migration_7 from invokeai.app.services.shared.sqlite_migrator.migrations.migration_8 import build_migration_8 from invokeai.app.services.shared.sqlite_migrator.migrations.migration_9 import build_migration_9 +from invokeai.app.services.shared.sqlite_migrator.migrations.migration_10 import build_migration_10 from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator @@ -41,6 +42,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto migrator.register_migration(build_migration_7()) migrator.register_migration(build_migration_8(app_config=config)) migrator.register_migration(build_migration_9()) + migrator.register_migration(build_migration_10()) migrator.run_migrations() return db diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_10.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_10.py new file mode 100644 index 0000000000..ce2cd2e965 --- /dev/null +++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_10.py @@ -0,0 +1,35 @@ +import sqlite3 + +from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration + + +class Migration10Callback: + def __call__(self, cursor: sqlite3.Cursor) -> None: + self._update_error_cols(cursor) + + def _update_error_cols(self, cursor: sqlite3.Cursor) -> None: + """ + - Adds `error_type` and `error_message` columns to the session queue table. + - Renames the `error` column to `error_traceback`. + """ + + cursor.execute("ALTER TABLE session_queue ADD COLUMN error_type TEXT;") + cursor.execute("ALTER TABLE session_queue ADD COLUMN error_message TEXT;") + cursor.execute("ALTER TABLE session_queue RENAME COLUMN error TO error_traceback;") + + +def build_migration_10() -> Migration: + """ + Build the migration from database version 9 to 10. + + This migration does the following: + - Adds `error_type` and `error_message` columns to the session queue table. + - Renames the `error` column to `error_traceback`. + """ + migration_10 = Migration( + from_version=9, + to_version=10, + callback=Migration10Callback(), + ) + + return migration_10 diff --git a/invokeai/app/util/custom_openapi.py b/invokeai/app/util/custom_openapi.py new file mode 100644 index 0000000000..50259c12cc --- /dev/null +++ b/invokeai/app/util/custom_openapi.py @@ -0,0 +1,116 @@ +from typing import Any, Callable, Optional + +from fastapi import FastAPI +from fastapi.openapi.utils import get_openapi +from pydantic.json_schema import models_json_schema + +from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, UIConfigBase +from invokeai.app.invocations.fields import InputFieldJSONSchemaExtra, OutputFieldJSONSchemaExtra +from invokeai.app.invocations.model import ModelIdentifierField +from invokeai.app.services.events.events_common import EventBase +from invokeai.app.services.session_processor.session_processor_common import ProgressImage + + +def move_defs_to_top_level(openapi_schema: dict[str, Any], component_schema: dict[str, Any]) -> None: + """Moves a component schema's $defs to the top level of the openapi schema. Useful when generating a schema + for a single model that needs to be added back to the top level of the schema. Mutates openapi_schema and + component_schema.""" + + defs = component_schema.pop("$defs", {}) + for schema_key, json_schema in defs.items(): + if schema_key in openapi_schema["components"]["schemas"]: + continue + openapi_schema["components"]["schemas"][schema_key] = json_schema + + +def get_openapi_func( + app: FastAPI, post_transform: Optional[Callable[[dict[str, Any]], dict[str, Any]]] = None +) -> Callable[[], dict[str, Any]]: + """Gets the OpenAPI schema generator function. + + Args: + app (FastAPI): The FastAPI app to generate the schema for. + post_transform (Optional[Callable[[dict[str, Any]], dict[str, Any]]], optional): A function to apply to the + generated schema before returning it. Defaults to None. + + Returns: + Callable[[], dict[str, Any]]: The OpenAPI schema generator function. When first called, the generated schema is + cached in `app.openapi_schema`. On subsequent calls, the cached schema is returned. This caching behaviour + matches FastAPI's default schema generation caching. + """ + + def openapi() -> dict[str, Any]: + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title=app.title, + description="An API for invoking AI image operations", + version="1.0.0", + routes=app.routes, + separate_input_output_schemas=False, # https://fastapi.tiangolo.com/how-to/separate-openapi-schemas/ + ) + + # We'll create a map of invocation type to output schema to make some types simpler on the client. + invocation_output_map_properties: dict[str, Any] = {} + invocation_output_map_required: list[str] = [] + + # We need to manually add all outputs to the schema - pydantic doesn't add them because they aren't used directly. + for output in BaseInvocationOutput.get_outputs(): + json_schema = output.model_json_schema(mode="serialization", ref_template="#/components/schemas/{model}") + move_defs_to_top_level(openapi_schema, json_schema) + openapi_schema["components"]["schemas"][output.__name__] = json_schema + + # Technically, invocations are added to the schema by pydantic, but we still need to manually set their output + # property, so we'll just do it all manually. + for invocation in BaseInvocation.get_invocations(): + json_schema = invocation.model_json_schema( + mode="serialization", ref_template="#/components/schemas/{model}" + ) + move_defs_to_top_level(openapi_schema, json_schema) + output_title = invocation.get_output_annotation().__name__ + outputs_ref = {"$ref": f"#/components/schemas/{output_title}"} + json_schema["output"] = outputs_ref + openapi_schema["components"]["schemas"][invocation.__name__] = json_schema + + # Add this invocation and its output to the output map + invocation_type = invocation.get_type() + invocation_output_map_properties[invocation_type] = json_schema["output"] + invocation_output_map_required.append(invocation_type) + + # Add the output map to the schema + openapi_schema["components"]["schemas"]["InvocationOutputMap"] = { + "type": "object", + "properties": invocation_output_map_properties, + "required": invocation_output_map_required, + } + + # Some models don't end up in the schemas as standalone definitions because they aren't used directly in the API. + # We need to add them manually here. WARNING: Pydantic can choke if you call `model.model_json_schema()` to get + # a schema. This has something to do with schema refs - not totally clear. For whatever reason, using + # `models_json_schema` seems to work fine. + additional_models = [ + *EventBase.get_events(), + UIConfigBase, + InputFieldJSONSchemaExtra, + OutputFieldJSONSchemaExtra, + ModelIdentifierField, + ProgressImage, + ] + + additional_schemas = models_json_schema( + [(m, "serialization") for m in additional_models], + ref_template="#/components/schemas/{model}", + ) + # additional_schemas[1] is a dict of $defs that we need to add to the top level of the schema + move_defs_to_top_level(openapi_schema, additional_schemas[1]) + + if post_transform is not None: + openapi_schema = post_transform(openapi_schema) + + openapi_schema["components"]["schemas"] = dict(sorted(openapi_schema["components"]["schemas"].items())) + + app.openapi_schema = openapi_schema + return app.openapi_schema + + return openapi diff --git a/invokeai/app/util/step_callback.py b/invokeai/app/util/step_callback.py index 8cb59f5b3a..8992e59ace 100644 --- a/invokeai/app/util/step_callback.py +++ b/invokeai/app/util/step_callback.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, Optional import torch from PIL import Image @@ -13,8 +13,36 @@ if TYPE_CHECKING: from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.shared.invocation_context import InvocationContextData +# fast latents preview matrix for sdxl +# generated by @StAlKeR7779 +SDXL_LATENT_RGB_FACTORS = [ + # R G B + [0.3816, 0.4930, 0.5320], + [-0.3753, 0.1631, 0.1739], + [0.1770, 0.3588, -0.2048], + [-0.4350, -0.2644, -0.4289], +] +SDXL_SMOOTH_MATRIX = [ + [0.0358, 0.0964, 0.0358], + [0.0964, 0.4711, 0.0964], + [0.0358, 0.0964, 0.0358], +] -def sample_to_lowres_estimated_image(samples, latent_rgb_factors, smooth_matrix=None): +# origingally adapted from code by @erucipe and @keturn here: +# https://discuss.huggingface.co/t/decoding-latents-to-rgb-without-upscaling/23204/7 +# these updated numbers for v1.5 are from @torridgristle +SD1_5_LATENT_RGB_FACTORS = [ + # R G B + [0.3444, 0.1385, 0.0670], # L1 + [0.1247, 0.4027, 0.1494], # L2 + [-0.3192, 0.2513, 0.2103], # L3 + [-0.1307, -0.1874, -0.7445], # L4 +] + + +def sample_to_lowres_estimated_image( + samples: torch.Tensor, latent_rgb_factors: torch.Tensor, smooth_matrix: Optional[torch.Tensor] = None +): latent_image = samples[0].permute(1, 2, 0) @ latent_rgb_factors if smooth_matrix is not None: @@ -47,64 +75,12 @@ def stable_diffusion_step_callback( else: sample = intermediate_state.latents - # TODO: This does not seem to be needed any more? - # # txt2img provides a Tensor in the step_callback - # # img2img provides a PipelineIntermediateState - # if isinstance(sample, PipelineIntermediateState): - # # this was an img2img - # print('img2img') - # latents = sample.latents - # step = sample.step - # else: - # print('txt2img') - # latents = sample - # step = intermediate_state.step - - # TODO: only output a preview image when requested - if base_model in [BaseModelType.StableDiffusionXL, BaseModelType.StableDiffusionXLRefiner]: - # fast latents preview matrix for sdxl - # generated by @StAlKeR7779 - sdxl_latent_rgb_factors = torch.tensor( - [ - # R G B - [0.3816, 0.4930, 0.5320], - [-0.3753, 0.1631, 0.1739], - [0.1770, 0.3588, -0.2048], - [-0.4350, -0.2644, -0.4289], - ], - dtype=sample.dtype, - device=sample.device, - ) - - sdxl_smooth_matrix = torch.tensor( - [ - [0.0358, 0.0964, 0.0358], - [0.0964, 0.4711, 0.0964], - [0.0358, 0.0964, 0.0358], - ], - dtype=sample.dtype, - device=sample.device, - ) - + sdxl_latent_rgb_factors = torch.tensor(SDXL_LATENT_RGB_FACTORS, dtype=sample.dtype, device=sample.device) + sdxl_smooth_matrix = torch.tensor(SDXL_SMOOTH_MATRIX, dtype=sample.dtype, device=sample.device) image = sample_to_lowres_estimated_image(sample, sdxl_latent_rgb_factors, sdxl_smooth_matrix) else: - # origingally adapted from code by @erucipe and @keturn here: - # https://discuss.huggingface.co/t/decoding-latents-to-rgb-without-upscaling/23204/7 - - # these updated numbers for v1.5 are from @torridgristle - v1_5_latent_rgb_factors = torch.tensor( - [ - # R G B - [0.3444, 0.1385, 0.0670], # L1 - [0.1247, 0.4027, 0.1494], # L2 - [-0.3192, 0.2513, 0.2103], # L3 - [-0.1307, -0.1874, -0.7445], # L4 - ], - dtype=sample.dtype, - device=sample.device, - ) - + v1_5_latent_rgb_factors = torch.tensor(SD1_5_LATENT_RGB_FACTORS, dtype=sample.dtype, device=sample.device) image = sample_to_lowres_estimated_image(sample, v1_5_latent_rgb_factors) (width, height) = image.size @@ -113,15 +89,9 @@ def stable_diffusion_step_callback( dataURL = image_to_dataURL(image, image_format="JPEG") - events.emit_generator_progress( - queue_id=context_data.queue_item.queue_id, - queue_item_id=context_data.queue_item.item_id, - queue_batch_id=context_data.queue_item.batch_id, - graph_execution_state_id=context_data.queue_item.session_id, - node_id=context_data.invocation.id, - source_node_id=context_data.source_invocation_id, - progress_image=ProgressImage(width=width, height=height, dataURL=dataURL), - step=intermediate_state.step, - order=intermediate_state.order, - total_steps=intermediate_state.total_steps, + events.emit_invocation_denoise_progress( + context_data.queue_item, + context_data.invocation, + intermediate_state, + ProgressImage(dataURL=dataURL, width=width, height=height), ) diff --git a/invokeai/backend/image_util/__init__.py b/invokeai/backend/image_util/__init__.py index dec2a92150..f45af9feb4 100644 --- a/invokeai/backend/image_util/__init__.py +++ b/invokeai/backend/image_util/__init__.py @@ -4,5 +4,4 @@ Initialization file for invokeai.backend.image_util methods. from .infill_methods.patchmatch import PatchMatch # noqa: F401 from .pngwriter import PngWriter, PromptFormatter, retrieve_metadata, write_metadata # noqa: F401 -from .seamless import configure_model_padding # noqa: F401 from .util import InitImageResizer, make_grid # noqa: F401 diff --git a/invokeai/backend/image_util/safety_checker.py b/invokeai/backend/image_util/safety_checker.py index 60dcd93fcc..ab09a29619 100644 --- a/invokeai/backend/image_util/safety_checker.py +++ b/invokeai/backend/image_util/safety_checker.py @@ -8,7 +8,7 @@ from pathlib import Path import numpy as np from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker -from PIL import Image +from PIL import Image, ImageFilter from transformers import AutoFeatureExtractor import invokeai.backend.util.logging as logger @@ -16,6 +16,7 @@ from invokeai.app.services.config.config_default import get_config from invokeai.backend.util.devices import TorchDevice from invokeai.backend.util.silence_warnings import SilenceWarnings +repo_id = "CompVis/stable-diffusion-safety-checker" CHECKER_PATH = "core/convert/stable-diffusion-safety-checker" @@ -24,30 +25,30 @@ class SafetyChecker: Wrapper around SafetyChecker model. """ - safety_checker = None feature_extractor = None - tried_load: bool = False + safety_checker = None @classmethod def _load_safety_checker(cls): - if cls.tried_load: + if cls.safety_checker is not None and cls.feature_extractor is not None: return try: - cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(get_config().models_path / CHECKER_PATH) - cls.feature_extractor = AutoFeatureExtractor.from_pretrained(get_config().models_path / CHECKER_PATH) + model_path = get_config().models_path / CHECKER_PATH + if model_path.exists(): + cls.feature_extractor = AutoFeatureExtractor.from_pretrained(model_path) + cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(model_path) + else: + model_path.mkdir(parents=True, exist_ok=True) + cls.feature_extractor = AutoFeatureExtractor.from_pretrained(repo_id) + cls.feature_extractor.save_pretrained(model_path, safe_serialization=True) + cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(repo_id) + cls.safety_checker.save_pretrained(model_path, safe_serialization=True) except Exception as e: logger.warning(f"Could not load NSFW checker: {str(e)}") - cls.tried_load = True - - @classmethod - def safety_checker_available(cls) -> bool: - return Path(get_config().models_path, CHECKER_PATH).exists() @classmethod def has_nsfw_concept(cls, image: Image.Image) -> bool: - if not cls.safety_checker_available() and cls.tried_load: - return False cls._load_safety_checker() if cls.safety_checker is None or cls.feature_extractor is None: return False @@ -60,3 +61,24 @@ class SafetyChecker: with SilenceWarnings(): checked_image, has_nsfw_concept = cls.safety_checker(images=x_image, clip_input=features.pixel_values) return has_nsfw_concept[0] + + @classmethod + def blur_if_nsfw(cls, image: Image.Image) -> Image.Image: + if cls.has_nsfw_concept(image): + logger.warning("A potentially NSFW image has been detected. Image will be blurred.") + blurry_image = image.filter(filter=ImageFilter.GaussianBlur(radius=32)) + caution = cls._get_caution_img() + # Center the caution image on the blurred image + x = (blurry_image.width - caution.width) // 2 + y = (blurry_image.height - caution.height) // 2 + blurry_image.paste(caution, (x, y), caution) + image = blurry_image + + return image + + @classmethod + def _get_caution_img(cls) -> Image.Image: + import invokeai.app.assets.images as image_assets + + caution = Image.open(Path(image_assets.__path__[0]) / "caution.png") + return caution.resize((caution.width // 2, caution.height // 2)) diff --git a/invokeai/backend/image_util/seamless.py b/invokeai/backend/image_util/seamless.py deleted file mode 100644 index 8a2580bfcc..0000000000 --- a/invokeai/backend/image_util/seamless.py +++ /dev/null @@ -1,52 +0,0 @@ -import torch.nn as nn - - -def _conv_forward_asymmetric(self, input, weight, bias): - """ - Patch for Conv2d._conv_forward that supports asymmetric padding - """ - working = nn.functional.pad(input, self.asymmetric_padding["x"], mode=self.asymmetric_padding_mode["x"]) - working = nn.functional.pad(working, self.asymmetric_padding["y"], mode=self.asymmetric_padding_mode["y"]) - return nn.functional.conv2d( - working, - weight, - bias, - self.stride, - nn.modules.utils._pair(0), - self.dilation, - self.groups, - ) - - -def configure_model_padding(model, seamless, seamless_axes): - """ - Modifies the 2D convolution layers to use a circular padding mode based on - the `seamless` and `seamless_axes` options. - """ - # TODO: get an explicit interface for this in diffusers: https://github.com/huggingface/diffusers/issues/556 - for m in model.modules(): - if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)): - if seamless: - m.asymmetric_padding_mode = {} - m.asymmetric_padding = {} - m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant" - m.asymmetric_padding["x"] = ( - m._reversed_padding_repeated_twice[0], - m._reversed_padding_repeated_twice[1], - 0, - 0, - ) - m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant" - m.asymmetric_padding["y"] = ( - 0, - 0, - m._reversed_padding_repeated_twice[2], - m._reversed_padding_repeated_twice[3], - ) - m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d) - else: - m._conv_forward = nn.Conv2d._conv_forward.__get__(m, nn.Conv2d) - if hasattr(m, "asymmetric_padding_mode"): - del m.asymmetric_padding_mode - if hasattr(m, "asymmetric_padding"): - del m.asymmetric_padding diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache_base.py b/invokeai/backend/model_manager/load/model_cache/model_cache_base.py index 6e6553db47..33e0c6683e 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_base.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_base.py @@ -43,9 +43,26 @@ T = TypeVar("T") @dataclass class CacheRecord(Generic[T]): - """Elements of the cache.""" + """ + Elements of the cache: + + key: Unique key for each model, same as used in the models database. + model: Model in memory. + state_dict: A read-only copy of the model's state dict in RAM. It will be + used as a template for creating a copy in the VRAM. + size: Size of the model + loaded: True if the model's state dict is currently in VRAM + + Before a model is executed, the state_dict template is copied into VRAM, + and then injected into the model. When the model is finished, the VRAM + copy of the state dict is deleted, and the RAM version is reinjected + into the model. + """ key: str + model: T + device: torch.device + state_dict: Optional[Dict[str, torch.Tensor]] size: int model: T loaded: bool = False diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py index 3cebfb8820..65420d233b 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py @@ -233,9 +233,10 @@ class ModelCache(ModelCacheBase[AnyModel]): if key in self._cached_models: return self.make_room(size) - cache_record = CacheRecord(key, model=model, size=size) - self._cached_models[key] = cache_record - self._cache_stack.append(key) + state_dict = model.state_dict() if isinstance(model, torch.nn.Module) else None + cache_record = CacheRecord(key=key, model=model, device=self.storage_device, state_dict=state_dict, size=size) + self._cached_models[key] = cache_record + self._cache_stack.append(key) def get( self, @@ -329,17 +330,37 @@ class ModelCache(ModelCacheBase[AnyModel]): if not (hasattr(cache_entry.model, "device") and hasattr(cache_entry.model, "to")): return - source_device = cache_entry.model.device + source_device = cache_entry.device # Note: We compare device types only so that 'cuda' == 'cuda:0'. # This would need to be revised to support multi-GPU. if torch.device(source_device).type == torch.device(target_device).type: return + # This roundabout method for moving the model around is done to avoid + # the cost of moving the model from RAM to VRAM and then back from VRAM to RAM. + # When moving to VRAM, we copy (not move) each element of the state dict from + # RAM to a new state dict in VRAM, and then inject it into the model. + # This operation is slightly faster than running `to()` on the whole model. + # + # When the model needs to be removed from VRAM we simply delete the copy + # of the state dict in VRAM, and reinject the state dict that is cached + # in RAM into the model. So this operation is very fast. start_model_to_time = time.time() snapshot_before = self._capture_memory_snapshot() + try: + if cache_entry.state_dict is not None: + assert hasattr(cache_entry.model, "load_state_dict") + if target_device == self.storage_device: + cache_entry.model.load_state_dict(cache_entry.state_dict, assign=True) + else: + new_dict: Dict[str, torch.Tensor] = {} + for k, v in cache_entry.state_dict.items(): + new_dict[k] = v.to(torch.device(target_device), copy=True) + cache_entry.model.load_state_dict(new_dict, assign=True) cache_entry.model.to(target_device) + cache_entry.device = target_device except Exception as e: # blow away cache entry self._delete_cache_entry(cache_entry) raise e @@ -419,43 +440,12 @@ class ModelCache(ModelCacheBase[AnyModel]): while current_size + bytes_needed > maximum_size and pos < len(self._cache_stack): model_key = self._cache_stack[pos] cache_entry = self._cached_models[model_key] - - refs = sys.getrefcount(cache_entry.model) - - # HACK: This is a workaround for a memory-management issue that we haven't tracked down yet. We are directly - # going against the advice in the Python docs by using `gc.get_referrers(...)` in this way: - # https://docs.python.org/3/library/gc.html#gc.get_referrers - - # manualy clear local variable references of just finished function calls - # for some reason python don't want to collect it even by gc.collect() immidiately - if refs > 2: - while True: - cleared = False - for referrer in gc.get_referrers(cache_entry.model): - if type(referrer).__name__ == "frame": - # RuntimeError: cannot clear an executing frame - with suppress(RuntimeError): - referrer.clear() - cleared = True - # break - - # repeat if referrers changes(due to frame clear), else exit loop - if cleared: - gc.collect() - else: - break - device = cache_entry.model.device if hasattr(cache_entry.model, "device") else None self.logger.debug( - f"Model: {model_key}, locks: {cache_entry._locks}, device: {device}, loaded: {cache_entry.loaded}," - f" refs: {refs}" + f"Model: {model_key}, locks: {cache_entry._locks}, device: {device}, loaded: {cache_entry.loaded}" ) - # Expected refs: - # 1 from cache_entry - # 1 from getrefcount function - # 1 from onnx runtime object - if not cache_entry.locked and refs <= (3 if "onnx" in model_key else 2): + if not cache_entry.locked: self.logger.debug( f"Removing {model_key} from RAM cache to free at least {(size/GIG):.2f} GB (-{(cache_entry.size/GIG):.2f} GB)" ) diff --git a/invokeai/backend/model_manager/load/model_cache/model_locker.py b/invokeai/backend/model_manager/load/model_cache/model_locker.py index b9349ea3dd..7bb411dfda 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_locker.py +++ b/invokeai/backend/model_manager/load/model_cache/model_locker.py @@ -80,5 +80,5 @@ class ModelLocker(ModelLockerBase): self._cache_entry.unlock() if not self._cache.lazy_offloading: - self._cache.offload_unlocked_models(self._cache_entry.size) + self._cache.offload_unlocked_models(0) self._cache.print_cuda_stats() diff --git a/invokeai/backend/stable_diffusion/seamless.py b/invokeai/backend/stable_diffusion/seamless.py index 2e22c19d0e..23ed978c6d 100644 --- a/invokeai/backend/stable_diffusion/seamless.py +++ b/invokeai/backend/stable_diffusion/seamless.py @@ -1,89 +1,51 @@ -from __future__ import annotations - from contextlib import contextmanager -from typing import Callable, List, Union +from typing import Callable, List, Optional, Tuple, Union +import torch import torch.nn as nn from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL from diffusers.models.autoencoders.autoencoder_tiny import AutoencoderTiny +from diffusers.models.lora import LoRACompatibleConv from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel -def _conv_forward_asymmetric(self, input, weight, bias): - """ - Patch for Conv2d._conv_forward that supports asymmetric padding - """ - working = nn.functional.pad(input, self.asymmetric_padding["x"], mode=self.asymmetric_padding_mode["x"]) - working = nn.functional.pad(working, self.asymmetric_padding["y"], mode=self.asymmetric_padding_mode["y"]) - return nn.functional.conv2d( - working, - weight, - bias, - self.stride, - nn.modules.utils._pair(0), - self.dilation, - self.groups, - ) - - @contextmanager def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL, AutoencoderTiny], seamless_axes: List[str]): if not seamless_axes: yield return - # Callable: (input: Tensor, weight: Tensor, bias: Optional[Tensor]) -> Tensor - to_restore: list[tuple[nn.Conv2d | nn.ConvTranspose2d, Callable]] = [] + # override conv_forward + # https://github.com/huggingface/diffusers/issues/556#issuecomment-1993287019 + def _conv_forward_asymmetric(self, input: torch.Tensor, weight: torch.Tensor, bias: Optional[torch.Tensor] = None): + self.paddingX = (self._reversed_padding_repeated_twice[0], self._reversed_padding_repeated_twice[1], 0, 0) + self.paddingY = (0, 0, self._reversed_padding_repeated_twice[2], self._reversed_padding_repeated_twice[3]) + working = torch.nn.functional.pad(input, self.paddingX, mode=x_mode) + working = torch.nn.functional.pad(working, self.paddingY, mode=y_mode) + return torch.nn.functional.conv2d( + working, weight, bias, self.stride, torch.nn.modules.utils._pair(0), self.dilation, self.groups + ) + + original_layers: List[Tuple[nn.Conv2d, Callable]] = [] + try: - # Hard coded to skip down block layers, allowing for seamless tiling at the expense of prompt adherence - skipped_layers = 1 - for m_name, m in model.named_modules(): - if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)): - continue + x_mode = "circular" if "x" in seamless_axes else "constant" + y_mode = "circular" if "y" in seamless_axes else "constant" - if isinstance(model, UNet2DConditionModel) and m_name.startswith("down_blocks.") and ".resnets." in m_name: - # down_blocks.1.resnets.1.conv1 - _, block_num, _, resnet_num, submodule_name = m_name.split(".") - block_num = int(block_num) - resnet_num = int(resnet_num) + conv_layers: List[torch.nn.Conv2d] = [] - if block_num >= len(model.down_blocks) - skipped_layers: - continue + for module in model.modules(): + if isinstance(module, torch.nn.Conv2d): + conv_layers.append(module) - # Skip the second resnet (could be configurable) - if resnet_num > 0: - continue - - # Skip Conv2d layers (could be configurable) - if submodule_name == "conv2": - continue - - m.asymmetric_padding_mode = {} - m.asymmetric_padding = {} - m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant" - m.asymmetric_padding["x"] = ( - m._reversed_padding_repeated_twice[0], - m._reversed_padding_repeated_twice[1], - 0, - 0, - ) - m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant" - m.asymmetric_padding["y"] = ( - 0, - 0, - m._reversed_padding_repeated_twice[2], - m._reversed_padding_repeated_twice[3], - ) - - to_restore.append((m, m._conv_forward)) - m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d) + for layer in conv_layers: + if isinstance(layer, LoRACompatibleConv) and layer.lora_layer is None: + layer.lora_layer = lambda *x: 0 + original_layers.append((layer, layer._conv_forward)) + layer._conv_forward = _conv_forward_asymmetric.__get__(layer, torch.nn.Conv2d) yield finally: - for module, orig_conv_forward in to_restore: - module._conv_forward = orig_conv_forward - if hasattr(module, "asymmetric_padding_mode"): - del module.asymmetric_padding_mode - if hasattr(module, "asymmetric_padding"): - del module.asymmetric_padding + for layer, orig_conv_forward in original_layers: + layer._conv_forward = orig_conv_forward diff --git a/invokeai/backend/textual_inversion.py b/invokeai/backend/textual_inversion.py index f7390979bb..98104f769e 100644 --- a/invokeai/backend/textual_inversion.py +++ b/invokeai/backend/textual_inversion.py @@ -1,7 +1,7 @@ """Textual Inversion wrapper class.""" from pathlib import Path -from typing import Dict, List, Optional, Union +from typing import Optional, Union import torch from compel.embeddings_provider import BaseTextualInversionManager @@ -66,35 +66,52 @@ class TextualInversionModelRaw(RawModel): return result -# no type hints for BaseTextualInversionManager? -class TextualInversionManager(BaseTextualInversionManager): # type: ignore - pad_tokens: Dict[int, List[int]] - tokenizer: CLIPTokenizer +class TextualInversionManager(BaseTextualInversionManager): + """TextualInversionManager implements the BaseTextualInversionManager ABC from the compel library.""" def __init__(self, tokenizer: CLIPTokenizer): - self.pad_tokens = {} + self.pad_tokens: dict[int, list[int]] = {} self.tokenizer = tokenizer def expand_textual_inversion_token_ids_if_necessary(self, token_ids: list[int]) -> list[int]: + """Given a list of tokens ids, expand any TI tokens to their corresponding pad tokens. + + For example, suppose we have a `` TI with 4 vectors that was added to the tokenizer with the following + mapping of tokens to token_ids: + ``` + : 49408 + : 49409 + : 49410 + : 49411 + ``` + `self.pad_tokens` would be set to `{49408: [49408, 49409, 49410, 49411]}`. + This function is responsible for expanding `49408` in the token_ids list to `[49408, 49409, 49410, 49411]`. + """ + # Short circuit if there are no pad tokens to save a little time. if len(self.pad_tokens) == 0: return token_ids + # This function assumes that compel has not included the BOS and EOS tokens in the token_ids list. We verify + # this assumption here. if token_ids[0] == self.tokenizer.bos_token_id: raise ValueError("token_ids must not start with bos_token_id") if token_ids[-1] == self.tokenizer.eos_token_id: raise ValueError("token_ids must not end with eos_token_id") - new_token_ids = [] + # Expand any TI tokens to their corresponding pad tokens. + new_token_ids: list[int] = [] for token_id in token_ids: new_token_ids.append(token_id) if token_id in self.pad_tokens: new_token_ids.extend(self.pad_tokens[token_id]) - # Do not exceed the max model input size - # The -2 here is compensating for compensate compel.embeddings_provider.get_token_ids(), - # which first removes and then adds back the start and end tokens. - max_length = list(self.tokenizer.max_model_input_sizes.values())[0] - 2 + # Do not exceed the max model input size. The -2 here is compensating for + # compel.embeddings_provider.get_token_ids(), which first removes and then adds back the start and end tokens. + max_length = self.tokenizer.model_max_length - 2 if len(new_token_ids) > max_length: + # HACK: If TI token expansion causes us to exceed the max text encoder input length, we silently discard + # tokens. Token expansion should happen in a way that is compatible with compel's default handling of long + # prompts. new_token_ids = new_token_ids[0:max_length] return new_token_ids diff --git a/invokeai/frontend/web/.eslintrc.js b/invokeai/frontend/web/.eslintrc.js index 18a6e3a9b9..519e725fb4 100644 --- a/invokeai/frontend/web/.eslintrc.js +++ b/invokeai/frontend/web/.eslintrc.js @@ -10,6 +10,8 @@ module.exports = { 'path/no-relative-imports': ['error', { maxDepth: 0 }], // https://github.com/edvardchen/eslint-plugin-i18next/blob/HEAD/docs/rules/no-literal-string.md 'i18next/no-literal-string': 'error', + // https://eslint.org/docs/latest/rules/no-console + 'no-console': 'error', }, overrides: [ /** diff --git a/invokeai/frontend/web/.gitignore b/invokeai/frontend/web/.gitignore index 3e8a372bc7..757d6ebcc8 100644 --- a/invokeai/frontend/web/.gitignore +++ b/invokeai/frontend/web/.gitignore @@ -43,4 +43,5 @@ stats.html yalc.lock # vitest -tsconfig.vitest-temp.json \ No newline at end of file +tsconfig.vitest-temp.json +coverage/ \ No newline at end of file diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 25a77cf918..f2210e4c68 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -35,6 +35,7 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "test": "vitest", + "test:ui": "vitest --coverage --ui", "test:no-watch": "vitest --no-watch" }, "madge": { @@ -52,48 +53,48 @@ }, "dependencies": { "@chakra-ui/react-use-size": "^2.1.0", - "@dagrejs/dagre": "^1.1.1", - "@dagrejs/graphlib": "^2.2.1", + "@dagrejs/dagre": "^1.1.2", + "@dagrejs/graphlib": "^2.2.2", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", - "@fontsource-variable/inter": "^5.0.17", + "@fontsource-variable/inter": "^5.0.18", "@invoke-ai/ui-library": "^0.0.25", "@nanostores/react": "^0.7.2", - "@reduxjs/toolkit": "2.2.2", + "@reduxjs/toolkit": "2.2.3", "@roarr/browser-log-writer": "^1.3.0", "chakra-react-select": "^4.7.6", "compare-versions": "^6.1.0", "dateformat": "^5.0.3", - "framer-motion": "^11.0.22", - "i18next": "^23.10.1", - "i18next-http-backend": "^2.5.0", + "fracturedjsonjs": "^4.0.1", + "framer-motion": "^11.1.8", + "i18next": "^23.11.3", + "i18next-http-backend": "^2.5.1", "idb-keyval": "^6.2.1", "jsondiffpatch": "^0.6.0", "konva": "^9.3.6", "lodash-es": "^4.17.21", - "nanostores": "^0.10.0", + "nanostores": "^0.10.3", "new-github-issue-url": "^1.0.0", - "overlayscrollbars": "^2.6.1", - "overlayscrollbars-react": "^0.5.5", + "overlayscrollbars": "^2.7.3", + "overlayscrollbars-react": "^0.5.6", "query-string": "^9.0.0", - "react": "^18.2.0", + "react": "^18.3.1", "react-colorful": "^5.6.1", - "react-dom": "^18.2.0", + "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-error-boundary": "^4.0.13", - "react-hook-form": "^7.51.2", + "react-hook-form": "^7.51.4", "react-hotkeys-hook": "4.5.0", - "react-i18next": "^14.1.0", - "react-icons": "^5.0.1", + "react-i18next": "^14.1.1", + "react-icons": "^5.2.0", "react-konva": "^18.2.10", - "react-redux": "9.1.0", - "react-resizable-panels": "^2.0.16", - "react-rnd": "^10.4.10", + "react-redux": "9.1.2", + "react-resizable-panels": "^2.0.19", "react-select": "5.8.0", "react-use": "^17.5.0", - "react-virtuoso": "^4.7.5", - "reactflow": "^11.10.4", + "react-virtuoso": "^4.7.10", + "reactflow": "^11.11.3", "redux-dynamic-middlewares": "^2.2.0", "redux-remember": "^5.1.0", "redux-undo": "^1.1.0", @@ -105,8 +106,8 @@ "use-device-pixel-ratio": "^1.1.2", "use-image": "^1.1.1", "uuid": "^9.0.1", - "zod": "^3.22.4", - "zod-validation-error": "^3.0.3" + "zod": "^3.23.6", + "zod-validation-error": "^3.2.0" }, "peerDependencies": { "@chakra-ui/react": "^2.8.2", @@ -117,40 +118,42 @@ "devDependencies": { "@invoke-ai/eslint-config-react": "^0.0.14", "@invoke-ai/prettier-config-react": "^0.0.7", - "@storybook/addon-essentials": "^8.0.4", - "@storybook/addon-interactions": "^8.0.4", - "@storybook/addon-links": "^8.0.4", - "@storybook/addon-storysource": "^8.0.4", - "@storybook/manager-api": "^8.0.4", - "@storybook/react": "^8.0.4", - "@storybook/react-vite": "^8.0.4", - "@storybook/theming": "^8.0.4", + "@storybook/addon-essentials": "^8.0.10", + "@storybook/addon-interactions": "^8.0.10", + "@storybook/addon-links": "^8.0.10", + "@storybook/addon-storysource": "^8.0.10", + "@storybook/manager-api": "^8.0.10", + "@storybook/react": "^8.0.10", + "@storybook/react-vite": "^8.0.10", + "@storybook/theming": "^8.0.10", "@types/dateformat": "^5.0.2", "@types/lodash-es": "^4.17.12", - "@types/node": "^20.11.30", - "@types/react": "^18.2.73", - "@types/react-dom": "^18.2.22", + "@types/node": "^20.12.10", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.0", "@types/uuid": "^9.0.8", "@vitejs/plugin-react-swc": "^3.6.0", + "@vitest/coverage-v8": "^1.5.0", + "@vitest/ui": "^1.5.0", "concurrently": "^8.2.2", "dpdm": "^3.14.0", "eslint": "^8.57.0", "eslint-plugin-i18next": "^6.0.3", "eslint-plugin-path": "^1.3.0", - "knip": "^5.6.1", + "knip": "^5.12.3", "openapi-types": "^12.1.3", "openapi-typescript": "^6.7.5", "prettier": "^3.2.5", "rollup-plugin-visualizer": "^5.12.0", - "storybook": "^8.0.4", + "storybook": "^8.0.10", "ts-toolbelt": "^9.6.0", "tsafe": "^1.6.6", - "typescript": "^5.4.3", - "vite": "^5.2.6", - "vite-plugin-css-injected-by-js": "^3.5.0", - "vite-plugin-dts": "^3.8.0", + "typescript": "^5.4.5", + "vite": "^5.2.11", + "vite-plugin-css-injected-by-js": "^3.5.1", + "vite-plugin-dts": "^3.9.1", "vite-plugin-eslint": "^1.8.1", "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.4.0" + "vitest": "^1.6.0" } } diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 3d688dddce..64189f0d82 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -7,58 +7,61 @@ settings: dependencies: '@chakra-ui/react': specifier: ^2.8.2 - version: 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) + version: 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.1)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1) '@chakra-ui/react-use-size': specifier: ^2.1.0 - version: 2.1.0(react@18.2.0) + version: 2.1.0(react@18.3.1) '@dagrejs/dagre': - specifier: ^1.1.1 - version: 1.1.1 + specifier: ^1.1.2 + version: 1.1.2 '@dagrejs/graphlib': - specifier: ^2.2.1 - version: 2.2.1 + specifier: ^2.2.2 + version: 2.2.2 '@dnd-kit/core': specifier: ^6.1.0 - version: 6.1.0(react-dom@18.2.0)(react@18.2.0) + version: 6.1.0(react-dom@18.3.1)(react@18.3.1) '@dnd-kit/sortable': specifier: ^8.0.0 - version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) + version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.3.1) '@dnd-kit/utilities': specifier: ^3.2.2 - version: 3.2.2(react@18.2.0) + version: 3.2.2(react@18.3.1) '@fontsource-variable/inter': - specifier: ^5.0.17 - version: 5.0.17 + specifier: ^5.0.18 + version: 5.0.18 '@invoke-ai/ui-library': specifier: ^0.0.25 - version: 0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.3)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) + version: 0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.18)(@internationalized/date@3.5.3)(@types/react@18.3.1)(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1) '@nanostores/react': specifier: ^0.7.2 - version: 0.7.2(nanostores@0.10.0)(react@18.2.0) + version: 0.7.2(nanostores@0.10.3)(react@18.3.1) '@reduxjs/toolkit': - specifier: 2.2.2 - version: 2.2.2(react-redux@9.1.0)(react@18.2.0) + specifier: 2.2.3 + version: 2.2.3(react-redux@9.1.2)(react@18.3.1) '@roarr/browser-log-writer': specifier: ^1.3.0 version: 1.3.0 chakra-react-select: specifier: ^4.7.6 - version: 4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.4)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + version: 4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.4)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) compare-versions: specifier: ^6.1.0 version: 6.1.0 dateformat: specifier: ^5.0.3 version: 5.0.3 + fracturedjsonjs: + specifier: ^4.0.1 + version: 4.0.1 framer-motion: - specifier: ^11.0.22 - version: 11.0.22(react-dom@18.2.0)(react@18.2.0) + specifier: ^11.1.8 + version: 11.1.8(react-dom@18.3.1)(react@18.3.1) i18next: - specifier: ^23.10.1 - version: 23.10.1 + specifier: ^23.11.3 + version: 23.11.3 i18next-http-backend: - specifier: ^2.5.0 - version: 2.5.0 + specifier: ^2.5.1 + version: 2.5.1 idb-keyval: specifier: ^6.2.1 version: 6.2.1 @@ -72,71 +75,68 @@ dependencies: specifier: ^4.17.21 version: 4.17.21 nanostores: - specifier: ^0.10.0 - version: 0.10.0 + specifier: ^0.10.3 + version: 0.10.3 new-github-issue-url: specifier: ^1.0.0 version: 1.0.0 overlayscrollbars: - specifier: ^2.6.1 - version: 2.6.1 + specifier: ^2.7.3 + version: 2.7.3 overlayscrollbars-react: - specifier: ^0.5.5 - version: 0.5.5(overlayscrollbars@2.6.1)(react@18.2.0) + specifier: ^0.5.6 + version: 0.5.6(overlayscrollbars@2.7.3)(react@18.3.1) query-string: specifier: ^9.0.0 version: 9.0.0 react: - specifier: ^18.2.0 - version: 18.2.0 + specifier: ^18.3.1 + version: 18.3.1 react-colorful: specifier: ^5.6.1 - version: 5.6.1(react-dom@18.2.0)(react@18.2.0) + version: 5.6.1(react-dom@18.3.1)(react@18.3.1) react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) react-dropzone: specifier: ^14.2.3 - version: 14.2.3(react@18.2.0) + version: 14.2.3(react@18.3.1) react-error-boundary: specifier: ^4.0.13 - version: 4.0.13(react@18.2.0) + version: 4.0.13(react@18.3.1) react-hook-form: - specifier: ^7.51.2 - version: 7.51.2(react@18.2.0) + specifier: ^7.51.4 + version: 7.51.4(react@18.3.1) react-hotkeys-hook: specifier: 4.5.0 - version: 4.5.0(react-dom@18.2.0)(react@18.2.0) + version: 4.5.0(react-dom@18.3.1)(react@18.3.1) react-i18next: - specifier: ^14.1.0 - version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) + specifier: ^14.1.1 + version: 14.1.1(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1) react-icons: - specifier: ^5.0.1 - version: 5.0.1(react@18.2.0) + specifier: ^5.2.0 + version: 5.2.0(react@18.3.1) react-konva: specifier: ^18.2.10 - version: 18.2.10(konva@9.3.6)(react-dom@18.2.0)(react@18.2.0) + version: 18.2.10(konva@9.3.6)(react-dom@18.3.1)(react@18.3.1) react-redux: - specifier: 9.1.0 - version: 9.1.0(@types/react@18.2.73)(react@18.2.0)(redux@5.0.1) + specifier: 9.1.2 + version: 9.1.2(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1) react-resizable-panels: - specifier: ^2.0.16 - version: 2.0.16(react-dom@18.2.0)(react@18.2.0) - react-rnd: - specifier: ^10.4.10 - version: 10.4.10(react-dom@18.2.0)(react@18.2.0) + specifier: ^2.0.19 + version: 2.0.19(react-dom@18.3.1)(react@18.3.1) react-select: specifier: 5.8.0 - version: 5.8.0(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + version: 5.8.0(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) react-use: specifier: ^17.5.0 - version: 17.5.0(react-dom@18.2.0)(react@18.2.0) + version: 17.5.0(react-dom@18.3.1)(react@18.3.1) react-virtuoso: - specifier: ^4.7.5 - version: 4.7.5(react-dom@18.2.0)(react@18.2.0) + specifier: ^4.7.10 + version: 4.7.10(react-dom@18.3.1)(react@18.3.1) reactflow: - specifier: ^11.10.4 - version: 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + specifier: ^11.11.3 + version: 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) redux-dynamic-middlewares: specifier: ^2.2.0 version: 2.2.0 @@ -160,54 +160,54 @@ dependencies: version: 4.7.5 use-debounce: specifier: ^10.0.0 - version: 10.0.0(react@18.2.0) + version: 10.0.0(react@18.3.1) use-device-pixel-ratio: specifier: ^1.1.2 - version: 1.1.2(react@18.2.0) + version: 1.1.2(react@18.3.1) use-image: specifier: ^1.1.1 - version: 1.1.1(react-dom@18.2.0)(react@18.2.0) + version: 1.1.1(react-dom@18.3.1)(react@18.3.1) uuid: specifier: ^9.0.1 version: 9.0.1 zod: - specifier: ^3.22.4 - version: 3.22.4 + specifier: ^3.23.6 + version: 3.23.6 zod-validation-error: - specifier: ^3.0.3 - version: 3.0.3(zod@3.22.4) + specifier: ^3.2.0 + version: 3.2.0(zod@3.23.6) devDependencies: '@invoke-ai/eslint-config-react': specifier: ^0.0.14 - version: 0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.3) + version: 0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5) '@invoke-ai/prettier-config-react': specifier: ^0.0.7 version: 0.0.7(prettier@3.2.5) '@storybook/addon-essentials': - specifier: ^8.0.4 - version: 8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + specifier: ^8.0.10 + version: 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) '@storybook/addon-interactions': - specifier: ^8.0.4 - version: 8.0.4(vitest@1.4.0) + specifier: ^8.0.10 + version: 8.0.10(vitest@1.6.0) '@storybook/addon-links': - specifier: ^8.0.4 - version: 8.0.4(react@18.2.0) + specifier: ^8.0.10 + version: 8.0.10(react@18.3.1) '@storybook/addon-storysource': - specifier: ^8.0.4 - version: 8.0.4 + specifier: ^8.0.10 + version: 8.0.10 '@storybook/manager-api': - specifier: ^8.0.4 - version: 8.0.4(react-dom@18.2.0)(react@18.2.0) + specifier: ^8.0.10 + version: 8.0.10(react-dom@18.3.1)(react@18.3.1) '@storybook/react': - specifier: ^8.0.4 - version: 8.0.4(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) + specifier: ^8.0.10 + version: 8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5) '@storybook/react-vite': - specifier: ^8.0.4 - version: 8.0.4(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3)(vite@5.2.6) + specifier: ^8.0.10 + version: 8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5)(vite@5.2.11) '@storybook/theming': - specifier: ^8.0.4 - version: 8.0.4(react-dom@18.2.0)(react@18.2.0) + specifier: ^8.0.10 + version: 8.0.10(react-dom@18.3.1)(react@18.3.1) '@types/dateformat': specifier: ^5.0.2 version: 5.0.2 @@ -215,20 +215,26 @@ devDependencies: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^20.11.30 - version: 20.11.30 + specifier: ^20.12.10 + version: 20.12.10 '@types/react': - specifier: ^18.2.73 - version: 18.2.73 + specifier: ^18.3.1 + version: 18.3.1 '@types/react-dom': - specifier: ^18.2.22 - version: 18.2.22 + specifier: ^18.3.0 + version: 18.3.0 '@types/uuid': specifier: ^9.0.8 version: 9.0.8 '@vitejs/plugin-react-swc': specifier: ^3.6.0 - version: 3.6.0(vite@5.2.6) + version: 3.6.0(vite@5.2.11) + '@vitest/coverage-v8': + specifier: ^1.5.0 + version: 1.6.0(vitest@1.6.0) + '@vitest/ui': + specifier: ^1.5.0 + version: 1.6.0(vitest@1.6.0) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -245,8 +251,8 @@ devDependencies: specifier: ^1.3.0 version: 1.3.0(eslint@8.57.0) knip: - specifier: ^5.6.1 - version: 5.6.1(@types/node@20.11.30)(typescript@5.4.3) + specifier: ^5.12.3 + version: 5.12.3(@types/node@20.12.10)(typescript@5.4.5) openapi-types: specifier: ^12.1.3 version: 12.1.3 @@ -260,8 +266,8 @@ devDependencies: specifier: ^5.12.0 version: 5.12.0 storybook: - specifier: ^8.0.4 - version: 8.0.4(react-dom@18.2.0)(react@18.2.0) + specifier: ^8.0.10 + version: 8.0.10(react-dom@18.3.1)(react@18.3.1) ts-toolbelt: specifier: ^9.6.0 version: 9.6.0 @@ -269,34 +275,29 @@ devDependencies: specifier: ^1.6.6 version: 1.6.6 typescript: - specifier: ^5.4.3 - version: 5.4.3 + specifier: ^5.4.5 + version: 5.4.5 vite: - specifier: ^5.2.6 - version: 5.2.6(@types/node@20.11.30) + specifier: ^5.2.11 + version: 5.2.11(@types/node@20.12.10) vite-plugin-css-injected-by-js: - specifier: ^3.5.0 - version: 3.5.0(vite@5.2.6) + specifier: ^3.5.1 + version: 3.5.1(vite@5.2.11) vite-plugin-dts: - specifier: ^3.8.0 - version: 3.8.0(@types/node@20.11.30)(typescript@5.4.3)(vite@5.2.6) + specifier: ^3.9.1 + version: 3.9.1(@types/node@20.12.10)(typescript@5.4.5)(vite@5.2.11) vite-plugin-eslint: specifier: ^1.8.1 - version: 1.8.1(eslint@8.57.0)(vite@5.2.6) + version: 1.8.1(eslint@8.57.0)(vite@5.2.11) vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.4.3)(vite@5.2.6) + version: 4.3.2(typescript@5.4.5)(vite@5.2.11) vitest: - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.11.30) + specifier: ^1.6.0 + version: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0) packages: - /@aashutoshrathi/word-wrap@1.2.6: - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - dev: true - /@adobe/css-tools@4.3.3: resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} dev: true @@ -348,7 +349,7 @@ packages: - '@internationalized/date' dev: false - /@ark-ui/react@1.3.0(@internationalized/date@3.5.3)(react-dom@18.2.0)(react@18.2.0): + /@ark-ui/react@1.3.0(@internationalized/date@3.5.3)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-JHjNoIX50+mUCTaEGMjfGQWGGi31pKsV646jZJlR/1xohpYJigzg8BvO97cTsVk8fwtur+cm11gz3Nf7f5QUnA==} peerDependencies: react: '>=18.0.0' @@ -378,7 +379,7 @@ packages: '@zag-js/progress': 0.32.1 '@zag-js/radio-group': 0.32.1 '@zag-js/rating-group': 0.32.1 - '@zag-js/react': 0.32.1(react-dom@18.2.0)(react@18.2.0) + '@zag-js/react': 0.32.1(react-dom@18.3.1)(react@18.3.1) '@zag-js/select': 0.32.1 '@zag-js/slider': 0.32.1 '@zag-js/splitter': 0.32.1 @@ -389,8 +390,8 @@ packages: '@zag-js/toggle-group': 0.32.1 '@zag-js/tooltip': 0.32.1 '@zag-js/types': 0.32.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@internationalized/date' dev: false @@ -406,28 +407,28 @@ packages: resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/highlight': 7.24.2 + '@babel/highlight': 7.24.5 picocolors: 1.0.0 - /@babel/compat-data@7.24.1: - resolution: {integrity: sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==} + /@babel/compat-data@7.24.4: + resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==} engines: {node: '>=6.9.0'} dev: true - /@babel/core@7.24.3: - resolution: {integrity: sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==} + /@babel/core@7.24.5: + resolution: {integrity: sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==} engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.1 + '@babel/generator': 7.24.5 '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3) - '@babel/helpers': 7.24.1 - '@babel/parser': 7.24.1 + '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) + '@babel/helpers': 7.24.5 + '@babel/parser': 7.24.5 '@babel/template': 7.24.0 - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 + '@babel/traverse': 7.24.5 + '@babel/types': 7.24.5 convert-source-map: 2.0.0 debug: 4.3.4 gensync: 1.0.0-beta.2 @@ -437,11 +438,11 @@ packages: - supports-color dev: true - /@babel/generator@7.24.1: - resolution: {integrity: sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==} + /@babel/generator@7.24.5: + resolution: {integrity: sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 @@ -451,65 +452,65 @@ packages: resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-compilation-targets@7.23.6: resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/compat-data': 7.24.1 + '@babel/compat-data': 7.24.4 '@babel/helper-validator-option': 7.23.5 browserslist: 4.23.0 lru-cache: 5.1.1 semver: 6.3.1 dev: true - /@babel/helper-create-class-features-plugin@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA==} + /@babel/helper-create-class-features-plugin@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 - '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.24.5 '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.3) + '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-split-export-declaration': 7.24.5 semver: 6.3.1 dev: true - /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.24.3): + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.24.5): resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-annotate-as-pure': 7.22.5 regexpu-core: 5.3.2 semver: 6.3.1 dev: true - /@babel/helper-define-polyfill-provider@0.6.1(@babel/core@7.24.3): - resolution: {integrity: sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==} + /@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.5): + resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-plugin-utils': 7.24.5 debug: 4.3.4 lodash.debounce: 4.0.8 resolve: 1.22.8 @@ -527,106 +528,106 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.24.0 - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true - /@babel/helper-member-expression-to-functions@7.23.0: - resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} + /@babel/helper-member-expression-to-functions@7.24.5: + resolution: {integrity: sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-module-imports@7.24.3: resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 - /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.3): - resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + /@babel/helper-module-transforms@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-module-imports': 7.24.3 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-simple-access': 7.24.5 + '@babel/helper-split-export-declaration': 7.24.5 + '@babel/helper-validator-identifier': 7.24.5 dev: true /@babel/helper-optimise-call-expression@7.22.5: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true - /@babel/helper-plugin-utils@7.24.0: - resolution: {integrity: sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==} + /@babel/helper-plugin-utils@7.24.5: + resolution: {integrity: sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==} engines: {node: '>=6.9.0'} dev: true - /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.24.3): + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.24.5): resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-wrap-function': 7.22.20 + '@babel/helper-wrap-function': 7.24.5 dev: true - /@babel/helper-replace-supers@7.24.1(@babel/core@7.24.3): + /@babel/helper-replace-supers@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-member-expression-to-functions': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.24.5 '@babel/helper-optimise-call-expression': 7.22.5 dev: true - /@babel/helper-simple-access@7.22.5: - resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + /@babel/helper-simple-access@7.24.5: + resolution: {integrity: sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-skip-transparent-expression-wrappers@7.22.5: resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true - /@babel/helper-split-export-declaration@7.22.6: - resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + /@babel/helper-split-export-declaration@7.24.5: + resolution: {integrity: sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@babel/helper-string-parser@7.24.1: resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-identifier@7.22.20: - resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + /@babel/helper-validator-identifier@7.24.5: + resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==} engines: {node: '>=6.9.0'} /@babel/helper-validator-option@7.23.5: @@ -634,974 +635,986 @@ packages: engines: {node: '>=6.9.0'} dev: true - /@babel/helper-wrap-function@7.22.20: - resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==} + /@babel/helper-wrap-function@7.24.5: + resolution: {integrity: sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-function-name': 7.23.0 '@babel/template': 7.24.0 - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true - /@babel/helpers@7.24.1: - resolution: {integrity: sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==} + /@babel/helpers@7.24.5: + resolution: {integrity: sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==} engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.24.0 - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 + '@babel/traverse': 7.24.5 + '@babel/types': 7.24.5 transitivePeerDependencies: - supports-color dev: true - /@babel/highlight@7.24.2: - resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} + /@babel/highlight@7.24.5: + resolution: {integrity: sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-validator-identifier': 7.24.5 chalk: 2.4.2 js-tokens: 4.0.0 picocolors: 1.0.0 - /@babel/parser@7.24.1: - resolution: {integrity: sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==} + /@babel/parser@7.24.5: + resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.1(@babel/core@7.24.3): + /@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-LdXRi1wEMTrHVR4Zc9F8OewC3vdm5h4QB6L71zy6StmYeqGi1b3ttIO8UC+BfZKcH9jdr4aI249rBkm+3+YvHw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-plugin-utils': 7.24.5 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.1(@babel/core@7.24.3): + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-transform-optional-chaining': 7.24.1(@babel/core@7.24.3) + '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.5) dev: true - /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.1(@babel/core@7.24.3): + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.3): + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.5): resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 dev: true - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.3): + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.5): resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.3): + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.5): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.3): + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.5): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-flow@7.24.1(@babel/core@7.24.3): + /@babel/plugin-syntax-flow@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-sxi2kLTI5DeW5vDtMUsk4mTPwvlUDbjOnoWayhynCwrw4QXRld4QEYwqzY8JmQXaJUtgUuCIurtSRH5sn4c7mA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-import-assertions@7.24.1(@babel/core@7.24.3): + /@babel/plugin-syntax-import-assertions@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-import-attributes@7.24.1(@babel/core@7.24.3): + /@babel/plugin-syntax-import-attributes@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.3): + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.5): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.3): + /@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.3): + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.5): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.3): + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.5): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.3): + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.5): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.3): + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.5): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.3): + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.5): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-typescript@7.24.1(@babel/core@7.24.3): + /@babel/plugin-syntax-typescript@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.3): + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.5): resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-arrow-functions@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-arrow-functions@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-async-generator-functions@7.24.3(@babel/core@7.24.3): + /@babel/plugin-transform-async-generator-functions@7.24.3(@babel/core@7.24.5): resolution: {integrity: sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.3) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.5) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-async-to-generator@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-async-to-generator@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-module-imports': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-block-scoped-functions@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-block-scoped-functions@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-block-scoping@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==} + /@babel/plugin-transform-block-scoping@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-class-properties@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-class-properties@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-class-features-plugin': 7.24.1(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-class-static-block@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-FUHlKCn6J3ERiu8Dv+4eoz7w8+kFLSyeVG4vDAikwADGjUCoHw/JHokyGtr8OR4UjpwPVivyF+h8Q5iv/JmrtA==} + /@babel/plugin-transform-class-static-block@7.24.4(@babel/core@7.24.5): + resolution: {integrity: sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-class-features-plugin': 7.24.1(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-classes@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q==} + /@babel/plugin-transform-classes@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.3) - '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) + '@babel/helper-split-export-declaration': 7.24.5 globals: 11.12.0 dev: true - /@babel/plugin-transform-computed-properties@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-computed-properties@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/template': 7.24.0 dev: true - /@babel/plugin-transform-destructuring@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw==} + /@babel/plugin-transform-destructuring@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-dotall-regex@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-dotall-regex@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-duplicate-keys@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-duplicate-keys@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-dynamic-import@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-dynamic-import@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-exponentiation-operator@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-exponentiation-operator@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-export-namespace-from@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-export-namespace-from@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-flow-strip-types@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-flow-strip-types@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-iIYPIWt3dUmUKKE10s3W+jsQ3icFkw0JyRVyY1B7G4yK/nngAOHLVx8xlhA6b/Jzl/Y0nis8gjqhqKtRDQqHWQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-flow': 7.24.1(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-flow': 7.24.1(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-for-of@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-for-of@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true - /@babel/plugin-transform-function-name@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-function-name@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-json-strings@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-json-strings@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-literals@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-literals@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-logical-assignment-operators@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-logical-assignment-operators@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-member-expression-literals@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-member-expression-literals@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-modules-amd@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-modules-amd@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-modules-commonjs@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-modules-commonjs@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-simple-access': 7.22.5 + '@babel/core': 7.24.5 + '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/helper-simple-access': 7.24.5 dev: true - /@babel/plugin-transform-modules-systemjs@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-modules-systemjs@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/helper-validator-identifier': 7.24.5 dev: true - /@babel/plugin-transform-modules-umd@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-modules-umd@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.3): + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.5): resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-new-target@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-new-target@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-nullish-coalescing-operator@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-nullish-coalescing-operator@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-numeric-separator@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-numeric-separator@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-object-rest-spread@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA==} + /@babel/plugin-transform-object-rest-spread@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-transform-parameters': 7.24.1(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-transform-parameters': 7.24.5(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-object-super@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-object-super@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-optional-catch-binding@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-optional-catch-binding@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-optional-chaining@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg==} + /@babel/plugin-transform-optional-chaining@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-parameters@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg==} + /@babel/plugin-transform-parameters@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-private-methods@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-private-methods@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-class-features-plugin': 7.24.1(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-private-property-in-object@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg==} + /@babel/plugin-transform-private-property-in-object@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.24.1(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.3) + '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-property-literals@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-property-literals@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-regenerator@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-regenerator@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 regenerator-transform: 0.15.2 dev: true - /@babel/plugin-transform-reserved-words@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-reserved-words@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-shorthand-properties@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-shorthand-properties@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-spread@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-spread@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true - /@babel/plugin-transform-sticky-regex@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-sticky-regex@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-template-literals@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-template-literals@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-typeof-symbol@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA==} + /@babel/plugin-transform-typeof-symbol@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-typescript@7.24.1(@babel/core@7.24.3): - resolution: {integrity: sha512-liYSESjX2fZ7JyBFkYG78nfvHlMKE6IpNdTVnxmlYUR+j5ZLsitFbaAE+eJSK2zPPkNWNw4mXL51rQ8WrvdK0w==} + /@babel/plugin-transform-typescript@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-E0VWu/hk83BIFUWnsKZ4D81KXjN5L3MobvevOHErASk9IPwKHOkTgvqzvNo1yP/ePJWqqK2SpUR5z+KQbl6NVw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.24.1(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.24.3) + '@babel/helper-create-class-features-plugin': 7.24.5(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 + '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.24.5) dev: true - /@babel/plugin-transform-unicode-escapes@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-unicode-escapes@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-unicode-property-regex@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-unicode-property-regex@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-unicode-regex@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-unicode-regex@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/plugin-transform-unicode-sets-regex@7.24.1(@babel/core@7.24.3): + /@babel/plugin-transform-unicode-sets-regex@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.3) - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.5 dev: true - /@babel/preset-env@7.24.3(@babel/core@7.24.3): - resolution: {integrity: sha512-fSk430k5c2ff8536JcPvPWK4tZDwehWLGlBp0wrsBUjZVdeQV6lePbwKWZaZfK2vnh/1kQX1PzAJWsnBmVgGJA==} + /@babel/preset-env@7.24.5(@babel/core@7.24.5): + resolution: {integrity: sha512-UGK2ifKtcC8i5AI4cH+sbLLuLc2ktYSFJgBAXorKAsHUZmrQ1q6aQ6i3BvU24wWs2AAKqQB6kq3N9V9Gw1HiMQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.24.1 - '@babel/core': 7.24.3 + '@babel/compat-data': 7.24.4 + '@babel/core': 7.24.5 '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.3) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.3) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.3) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.3) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-import-assertions': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-syntax-import-attributes': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.3) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.3) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.3) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.3) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.3) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.3) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.3) - '@babel/plugin-transform-arrow-functions': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-async-generator-functions': 7.24.3(@babel/core@7.24.3) - '@babel/plugin-transform-async-to-generator': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-block-scoped-functions': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-block-scoping': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-class-static-block': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-classes': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-computed-properties': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-destructuring': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-dotall-regex': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-duplicate-keys': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-dynamic-import': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-exponentiation-operator': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-export-namespace-from': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-for-of': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-function-name': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-json-strings': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-literals': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-logical-assignment-operators': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-member-expression-literals': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-modules-amd': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-modules-systemjs': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-modules-umd': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.24.3) - '@babel/plugin-transform-new-target': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-numeric-separator': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-object-rest-spread': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-object-super': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-optional-catch-binding': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-optional-chaining': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-parameters': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-private-methods': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-private-property-in-object': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-property-literals': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-regenerator': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-reserved-words': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-shorthand-properties': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-spread': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-sticky-regex': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-template-literals': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-typeof-symbol': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-unicode-escapes': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-unicode-property-regex': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-unicode-regex': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-unicode-sets-regex': 7.24.1(@babel/core@7.24.3) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.3) - babel-plugin-polyfill-corejs2: 0.4.10(@babel/core@7.24.3) - babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.3) - babel-plugin-polyfill-regenerator: 0.6.1(@babel/core@7.24.3) - core-js-compat: 3.36.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.5) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.5) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.5) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.5) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-import-assertions': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-syntax-import-attributes': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.5) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.5) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.5) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.5) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.5) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.5) + '@babel/plugin-transform-arrow-functions': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-async-generator-functions': 7.24.3(@babel/core@7.24.5) + '@babel/plugin-transform-async-to-generator': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-block-scoped-functions': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-block-scoping': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-class-static-block': 7.24.4(@babel/core@7.24.5) + '@babel/plugin-transform-classes': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-computed-properties': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-destructuring': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-dotall-regex': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-duplicate-keys': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-dynamic-import': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-exponentiation-operator': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-export-namespace-from': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-for-of': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-function-name': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-json-strings': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-literals': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-logical-assignment-operators': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-member-expression-literals': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-modules-amd': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-modules-systemjs': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-modules-umd': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.24.5) + '@babel/plugin-transform-new-target': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-numeric-separator': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-object-rest-spread': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-object-super': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-optional-catch-binding': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-parameters': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-private-methods': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-private-property-in-object': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-property-literals': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-regenerator': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-reserved-words': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-shorthand-properties': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-spread': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-sticky-regex': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-template-literals': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-typeof-symbol': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-unicode-escapes': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-unicode-property-regex': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-unicode-regex': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-unicode-sets-regex': 7.24.1(@babel/core@7.24.5) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.5) + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.5) + babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.5) + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.5) + core-js-compat: 3.37.0 semver: 6.3.1 transitivePeerDependencies: - supports-color dev: true - /@babel/preset-flow@7.24.1(@babel/core@7.24.3): + /@babel/preset-flow@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-sWCV2G9pcqZf+JHyv/RyqEIpFypxdCSxWIxQjpdaQxenNog7cN1pr76hg8u0Fz8Qgg0H4ETkGcJnXL8d4j0PPA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-transform-flow-strip-types': 7.24.1(@babel/core@7.24.3) + '@babel/plugin-transform-flow-strip-types': 7.24.1(@babel/core@7.24.5) dev: true - /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.3): + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.5): resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/types': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + '@babel/types': 7.24.5 esutils: 2.0.3 dev: true - /@babel/preset-typescript@7.24.1(@babel/core@7.24.3): + /@babel/preset-typescript@7.24.1(@babel/core@7.24.5): resolution: {integrity: sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-typescript': 7.24.1(@babel/core@7.24.3) + '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-typescript': 7.24.5(@babel/core@7.24.5) dev: true - /@babel/register@7.23.7(@babel/core@7.24.3): + /@babel/register@7.23.7(@babel/core@7.24.5): resolution: {integrity: sha512-EjJeB6+kvpk+Y5DAkEAmbOBEFkh9OASx0huoEkqYTFxAZHzOAX2Oh5uwAUuL2rUddqfM0SA+KPXV2TbzoZ2kvQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 clone-deep: 4.0.1 find-cache-dir: 2.1.0 make-dir: 2.1.0 @@ -1625,127 +1638,138 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.1 + dev: false + + /@babel/runtime@7.24.5: + resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 /@babel/template@7.24.0: resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.24.2 - '@babel/parser': 7.24.1 - '@babel/types': 7.24.0 + '@babel/parser': 7.24.5 + '@babel/types': 7.24.5 dev: true - /@babel/traverse@7.24.1: - resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} + /@babel/traverse@7.24.5: + resolution: {integrity: sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.1 + '@babel/generator': 7.24.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.24.1 - '@babel/types': 7.24.0 + '@babel/helper-split-export-declaration': 7.24.5 + '@babel/parser': 7.24.5 + '@babel/types': 7.24.5 debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color dev: true - /@babel/types@7.24.0: - resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} + /@babel/types@7.24.5: + resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-string-parser': 7.24.1 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-validator-identifier': 7.24.5 to-fast-properties: 2.0.0 /@base2/pretty-print-object@1.0.1: resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} dev: true - /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0): + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + + /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/descendant': 3.1.0(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/descendant': 3.1.0(react@18.3.1) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): + /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1): resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/descendant': 3.1.0(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/descendant': 3.1.0(react@18.3.1) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@11.1.8)(react@18.3.1) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/alert@2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/alert@2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-jHg4LYMRNOJH830ViLuicjb3F+v6iriE/2G5T+Sd0Hna04nukNJ1MxUmBPE+vI22me2dIflfelu2v9wdB6Pojw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/anatomy@2.2.2: resolution: {integrity: sha512-MV6D4VLRIHr4PkW4zMyqfrNS1mPlCTiCXwvYGtDFQYr+xHFfonhAuf9WjsSc0nyp2m0OdkSLnzmVKkZFLo25Tg==} dev: false - /@chakra-ui/avatar@2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/avatar@2.3.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-8gKSyLfygnaotbJbDMHDiJoF38OHXUYVme4gGxZ1fLnQEdPVEaIWfH+NndIjOM0z8S+YEFnT9KyGMUtvPrBk3g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/breadcrumb@2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/breadcrumb@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-4cWCG24flYBxjruRi4RJREWTGF74L/KzI2CognAW/d/zWR0CjiScuJhf37Am3LFbCySP6WSoyBOtTIoTA4yLEA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/breakpoint-utils@2.0.8: @@ -1754,334 +1778,334 @@ packages: '@chakra-ui/shared-utils': 2.0.5 dev: false - /@chakra-ui/button@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/button@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-95CplwlRKmmUXkdEp/21VkEWgnwcx2TOBG6NfYlsuLBDHSLlo5FKIiE2oSi4zXc4TLcopGcWPNcm/NDaSC5pvA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/card@2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/card@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-xUB/k5MURj4CtPAhdSoXZidUbm8j3hci9vnc+eZJVDqhDOShNlD6QeniQNRPRys4lWAQLCbFcrwL29C8naDi6g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/checkbox@2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/checkbox@2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-85g38JIXMEv6M+AcyIGLh7igNtfpAN6KGQFYxY9tBj0eWvWk4NKQxvqqyVta0bSAyIl1rixNIIezNpNWk2iO4g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@zag-js/focus-visible': 0.16.0 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/clickable@2.1.0(react@18.2.0): + /@chakra-ui/clickable@2.1.0(react@18.3.1): resolution: {integrity: sha512-flRA/ClPUGPYabu+/GLREZVZr9j2uyyazCAUHAdrTUEdDYCr31SVGhgh7dgKdtq23bOvAQJpIJjw/0Bs0WvbXw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/close-button@2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/close-button@2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-gnpENKOanKexswSVpVz7ojZEALl2x5qjLYNqSQGbxz+aP9sOXPfUS56ebyBrre7T7exuWGiFeRwnM0oVeGPaiw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/color-mode@2.2.0(react@18.2.0): + /@chakra-ui/color-mode@2.2.0(react@18.3.1): resolution: {integrity: sha512-niTEA8PALtMWRI9wJ4LL0CSBDo8NBfLNp4GD6/0hstcm3IlbBHTVKxN6HwSaoNYfphDQLxCjT4yG+0BJA5tFpg==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/control-box@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/control-box@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-gVrRDyXFdMd8E7rulL0SKeoljkLQiPITFnsyMO8EFHNZ+AHt5wK4LIguYVEq88APqAGZGfHFWXr79RYrNiE3Mg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/counter@2.1.0(react@18.2.0): + /@chakra-ui/counter@2.1.0(react@18.3.1): resolution: {integrity: sha512-s6hZAEcWT5zzjNz2JIWUBzRubo9la/oof1W7EKZVVfPYHERnl5e16FmBC79Yfq8p09LQ+aqFKm/etYoJMMgghw==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/number-utils': 2.0.7 - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/css-reset@2.3.0(@emotion/react@11.11.4)(react@18.2.0): + /@chakra-ui/css-reset@2.3.0(@emotion/react@11.11.4)(react@18.3.1): resolution: {integrity: sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==} peerDependencies: '@emotion/react': '>=10.0.35' react: '>=18' dependencies: - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - react: 18.2.0 + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/descendant@3.1.0(react@18.2.0): + /@chakra-ui/descendant@3.1.0(react@18.3.1): resolution: {integrity: sha512-VxCIAir08g5w27klLyi7PVo8BxhW4tgU/lxQyujkmi4zx7hT9ZdrcQLAted/dAa+aSIZ14S1oV0Q9lGjsAdxUQ==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/dom-utils@2.1.0: resolution: {integrity: sha512-ZmF2qRa1QZ0CMLU8M1zCfmw29DmPNtfjR9iTo74U5FPr3i1aoAh7fbJ4qAlZ197Xw9eAW28tvzQuoVWeL5C7fQ==} dev: false - /@chakra-ui/editable@3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/editable@3.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-j2JLrUL9wgg4YA6jLlbU88370eCRyor7DZQD9lzpY95tSOXpTljeg3uF9eOmDnCs6fxp3zDWIfkgMm/ExhcGTg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/event-utils@2.0.8: resolution: {integrity: sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==} dev: false - /@chakra-ui/focus-lock@2.1.0(@types/react@18.2.73)(react@18.2.0): + /@chakra-ui/focus-lock@2.1.0(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/dom-utils': 2.1.0 - react: 18.2.0 - react-focus-lock: 2.11.1(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-focus-lock: 2.11.1(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/form-control@2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/form-control@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-wehLC1t4fafCVJ2RvJQT2jyqsAwX7KymmiGqBu7nQoQz8ApTkGABWpo/QwDh3F/dBLrouHDoOvGmYTqft3Mirw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/hooks@2.2.1(react@18.2.0): + /@chakra-ui/hooks@2.2.1(react@18.3.1): resolution: {integrity: sha512-RQbTnzl6b1tBjbDPf9zGRo9rf/pQMholsOudTxjy4i9GfTfz6kgp5ValGjQm2z7ng6Z31N1cnjZ1AlSzQ//ZfQ==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-utils': 2.0.12(react@18.2.0) + '@chakra-ui/react-utils': 2.0.12(react@18.3.1) '@chakra-ui/utils': 2.0.15 compute-scroll-into-view: 3.0.3 copy-to-clipboard: 3.3.3 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/icon@3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/icon@3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-xxjGLvlX2Ys4H0iHrI16t74rG9EBcpFvJ3Y3B7KMQTrnW34Kf7Da/UC8J67Gtx85mTHW020ml85SVPKORWNNKQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/icons@2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/icons@2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-3p30hdo4LlRZTT5CwoAJq3G9fHI0wDc0pBaMHj4SUn0yomO+RcDRlzhdXqdr5cVnzax44sqXJVnf3oQG0eI+4g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/image@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/image@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-bskumBYKLiLMySIWDGcz0+D9Th0jPvmX6xnRMs4o92tT3Od/bW26lahmV2a2Op2ItXeCmRMY+XxJH5Gy1i46VA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/input@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/input@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-GiBbb3EqAA8Ph43yGa6Mc+kUPjh4Spmxp1Pkelr8qtudpc3p2PJOOebLpd90mcqw8UePPa+l6YhhPtp6o0irhw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/object-utils': 2.1.0 - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/layout@2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/layout@2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-nXuZ6WRbq0WdgnRgLw+QuxWAHuhDtVX8ElWqcTK+cSMFg/52eVP47czYBE5F35YhnoW2XBwfNoNgZ7+e8Z01Rg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: '@chakra-ui/breakpoint-utils': 2.0.8 - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/object-utils': 2.1.0 - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/lazy-utils@2.0.5: resolution: {integrity: sha512-UULqw7FBvcckQk2n3iPO56TMJvDsNv0FKZI6PlUNJVaGsPbsYxK/8IQ60vZgaTVPtVcjY6BE+y6zg8u9HOqpyg==} dev: false - /@chakra-ui/live-region@2.1.0(react@18.2.0): + /@chakra-ui/live-region@2.1.0(react@18.3.1): resolution: {integrity: sha512-ZOxFXwtaLIsXjqnszYYrVuswBhnIHHP+XIgK1vC6DePKtyK590Wg+0J0slDwThUAd4MSSIUa/nNX84x1GMphWw==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/media-query@3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/media-query@3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-IsTGgFLoICVoPRp9ykOgqmdMotJG0CnPsKvGQeSFOB/dZfIujdVb14TYxDU4+MURXry1MhJ7LzZhv+Ml7cr8/g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: '@chakra-ui/breakpoint-utils': 2.0.8 - '@chakra-ui/react-env': 3.1.0(react@18.2.0) + '@chakra-ui/react-env': 3.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0): + /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): resolution: {integrity: sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/clickable': 2.1.0(react@18.2.0) - '@chakra-ui/descendant': 3.1.0(react@18.2.0) + '@chakra-ui/clickable': 2.1.0(react@18.3.1) + '@chakra-ui/descendant': 3.1.0(react@18.3.1) '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-outside-click': 2.2.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-animation-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-outside-click': 2.2.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): + /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1): resolution: {integrity: sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/clickable': 2.1.0(react@18.2.0) - '@chakra-ui/descendant': 3.1.0(react@18.2.0) + '@chakra-ui/clickable': 2.1.0(react@18.3.1) + '@chakra-ui/descendant': 3.1.0(react@18.3.1) '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-outside-click': 2.2.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-animation-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-outside-click': 2.2.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@11.1.8)(react@18.3.1) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.3.1)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2089,25 +2113,25 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.1)(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) aria-hidden: 1.2.3 - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.7(@types/react@18.2.73)(react@18.2.0) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.3.1)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2115,44 +2139,44 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.1)(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@11.1.8)(react@18.3.1) aria-hidden: 1.2.3 - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.7(@types/react@18.2.73)(react@18.2.0) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/number-input@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/number-input@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-pfOdX02sqUN0qC2ysuvgVDiws7xZ20XDIlcNhva55Jgm095xjm8eVdIBfNm3SFbSUNxyXvLTW/YQanX74tKmuA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/counter': 2.1.0(react@18.2.0) - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-interval': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/counter': 2.1.0(react@18.3.1) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-interval': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/number-utils@2.0.7: @@ -2163,103 +2187,103 @@ packages: resolution: {integrity: sha512-tgIZOgLHaoti5PYGPTwK3t/cqtcycW0owaiOXoZOcpwwX/vlVb+H1jFsQyWiiwQVPt9RkoSLtxzXamx+aHH+bQ==} dev: false - /@chakra-ui/pin-input@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/pin-input@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-x4vBqLStDxJFMt+jdAHHS8jbh294O53CPQJoL4g228P513rHylV/uPscYUHrVJXRxsHfRztQO9k45jjTYaPRMw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/descendant': 3.1.0(react@18.2.0) - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/descendant': 3.1.0(react@18.3.1) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0): + /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): resolution: {integrity: sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-animation-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): + /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1): resolution: {integrity: sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-animation-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/popper@3.1.0(react@18.2.0): + /@chakra-ui/popper@3.1.0(react@18.3.1): resolution: {integrity: sha512-ciDdpdYbeFG7og6/6J8lkTFxsSvwTdMLFkpVylAF6VNC22jssiWfquj2eyD4rJnzkRFPvIWJq8hvbfhsm+AjSg==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@popperjs/core': 2.11.8 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/portal@2.1.0(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/portal@2.1.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-9q9KWf6SArEcIq1gGofNcFPSWEyl+MfJjEUg/un1SMlQjaROOh3zYr+6JAwvcORiX7tyHosnmWC3d3wI2aPSQg==} peerDependencies: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /@chakra-ui/progress@2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/progress@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-qUXuKbuhN60EzDD9mHR7B67D7p/ZqNS2Aze4Pbl1qGGZfulPW0PY8Rof32qDtttDQBkzQIzFGE8d9QpAemToIQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/provider@2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/provider@2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==} peerDependencies: '@emotion/react': ^11.0.0 @@ -2267,229 +2291,229 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-env': 3.1.0(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-env': 3.1.0(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /@chakra-ui/radio@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/radio@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-n10M46wJrMGbonaghvSRnZ9ToTv/q76Szz284gv4QUWvyljQACcGrXIONUnQ3BIwbOfkRqSk7Xl/JgZtVfll+w==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) '@zag-js/focus-visible': 0.16.0 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-children-utils@2.0.6(react@18.2.0): + /@chakra-ui/react-children-utils@2.0.6(react@18.3.1): resolution: {integrity: sha512-QVR2RC7QsOsbWwEnq9YduhpqSFnZGvjjGREV8ygKi8ADhXh93C8azLECCUVgRJF2Wc+So1fgxmjLcbZfY2VmBA==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-context@2.1.0(react@18.2.0): + /@chakra-ui/react-context@2.1.0(react@18.3.1): resolution: {integrity: sha512-iahyStvzQ4AOwKwdPReLGfDesGG+vWJfEsn0X/NoGph/SkN+HXtv2sCfYFFR9k7bb+Kvc6YfpLlSuLvKMHi2+w==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-env@3.1.0(react@18.2.0): + /@chakra-ui/react-env@3.1.0(react@18.3.1): resolution: {integrity: sha512-Vr96GV2LNBth3+IKzr/rq1IcnkXv+MLmwjQH6C8BRtn3sNskgDFD5vLkVXcEhagzZMCh8FR3V/bzZPojBOyNhw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-types@2.0.7(react@18.2.0): + /@chakra-ui/react-types@2.0.7(react@18.3.1): resolution: {integrity: sha512-12zv2qIZ8EHwiytggtGvo4iLT0APris7T0qaAWqzpUGS0cdUtR8W+V1BJ5Ocq+7tA6dzQ/7+w5hmXih61TuhWQ==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-animation-state@2.1.0(react@18.2.0): + /@chakra-ui/react-use-animation-state@2.1.0(react@18.3.1): resolution: {integrity: sha512-CFZkQU3gmDBwhqy0vC1ryf90BVHxVN8cTLpSyCpdmExUEtSEInSCGMydj2fvn7QXsz/za8JNdO2xxgJwxpLMtg==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/dom-utils': 2.1.0 - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-callback-ref@2.1.0(react@18.2.0): + /@chakra-ui/react-use-callback-ref@2.1.0(react@18.3.1): resolution: {integrity: sha512-efnJrBtGDa4YaxDzDE90EnKD3Vkh5a1t3w7PhnRQmsphLy3g2UieasoKTlT2Hn118TwDjIv5ZjHJW6HbzXA9wQ==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-controllable-state@2.1.0(react@18.2.0): + /@chakra-ui/react-use-controllable-state@2.1.0(react@18.3.1): resolution: {integrity: sha512-QR/8fKNokxZUs4PfxjXuwl0fj/d71WPrmLJvEpCTkHjnzu7LnYvzoe2wB867IdooQJL0G1zBxl0Dq+6W1P3jpg==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-disclosure@2.1.0(react@18.2.0): + /@chakra-ui/react-use-disclosure@2.1.0(react@18.3.1): resolution: {integrity: sha512-Ax4pmxA9LBGMyEZJhhUZobg9C0t3qFE4jVF1tGBsrLDcdBeLR9fwOogIPY9Hf0/wqSlAryAimICbr5hkpa5GSw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-event-listener@2.1.0(react@18.2.0): + /@chakra-ui/react-use-event-listener@2.1.0(react@18.3.1): resolution: {integrity: sha512-U5greryDLS8ISP69DKDsYcsXRtAdnTQT+jjIlRYZ49K/XhUR/AqVZCK5BkR1spTDmO9H8SPhgeNKI70ODuDU/Q==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-focus-effect@2.1.0(react@18.2.0): + /@chakra-ui/react-use-focus-effect@2.1.0(react@18.3.1): resolution: {integrity: sha512-xzVboNy7J64xveLcxTIJ3jv+lUJKDwRM7Szwn9tNzUIPD94O3qwjV7DDCUzN2490nSYDF4OBMt/wuDBtaR3kUQ==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/dom-utils': 2.1.0 - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-focus-on-pointer-down@2.1.0(react@18.2.0): + /@chakra-ui/react-use-focus-on-pointer-down@2.1.0(react@18.3.1): resolution: {integrity: sha512-2jzrUZ+aiCG/cfanrolsnSMDykCAbv9EK/4iUyZno6BYb3vziucmvgKuoXbMPAzWNtwUwtuMhkby8rc61Ue+Lg==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-interval@2.1.0(react@18.2.0): + /@chakra-ui/react-use-interval@2.1.0(react@18.3.1): resolution: {integrity: sha512-8iWj+I/+A0J08pgEXP1J1flcvhLBHkk0ln7ZvGIyXiEyM6XagOTJpwNhiu+Bmk59t3HoV/VyvyJTa+44sEApuw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-latest-ref@2.1.0(react@18.2.0): + /@chakra-ui/react-use-latest-ref@2.1.0(react@18.3.1): resolution: {integrity: sha512-m0kxuIYqoYB0va9Z2aW4xP/5b7BzlDeWwyXCH6QpT2PpW3/281L3hLCm1G0eOUcdVlayqrQqOeD6Mglq+5/xoQ==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-merge-refs@2.1.0(react@18.2.0): + /@chakra-ui/react-use-merge-refs@2.1.0(react@18.3.1): resolution: {integrity: sha512-lERa6AWF1cjEtWSGjxWTaSMvneccnAVH4V4ozh8SYiN9fSPZLlSG3kNxfNzdFvMEhM7dnP60vynF7WjGdTgQbQ==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-outside-click@2.2.0(react@18.2.0): + /@chakra-ui/react-use-outside-click@2.2.0(react@18.3.1): resolution: {integrity: sha512-PNX+s/JEaMneijbgAM4iFL+f3m1ga9+6QK0E5Yh4s8KZJQ/bLwZzdhMz8J/+mL+XEXQ5J0N8ivZN28B82N1kNw==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-pan-event@2.1.0(react@18.2.0): + /@chakra-ui/react-use-pan-event@2.1.0(react@18.3.1): resolution: {integrity: sha512-xmL2qOHiXqfcj0q7ZK5s9UjTh4Gz0/gL9jcWPA6GVf+A0Od5imEDa/Vz+533yQKWiNSm1QGrIj0eJAokc7O4fg==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/event-utils': 2.0.8 - '@chakra-ui/react-use-latest-ref': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-latest-ref': 2.1.0(react@18.3.1) framesync: 6.1.2 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-previous@2.1.0(react@18.2.0): + /@chakra-ui/react-use-previous@2.1.0(react@18.3.1): resolution: {integrity: sha512-pjxGwue1hX8AFcmjZ2XfrQtIJgqbTF3Qs1Dy3d1krC77dEsiCUbQ9GzOBfDc8pfd60DrB5N2tg5JyHbypqh0Sg==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-safe-layout-effect@2.1.0(react@18.2.0): + /@chakra-ui/react-use-safe-layout-effect@2.1.0(react@18.3.1): resolution: {integrity: sha512-Knbrrx/bcPwVS1TorFdzrK/zWA8yuU/eaXDkNj24IrKoRlQrSBFarcgAEzlCHtzuhufP3OULPkELTzz91b0tCw==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-size@2.1.0(react@18.2.0): + /@chakra-ui/react-use-size@2.1.0(react@18.3.1): resolution: {integrity: sha512-tbLqrQhbnqOjzTaMlYytp7wY8BW1JpL78iG7Ru1DlV4EWGiAmXFGvtnEt9HftU0NJ0aJyjgymkxfVGI55/1Z4A==} peerDependencies: react: '>=18' dependencies: '@zag-js/element-size': 0.10.5 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-use-timeout@2.1.0(react@18.2.0): + /@chakra-ui/react-use-timeout@2.1.0(react@18.3.1): resolution: {integrity: sha512-cFN0sobKMM9hXUhyCofx3/Mjlzah6ADaEl/AXl5Y+GawB5rgedgAcu2ErAgarEkwvsKdP6c68CKjQ9dmTQlJxQ==} peerDependencies: react: '>=18' dependencies: - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/react-use-update-effect@2.1.0(react@18.2.0): + /@chakra-ui/react-use-update-effect@2.1.0(react@18.3.1): resolution: {integrity: sha512-ND4Q23tETaR2Qd3zwCKYOOS1dfssojPLJMLvUtUbW5M9uW1ejYWgGUobeAiOVfSplownG8QYMmHTP86p/v0lbA==} peerDependencies: react: '>=18' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react-utils@2.0.12(react@18.2.0): + /@chakra-ui/react-utils@2.0.12(react@18.3.1): resolution: {integrity: sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/utils': 2.0.15 - react: 18.2.0 + react: 18.3.1 dev: false - /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.1)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} peerDependencies: '@emotion/react': ^11.0.0 @@ -2498,69 +2522,69 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0) - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/counter': 2.1.0(react@18.2.0) - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.2.0) - '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0) - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/hooks': 2.2.1(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/live-region': 2.1.0(react@18.2.0) - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0) - '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0) - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-env': 3.1.0(react@18.2.0) - '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) + '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/counter': 2.1.0(react@18.3.1) + '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.3.1) + '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.1)(react@18.3.1) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/hooks': 2.2.1(react@18.3.1) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/live-region': 2.1.0(react@18.3.1) + '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) + '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.3.1)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-env': 3.1.0(react@18.3.1) + '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) '@chakra-ui/theme-utils': 2.0.21 - '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.2.0) + '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.3.1) '@chakra-ui/utils': 2.0.15 - '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/react@2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.1)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} peerDependencies: '@emotion/react': ^11.0.0 @@ -2569,162 +2593,162 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/counter': 2.1.0(react@18.2.0) - '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.2.0) - '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.73)(react@18.2.0) - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/hooks': 2.2.1(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/live-region': 2.1.0(react@18.2.0) - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) - '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.73)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-env': 3.1.0(react@18.2.0) - '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) + '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/counter': 2.1.0(react@18.3.1) + '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.4)(react@18.3.1) + '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.3.1)(react@18.3.1) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/hooks': 2.2.1(react@18.3.1) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/live-region': 2.1.0(react@18.3.1) + '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) + '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.3.1)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-env': 3.1.0(react@18.3.1) + '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) '@chakra-ui/theme-utils': 2.0.21 - '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/transition': 2.1.0(framer-motion@11.0.22)(react@18.2.0) + '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/transition': 2.1.0(framer-motion@11.1.8)(react@18.3.1) '@chakra-ui/utils': 2.0.15 - '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /@chakra-ui/select@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/select@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/shared-utils@2.0.5: resolution: {integrity: sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q==} dev: false - /@chakra-ui/skeleton@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/skeleton@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-JNRuMPpdZGd6zFVKjVQ0iusu3tXAdI29n4ZENYwAJEMf/fN0l12sVeirOxkJ7oEL0yOx2AgEYFSKdbcAgfUsAQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-use-previous': 2.1.0(react@18.2.0) + '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-use-previous': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/skip-nav@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/skip-nav@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-Hk+FG+vadBSH0/7hwp9LJnLjkO0RPGnx7gBJWI4/SpoJf3e4tZlWYtwGj0toYY4aGKl93jVghuwGbDBEMoHDug==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/slider@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/slider@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-lUOBcLMCnFZiA/s2NONXhELJh6sY5WtbRykPtclGfynqqOo47lwWJx+VP7xaeuhDOPcWSSecWc9Y1BfPOCz9cQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: '@chakra-ui/number-utils': 2.0.7 - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-latest-ref': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-pan-event': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-size': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-callback-ref': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-latest-ref': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-pan-event': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-size': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/spinner@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/spinner@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-hczbnoXt+MMv/d3gE+hjQhmkzLiKuoTo42YhUG7Bs9OSv2lg1fZHW1fGNRFP3wTi6OIbD044U1P9HK+AOgFH3g==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/stat@2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/stat@2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-LDn0d/LXQNbAn2KaR3F1zivsZCewY4Jsy1qShmfBMKwn6rI8yVlbvu6SiA3OpHS0FhxbsZxQI6HefEoIgtqY6Q==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/stepper@2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/stepper@2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-ky77lZbW60zYkSXhYz7kbItUpAQfEdycT0Q4bkHLxfqbuiGMf8OmgZOQkOB9uM4v0zPwy2HXhe0vq4Dd0xa55Q==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/styled-system@2.9.2: @@ -2735,106 +2759,106 @@ packages: lodash.mergewith: 4.6.2 dev: false - /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0): + /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.3.1): resolution: {integrity: sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0): + /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1): resolution: {integrity: sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' framer-motion: '>=4.0.0' react: '>=18' dependencies: - '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/system@2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0): + /@chakra-ui/system@2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1): resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==} peerDependencies: '@emotion/react': ^11.0.0 '@emotion/styled': ^11.0.0 react: '>=18' dependencies: - '@chakra-ui/color-mode': 2.2.0(react@18.2.0) + '@chakra-ui/color-mode': 2.2.0(react@18.3.1) '@chakra-ui/object-utils': 2.1.0 - '@chakra-ui/react-utils': 2.0.12(react@18.2.0) + '@chakra-ui/react-utils': 2.0.12(react@18.3.1) '@chakra-ui/styled-system': 2.9.2 '@chakra-ui/theme-utils': 2.0.21 '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - react: 18.2.0 + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 react-fast-compare: 3.2.2 dev: false - /@chakra-ui/table@2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/table@2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-o5OrjoHCh5uCLdiUb0Oc0vq9rIAeHSIRScc2ExTC9Qg/uVZl2ygLrjToCaKfaaKl1oQexIeAcZDKvPG8tVkHyQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/tabs@3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/tabs@3.0.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-6Mlclp8L9lqXmsGWF5q5gmemZXOiOYuh0SGT/7PgJVNPz3LXREXlXg2an4MBUD8W5oTkduCX+3KTMCwRrVrDYw==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/clickable': 2.1.0(react@18.2.0) - '@chakra-ui/descendant': 3.1.0(react@18.2.0) + '@chakra-ui/clickable': 2.1.0(react@18.3.1) + '@chakra-ui/descendant': 3.1.0(react@18.3.1) '@chakra-ui/lazy-utils': 2.0.5 - '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.2.0) + '@chakra-ui/react-children-utils': 2.0.6(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-safe-layout-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/tag@3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/tag@3.1.1(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-Bdel79Dv86Hnge2PKOU+t8H28nm/7Y3cKd4Kfk9k3lOpUh4+nkSGe58dhRzht59lEqa4N9waCgQiBdkydjvBXQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/textarea@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/textarea@2.1.2(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-ip7tvklVCZUb2fOHDb23qPy/Fr2mzDOGdkrpbNi50hDCiV4hFX02jdQJdi3ydHZUyVgZVBKPOJ+lT9i7sKA2wA==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/theme-tools@2.1.2(@chakra-ui/styled-system@2.9.2): @@ -2868,7 +2892,7 @@ packages: '@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2) dev: false - /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==} peerDependencies: '@chakra-ui/system': 2.6.2 @@ -2876,22 +2900,22 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-timeout': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-timeout': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==} peerDependencies: '@chakra-ui/system': 2.6.2 @@ -2899,22 +2923,22 @@ packages: react: '>=18' react-dom: '>=18' dependencies: - '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-context': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-timeout': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-context': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-timeout': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 '@chakra-ui/styled-system': 2.9.2 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2923,20 +2947,20 @@ packages: react-dom: '>=18' dependencies: '@chakra-ui/dom-utils': 2.1.0 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2945,39 +2969,39 @@ packages: react-dom: '>=18' dependencies: '@chakra-ui/dom-utils': 2.1.0 - '@chakra-ui/popper': 3.1.0(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react-types': 2.0.7(react@18.2.0) - '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) - '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react-types': 2.0.7(react@18.3.1) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.3.1) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.3.1) '@chakra-ui/shared-utils': 2.0.5 - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /@chakra-ui/transition@2.1.0(framer-motion@10.18.0)(react@18.2.0): + /@chakra-ui/transition@2.1.0(framer-motion@10.18.0)(react@18.3.1): resolution: {integrity: sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==} peerDependencies: framer-motion: '>=4.0.0' react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false - /@chakra-ui/transition@2.1.0(framer-motion@11.0.22)(react@18.2.0): + /@chakra-ui/transition@2.1.0(framer-motion@11.1.8)(react@18.3.1): resolution: {integrity: sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==} peerDependencies: framer-motion: '>=4.0.0' react: '>=18' dependencies: '@chakra-ui/shared-utils': 2.0.5 - framer-motion: 11.0.22(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 + framer-motion: 11.1.8(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 dev: false /@chakra-ui/utils@2.0.15: @@ -2989,14 +3013,14 @@ packages: lodash.mergewith: 4.6.2 dev: false - /@chakra-ui/visually-hidden@2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0): + /@chakra-ui/visually-hidden@2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1): resolution: {integrity: sha512-KmKDg01SrQ7VbTD3+cPWf/UfpF5MSwm3v7MWi0n5t8HnnadT13MF0MJCDSXbBWnzLv1ZKJ6zlyAOeARWX+DpjQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' react: '>=18' dependencies: - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - react: 18.2.0 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + react: 18.3.1 dev: false /@colors/colors@1.5.0: @@ -3006,14 +3030,14 @@ packages: dev: true optional: true - /@dagrejs/dagre@1.1.1: - resolution: {integrity: sha512-AQfT6pffEuPE32weFzhS/u3UpX+bRXUARIXL7UqLaxz497cN8pjuBlX6axO4IIECE2gBV8eLFQkGCtKX5sDaUA==} + /@dagrejs/dagre@1.1.2: + resolution: {integrity: sha512-F09dphqvHsbe/6C2t2unbmpr5q41BNPEfJCdn8Z7aEBpVSy/zFQ/b4SWsweQjWNsYMDvE2ffNUN8X0CeFsEGNw==} dependencies: - '@dagrejs/graphlib': 2.2.1 + '@dagrejs/graphlib': 2.2.2 dev: false - /@dagrejs/graphlib@2.2.1: - resolution: {integrity: sha512-xJsN1v6OAxXk6jmNdM+OS/bBE8nDCwM0yDNprXR18ZNatL6to9ggod9+l2XtiLhXfLm0NkE7+Er/cpdlM+SkUA==} + /@dagrejs/graphlib@2.2.2: + resolution: {integrity: sha512-CbyGpCDKsiTg/wuk79S7Muoj8mghDGAESWGxcSyhHX5jD35vYMBZochYVFzlHxynpE9unpu6O+4ZuhrLxASsOg==} engines: {node: '>17.0.0'} dev: false @@ -3022,46 +3046,46 @@ packages: engines: {node: '>=10.0.0'} dev: true - /@dnd-kit/accessibility@3.1.0(react@18.2.0): + /@dnd-kit/accessibility@3.1.0(react@18.3.1): resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} peerDependencies: react: '>=16.8.0' dependencies: - react: 18.2.0 + react: 18.3.1 tslib: 2.6.2 dev: false - /@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0): + /@dnd-kit/core@6.1.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - '@dnd-kit/accessibility': 3.1.0(react@18.2.0) - '@dnd-kit/utilities': 3.2.2(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@dnd-kit/accessibility': 3.1.0(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) tslib: 2.6.2 dev: false - /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): + /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.3.1): resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} peerDependencies: '@dnd-kit/core': ^6.1.0 react: '>=16.8.0' dependencies: - '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) - '@dnd-kit/utilities': 3.2.2(react@18.2.0) - react: 18.2.0 + '@dnd-kit/core': 6.1.0(react-dom@18.3.1)(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 tslib: 2.6.2 dev: false - /@dnd-kit/utilities@3.2.2(react@18.2.0): + /@dnd-kit/utilities@3.2.2(react@18.3.1): resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} peerDependencies: react: '>=16.8.0' dependencies: - react: 18.2.0 + react: 18.3.1 tslib: 2.6.2 dev: false @@ -3069,10 +3093,10 @@ packages: resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} dependencies: '@babel/helper-module-imports': 7.24.3 - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 '@emotion/hash': 0.9.1 '@emotion/memoize': 0.8.1 - '@emotion/serialize': 1.1.3 + '@emotion/serialize': 1.1.4 babel-plugin-macros: 3.1.0 convert-source-map: 1.9.0 escape-string-regexp: 4.0.0 @@ -3119,7 +3143,7 @@ packages: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: false - /@emotion/react@11.11.4(@types/react@18.2.73)(react@18.2.0): + /@emotion/react@11.11.4(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==} peerDependencies: '@types/react': '*' @@ -3128,20 +3152,20 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 '@emotion/babel-plugin': 11.11.0 '@emotion/cache': 11.11.0 - '@emotion/serialize': 1.1.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/serialize': 1.1.4 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) '@emotion/utils': 1.2.1 '@emotion/weak-memoize': 0.3.1 - '@types/react': 18.2.73 + '@types/react': 18.3.1 hoist-non-react-statics: 3.3.2 - react: 18.2.0 + react: 18.3.1 dev: false - /@emotion/serialize@1.1.3: - resolution: {integrity: sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==} + /@emotion/serialize@1.1.4: + resolution: {integrity: sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==} dependencies: '@emotion/hash': 0.9.1 '@emotion/memoize': 0.8.1 @@ -3154,8 +3178,8 @@ packages: resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} dev: false - /@emotion/styled@11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0): - resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} + /@emotion/styled@11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==} peerDependencies: '@emotion/react': ^11.0.0-rc.0 '@types/react': '*' @@ -3164,27 +3188,27 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 '@emotion/babel-plugin': 11.11.0 '@emotion/is-prop-valid': 1.2.2 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/serialize': 1.1.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/serialize': 1.1.4 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) '@emotion/utils': 1.2.1 - '@types/react': 18.2.73 - react: 18.2.0 + '@types/react': 18.3.1 + react: 18.3.1 dev: false /@emotion/unitless@0.8.1: resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} dev: false - /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0): + /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.3.1): resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} peerDependencies: react: '>=16.8.0' dependencies: - react: 18.2.0 + react: 18.3.1 /@emotion/utils@1.2.1: resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==} @@ -3472,39 +3496,39 @@ packages: engines: {node: '>=14'} dev: true - /@floating-ui/core@1.6.0: - resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} + /@floating-ui/core@1.6.1: + resolution: {integrity: sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==} dependencies: - '@floating-ui/utils': 0.2.1 + '@floating-ui/utils': 0.2.2 dev: false /@floating-ui/dom@1.5.4: resolution: {integrity: sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==} dependencies: - '@floating-ui/core': 1.6.0 - '@floating-ui/utils': 0.2.1 + '@floating-ui/core': 1.6.1 + '@floating-ui/utils': 0.2.2 dev: false - /@floating-ui/dom@1.6.3: - resolution: {integrity: sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==} + /@floating-ui/dom@1.6.5: + resolution: {integrity: sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==} dependencies: - '@floating-ui/core': 1.6.0 - '@floating-ui/utils': 0.2.1 + '@floating-ui/core': 1.6.1 + '@floating-ui/utils': 0.2.2 dev: false - /@floating-ui/utils@0.2.1: - resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} + /@floating-ui/utils@0.2.2: + resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==} dev: false - /@fontsource-variable/inter@5.0.17: - resolution: {integrity: sha512-sa80nNnqF8kzhBvqusWiL9vlPMVpdmOwMmDBup46Jggsr1VBqo+YuzwB36Ls+X6uHJtb8Yv3ALBHL/zGmT862A==} + /@fontsource-variable/inter@5.0.18: + resolution: {integrity: sha512-rJzSrtJ3b7djiGFvRuTe6stDfbYJGhdQSfn2SI2WfXviee7Er0yKAHE5u7FU7OWVQQQ1x3+cxdmx9NdiAkcrcA==} dev: false /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} dependencies: - '@humanwhocodes/object-schema': 2.0.2 + '@humanwhocodes/object-schema': 2.0.3 debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: @@ -3516,8 +3540,8 @@ packages: engines: {node: '>=12.22'} dev: true - /@humanwhocodes/object-schema@2.0.2: - resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + /@humanwhocodes/object-schema@2.0.3: + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} dev: true /@internationalized/date@3.5.3: @@ -3526,32 +3550,32 @@ packages: '@swc/helpers': 0.5.11 dev: false - /@internationalized/number@3.5.1: - resolution: {integrity: sha512-N0fPU/nz15SwR9IbfJ5xaS9Ss/O5h1sVXMZf43vc9mxEG48ovglvvzBjF53aHlq20uoR6c+88CrIXipU/LSzwg==} + /@internationalized/number@3.5.2: + resolution: {integrity: sha512-4FGHTi0rOEX1giSkt5MH4/te0eHBq3cvAYsfLlpguV6pzJAReXymiYpE5wPCqKqjkUO3PIsyvk+tBiIV1pZtbA==} dependencies: '@swc/helpers': 0.5.11 dev: false - /@invoke-ai/eslint-config-react@0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.3): + /@invoke-ai/eslint-config-react@0.0.14(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5): resolution: {integrity: sha512-6ZUY9zgdDhv2WUoLdDKOQdU9ImnH0CBOFtRlOaNOh34IOsNRfn+JA7wqA0PKnkiNrlfPkIQWhn4GRJp68NT5bw==} peerDependencies: eslint: ^8.56.0 prettier: ^3.2.5 typescript: ^5.3.3 dependencies: - '@typescript-eslint/eslint-plugin': 7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.3) - '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/eslint-plugin': 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 eslint-config-prettier: 9.1.0(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.4.0)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.8.0)(eslint@8.57.0) eslint-plugin-react: 7.34.1(eslint@8.57.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0) + eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) eslint-plugin-react-refresh: 0.4.6(eslint@8.57.0) - eslint-plugin-simple-import-sort: 12.0.0(eslint@8.57.0) - eslint-plugin-storybook: 0.8.0(eslint@8.57.0)(typescript@5.4.3) - eslint-plugin-unused-imports: 3.1.0(@typescript-eslint/eslint-plugin@7.4.0)(eslint@8.57.0) + eslint-plugin-simple-import-sort: 12.1.0(eslint@8.57.0) + eslint-plugin-storybook: 0.8.0(eslint@8.57.0)(typescript@5.4.5) + eslint-plugin-unused-imports: 3.2.0(@typescript-eslint/eslint-plugin@7.8.0)(eslint@8.57.0) prettier: 3.2.5 - typescript: 5.4.3 + typescript: 5.4.5 transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -3566,36 +3590,36 @@ packages: prettier: 3.2.5 dev: true - /@invoke-ai/ui-library@0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.17)(@internationalized/date@3.5.3)(@types/react@18.2.73)(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0): + /@invoke-ai/ui-library@0.0.25(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.18)(@internationalized/date@3.5.3)(@types/react@18.3.1)(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Fmjdlu62NXHgairYXGjcuCrxPEAl1G6Q6ban8g3excF6pDDdBeS7CmSNCyEDMxnSIOZrQlI04OhaMB17Imi9Uw==} peerDependencies: '@fontsource-variable/inter': ^5.0.16 react: ^18.2.0 react-dom: ^18.2.0 dependencies: - '@ark-ui/react': 1.3.0(@internationalized/date@3.5.3)(react-dom@18.2.0)(react@18.2.0) + '@ark-ui/react': 1.3.0(@internationalized/date@3.5.3)(react-dom@18.3.1)(react@18.3.1) '@chakra-ui/anatomy': 2.2.2 - '@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react': 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.73)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/portal': 2.1.0(react-dom@18.3.1)(react@18.3.1) + '@chakra-ui/react': 2.8.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.1)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) '@chakra-ui/styled-system': 2.9.2 '@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2) - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.73)(react@18.2.0) - '@fontsource-variable/inter': 5.0.17 - '@nanostores/react': 0.7.2(nanostores@0.9.5)(react@18.2.0) - chakra-react-select: 4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.4)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.1)(react@18.3.1) + '@fontsource-variable/inter': 5.0.18 + '@nanostores/react': 0.7.2(nanostores@0.9.5)(react@18.3.1) + chakra-react-select: 4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.4)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1) lodash-es: 4.17.21 nanostores: 0.9.5 - overlayscrollbars: 2.6.1 - overlayscrollbars-react: 0.5.5(overlayscrollbars@2.6.1)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-i18next: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) - react-icons: 5.0.1(react@18.2.0) - react-select: 5.8.0(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + overlayscrollbars: 2.7.3 + overlayscrollbars-react: 0.5.6(overlayscrollbars@2.7.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-i18next: 14.1.1(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1) + react-icons: 5.2.0(react@18.3.1) + react-select: 5.8.0(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@chakra-ui/form-control' - '@chakra-ui/icon' @@ -3621,6 +3645,11 @@ packages: wrap-ansi-cjs: /wrap-ansi@7.0.0 dev: true + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3628,7 +3657,7 @@ packages: '@sinclair/typebox': 0.27.8 dev: true - /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.4.3)(vite@5.2.6): + /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.4.5)(vite@5.2.11): resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==} peerDependencies: typescript: '>= 4.3.x' @@ -3640,9 +3669,9 @@ packages: glob: 7.2.3 glob-promise: 4.2.2(glob@7.2.3) magic-string: 0.27.0 - react-docgen-typescript: 2.2.2(typescript@5.4.3) - typescript: 5.4.3 - vite: 5.2.6(@types/node@20.11.30) + react-docgen-typescript: 2.2.2(typescript@5.4.5) + typescript: 5.4.5 + vite: 5.2.11(@types/node@20.12.10) dev: true /@jridgewell/gen-mapping@0.3.5: @@ -3674,38 +3703,38 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@mdx-js/react@3.0.1(@types/react@18.2.73)(react@18.2.0): + /@mdx-js/react@3.0.1(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==} peerDependencies: '@types/react': '>=16' react: '>=16' dependencies: - '@types/mdx': 2.0.12 - '@types/react': 18.2.73 - react: 18.2.0 + '@types/mdx': 2.0.13 + '@types/react': 18.3.1 + react: 18.3.1 dev: true - /@microsoft/api-extractor-model@7.28.13(@types/node@20.11.30): + /@microsoft/api-extractor-model@7.28.13(@types/node@20.12.10): resolution: {integrity: sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==} dependencies: '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 4.0.2(@types/node@20.11.30) + '@rushstack/node-core-library': 4.0.2(@types/node@20.12.10) transitivePeerDependencies: - '@types/node' dev: true - /@microsoft/api-extractor@7.43.0(@types/node@20.11.30): + /@microsoft/api-extractor@7.43.0(@types/node@20.12.10): resolution: {integrity: sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==} hasBin: true dependencies: - '@microsoft/api-extractor-model': 7.28.13(@types/node@20.11.30) + '@microsoft/api-extractor-model': 7.28.13(@types/node@20.12.10) '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 4.0.2(@types/node@20.11.30) + '@rushstack/node-core-library': 4.0.2(@types/node@20.12.10) '@rushstack/rig-package': 0.5.2 - '@rushstack/terminal': 0.10.0(@types/node@20.11.30) - '@rushstack/ts-command-line': 4.19.1(@types/node@20.11.30) + '@rushstack/terminal': 0.10.0(@types/node@20.12.10) + '@rushstack/ts-command-line': 4.19.1(@types/node@20.12.10) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.8 @@ -3729,18 +3758,18 @@ packages: resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} dev: true - /@nanostores/react@0.7.2(nanostores@0.10.0)(react@18.2.0): + /@nanostores/react@0.7.2(nanostores@0.10.3)(react@18.3.1): resolution: {integrity: sha512-e3OhHJFv3NMSFYDgREdlAQqkyBTHJM91s31kOZ4OvZwJKdFk5BLk0MLbh51EOGUz9QGX2aCHfy1RvweSi7fgwA==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: nanostores: ^0.9.0 || ^0.10.0 react: '>=18.0.0' dependencies: - nanostores: 0.10.0 - react: 18.2.0 + nanostores: 0.10.3 + react: 18.3.1 dev: false - /@nanostores/react@0.7.2(nanostores@0.9.5)(react@18.2.0): + /@nanostores/react@0.7.2(nanostores@0.9.5)(react@18.3.1): resolution: {integrity: sha512-e3OhHJFv3NMSFYDgREdlAQqkyBTHJM91s31kOZ4OvZwJKdFk5BLk0MLbh51EOGUz9QGX2aCHfy1RvweSi7fgwA==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: @@ -3748,7 +3777,7 @@ packages: react: '>=18.0.0' dependencies: nanostores: 0.9.5 - react: 18.2.0 + react: 18.3.1 dev: false /@ndelangen/get-tarball@3.0.9: @@ -3801,59 +3830,6 @@ packages: fastq: 1.17.1 dev: true - /@npmcli/git@5.0.4: - resolution: {integrity: sha512-nr6/WezNzuYUppzXRaYu/W4aT5rLxdXqEFupbh6e/ovlYFQ8hpu1UUPV3Ir/YTl+74iXl2ZOMlGzudh9ZPUchQ==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - '@npmcli/promise-spawn': 7.0.1 - lru-cache: 10.2.0 - npm-pick-manifest: 9.0.0 - proc-log: 3.0.0 - promise-inflight: 1.0.1 - promise-retry: 2.0.1 - semver: 7.6.0 - which: 4.0.0 - transitivePeerDependencies: - - bluebird - dev: true - - /@npmcli/map-workspaces@3.0.4: - resolution: {integrity: sha512-Z0TbvXkRbacjFFLpVpV0e2mheCh+WzQpcqL+4xp49uNJOxOnIAPZyXtUxZ5Qn3QBTGKA11Exjd9a5411rBrhDg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - '@npmcli/name-from-folder': 2.0.0 - glob: 10.3.10 - minimatch: 9.0.3 - read-package-json-fast: 3.0.2 - dev: true - - /@npmcli/name-from-folder@2.0.0: - resolution: {integrity: sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - - /@npmcli/package-json@5.0.0: - resolution: {integrity: sha512-OI2zdYBLhQ7kpNPaJxiflofYIpkNLi+lnGdzqUOfRmCF3r2l1nadcjtCYMJKv/Utm/ZtlffaUuTiAktPHbc17g==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - '@npmcli/git': 5.0.4 - glob: 10.3.10 - hosted-git-info: 7.0.1 - json-parse-even-better-errors: 3.0.1 - normalize-package-data: 6.0.0 - proc-log: 3.0.0 - semver: 7.6.0 - transitivePeerDependencies: - - bluebird - dev: true - - /@npmcli/promise-spawn@7.0.1: - resolution: {integrity: sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - which: 4.0.0 - dev: true - /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3861,135 +3837,15 @@ packages: dev: true optional: true - /@pnpm/constants@7.1.1: - resolution: {integrity: sha512-31pZqMtjwV+Vaq7MaPrT1EoDFSYwye3dp6BiHIGRJmVThCQwySRKM7hCvqqI94epNkqFAAYoWrNynWoRYosGdw==} - engines: {node: '>=16.14'} - dev: true - - /@pnpm/core-loggers@9.0.6(@pnpm/logger@5.0.0): - resolution: {integrity: sha512-iK67SGbp+06bA/elpg51wygPFjNA7JKHtKkpLxqXXHw+AjFFBC3f2OznJsCIuDK6HdGi5UhHLYqo5QxJ2gMqJQ==} - engines: {node: '>=16.14'} - peerDependencies: - '@pnpm/logger': ^5.0.0 - dependencies: - '@pnpm/logger': 5.0.0 - '@pnpm/types': 9.4.2 - dev: true - - /@pnpm/error@5.0.3: - resolution: {integrity: sha512-ONJU5cUeoeJSy50qOYsMZQHTA/9QKmGgh1ATfEpCLgtbdwqUiwD9MxHNeXUYYI/pocBCz6r1ZCFqiQvO+8SUKA==} - engines: {node: '>=16.14'} - dependencies: - '@pnpm/constants': 7.1.1 - dev: true - - /@pnpm/fetching-types@5.0.0: - resolution: {integrity: sha512-o9gdO1v8Uc5P2fBBuW6GSpfTqIivQmQlqjQJdFiQX0m+tgxlrMRneIg392jZuc6fk7kFqjLheInlslgJfwY+4Q==} - engines: {node: '>=16.14'} - dependencies: - '@zkochan/retry': 0.2.0 - node-fetch: 3.0.0-beta.9 - transitivePeerDependencies: - - domexception - dev: true - - /@pnpm/graceful-fs@3.2.0: - resolution: {integrity: sha512-vRoXJxscDpHak7YE9SqCkzfrayn+Lw+YueOeHIPEqkgokrHeYgYeONoc2kGh0ObHaRtNSsonozVfJ456kxLNvA==} - engines: {node: '>=16.14'} - dependencies: - graceful-fs: 4.2.11 - dev: true - - /@pnpm/logger@5.0.0: - resolution: {integrity: sha512-YfcB2QrX+Wx1o6LD1G2Y2fhDhOix/bAY/oAnMpHoNLsKkWIRbt1oKLkIFvxBMzLwAEPqnYWguJrYC+J6i4ywbw==} - engines: {node: '>=12.17'} - dependencies: - bole: 5.0.11 - ndjson: 2.0.0 - dev: true - - /@pnpm/npm-package-arg@1.0.0: - resolution: {integrity: sha512-oQYP08exi6mOPdAZZWcNIGS+KKPsnNwUBzSuAEGWuCcqwMAt3k/WVCqVIXzBxhO5sP2b43og69VHmPj6IroKqw==} - engines: {node: '>=14.6'} - dependencies: - hosted-git-info: 4.1.0 - semver: 7.6.0 - validate-npm-package-name: 4.0.0 - dev: true - - /@pnpm/npm-resolver@18.1.1(@pnpm/logger@5.0.0): - resolution: {integrity: sha512-NptzncmMD5ZMimbjWkGpMzuBRhlCY+sh7mzypPdBOTNlh5hmEQe/VaRKjNK4V9/b0C/llElkvIePL6acybu86w==} - engines: {node: '>=16.14'} - peerDependencies: - '@pnpm/logger': ^5.0.0 - dependencies: - '@pnpm/core-loggers': 9.0.6(@pnpm/logger@5.0.0) - '@pnpm/error': 5.0.3 - '@pnpm/fetching-types': 5.0.0 - '@pnpm/graceful-fs': 3.2.0 - '@pnpm/logger': 5.0.0 - '@pnpm/resolve-workspace-range': 5.0.1 - '@pnpm/resolver-base': 11.1.0 - '@pnpm/types': 9.4.2 - '@zkochan/retry': 0.2.0 - encode-registry: 3.0.1 - load-json-file: 6.2.0 - lru-cache: 10.2.0 - normalize-path: 3.0.0 - p-limit: 3.1.0 - p-memoize: 4.0.1 - parse-npm-tarball-url: 3.0.0 - path-temp: 2.1.0 - ramda: /@pnpm/ramda@0.28.1 - rename-overwrite: 5.0.0 - semver: 7.6.0 - ssri: 10.0.5 - version-selector-type: 3.0.0 - transitivePeerDependencies: - - domexception - dev: true - - /@pnpm/ramda@0.28.1: - resolution: {integrity: sha512-zcAG+lvU0fMziNeGXpPyCyCJYp5ZVrPElEE4t14jAmViaihohocZ+dDkcRIyAomox8pQsuZnv1EyHR+pOhmUWw==} - dev: true - - /@pnpm/resolve-workspace-range@5.0.1: - resolution: {integrity: sha512-yQ0pMthlw8rTgS/C9hrjne+NEnnSNevCjtdodd7i15I59jMBYciHifZ/vjg0NY+Jl+USTc3dBE+0h/4tdYjMKg==} - engines: {node: '>=16.14'} - dependencies: - semver: 7.6.0 - dev: true - - /@pnpm/resolver-base@11.1.0: - resolution: {integrity: sha512-y2qKaj18pwe1VWc3YXEitdYFo+WqOOt60aqTUuOVkJAirUzz0DzuYh3Ifct4znYWPdgUXHaN5DMphNF5iL85rA==} - engines: {node: '>=16.14'} - dependencies: - '@pnpm/types': 9.4.2 - dev: true - - /@pnpm/types@9.4.2: - resolution: {integrity: sha512-g1hcF8Nv4gd76POilz9gD4LITAPXOe5nX4ijgr8ixCbLQZfcpYiMfJ+C1RlMNRUDo8vhlNB4O3bUlxmT6EAQXA==} - engines: {node: '>=16.14'} - dev: true - - /@pnpm/workspace.pkgs-graph@2.0.15(@pnpm/logger@5.0.0): - resolution: {integrity: sha512-Txxd5FzzVfBfGCTngISaxFlJzZhzdS8BUrCEtAWJfZOFbQzpWy27rzkaS7TaWW2dHiFcCVYzPI/2vgxfeRansA==} - engines: {node: '>=16.14'} - dependencies: - '@pnpm/npm-package-arg': 1.0.0 - '@pnpm/npm-resolver': 18.1.1(@pnpm/logger@5.0.0) - '@pnpm/resolve-workspace-range': 5.0.1 - ramda: /@pnpm/ramda@0.28.1 - transitivePeerDependencies: - - '@pnpm/logger' - - domexception + /@polka/url@1.0.0-next.25: + resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} dev: true /@popperjs/core@2.11.8: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false - /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.73)(react@18.2.0): + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: '@types/react': '*' @@ -3998,12 +3854,12 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.1 - '@types/react': 18.2.73 - react: 18.2.0 + '@babel/runtime': 7.24.5 + '@types/react': 18.3.1 + react: 18.3.1 dev: true - /@radix-ui/react-slot@1.0.2(@types/react@18.2.73)(react@18.2.0): + /@radix-ui/react-slot@1.0.2(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: '@types/react': '*' @@ -4012,46 +3868,46 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.24.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.73)(react@18.2.0) - '@types/react': 18.2.73 - react: 18.2.0 + '@babel/runtime': 7.24.5 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@types/react': 18.3.1 + react: 18.3.1 dev: true - /@reactflow/background@11.3.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-byj/G9pEC8tN0wT/ptcl/LkEP/BBfa33/SvBkqE4XwyofckqF87lKp573qGlisfnsijwAbpDlf81PuFL41So4Q==} + /@reactflow/background@11.3.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-hkvpVEhgvfTDyCvdlitw4ioKCYLaaiRXnuEG+1QM3Np+7N1DiWF1XOv5I8AFyNoJL07yXEkbECUTsHvkBvcG5A==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - classcat: 5.0.4 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.73)(react@18.2.0) + '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/controls@11.2.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-e8nWplbYfOn83KN1BrxTXS17+enLyFnjZPbyDgHSRLtI5ZGPKF/8iRXV+VXb2LFVzlu4Wh3la/pkxtfP/0aguA==} + /@reactflow/controls@11.2.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-3xgEg6ALIVkAQCS4NiBjb7ad8Cb3D8CtA7Vvl4Hf5Ar2PIVs6FOaeft9s2iDZGtsWP35ECDYId1rIFVhQL8r+A==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - classcat: 5.0.4 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.73)(react@18.2.0) + '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/core@11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-j3i9b2fsTX/sBbOm+RmNzYEFWbNx4jGWGuGooh2r1jQaE2eV+TLJgiG/VNOp0q5mBl9f6g1IXs3Gm86S9JfcGw==} + /@reactflow/core@11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-+adHdUa7fJSEM93fWfjQwyWXeI92a1eLKwWbIstoCakHpL8UjzwhEh6sn+mN2h/59MlVI7Ehr1iGTt3MsfcIFA==} peerDependencies: react: '>=17' react-dom: '>=17' @@ -4060,74 +3916,74 @@ packages: '@types/d3-drag': 3.0.7 '@types/d3-selection': 3.0.10 '@types/d3-zoom': 3.0.8 - classcat: 5.0.4 + classcat: 5.0.5 d3-drag: 3.0.0 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/minimap@11.7.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-le95jyTtt3TEtJ1qa7tZ5hyM4S7gaEQkW43cixcMOZLu33VAdc2aCpJg/fXcRrrf7moN2Mbl9WIMNXUKsp5ILA==} + /@reactflow/minimap@11.7.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-m2MvdiGSyOu44LEcERDEl1Aj6x//UQRWo3HEAejNU4HQTlJnYrSN8tgrYF8TxC1+c/9UdyzQY5VYgrTwW4QWdg==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) '@types/d3-selection': 3.0.10 '@types/d3-zoom': 3.0.8 - classcat: 5.0.4 + classcat: 5.0.5 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-resizer@2.2.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-HfickMm0hPDIHt9qH997nLdgLt0kayQyslKE0RS/GZvZ4UMQJlx/NRRyj5y47Qyg0NnC66KYOQWDM9LLzRTnUg==} + /@reactflow/node-resizer@2.2.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-X7ceQ2s3jFLgbkg03n2RYr4hm3jTVrzkW2W/8ANv/SZfuVmF8XJxlERuD8Eka5voKqLda0ywIZGAbw9GoHLfUQ==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - classcat: 5.0.4 + '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + classcat: 5.0.5 d3-drag: 3.0.0 d3-selection: 3.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-toolbar@1.3.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-VmgxKmToax4sX1biZ9LXA7cj/TBJ+E5cklLGwquCCVVxh+lxpZGTBF3a5FJGVHiUNBBtFsC8ldcSZIK4cAlQww==} + /@reactflow/node-toolbar@1.3.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-aknvNICO10uWdthFSpgD6ctY/CTBeJUMV9co8T9Ilugr08Nb89IQ4uD0dPmr031ewMQxixtYIkw+sSDDzd2aaQ==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - classcat: 5.0.4 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.73)(react@18.2.0) + '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reduxjs/toolkit@2.2.2(react-redux@9.1.0)(react@18.2.0): - resolution: {integrity: sha512-454GZrEx3G6QSYwIx9ROaso1HR6sTH8qyZBe3KEsdWVGU3ayV8jYCwdaEJV3vl9V6+pi3GRl+7Xl7AeDna6qwQ==} + /@reduxjs/toolkit@2.2.3(react-redux@9.1.2)(react@18.3.1): + resolution: {integrity: sha512-76dll9EnJXg4EVcI5YNxZA/9hSAmZsFqzMmNRHvIlzw2WS/twfcVX3ysYrWGJMClwEmChQFC4yRq74tn6fdzRA==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 @@ -4137,9 +3993,9 @@ packages: react-redux: optional: true dependencies: - immer: 10.0.4 - react: 18.2.0 - react-redux: 9.1.0(@types/react@18.2.73)(react@18.2.0)(redux@5.0.1) + immer: 10.1.1 + react: 18.3.1 + react-redux: 9.1.2(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1) redux: 5.0.1 redux-thunk: 3.1.0(redux@5.0.1) reselect: 5.1.0 @@ -4150,7 +4006,7 @@ packages: engines: {node: '>=12.0'} dependencies: boolean: 3.2.0 - globalthis: 1.0.3 + globalthis: 1.0.4 liqe: 3.8.0 dev: false @@ -4176,119 +4032,135 @@ packages: picomatch: 2.3.1 dev: true - /@rollup/rollup-android-arm-eabi@4.13.1: - resolution: {integrity: sha512-4C4UERETjXpC4WpBXDbkgNVgHyWfG3B/NKY46e7w5H134UDOFqUJKpsLm0UYmuupW+aJmRgeScrDNfvZ5WV80A==} + /@rollup/rollup-android-arm-eabi@4.17.2: + resolution: {integrity: sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.13.1: - resolution: {integrity: sha512-TrTaFJ9pXgfXEiJKQ3yQRelpQFqgRzVR9it8DbeRzG0RX7mKUy0bqhCFsgevwXLJepQKTnLl95TnPGf9T9AMOA==} + /@rollup/rollup-android-arm64@4.17.2: + resolution: {integrity: sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.13.1: - resolution: {integrity: sha512-fz7jN6ahTI3cKzDO2otQuybts5cyu0feymg0bjvYCBrZQ8tSgE8pc0sSNEuGvifrQJWiwx9F05BowihmLxeQKw==} + /@rollup/rollup-darwin-arm64@4.17.2: + resolution: {integrity: sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.13.1: - resolution: {integrity: sha512-WTvdz7SLMlJpektdrnWRUN9C0N2qNHwNbWpNo0a3Tod3gb9leX+yrYdCeB7VV36OtoyiPAivl7/xZ3G1z5h20g==} + /@rollup/rollup-darwin-x64@4.17.2: + resolution: {integrity: sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.13.1: - resolution: {integrity: sha512-dBHQl+7wZzBYcIF6o4k2XkAfwP2ks1mYW2q/Gzv9n39uDcDiAGDqEyml08OdY0BIct0yLSPkDTqn4i6czpBLLw==} + /@rollup/rollup-linux-arm-gnueabihf@4.17.2: + resolution: {integrity: sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.13.1: - resolution: {integrity: sha512-bur4JOxvYxfrAmocRJIW0SADs3QdEYK6TQ7dTNz6Z4/lySeu3Z1H/+tl0a4qDYv0bCdBpUYM0sYa/X+9ZqgfSQ==} + /@rollup/rollup-linux-arm-musleabihf@4.17.2: + resolution: {integrity: sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.17.2: + resolution: {integrity: sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.13.1: - resolution: {integrity: sha512-ssp77SjcDIUSoUyj7DU7/5iwM4ZEluY+N8umtCT9nBRs3u045t0KkW02LTyHouHDomnMXaXSZcCSr2bdMK63kA==} + /@rollup/rollup-linux-arm64-musl@4.17.2: + resolution: {integrity: sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-riscv64-gnu@4.13.1: - resolution: {integrity: sha512-Jv1DkIvwEPAb+v25/Unrnnq9BO3F5cbFPT821n3S5litkz+O5NuXuNhqtPx5KtcwOTtaqkTsO+IVzJOsxd11aQ==} + /@rollup/rollup-linux-powerpc64le-gnu@4.17.2: + resolution: {integrity: sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.17.2: + resolution: {integrity: sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==} cpu: [riscv64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-s390x-gnu@4.13.1: - resolution: {integrity: sha512-U564BrhEfaNChdATQaEODtquCC7Ez+8Hxz1h5MAdMYj0AqD0GA9rHCpElajb/sQcaFL6NXmHc5O+7FXpWMa73Q==} + /@rollup/rollup-linux-s390x-gnu@4.17.2: + resolution: {integrity: sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==} cpu: [s390x] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.13.1: - resolution: {integrity: sha512-zGRDulLTeDemR8DFYyFIQ8kMP02xpUsX4IBikc7lwL9PrwR3gWmX2NopqiGlI2ZVWMl15qZeUjumTwpv18N7sQ==} + /@rollup/rollup-linux-x64-gnu@4.17.2: + resolution: {integrity: sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.13.1: - resolution: {integrity: sha512-VTk/MveyPdMFkYJJPCkYBw07KcTkGU2hLEyqYMsU4NjiOfzoaDTW9PWGRsNwiOA3qI0k/JQPjkl/4FCK1smskQ==} + /@rollup/rollup-linux-x64-musl@4.17.2: + resolution: {integrity: sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.13.1: - resolution: {integrity: sha512-L+hX8Dtibb02r/OYCsp4sQQIi3ldZkFI0EUkMTDwRfFykXBPptoz/tuuGqEd3bThBSLRWPR6wsixDSgOx/U3Zw==} + /@rollup/rollup-win32-arm64-msvc@4.17.2: + resolution: {integrity: sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.13.1: - resolution: {integrity: sha512-+dI2jVPfM5A8zme8riEoNC7UKk0Lzc7jCj/U89cQIrOjrZTCWZl/+IXUeRT2rEZ5j25lnSA9G9H1Ob9azaF/KQ==} + /@rollup/rollup-win32-ia32-msvc@4.17.2: + resolution: {integrity: sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.13.1: - resolution: {integrity: sha512-YY1Exxo2viZ/O2dMHuwQvimJ0SqvL+OAWQLLY6rvXavgQKjhQUzn7nc1Dd29gjB5Fqi00nrBWctJBOyfVMIVxw==} + /@rollup/rollup-win32-x64-msvc@4.17.2: + resolution: {integrity: sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /@rushstack/node-core-library@4.0.2(@types/node@20.11.30): + /@rushstack/node-core-library@4.0.2(@types/node@20.12.10): resolution: {integrity: sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==} peerDependencies: '@types/node': '*' @@ -4296,7 +4168,7 @@ packages: '@types/node': optional: true dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.10 fs-extra: 7.0.1 import-lazy: 4.0.0 jju: 1.4.0 @@ -4312,7 +4184,7 @@ packages: strip-json-comments: 3.1.1 dev: true - /@rushstack/terminal@0.10.0(@types/node@20.11.30): + /@rushstack/terminal@0.10.0(@types/node@20.12.10): resolution: {integrity: sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==} peerDependencies: '@types/node': '*' @@ -4320,15 +4192,15 @@ packages: '@types/node': optional: true dependencies: - '@rushstack/node-core-library': 4.0.2(@types/node@20.11.30) - '@types/node': 20.11.30 + '@rushstack/node-core-library': 4.0.2(@types/node@20.12.10) + '@types/node': 20.12.10 supports-color: 8.1.1 dev: true - /@rushstack/ts-command-line@4.19.1(@types/node@20.11.30): + /@rushstack/ts-command-line@4.19.1(@types/node@20.12.10): resolution: {integrity: sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==} dependencies: - '@rushstack/terminal': 0.10.0(@types/node@20.11.30) + '@rushstack/terminal': 0.10.0(@types/node@20.12.10) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -4350,14 +4222,14 @@ packages: p-map: 4.0.0 dev: true - /@socket.io/component-emitter@3.1.0: - resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + /@socket.io/component-emitter@3.1.2: + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} dev: false - /@storybook/addon-actions@8.0.4: - resolution: {integrity: sha512-EyCWo+8T11/TJGYNL/AXtW4yaB+q1v2E9mixbumryCLxpTl2NtaeGZ4e0dlwfIMuw/7RWgHk2uIypcIPR/UANQ==} + /@storybook/addon-actions@8.0.10: + resolution: {integrity: sha512-IEuc30UAFl7Ws0GwaY/whjBnGaViVEVjmPc+MXUym2wwwJbnCbI+BKJxPoYi/I7QJb5aUNToAE6pl2pDda2g3Q==} dependencies: - '@storybook/core-events': 8.0.4 + '@storybook/core-events': 8.0.10 '@storybook/global': 5.0.0 '@types/uuid': 9.0.8 dequal: 2.0.3 @@ -4365,18 +4237,18 @@ packages: uuid: 9.0.1 dev: true - /@storybook/addon-backgrounds@8.0.4: - resolution: {integrity: sha512-fef0KD2GhJx2zpicOf8iL7k2LiIsNzEbGaQpIIjoy4DMqM1hIfNCt3DGTLH7LN5O8G+NVCLS1xmQg7RLvIVSCA==} + /@storybook/addon-backgrounds@8.0.10: + resolution: {integrity: sha512-445SUQqOH5xFJWlNeMu74FEgk26O9Zm/5aqnvmeteB0Q2JLaw7k2q9i/W6XFu97QkRxqA1EGbDxLR3+e1xCjaA==} dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 ts-dedent: 2.2.0 dev: true - /@storybook/addon-controls@8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-K5EYBTsUOTJlvIdA7p6Xj31wnV+RbZAkk56UKQvA7nJD7oDuLOq3E9u46F/uZD1vxddd9zFhf2iONfMe3KTTwQ==} + /@storybook/addon-controls@8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-MAUtIJGayNSsfn3VZ6SjQwpRkb4ky+10oVfos+xX9GQ5+7RCs+oYMuE4+aiQvvfXNdV8v0pUGPUPeUzqfJmhOA==} dependencies: - '@storybook/blocks': 8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@storybook/blocks': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) lodash: 4.17.21 ts-dedent: 2.2.0 transitivePeerDependencies: @@ -4387,26 +4259,26 @@ packages: - supports-color dev: true - /@storybook/addon-docs@8.0.4: - resolution: {integrity: sha512-m0Y7qGAMnNPLEOEgzW/SBm8GX0xabJBaRN+aYijO6UKTln7F6oXXVve+xPC0Y4s6Gc9HZFdJY8WXZr1YSGEUVA==} + /@storybook/addon-docs@8.0.10: + resolution: {integrity: sha512-y+Agoez/hXZHKUMIZHU96T5V1v0cs4ArSNfjqDg9DPYcyQ88ihJNb6ZabIgzmEaJF/NncCW+LofWeUtkTwalkw==} dependencies: - '@babel/core': 7.24.3 - '@mdx-js/react': 3.0.1(@types/react@18.2.73)(react@18.2.0) - '@storybook/blocks': 8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 8.0.4 - '@storybook/components': 8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@storybook/csf-plugin': 8.0.4 - '@storybook/csf-tools': 8.0.4 + '@babel/core': 7.24.5 + '@mdx-js/react': 3.0.1(@types/react@18.3.1)(react@18.3.1) + '@storybook/blocks': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@storybook/client-logger': 8.0.10 + '@storybook/components': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@storybook/csf-plugin': 8.0.10 + '@storybook/csf-tools': 8.0.10 '@storybook/global': 5.0.0 - '@storybook/node-logger': 8.0.4 - '@storybook/preview-api': 8.0.4 - '@storybook/react-dom-shim': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 8.0.4 - '@types/react': 18.2.73 + '@storybook/node-logger': 8.0.10 + '@storybook/preview-api': 8.0.10 + '@storybook/react-dom-shim': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/theming': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/types': 8.0.10 + '@types/react': 18.3.1 fs-extra: 11.2.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) rehype-external-links: 3.0.0 rehype-slug: 6.0.0 ts-dedent: 2.2.0 @@ -4415,22 +4287,22 @@ packages: - supports-color dev: true - /@storybook/addon-essentials@8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-mUIqhAkSz6Qv7nRqAAyCqMLiXBWVsY/8qN7HEIoaMQgdFq38KW3rYwNdzd2JLeXNWP1bBXwfvfcFe7/eqhYJFA==} + /@storybook/addon-essentials@8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-Uy3+vm7QX+b/9rhW/iFa3EYAAbV1T2LljY9Bj4aTPZHas9Bpvl5ZPnOm/PhybcE8UFHEoVTJ0v3uWb0dsUEigw==} dependencies: - '@storybook/addon-actions': 8.0.4 - '@storybook/addon-backgrounds': 8.0.4 - '@storybook/addon-controls': 8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-docs': 8.0.4 - '@storybook/addon-highlight': 8.0.4 - '@storybook/addon-measure': 8.0.4 - '@storybook/addon-outline': 8.0.4 - '@storybook/addon-toolbars': 8.0.4 - '@storybook/addon-viewport': 8.0.4 - '@storybook/core-common': 8.0.4 - '@storybook/manager-api': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/node-logger': 8.0.4 - '@storybook/preview-api': 8.0.4 + '@storybook/addon-actions': 8.0.10 + '@storybook/addon-backgrounds': 8.0.10 + '@storybook/addon-controls': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@storybook/addon-docs': 8.0.10 + '@storybook/addon-highlight': 8.0.10 + '@storybook/addon-measure': 8.0.10 + '@storybook/addon-outline': 8.0.10 + '@storybook/addon-toolbars': 8.0.10 + '@storybook/addon-viewport': 8.0.10 + '@storybook/core-common': 8.0.10 + '@storybook/manager-api': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/node-logger': 8.0.10 + '@storybook/preview-api': 8.0.10 ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' @@ -4440,19 +4312,19 @@ packages: - supports-color dev: true - /@storybook/addon-highlight@8.0.4: - resolution: {integrity: sha512-tnEiVaJlXL07v8JBox+QtRPVruoy0YovOTAOWY7fKDiKzF1I9wLaJjQF3wOsvwspHTHu00OZw2gsazgXiH4wLQ==} + /@storybook/addon-highlight@8.0.10: + resolution: {integrity: sha512-40GB82t1e2LCCjqXcC6Z5lq1yIpA1+Yl5E2tKeggOVwg5HHAX02ESNDdBaIOlCqMkU3WKzjGPurDNOLUAbsV2g==} dependencies: '@storybook/global': 5.0.0 dev: true - /@storybook/addon-interactions@8.0.4(vitest@1.4.0): - resolution: {integrity: sha512-wTEOnVUbF1lNJxxocr5IKmpgnmwyO8YsQf6Baw3tTWCHAa/MaWWQYq1OA6CfFfmVGGRjv/w2GTuf1Vyq99O7mg==} + /@storybook/addon-interactions@8.0.10(vitest@1.6.0): + resolution: {integrity: sha512-6yFNmk6+7082/8TRVyjUsKlwumalEdO0XQ5amPbVGuECzc3HFn0ELwzPrQ4TBlN5MRtX4+buoh5dc/1RUDrh9w==} dependencies: '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.0.4 - '@storybook/test': 8.0.4(vitest@1.4.0) - '@storybook/types': 8.0.4 + '@storybook/instrumenter': 8.0.10 + '@storybook/test': 8.0.10(vitest@1.6.0) + '@storybook/types': 8.0.10 polished: 4.3.1 ts-dedent: 2.2.0 transitivePeerDependencies: @@ -4463,54 +4335,54 @@ packages: - vitest dev: true - /@storybook/addon-links@8.0.4(react@18.2.0): - resolution: {integrity: sha512-SzE+JPZ4mxjprZqbLHf8Hx7UA2fXfMajFjeY9c3JREKQrDoOF1e4r28nAoVsZYF+frWxQB51U4+hOqjlx06wEA==} + /@storybook/addon-links@8.0.10(react@18.3.1): + resolution: {integrity: sha512-+mIyH2UcrgQfAyRM4+ARkB/D0OOY8UMwkZsD8dD23APZ8oru7W/NHX3lXl0WjPfQcOIx/QwWNWI3+DgVZJY3jw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: react: optional: true dependencies: - '@storybook/csf': 0.1.3 + '@storybook/csf': 0.1.7 '@storybook/global': 5.0.0 - react: 18.2.0 + react: 18.3.1 ts-dedent: 2.2.0 dev: true - /@storybook/addon-measure@8.0.4: - resolution: {integrity: sha512-GZYKo2ss5Br+dfHinoK3bgTaS90z3oKKDkhv6lrFfjjU1mDYzzMJpxajQhd3apCYxHLr3MbUqMQibWu2T/q2DQ==} + /@storybook/addon-measure@8.0.10: + resolution: {integrity: sha512-quXQwmZJUhOxDIlbXTH6aKYQkwkDpL0UQRkUZn1xuZ2sVKJeaee73QSWqw8HDD4Rz9huS+OrAdVoq/Cz5FoC6A==} dependencies: '@storybook/global': 5.0.0 tiny-invariant: 1.3.3 dev: true - /@storybook/addon-outline@8.0.4: - resolution: {integrity: sha512-6J9ezNDUxdA3rMCh8sUEQbUwAgkrr+M9QdiFr1t+gKrk5FKP5gwubw1sr3sF1IRB9+s/AjljcOtJAVulSfq05w==} + /@storybook/addon-outline@8.0.10: + resolution: {integrity: sha512-1eDO2s/vHhhSJo7W5SetqjleUBTZLI08VNP89c4j7vdRKiMZ1DYhr0dqUGIC3w7cDsawI/nQ24wancHHayAnqw==} dependencies: '@storybook/global': 5.0.0 ts-dedent: 2.2.0 dev: true - /@storybook/addon-storysource@8.0.4: - resolution: {integrity: sha512-qFoB/s4vjjHYFJA6rnOVTeXZ99Y4RTXhCjUrrY2B/c9hssZbEyP/oj57ojQsaIENK8ItCoD7sOExqANwx41qqw==} + /@storybook/addon-storysource@8.0.10: + resolution: {integrity: sha512-LCNgp5pWyI9ZlJMFeN0nvt9gvgHMWneDjfUoAHTOP7Smi0xz4lUDYKB4P53kgE1peHn2+nxAauSBdA1IEFBIRA==} dependencies: - '@storybook/source-loader': 8.0.4 + '@storybook/source-loader': 8.0.10 estraverse: 5.3.0 tiny-invariant: 1.3.3 dev: true - /@storybook/addon-toolbars@8.0.4: - resolution: {integrity: sha512-yodRXDYog/90cNEy84kg6s7L+nxQ+egBjHBTsav1L4cJmQI/uAX8yISHHiX4I5ppNc120Jz3UdHdRxXRlo345g==} + /@storybook/addon-toolbars@8.0.10: + resolution: {integrity: sha512-67HP6mTJU/gjRju01Z5HjeqoRiJMDlrMvMvjGBg7w5+tPNtjYqdelfe2+kcfU+Hf6dfcuqaBDwaUUGSv+RYtRQ==} dev: true - /@storybook/addon-viewport@8.0.4: - resolution: {integrity: sha512-E5IKOsxKcOtlOYc0cWgzVJohQB+dVBWwaJcg5FlslToknfVB9M0kfQ/SQcp3KB0C9/cOmJK1Jm388InW+EjrBQ==} + /@storybook/addon-viewport@8.0.10: + resolution: {integrity: sha512-NJ88Nd/tXreHLyLeF3VP+b8Fu2KtUuJ0L4JYpEMmcdaejGARTrJJOU+pcZBiUqEHFeXQ8rDY8DKXhUJZQFQ1Wg==} dependencies: memoizerific: 1.11.3 dev: true - /@storybook/blocks@8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-9dRXk9zLJVPOmEWsSXm10XUmIfvS/tVgeBgFXNbusFQZXPpexIPNdRgB004pDGg9RvlY78ykpnd3yP143zaXMg==} + /@storybook/blocks@8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-LOaxvcO2d4dT4YoWlQ0bq/c8qA3aHoqtyuvBjwbVn+359bjMtgj/91YuP9Y2+ggZZ4p+ttgvk39PcmJlNXlJsw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -4520,30 +4392,30 @@ packages: react-dom: optional: true dependencies: - '@storybook/channels': 8.0.4 - '@storybook/client-logger': 8.0.4 - '@storybook/components': 8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 8.0.4 - '@storybook/csf': 0.1.3 - '@storybook/docs-tools': 8.0.4 + '@storybook/channels': 8.0.10 + '@storybook/client-logger': 8.0.10 + '@storybook/components': 8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@storybook/core-events': 8.0.10 + '@storybook/csf': 0.1.7 + '@storybook/docs-tools': 8.0.10 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.2.0)(react@18.2.0) - '@storybook/manager-api': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 8.0.4 - '@storybook/theming': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 8.0.4 - '@types/lodash': 4.17.0 + '@storybook/icons': 1.2.9(react-dom@18.3.1)(react@18.3.1) + '@storybook/manager-api': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/preview-api': 8.0.10 + '@storybook/theming': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/types': 8.0.10 + '@types/lodash': 4.17.1 color-convert: 2.0.1 dequal: 2.0.3 lodash: 4.17.21 - markdown-to-jsx: 7.3.2(react@18.2.0) + markdown-to-jsx: 7.3.2(react@18.3.1) memoizerific: 1.11.3 polished: 4.3.1 - react: 18.2.0 - react-colorful: 5.6.1(react-dom@18.2.0)(react@18.2.0) - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-colorful: 5.6.1(react-dom@18.3.1)(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) telejson: 7.2.0 - tocbot: 4.25.0 + tocbot: 4.27.19 ts-dedent: 2.2.0 util-deprecate: 1.0.2 transitivePeerDependencies: @@ -4552,17 +4424,17 @@ packages: - supports-color dev: true - /@storybook/builder-manager@8.0.4: - resolution: {integrity: sha512-BafYVxq77uuTmXdjYo5by42OyOrb6qcpWYKva3ntWK2ZhTaLJlwwqAOdahT1DVzi4VeUP6465YvsTCzIE8fuIw==} + /@storybook/builder-manager@8.0.10: + resolution: {integrity: sha512-lo57jeeYuYCKYrmGOdLg25rMyiGYSTwJ+zYsQ3RvClVICjP6X0I1RCKAJDzkI0BixH6s1+w5ynD6X3PtDnhUuw==} dependencies: '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@storybook/core-common': 8.0.4 - '@storybook/manager': 8.0.4 - '@storybook/node-logger': 8.0.4 + '@storybook/core-common': 8.0.10 + '@storybook/manager': 8.0.10 + '@storybook/node-logger': 8.0.10 '@types/ejs': 3.1.5 '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.20.2) browser-assert: 1.2.1 - ejs: 3.1.9 + ejs: 3.1.10 esbuild: 0.20.2 esbuild-plugin-alias: 0.2.1 express: 4.19.2 @@ -4574,8 +4446,8 @@ packages: - supports-color dev: true - /@storybook/builder-vite@8.0.4(typescript@5.4.3)(vite@5.2.6): - resolution: {integrity: sha512-Whb001bGkoGQ6/byp9QTQJ4NO61Qa5bh1p5WEEMJ5wYvHm83b+B/IwwilUfU5mL9bJB/RjbwyKcSQqGP6AxMzA==} + /@storybook/builder-vite@8.0.10(typescript@5.4.5)(vite@5.2.11): + resolution: {integrity: sha512-Rod/2jYvF4Ng1MjIMZEXe/3z0lPuxkRtetCTr3ECPgi83lHXpHJ+N0NVfJEMs+pXsVqkLP3iGt2hLn6D6yFMwA==} peerDependencies: '@preact/preset-vite': '*' typescript: '>= 4.3.x' @@ -4589,55 +4461,55 @@ packages: vite-plugin-glimmerx: optional: true dependencies: - '@storybook/channels': 8.0.4 - '@storybook/client-logger': 8.0.4 - '@storybook/core-common': 8.0.4 - '@storybook/core-events': 8.0.4 - '@storybook/csf-plugin': 8.0.4 - '@storybook/node-logger': 8.0.4 - '@storybook/preview': 8.0.4 - '@storybook/preview-api': 8.0.4 - '@storybook/types': 8.0.4 + '@storybook/channels': 8.0.10 + '@storybook/client-logger': 8.0.10 + '@storybook/core-common': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/csf-plugin': 8.0.10 + '@storybook/node-logger': 8.0.10 + '@storybook/preview': 8.0.10 + '@storybook/preview-api': 8.0.10 + '@storybook/types': 8.0.10 '@types/find-cache-dir': 3.2.1 browser-assert: 1.2.1 es-module-lexer: 0.9.3 express: 4.19.2 find-cache-dir: 3.3.2 fs-extra: 11.2.0 - magic-string: 0.30.8 + magic-string: 0.30.10 ts-dedent: 2.2.0 - typescript: 5.4.3 - vite: 5.2.6(@types/node@20.11.30) + typescript: 5.4.5 + vite: 5.2.11(@types/node@20.12.10) transitivePeerDependencies: - encoding - supports-color dev: true - /@storybook/channels@8.0.4: - resolution: {integrity: sha512-haKV+8RbiSzLjicowUfc7h2fTClZHX/nz9SRUecf4IEZUEu2T78OgM/TzqZvL7rA3+/fKqp5iI+3PN3OA75Sdg==} + /@storybook/channels@8.0.10: + resolution: {integrity: sha512-3JLxfD7czlx31dAGvAYJ4J4BNE/Y2+hhj/dsV3xlQTHKVpnWknaoeYEC1a6YScyfsH6W+XmP2rzZKzH4EkLSGQ==} dependencies: - '@storybook/client-logger': 8.0.4 - '@storybook/core-events': 8.0.4 + '@storybook/client-logger': 8.0.10 + '@storybook/core-events': 8.0.10 '@storybook/global': 5.0.0 telejson: 7.2.0 tiny-invariant: 1.3.3 dev: true - /@storybook/cli@8.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-8jb8hrulRMfyFyNXFEapxHBS51xb42ZZGfVAacXIsHOJtjOd5CnOoSUYn0aOkVl19VF/snoa9JOW7BaW/50Eqw==} + /@storybook/cli@8.0.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-KUZEO2lyvOS2sRJEFXovt6+5b65iWsh7F8e8S1cM20fCM1rZAlWtwmoxmDVXDmyEp0wTrq4FrRxKnbo9UO518w==} hasBin: true dependencies: - '@babel/core': 7.24.3 - '@babel/types': 7.24.0 + '@babel/core': 7.24.5 + '@babel/types': 7.24.5 '@ndelangen/get-tarball': 3.0.9 - '@storybook/codemod': 8.0.4 - '@storybook/core-common': 8.0.4 - '@storybook/core-events': 8.0.4 - '@storybook/core-server': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/csf-tools': 8.0.4 - '@storybook/node-logger': 8.0.4 - '@storybook/telemetry': 8.0.4 - '@storybook/types': 8.0.4 + '@storybook/codemod': 8.0.10 + '@storybook/core-common': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/core-server': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/csf-tools': 8.0.10 + '@storybook/node-logger': 8.0.10 + '@storybook/telemetry': 8.0.10 + '@storybook/types': 8.0.10 '@types/semver': 7.5.8 '@yarnpkg/fslib': 2.10.3 '@yarnpkg/libzip': 2.3.0 @@ -4645,14 +4517,14 @@ packages: commander: 6.2.1 cross-spawn: 7.0.3 detect-indent: 6.1.0 - envinfo: 7.11.1 + envinfo: 7.13.0 execa: 5.1.1 find-up: 5.0.0 fs-extra: 11.2.0 get-npm-tarball-url: 2.1.0 giget: 1.2.3 globby: 11.1.0 - jscodeshift: 0.15.2(@babel/preset-env@7.24.3) + jscodeshift: 0.15.2(@babel/preset-env@7.24.5) leven: 3.1.0 ora: 5.4.1 prettier: 3.2.5 @@ -4673,26 +4545,26 @@ packages: - utf-8-validate dev: true - /@storybook/client-logger@8.0.4: - resolution: {integrity: sha512-2SeEg3PT/d0l/+EAVtyj9hmMLTyTPp+bRBSzxYouBjtJPM1jrdKpFagj1o3uBRovwWm9SIVX6/ZsoRC33PEV1g==} + /@storybook/client-logger@8.0.10: + resolution: {integrity: sha512-u38SbZNAunZzxZNHMJb9jkUwFkLyWxmvp4xtiRM3u9sMUShXoTnzbw1yKrxs+kYJjg+58UQPZ1JhEBRcHt5Oww==} dependencies: '@storybook/global': 5.0.0 dev: true - /@storybook/codemod@8.0.4: - resolution: {integrity: sha512-bysG46P4wjlR3RCpr/ntNAUaupWpzLcWYWti3iNtIyZ/iPrX6KtXoA9QCIwJZrlv41us6F+KEZbzLzkgWbymtQ==} + /@storybook/codemod@8.0.10: + resolution: {integrity: sha512-t45jKGs/eyR/nKVX6QgRtMZSAjJo5aXWWk3B24xVbW6ywr0jt1LC100FkHG4Af8cApIfh8uUmS9X05hMG5zGGA==} dependencies: - '@babel/core': 7.24.3 - '@babel/preset-env': 7.24.3(@babel/core@7.24.3) - '@babel/types': 7.24.0 - '@storybook/csf': 0.1.3 - '@storybook/csf-tools': 8.0.4 - '@storybook/node-logger': 8.0.4 - '@storybook/types': 8.0.4 + '@babel/core': 7.24.5 + '@babel/preset-env': 7.24.5(@babel/core@7.24.5) + '@babel/types': 7.24.5 + '@storybook/csf': 0.1.7 + '@storybook/csf-tools': 8.0.10 + '@storybook/node-logger': 8.0.10 + '@storybook/types': 8.0.10 '@types/cross-spawn': 6.0.6 cross-spawn: 7.0.3 globby: 11.1.0 - jscodeshift: 0.15.2(@babel/preset-env@7.24.3) + jscodeshift: 0.15.2(@babel/preset-env@7.24.5) lodash: 4.17.21 prettier: 3.2.5 recast: 0.23.6 @@ -4701,34 +4573,34 @@ packages: - supports-color dev: true - /@storybook/components@8.0.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-i5ngl5GTOLB9nZ1cmpxTjtWct5IuH9UxzFC73a0jHMkCwN26w16IqufRVDaoQv0AvZN4pd4fNM2in/XVHA10dw==} + /@storybook/components@8.0.10(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-eo+oDDcm35YBB3dtDYDfcjJypNVPmRty85VWpAOBsJXpwp/fgU8csx0DM3KmhrQ4cWLf2WzcFowJwI1w+J88Sw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.73)(react@18.2.0) - '@storybook/client-logger': 8.0.4 - '@storybook/csf': 0.1.3 + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.1)(react@18.3.1) + '@storybook/client-logger': 8.0.10 + '@storybook/csf': 0.1.7 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 8.0.4 + '@storybook/icons': 1.2.9(react-dom@18.3.1)(react@18.3.1) + '@storybook/theming': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/types': 8.0.10 memoizerific: 1.11.3 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) util-deprecate: 1.0.2 transitivePeerDependencies: - '@types/react' dev: true - /@storybook/core-common@8.0.4: - resolution: {integrity: sha512-dzFRLm5FxUa2EFE6Rx/KLDTJNLBIp1S2/+Q1K+rG8V+CLvewCc2Cd486rStZqSXEKI7vDnsRs/aMla+N0X/++Q==} + /@storybook/core-common@8.0.10: + resolution: {integrity: sha512-hsFlPieputaDQoxstnPa3pykTc4bUwEDgCHf8U43+/Z7qmLOQ9fpG+2CFW930rsCRghYpPreOvsmhY7lsGKWLQ==} dependencies: - '@storybook/core-events': 8.0.4 - '@storybook/csf-tools': 8.0.4 - '@storybook/node-logger': 8.0.4 - '@storybook/types': 8.0.4 + '@storybook/core-events': 8.0.10 + '@storybook/csf-tools': 8.0.10 + '@storybook/node-logger': 8.0.10 + '@storybook/types': 8.0.10 '@yarnpkg/fslib': 2.10.3 '@yarnpkg/libzip': 2.3.0 chalk: 4.1.2 @@ -4740,7 +4612,7 @@ packages: find-cache-dir: 3.3.2 find-up: 5.0.0 fs-extra: 11.2.0 - glob: 10.3.10 + glob: 10.3.12 handlebars: 4.7.8 lazy-universal-dotenv: 4.0.0 node-fetch: 2.7.0 @@ -4758,34 +4630,34 @@ packages: - supports-color dev: true - /@storybook/core-events@8.0.4: - resolution: {integrity: sha512-1FgLacIGi9i6/fyxw7ZJDC621RK47IMaA3keH4lc11ASRzCSwJ4YOrXjBFjfPc79EF2BuX72DDJNbhj6ynfF3g==} + /@storybook/core-events@8.0.10: + resolution: {integrity: sha512-TuHPS6p5ZNr4vp4butLb4R98aFx0NRYCI/7VPhJEUH5rPiqNzE3PZd8DC8rnVxavsJ+jO1/y+egNKXRYkEcoPQ==} dependencies: ts-dedent: 2.2.0 dev: true - /@storybook/core-server@8.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-/633Pp7LPcDWXkPLSW+W9VUYUbVkdVBG6peXjuzogV0vzdM0dM9af/T0uV2NQxUhzoy6/7QdSDljE+eEOBs2Lw==} + /@storybook/core-server@8.0.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-HYDw2QFBxg1X/d6g0rUhirOB5Jq6g90HBnyrZzxKoqKWJCNsCADSgM+h9HgtUw0jA97qBpIqmNO9n3mXFPWU/Q==} dependencies: '@aw-web-design/x-default-browser': 1.4.126 - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 8.0.4 - '@storybook/channels': 8.0.4 - '@storybook/core-common': 8.0.4 - '@storybook/core-events': 8.0.4 - '@storybook/csf': 0.1.3 - '@storybook/csf-tools': 8.0.4 + '@storybook/builder-manager': 8.0.10 + '@storybook/channels': 8.0.10 + '@storybook/core-common': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/csf': 0.1.7 + '@storybook/csf-tools': 8.0.10 '@storybook/docs-mdx': 3.0.0 '@storybook/global': 5.0.0 - '@storybook/manager': 8.0.4 - '@storybook/manager-api': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/node-logger': 8.0.4 - '@storybook/preview-api': 8.0.4 - '@storybook/telemetry': 8.0.4 - '@storybook/types': 8.0.4 + '@storybook/manager': 8.0.10 + '@storybook/manager-api': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/node-logger': 8.0.10 + '@storybook/preview-api': 8.0.10 + '@storybook/telemetry': 8.0.10 + '@storybook/types': 8.0.10 '@types/detect-port': 1.3.5 - '@types/node': 18.19.26 + '@types/node': 18.19.32 '@types/pretty-hrtime': 1.0.3 '@types/semver': 7.5.8 better-opn: 3.0.2 @@ -4809,7 +4681,7 @@ packages: util: 0.12.5 util-deprecate: 1.0.2 watchpack: 2.4.1 - ws: 8.16.0 + ws: 8.17.0 transitivePeerDependencies: - bufferutil - encoding @@ -4819,24 +4691,24 @@ packages: - utf-8-validate dev: true - /@storybook/csf-plugin@8.0.4: - resolution: {integrity: sha512-pEgctWuS/qeKMFZJJUM2JuKwjKBt27ye+216ft7xhNqpsrmCgumJYrkU/ii2CsFJU/qr5Fu9EYw+N+vof1OalQ==} + /@storybook/csf-plugin@8.0.10: + resolution: {integrity: sha512-0EsyEx/06sCjI8sn40r7cABtBU1vUKPMPD+S5mJiZymm73BgdARj0qZOlLoK2LP+t2pcaB/Cn7KX/uyhhv7M2g==} dependencies: - '@storybook/csf-tools': 8.0.4 - unplugin: 1.10.0 + '@storybook/csf-tools': 8.0.10 + unplugin: 1.10.1 transitivePeerDependencies: - supports-color dev: true - /@storybook/csf-tools@8.0.4: - resolution: {integrity: sha512-dMSZxWnXBhmXGOZZOAJ4DKZRCYdA0HaqqZ4/eF9MLLsI+qvW4EklcpjVY6bsIzACgubRWtRZkTpxTnjExi/N1A==} + /@storybook/csf-tools@8.0.10: + resolution: {integrity: sha512-xUc6fVIKoCujf/7JZhkYjrVXeNsTSoDrZFNmqLEmtfktJVqYdXY4LuSAtlBmAIyETi09ULTuuVexrcKFwjzuBA==} dependencies: - '@babel/generator': 7.24.1 - '@babel/parser': 7.24.1 - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 - '@storybook/csf': 0.1.3 - '@storybook/types': 8.0.4 + '@babel/generator': 7.24.5 + '@babel/parser': 7.24.5 + '@babel/traverse': 7.24.5 + '@babel/types': 7.24.5 + '@storybook/csf': 0.1.7 + '@storybook/types': 8.0.10 fs-extra: 11.2.0 recast: 0.23.6 ts-dedent: 2.2.0 @@ -4850,8 +4722,8 @@ packages: lodash: 4.17.21 dev: true - /@storybook/csf@0.1.3: - resolution: {integrity: sha512-IPZvXXo4b3G+gpmgBSBqVM81jbp2ePOKsvhgJdhyZJtkYQCII7rg9KKLQhvBQM5sLaF1eU6r0iuwmyynC9d9SA==} + /@storybook/csf@0.1.7: + resolution: {integrity: sha512-53JeLZBibjQxi0Ep+/AJTfxlofJlxy1jXcSKENlnKxHjWEYyHQCumMP5yTFjf7vhNnMjEpV3zx6t23ssFiGRyw==} dependencies: type-fest: 2.19.0 dev: true @@ -4860,12 +4732,13 @@ packages: resolution: {integrity: sha512-NmiGXl2HU33zpwTv1XORe9XG9H+dRUC1Jl11u92L4xr062pZtrShLmD4VKIsOQujxhhOrbxpwhNOt+6TdhyIdQ==} dev: true - /@storybook/docs-tools@8.0.4: - resolution: {integrity: sha512-PONfG8j/AOHi79NbEkneFRZIscrShbA0sgA+62zeejH4r9+fuIkIKtLnKcAxvr8Bm6uo9aSQbISJZUcBG42WhQ==} + /@storybook/docs-tools@8.0.10: + resolution: {integrity: sha512-rg9KS81vEh13VMr4mAgs+7L4kYqoRtG7kVfV1WHxzJxjR3wYcVR0kP9gPTWV4Xha/TA3onHu9sxKxMTWha0urQ==} dependencies: - '@storybook/core-common': 8.0.4 - '@storybook/preview-api': 8.0.4 - '@storybook/types': 8.0.4 + '@storybook/core-common': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/preview-api': 8.0.10 + '@storybook/types': 8.0.10 '@types/doctrine': 0.0.3 assert: 2.1.0 doctrine: 3.0.0 @@ -4879,41 +4752,41 @@ packages: resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} dev: true - /@storybook/icons@1.2.9(react-dom@18.2.0)(react@18.2.0): + /@storybook/icons@1.2.9(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-cOmylsz25SYXaJL/gvTk/dl3pyk7yBFRfeXTsHvTA3dfhoU/LWSq0NKL9nM7WBasJyn6XPSGnLS4RtKXLw5EUg==} engines: {node: '>=14.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: true - /@storybook/instrumenter@8.0.4: - resolution: {integrity: sha512-lkHv1na12oMTZvuDbzufgqrtFlV1XqdXrAAg7YXZOia/oMz6Z/XMldEqwLPUCLGVodbFJofrpE67Wtw8dNTDQg==} + /@storybook/instrumenter@8.0.10: + resolution: {integrity: sha512-6IYjWeQFA5x68xRoW5dU4yAc1Hwq1ZBkZbXVgJbr5LJw5x+y8eKdZzIaOmSsSKOI96R7J5YWWd2WA1Q0nRurtg==} dependencies: - '@storybook/channels': 8.0.4 - '@storybook/client-logger': 8.0.4 - '@storybook/core-events': 8.0.4 + '@storybook/channels': 8.0.10 + '@storybook/client-logger': 8.0.10 + '@storybook/core-events': 8.0.10 '@storybook/global': 5.0.0 - '@storybook/preview-api': 8.0.4 - '@vitest/utils': 1.4.0 + '@storybook/preview-api': 8.0.10 + '@vitest/utils': 1.6.0 util: 0.12.5 dev: true - /@storybook/manager-api@8.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-TudiRmWlsi8kdjwqW0DDLen76Zp4Sci/AnvTbZvZOWe8C2mruxcr6aaGwuIug6y+uxIyXDvURF6Cek5Twz4isg==} + /@storybook/manager-api@8.0.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-LLu6YKQLWf5QB3h3RO8IevjLrSOew7aidIQPr9DIr9xC8wA7N2fQabr+qrJdE306p3cHZ0nzhYNYZxSjm4Dvdw==} dependencies: - '@storybook/channels': 8.0.4 - '@storybook/client-logger': 8.0.4 - '@storybook/core-events': 8.0.4 - '@storybook/csf': 0.1.3 + '@storybook/channels': 8.0.10 + '@storybook/client-logger': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/csf': 0.1.7 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.2.0)(react@18.2.0) - '@storybook/router': 8.0.4 - '@storybook/theming': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 8.0.4 + '@storybook/icons': 1.2.9(react-dom@18.3.1)(react@18.3.1) + '@storybook/router': 8.0.10 + '@storybook/theming': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/types': 8.0.10 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 @@ -4925,68 +4798,68 @@ packages: - react-dom dev: true - /@storybook/manager@8.0.4: - resolution: {integrity: sha512-M5IofDSxbIQIdAglxUtZOGKjZ1EAq1Mdbh4UolVsF1PKF6dAvBQJLVW6TiLjEbmPBtqgeYKMgrmmYiFNqVcdBQ==} + /@storybook/manager@8.0.10: + resolution: {integrity: sha512-bojGglUQNry48L4siURc2zQKswavLzMh69rqsfL3ZXx+i+USfRfB7593azTlaZh0q6HO4bUAjB24RfQCyifLLQ==} dev: true - /@storybook/node-logger@8.0.4: - resolution: {integrity: sha512-cALLHuX53vLQsoJamGRlquh2pfhPq9copXou2JTmFT6mrCcipo77SzhBDfeeuhaGv6vUWPfmGjPBEHXWGPe4+g==} + /@storybook/node-logger@8.0.10: + resolution: {integrity: sha512-UMmaUaA3VOX/mKLsSvOnbZre2/1tZ6hazA6H0eAnClKb51jRD1AJrsBYK+uHr/CAp7t710bB5U8apPov7hayDw==} dev: true - /@storybook/preview-api@8.0.4: - resolution: {integrity: sha512-uZCgZ/7BZkFTNudCBWx3YPFVdReMQSZJj9EfQVhQaPmfGORHGMvZMRsQXl0ONhPy7zDD4rVQxu5dSKWmIiYoWQ==} + /@storybook/preview-api@8.0.10: + resolution: {integrity: sha512-uZ6btF7Iloz9TnDcKLQ5ydi2YK0cnulv/8FLQhBCwSrzLLLb+T2DGz0cAeuWZEvMUNWNmkWJ9PAFQFs09/8p/Q==} dependencies: - '@storybook/channels': 8.0.4 - '@storybook/client-logger': 8.0.4 - '@storybook/core-events': 8.0.4 - '@storybook/csf': 0.1.3 + '@storybook/channels': 8.0.10 + '@storybook/client-logger': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/csf': 0.1.7 '@storybook/global': 5.0.0 - '@storybook/types': 8.0.4 - '@types/qs': 6.9.14 + '@storybook/types': 8.0.10 + '@types/qs': 6.9.15 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 - qs: 6.12.0 + qs: 6.12.1 tiny-invariant: 1.3.3 ts-dedent: 2.2.0 util-deprecate: 1.0.2 dev: true - /@storybook/preview@8.0.4: - resolution: {integrity: sha512-dJa13bIxQBfa5ZsXAeL6X/oXI6b87Fy31pvpKPkW1o+7M6MC4OvwGQBqgAd7m8yn6NuIHxrdwjEupa7l7PGb6w==} + /@storybook/preview@8.0.10: + resolution: {integrity: sha512-op7gZqop8PSFyPA4tc1Zds8jG6VnskwpYUUsa44pZoEez9PKEFCf4jE+7AQwbBS3hnuCb0CKBfASN8GRyoznbw==} dev: true - /@storybook/react-dom-shim@8.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-H8bci23e+G40WsdYPuPrhAjCeeXypXuAV6mTVvLHGKH+Yb+3wiB1weaXrot/TgzPbkDNybuhTI3Qm48FPLt0bw==} + /@storybook/react-dom-shim@8.0.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-3x8EWEkZebpWpp1pwXEzdabGINwOQt8odM5+hsOlDRtFZBmUqmmzK0rtn7orlcGlOXO4rd6QuZj4Tc5WV28dVQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: true - /@storybook/react-vite@8.0.4(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3)(vite@5.2.6): - resolution: {integrity: sha512-SlAsLSDc9I1nhMbf0YgXCHaZbnjzDdv458xirmUj4aJhn45e8yhmODpkPYQ8nGn45VWYMyd0sC66lJNWRvI/FA==} + /@storybook/react-vite@8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5)(vite@5.2.11): + resolution: {integrity: sha512-J0Tw1jWSQYzc37AWaJCbrFQLlWsCHby0ie0yPx8DVehlnTT6xZWkohiKBq5iwMyYfF9SGrOfZ/dVRiB5q2sOIA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 vite: ^4.0.0 || ^5.0.0 dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.4.3)(vite@5.2.6) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.4.5)(vite@5.2.11) '@rollup/pluginutils': 5.1.0 - '@storybook/builder-vite': 8.0.4(typescript@5.4.3)(vite@5.2.6) - '@storybook/node-logger': 8.0.4 - '@storybook/react': 8.0.4(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3) + '@storybook/builder-vite': 8.0.10(typescript@5.4.5)(vite@5.2.11) + '@storybook/node-logger': 8.0.10 + '@storybook/react': 8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5) find-up: 5.0.0 - magic-string: 0.30.8 - react: 18.2.0 + magic-string: 0.30.10 + react: 18.3.1 react-docgen: 7.0.3 - react-dom: 18.2.0(react@18.2.0) + react-dom: 18.3.1(react@18.3.1) resolve: 1.22.8 tsconfig-paths: 4.2.0 - vite: 5.2.6(@types/node@20.11.30) + vite: 5.2.11(@types/node@20.12.10) transitivePeerDependencies: - '@preact/preset-vite' - encoding @@ -4996,8 +4869,8 @@ packages: - vite-plugin-glimmerx dev: true - /@storybook/react@8.0.4(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.3): - resolution: {integrity: sha512-p4wQSJIhG48UD2fZ6tFDT9zaqrVnvZxjV18+VjSi3dez/pDoEMJ3SWZWcmeDenKwvvk+SPdRH7k5mUHW1Rh0xg==} + /@storybook/react@8.0.10(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5): + resolution: {integrity: sha512-/MIMc02TNmiNXDzk55dm9+ujfNE5LVNeqqK+vxXWLlCZ0aXRAd1/ZLYeRFuYLgEETB7mh7IP8AXjvM68NX5HYg==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5007,15 +4880,15 @@ packages: typescript: optional: true dependencies: - '@storybook/client-logger': 8.0.4 - '@storybook/docs-tools': 8.0.4 + '@storybook/client-logger': 8.0.10 + '@storybook/docs-tools': 8.0.10 '@storybook/global': 5.0.0 - '@storybook/preview-api': 8.0.4 - '@storybook/react-dom-shim': 8.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 8.0.4 + '@storybook/preview-api': 8.0.10 + '@storybook/react-dom-shim': 8.0.10(react-dom@18.3.1)(react@18.3.1) + '@storybook/types': 8.0.10 '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 - '@types/node': 18.19.26 + '@types/node': 18.19.32 acorn: 7.4.1 acorn-jsx: 5.3.2(acorn@7.4.1) acorn-walk: 7.2.0 @@ -5023,43 +4896,43 @@ packages: html-tags: 3.3.1 lodash: 4.17.21 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-element-to-jsx-string: 15.0.0(react-dom@18.2.0)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-element-to-jsx-string: 15.0.0(react-dom@18.3.1)(react@18.3.1) semver: 7.6.0 ts-dedent: 2.2.0 type-fest: 2.19.0 - typescript: 5.4.3 + typescript: 5.4.5 util-deprecate: 1.0.2 transitivePeerDependencies: - encoding - supports-color dev: true - /@storybook/router@8.0.4: - resolution: {integrity: sha512-hlR80QvmLBflAqMeGcgtDuSe6TJlzdizwEAkBLE1lDvFI6tvvEyAliCAXBpIDdOZTe0u/zeeJkOUXKSx33caoQ==} + /@storybook/router@8.0.10: + resolution: {integrity: sha512-AZhgiet+EK0ZsPbaDgbbVTAHW2LAMCP1z/Un2uMBbdDeD0Ys29Af47AbEj/Ome5r1cqasLvzq2WXJlVXPNB0Zw==} dependencies: - '@storybook/client-logger': 8.0.4 + '@storybook/client-logger': 8.0.10 memoizerific: 1.11.3 - qs: 6.12.0 + qs: 6.12.1 dev: true - /@storybook/source-loader@8.0.4: - resolution: {integrity: sha512-pqaOMMV+dZvjbTdOzuc5RCFN9mGJ81GDtiaxYTKiIYrAyeK+V4JEZi7vHxCbZV4Tci5byeGNhx4FvQgVF2q0Wg==} + /@storybook/source-loader@8.0.10: + resolution: {integrity: sha512-bv9FRPzELjcoMJLWLDqkUNh1zY0DiCgcvM+9qsZva8pxAD4fzrX+mgCS2vZVJHRg8wMAhw/ymdXixDUodHAvsw==} dependencies: - '@storybook/csf': 0.1.3 - '@storybook/types': 8.0.4 + '@storybook/csf': 0.1.7 + '@storybook/types': 8.0.10 estraverse: 5.3.0 lodash: 4.17.21 prettier: 3.2.5 dev: true - /@storybook/telemetry@8.0.4: - resolution: {integrity: sha512-Q3ITY6J46R/TrrPRIU1fs3WNs69ExpTJZ9UlB8087qOUyV90Ex33SYk3i10xVWRczxCmyC1V58Xuht6nxz7mNQ==} + /@storybook/telemetry@8.0.10: + resolution: {integrity: sha512-s4Uc+KZQkdmD2d+64Qf8wYknhQZwmjf2CxjIjv9b4KLsU/nyfDheK7Fzd1jhBKb2UQUlLW5HhZkBgs1RsZcDHA==} dependencies: - '@storybook/client-logger': 8.0.4 - '@storybook/core-common': 8.0.4 - '@storybook/csf-tools': 8.0.4 + '@storybook/client-logger': 8.0.10 + '@storybook/core-common': 8.0.10 + '@storybook/csf-tools': 8.0.10 chalk: 4.1.2 detect-package-manager: 2.0.1 fetch-retry: 5.0.6 @@ -5070,19 +4943,18 @@ packages: - supports-color dev: true - /@storybook/test@8.0.4(vitest@1.4.0): - resolution: {integrity: sha512-/uvE8Rtu7tIcuyQBUzKq7uuDCsjmADI18BApLdwo/qthmN8ERDxRSz0Ngj2gvBMQFv99At8ESi/xh6oFGu3rWg==} + /@storybook/test@8.0.10(vitest@1.6.0): + resolution: {integrity: sha512-VqjzKJiOCjaZ0CjLeKygYk8uetiaiKbpIox+BrND9GtpEBHcRZA5AeFY2P1aSCOhsaDwuh4KRBxJWFug7DhWGQ==} dependencies: - '@storybook/client-logger': 8.0.4 - '@storybook/core-events': 8.0.4 - '@storybook/instrumenter': 8.0.4 - '@storybook/preview-api': 8.0.4 + '@storybook/client-logger': 8.0.10 + '@storybook/core-events': 8.0.10 + '@storybook/instrumenter': 8.0.10 + '@storybook/preview-api': 8.0.10 '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.4.2(vitest@1.4.0) + '@testing-library/jest-dom': 6.4.5(vitest@1.6.0) '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) '@vitest/expect': 1.3.1 - '@vitest/spy': 1.4.0 - chai: 4.4.1 + '@vitest/spy': 1.6.0 util: 0.12.5 transitivePeerDependencies: - '@jest/globals' @@ -5092,8 +4964,8 @@ packages: - vitest dev: true - /@storybook/theming@8.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-NxtTU2wMC0lj375ejoT3Npdcqwv6NeUpLaJl6EZCMXSR41ve9WG4suUNWQ63olhqKxirjzAz0IL7ggH7c3hPvA==} + /@storybook/theming@8.0.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-7NHt7bMC7lPkwz9KdDpa6DkLoQZz5OV6jsx/qY91kcdLo1rpnRPAiVlJvmWesFxi1oXOpVDpHHllWzf8KDBv8A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5103,24 +4975,24 @@ packages: react-dom: optional: true dependencies: - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) - '@storybook/client-logger': 8.0.4 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) + '@storybook/client-logger': 8.0.10 '@storybook/global': 5.0.0 memoizerific: 1.11.3 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: true - /@storybook/types@8.0.4: - resolution: {integrity: sha512-OO7QY+qZFCYkItDUBACtIV32p75O7sNziAiyS1V2Oxgo7Ln7fwZwr3mJcA1ruBed6ZcrW3c87k7Xs40T2zAWcg==} + /@storybook/types@8.0.10: + resolution: {integrity: sha512-S/hKS7+SqNnYIehwxdQ4M2nnlfGDdYWAXdtPCVJCmS+YF2amgAxeuisiHbUg7eypds6VL0Oxk/j2nPEHOHk9pg==} dependencies: - '@storybook/channels': 8.0.4 + '@storybook/channels': 8.0.10 '@types/express': 4.17.21 file-system-cache: 2.3.0 dev: true - /@swc/core-darwin-arm64@1.4.11: - resolution: {integrity: sha512-C1j1Qp/IHSelVWdEnT7f0iONWxQz6FAqzjCF2iaL+0vFg4V5f2nlgrueY8vj5pNNzSGhrAlxsMxEIp4dj1MXkg==} + /@swc/core-darwin-arm64@1.5.3: + resolution: {integrity: sha512-kRmmV2XqWegzGXvJfVVOj10OXhLgaVOOBjaX3p3Aqg7Do5ksg+bY5wi1gAN/Eul7B08Oqf7GG7WJevjDQGWPOg==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] @@ -5128,8 +5000,8 @@ packages: dev: true optional: true - /@swc/core-darwin-x64@1.4.11: - resolution: {integrity: sha512-0TTy3Ni8ncgaMCchSQ7FK8ZXQLlamy0FXmGWbR58c+pVZWYZltYPTmheJUvVcR0H2+gPAymRKyfC0iLszDALjg==} + /@swc/core-darwin-x64@1.5.3: + resolution: {integrity: sha512-EYs0+ovaRw6ZN9GBr2nIeC7gUXWA0q4RYR+Og3Vo0Qgv2Mt/XudF44A2lPK9X7M3JIfu6JjnxnTuvsK1Lqojfw==} engines: {node: '>=10'} cpu: [x64] os: [darwin] @@ -5137,8 +5009,8 @@ packages: dev: true optional: true - /@swc/core-linux-arm-gnueabihf@1.4.11: - resolution: {integrity: sha512-XJLB71uw0rog4DjYAPxFGAuGCBQpgJDlPZZK6MTmZOvI/1t0+DelJ24IjHIxk500YYM26Yv47xPabqFPD7I2zQ==} + /@swc/core-linux-arm-gnueabihf@1.5.3: + resolution: {integrity: sha512-RBVUTidSf4wgPdv98VrgJ4rMzMDN/3LBWdT7l+R7mNFH+mtID7ZAhTON0o/m1HkECgAgi1xcbTOVAw1xgd5KLA==} engines: {node: '>=10'} cpu: [arm] os: [linux] @@ -5146,8 +5018,8 @@ packages: dev: true optional: true - /@swc/core-linux-arm64-gnu@1.4.11: - resolution: {integrity: sha512-vYQwzJvm/iu052d5Iw27UFALIN5xSrGkPZXxLNMHPySVko2QMNNBv35HLatkEQHbQ3X+VKSW9J9SkdtAvAVRAQ==} + /@swc/core-linux-arm64-gnu@1.5.3: + resolution: {integrity: sha512-DCC6El3MiTYfv98CShxz/g2s4Pxn6tV0mldCQ0UdRqaN2ApUn7E+zTrqaj5bk7yII3A43WhE9Mr6wNPbXUeVyg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -5155,8 +5027,8 @@ packages: dev: true optional: true - /@swc/core-linux-arm64-musl@1.4.11: - resolution: {integrity: sha512-eV+KduiRYUFjPsvbZuJ9aknQH9Tj0U2/G9oIZSzLx/18WsYi+upzHbgxmIIHJ2VJgfd7nN40RI/hMtxNsUzR/g==} + /@swc/core-linux-arm64-musl@1.5.3: + resolution: {integrity: sha512-p04ysjYXEyaCGpJvwHm0T0nkPawXtdKBTThWnlh8M5jYULVNVA1YmC9azG2Avs1GDaLgBPVUgodmFYpdSupOYA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -5164,8 +5036,8 @@ packages: dev: true optional: true - /@swc/core-linux-x64-gnu@1.4.11: - resolution: {integrity: sha512-WA1iGXZ2HpqM1OR9VCQZJ8sQ1KP2or9O4bO8vWZo6HZJIeoQSo7aa9waaCLRpkZvkng1ct/TF/l6ymqSNFXIzQ==} + /@swc/core-linux-x64-gnu@1.5.3: + resolution: {integrity: sha512-/l4KJu0xwYm6tcVSOvF8RbXrIeIHJAhWnKvuX4ZnYKFkON968kB8Ghx+1yqBQcZf36tMzSuZUC5xBUA9u66lGA==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -5173,8 +5045,8 @@ packages: dev: true optional: true - /@swc/core-linux-x64-musl@1.4.11: - resolution: {integrity: sha512-UkVJToKf0owwQYRnGvjHAeYVDfeimCEcx0VQSbJoN7Iy0ckRZi7YPlmWJU31xtKvikE2bQWCOVe0qbSDqqcWXA==} + /@swc/core-linux-x64-musl@1.5.3: + resolution: {integrity: sha512-54DmSnrTXq4fYEKNR0nFAImG3+FxsHlQ6Tol/v3l+rxmg2K0FeeDOpH7wTXeWhMGhFlGrLIyLSnA+SzabfoDIA==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -5182,8 +5054,8 @@ packages: dev: true optional: true - /@swc/core-win32-arm64-msvc@1.4.11: - resolution: {integrity: sha512-35khwkyly7lF5NDSyvIrukBMzxPorgc5iTSDfVO/LvnmN5+fm4lTlrDr4tUfTdOhv3Emy7CsKlsNAeFRJ+Pm+w==} + /@swc/core-win32-arm64-msvc@1.5.3: + resolution: {integrity: sha512-piUMqoHNwDXChBfaaFIMzYgoxepfd8Ci1uXXNVEnuiRKz3FiIcNLmvXaBD7lKUwKcnGgVziH/CrndX6SldKQNQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] @@ -5191,8 +5063,8 @@ packages: dev: true optional: true - /@swc/core-win32-ia32-msvc@1.4.11: - resolution: {integrity: sha512-Wx8/6f0ufgQF2pbVPsJ2dAmFLwIOW+xBE5fxnb7VnEbGkTgP1qMDWiiAtD9rtvDSuODG3i1AEmAak/2HAc6i6A==} + /@swc/core-win32-ia32-msvc@1.5.3: + resolution: {integrity: sha512-zV5utPYBUzYhBOomCByAjKAvfVBcOCJtnszx7Zlfz7SAv/cGm8D1QzPDCvv6jDhIlUtLj6KyL8JXeFr+f95Fjw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] @@ -5200,8 +5072,8 @@ packages: dev: true optional: true - /@swc/core-win32-x64-msvc@1.4.11: - resolution: {integrity: sha512-0xRFW6K9UZQH2NVC/0pVB0GJXS45lY24f+6XaPBF1YnMHd8A8GoHl7ugyM5yNUTe2AKhSgk5fJV00EJt/XBtdQ==} + /@swc/core-win32-x64-msvc@1.5.3: + resolution: {integrity: sha512-QmUiXiPIV5gBADfDh8e2jKynEhyRC+dcKP/zF9y5KqDUErYzlhocLd68uYS4uIegP6AylYlmigHgcaktGEE9VQ==} engines: {node: '>=10'} cpu: [x64] os: [win32] @@ -5209,8 +5081,8 @@ packages: dev: true optional: true - /@swc/core@1.4.11: - resolution: {integrity: sha512-WKEakMZxkVwRdgMN4AMJ9K5nysY8g8npgQPczmjBeNK5In7QEAZAJwnyccrWwJZU0XjVeHn2uj+XbOKdDW17rg==} + /@swc/core@1.5.3: + resolution: {integrity: sha512-pSEglypnBGLHBoBcv3aYS7IM2t2LRinubYMyP88UoFIcD2pear2CeB15CbjJ2IzuvERD0ZL/bthM7cDSR9g+aQ==} engines: {node: '>=10'} requiresBuild: true peerDependencies: @@ -5222,16 +5094,16 @@ packages: '@swc/counter': 0.1.3 '@swc/types': 0.1.6 optionalDependencies: - '@swc/core-darwin-arm64': 1.4.11 - '@swc/core-darwin-x64': 1.4.11 - '@swc/core-linux-arm-gnueabihf': 1.4.11 - '@swc/core-linux-arm64-gnu': 1.4.11 - '@swc/core-linux-arm64-musl': 1.4.11 - '@swc/core-linux-x64-gnu': 1.4.11 - '@swc/core-linux-x64-musl': 1.4.11 - '@swc/core-win32-arm64-msvc': 1.4.11 - '@swc/core-win32-ia32-msvc': 1.4.11 - '@swc/core-win32-x64-msvc': 1.4.11 + '@swc/core-darwin-arm64': 1.5.3 + '@swc/core-darwin-x64': 1.5.3 + '@swc/core-linux-arm-gnueabihf': 1.5.3 + '@swc/core-linux-arm64-gnu': 1.5.3 + '@swc/core-linux-arm64-musl': 1.5.3 + '@swc/core-linux-x64-gnu': 1.5.3 + '@swc/core-linux-x64-musl': 1.5.3 + '@swc/core-win32-arm64-msvc': 1.5.3 + '@swc/core-win32-ia32-msvc': 1.5.3 + '@swc/core-win32-x64-msvc': 1.5.3 dev: true /@swc/counter@0.1.3: @@ -5255,7 +5127,7 @@ packages: engines: {node: '>=14'} dependencies: '@babel/code-frame': 7.24.2 - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 '@types/aria-query': 5.0.4 aria-query: 5.1.3 chalk: 4.1.2 @@ -5264,8 +5136,8 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/jest-dom@6.4.2(vitest@1.4.0): - resolution: {integrity: sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==} + /@testing-library/jest-dom@6.4.5(vitest@1.6.0): + resolution: {integrity: sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} peerDependencies: '@jest/globals': '>= 28' @@ -5286,14 +5158,14 @@ packages: optional: true dependencies: '@adobe/css-tools': 4.3.3 - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 aria-query: 5.3.0 chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 - vitest: 1.4.0(@types/node@20.11.30) + vitest: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0) dev: true /@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4): @@ -5316,8 +5188,8 @@ packages: /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: - '@babel/parser': 7.24.1 - '@babel/types': 7.24.0 + '@babel/parser': 7.24.5 + '@babel/types': 7.24.5 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.5 @@ -5326,39 +5198,39 @@ packages: /@types/babel__generator@7.6.8: resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@types/babel__template@7.4.4: resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} dependencies: - '@babel/parser': 7.24.1 - '@babel/types': 7.24.0 + '@babel/parser': 7.24.5 + '@babel/types': 7.24.5 dev: true /@types/babel__traverse@7.20.5: resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.5 dev: true /@types/body-parser@1.19.5: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: '@types/connect': 3.4.38 - '@types/node': 20.11.30 + '@types/node': 20.12.10 dev: true /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.10 dev: true /@types/cross-spawn@6.0.6: resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.10 dev: true /@types/d3-array@3.2.1: @@ -5564,16 +5436,16 @@ packages: resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==} dev: true - /@types/emscripten@1.39.10: - resolution: {integrity: sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==} + /@types/emscripten@1.39.11: + resolution: {integrity: sha512-dOeX2BeNA7j6BTEqJQL3ut0bRCfsyQMd5i4FT8JfHfYhAOuJPCGh0dQFbxVJxUyQ+75x6enhDdndGb624/QszA==} dev: true /@types/escodegen@0.0.6: resolution: {integrity: sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==} dev: true - /@types/eslint@8.56.6: - resolution: {integrity: sha512-ymwc+qb1XkjT/gfoQwxIeHZ6ixH23A+tCT2ADSA/DPVKzAjwYkTXBMCQ/f6fe4wEa85Lhp26VPeUxI7wMhAi7A==} + /@types/eslint@8.56.10: + resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} dependencies: '@types/estree': 1.0.5 '@types/json-schema': 7.0.15 @@ -5587,11 +5459,11 @@ packages: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true - /@types/express-serve-static-core@4.17.43: - resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} + /@types/express-serve-static-core@4.19.0: + resolution: {integrity: sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==} dependencies: - '@types/node': 20.11.30 - '@types/qs': 6.9.14 + '@types/node': 20.12.10 + '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 dev: true @@ -5600,9 +5472,9 @@ packages: resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.17.43 - '@types/qs': 6.9.14 - '@types/serve-static': 1.15.5 + '@types/express-serve-static-core': 4.19.0 + '@types/qs': 6.9.15 + '@types/serve-static': 1.15.7 dev: true /@types/find-cache-dir@3.2.1: @@ -5617,7 +5489,7 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.11.30 + '@types/node': 20.12.10 dev: true /@types/hast@3.0.4: @@ -5645,42 +5517,38 @@ packages: /@types/lodash-es@4.17.12: resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} dependencies: - '@types/lodash': 4.17.0 + '@types/lodash': 4.17.1 dev: true /@types/lodash.mergewith@4.6.7: resolution: {integrity: sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==} dependencies: - '@types/lodash': 4.17.0 + '@types/lodash': 4.17.1 dev: false - /@types/lodash@4.17.0: - resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==} + /@types/lodash@4.17.1: + resolution: {integrity: sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==} - /@types/mdx@2.0.12: - resolution: {integrity: sha512-H9VZ9YqE+H28FQVchC83RCs5xQ2J7mAAv6qdDEaWmXEVl3OpdH+xfrSUzQ1lp7U7oSTRZ0RvW08ASPJsYBi7Cw==} + /@types/mdx@2.0.13: + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} dev: true /@types/mime@1.3.5: resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} dev: true - /@types/mime@3.0.4: - resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} - dev: true - /@types/minimatch@5.1.2: resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} dev: true - /@types/node@18.19.26: - resolution: {integrity: sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw==} + /@types/node@18.19.32: + resolution: {integrity: sha512-2bkg93YBSDKk8DLmmHnmj/Rwr18TLx7/n+I23BigFwgexUJoMHZOd8X1OFxuF/W3NN0S2W2E5sVabI5CPinNvA==} dependencies: undici-types: 5.26.5 dev: true - /@types/node@20.11.30: - resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} + /@types/node@20.12.10: + resolution: {integrity: sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==} dependencies: undici-types: 5.26.5 dev: true @@ -5693,10 +5561,6 @@ packages: resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} dev: false - /@types/picomatch@2.3.3: - resolution: {integrity: sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==} - dev: true - /@types/pretty-hrtime@1.0.3: resolution: {integrity: sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==} dev: true @@ -5704,34 +5568,34 @@ packages: /@types/prop-types@15.7.12: resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} - /@types/qs@6.9.14: - resolution: {integrity: sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==} + /@types/qs@6.9.15: + resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} dev: true /@types/range-parser@1.2.7: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: true - /@types/react-dom@18.2.22: - resolution: {integrity: sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==} + /@types/react-dom@18.3.0: + resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} dependencies: - '@types/react': 18.2.73 + '@types/react': 18.3.1 dev: true /@types/react-reconciler@0.28.8: resolution: {integrity: sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==} dependencies: - '@types/react': 18.2.73 + '@types/react': 18.3.1 dev: false /@types/react-transition-group@4.4.10: resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} dependencies: - '@types/react': 18.2.73 + '@types/react': 18.3.1 dev: false - /@types/react@18.2.73: - resolution: {integrity: sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==} + /@types/react@18.3.1: + resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==} dependencies: '@types/prop-types': 15.7.12 csstype: 3.1.3 @@ -5748,15 +5612,15 @@ packages: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 20.11.30 + '@types/node': 20.12.10 dev: true - /@types/serve-static@1.15.5: - resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} + /@types/serve-static@1.15.7: + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} dependencies: '@types/http-errors': 2.0.4 - '@types/mime': 3.0.4 - '@types/node': 20.11.30 + '@types/node': 20.12.10 + '@types/send': 0.17.4 dev: true /@types/unist@3.0.2: @@ -5771,8 +5635,8 @@ packages: resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} dev: true - /@typescript-eslint/eslint-plugin@7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.3): - resolution: {integrity: sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==} + /@typescript-eslint/eslint-plugin@7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: '@typescript-eslint/parser': ^7.0.0 @@ -5783,25 +5647,25 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.3) - '@typescript-eslint/scope-manager': 7.4.0 - '@typescript-eslint/type-utils': 7.4.0(eslint@8.57.0)(typescript@5.4.3) - '@typescript-eslint/utils': 7.4.0(eslint@8.57.0)(typescript@5.4.3) - '@typescript-eslint/visitor-keys': 7.4.0 + '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/scope-manager': 7.8.0 + '@typescript-eslint/type-utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 7.8.0 debug: 4.3.4 eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.3) - typescript: 5.4.3 + ts-api-utils: 1.3.0(typescript@5.4.5) + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@7.4.0(eslint@8.57.0)(typescript@5.4.3): - resolution: {integrity: sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==} + /@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -5810,13 +5674,13 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 7.4.0 - '@typescript-eslint/types': 7.4.0 - '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.3) - '@typescript-eslint/visitor-keys': 7.4.0 + '@typescript-eslint/scope-manager': 7.8.0 + '@typescript-eslint/types': 7.8.0 + '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) + '@typescript-eslint/visitor-keys': 7.8.0 debug: 4.3.4 eslint: 8.57.0 - typescript: 5.4.3 + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true @@ -5829,16 +5693,16 @@ packages: '@typescript-eslint/visitor-keys': 5.62.0 dev: true - /@typescript-eslint/scope-manager@7.4.0: - resolution: {integrity: sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==} + /@typescript-eslint/scope-manager@7.8.0: + resolution: {integrity: sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==} engines: {node: ^18.18.0 || >=20.0.0} dependencies: - '@typescript-eslint/types': 7.4.0 - '@typescript-eslint/visitor-keys': 7.4.0 + '@typescript-eslint/types': 7.8.0 + '@typescript-eslint/visitor-keys': 7.8.0 dev: true - /@typescript-eslint/type-utils@7.4.0(eslint@8.57.0)(typescript@5.4.3): - resolution: {integrity: sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==} + /@typescript-eslint/type-utils@7.8.0(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -5847,12 +5711,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.3) - '@typescript-eslint/utils': 7.4.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) + '@typescript-eslint/utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) debug: 4.3.4 eslint: 8.57.0 - ts-api-utils: 1.3.0(typescript@5.4.3) - typescript: 5.4.3 + ts-api-utils: 1.3.0(typescript@5.4.5) + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true @@ -5862,12 +5726,12 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/types@7.4.0: - resolution: {integrity: sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==} + /@typescript-eslint/types@7.8.0: + resolution: {integrity: sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==} engines: {node: ^18.18.0 || >=20.0.0} dev: true - /@typescript-eslint/typescript-estree@5.62.0(typescript@5.4.3): + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.4.5): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5882,14 +5746,14 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.0 - tsutils: 3.21.0(typescript@5.4.3) - typescript: 5.4.3 + tsutils: 3.21.0(typescript@5.4.5) + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/typescript-estree@7.4.0(typescript@5.4.3): - resolution: {integrity: sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==} + /@typescript-eslint/typescript-estree@7.8.0(typescript@5.4.5): + resolution: {integrity: sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: typescript: '*' @@ -5897,20 +5761,20 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 7.4.0 - '@typescript-eslint/visitor-keys': 7.4.0 + '@typescript-eslint/types': 7.8.0 + '@typescript-eslint/visitor-keys': 7.8.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - minimatch: 9.0.3 + minimatch: 9.0.4 semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.3) - typescript: 5.4.3 + ts-api-utils: 1.3.0(typescript@5.4.5) + typescript: 5.4.5 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.4.3): + /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.4.5): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5921,7 +5785,7 @@ packages: '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.3) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) eslint: 8.57.0 eslint-scope: 5.1.1 semver: 7.6.0 @@ -5930,8 +5794,8 @@ packages: - typescript dev: true - /@typescript-eslint/utils@7.4.0(eslint@8.57.0)(typescript@5.4.3): - resolution: {integrity: sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==} + /@typescript-eslint/utils@7.8.0(eslint@8.57.0)(typescript@5.4.5): + resolution: {integrity: sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -5939,9 +5803,9 @@ packages: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 7.4.0 - '@typescript-eslint/types': 7.4.0 - '@typescript-eslint/typescript-estree': 7.4.0(typescript@5.4.3) + '@typescript-eslint/scope-manager': 7.8.0 + '@typescript-eslint/types': 7.8.0 + '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) eslint: 8.57.0 semver: 7.6.0 transitivePeerDependencies: @@ -5957,11 +5821,11 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@typescript-eslint/visitor-keys@7.4.0: - resolution: {integrity: sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==} + /@typescript-eslint/visitor-keys@7.8.0: + resolution: {integrity: sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==} engines: {node: ^18.18.0 || >=20.0.0} dependencies: - '@typescript-eslint/types': 7.4.0 + '@typescript-eslint/types': 7.8.0 eslint-visitor-keys: 3.4.3 dev: true @@ -5969,17 +5833,40 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-react-swc@3.6.0(vite@5.2.6): + /@vitejs/plugin-react-swc@3.6.0(vite@5.2.11): resolution: {integrity: sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==} peerDependencies: vite: ^4 || ^5 dependencies: - '@swc/core': 1.4.11 - vite: 5.2.6(@types/node@20.11.30) + '@swc/core': 1.5.3 + vite: 5.2.11(@types/node@20.12.10) transitivePeerDependencies: - '@swc/helpers' dev: true + /@vitest/coverage-v8@1.6.0(vitest@1.6.0): + resolution: {integrity: sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==} + peerDependencies: + vitest: 1.6.0 + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.4 + istanbul-reports: 3.1.7 + magic-string: 0.30.10 + magicast: 0.3.4 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.1.0 + test-exclude: 6.0.0 + vitest: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0) + transitivePeerDependencies: + - supports-color + dev: true + /@vitest/expect@1.3.1: resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==} dependencies: @@ -5988,26 +5875,26 @@ packages: chai: 4.4.1 dev: true - /@vitest/expect@1.4.0: - resolution: {integrity: sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==} + /@vitest/expect@1.6.0: + resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} dependencies: - '@vitest/spy': 1.4.0 - '@vitest/utils': 1.4.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 chai: 4.4.1 dev: true - /@vitest/runner@1.4.0: - resolution: {integrity: sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==} + /@vitest/runner@1.6.0: + resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} dependencies: - '@vitest/utils': 1.4.0 + '@vitest/utils': 1.6.0 p-limit: 5.0.0 pathe: 1.1.2 dev: true - /@vitest/snapshot@1.4.0: - resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==} + /@vitest/snapshot@1.6.0: + resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} dependencies: - magic-string: 0.30.8 + magic-string: 0.30.10 pathe: 1.1.2 pretty-format: 29.7.0 dev: true @@ -6018,12 +5905,27 @@ packages: tinyspy: 2.2.1 dev: true - /@vitest/spy@1.4.0: - resolution: {integrity: sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==} + /@vitest/spy@1.6.0: + resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} dependencies: tinyspy: 2.2.1 dev: true + /@vitest/ui@1.6.0(vitest@1.6.0): + resolution: {integrity: sha512-k3Lyo+ONLOgylctiGovRKy7V4+dIN2yxstX3eY5cWFXH6WP+ooVX79YSyi0GagdTQzLmT43BF27T0s6dOIPBXA==} + peerDependencies: + vitest: 1.6.0 + dependencies: + '@vitest/utils': 1.6.0 + fast-glob: 3.3.2 + fflate: 0.8.2 + flatted: 3.3.1 + pathe: 1.1.2 + picocolors: 1.0.0 + sirv: 2.0.4 + vitest: 1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0) + dev: true + /@vitest/utils@1.3.1: resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==} dependencies: @@ -6033,8 +5935,8 @@ packages: pretty-format: 29.7.0 dev: true - /@vitest/utils@1.4.0: - resolution: {integrity: sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==} + /@vitest/utils@1.6.0: + resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} dependencies: diff-sequences: 29.6.3 estree-walker: 3.0.3 @@ -6061,24 +5963,24 @@ packages: path-browserify: 1.0.1 dev: true - /@vue/compiler-core@3.4.21: - resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==} + /@vue/compiler-core@3.4.26: + resolution: {integrity: sha512-N9Vil6Hvw7NaiyFUFBPXrAyETIGlQ8KcFMkyk6hW1Cl6NvoqvP+Y8p1Eqvx+UdqsnrnI9+HMUEJegzia3mhXmQ==} dependencies: - '@babel/parser': 7.24.1 - '@vue/shared': 3.4.21 + '@babel/parser': 7.24.5 + '@vue/shared': 3.4.26 entities: 4.5.0 estree-walker: 2.0.2 source-map-js: 1.2.0 dev: true - /@vue/compiler-dom@3.4.21: - resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==} + /@vue/compiler-dom@3.4.26: + resolution: {integrity: sha512-4CWbR5vR9fMg23YqFOhr6t6WB1Fjt62d6xdFPyj8pxrYub7d+OgZaObMsoxaF9yBUHPMiPFK303v61PwAuGvZA==} dependencies: - '@vue/compiler-core': 3.4.21 - '@vue/shared': 3.4.21 + '@vue/compiler-core': 3.4.26 + '@vue/shared': 3.4.26 dev: true - /@vue/language-core@1.8.27(typescript@5.4.3): + /@vue/language-core@1.8.27(typescript@5.4.5): resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} peerDependencies: typescript: '*' @@ -6088,18 +5990,18 @@ packages: dependencies: '@volar/language-core': 1.11.1 '@volar/source-map': 1.11.1 - '@vue/compiler-dom': 3.4.21 - '@vue/shared': 3.4.21 + '@vue/compiler-dom': 3.4.26 + '@vue/shared': 3.4.26 computeds: 0.0.1 - minimatch: 9.0.3 + minimatch: 9.0.4 muggle-string: 0.3.1 path-browserify: 1.0.1 - typescript: 5.4.3 + typescript: 5.4.5 vue-template-compiler: 2.7.16 dev: true - /@vue/shared@3.4.21: - resolution: {integrity: sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==} + /@vue/shared@3.4.26: + resolution: {integrity: sha512-Fg4zwR0GNnjzodMt3KRy2AWGMKQXByl56+4HjN87soxLNU9P5xcJkstAlIeEF3cU6UYOzmJl1tV0dVPGIljCnQ==} dev: true /@xobotyi/scrollbar-width@1.9.5: @@ -6128,7 +6030,7 @@ packages: resolution: {integrity: sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==} engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'} dependencies: - '@types/emscripten': 1.39.10 + '@types/emscripten': 1.39.11 tslib: 1.14.1 dev: true @@ -6409,7 +6311,7 @@ packages: /@zag-js/number-input@0.32.1: resolution: {integrity: sha512-atyIOvoMITb4hZtQym7yD6I7grvPW83UeMFO8hCQg3HWwd2zR4+63mouWuyMoWb4QrzVFRVQBaU8OG5xGlknEw==} dependencies: - '@internationalized/number': 3.5.1 + '@internationalized/number': 3.5.2 '@zag-js/anatomy': 0.32.1 '@zag-js/core': 0.32.1 '@zag-js/dom-event': 0.32.1 @@ -6519,7 +6421,7 @@ packages: '@zag-js/utils': 0.32.1 dev: false - /@zag-js/react@0.32.1(react-dom@18.2.0)(react@18.2.0): + /@zag-js/react@0.32.1(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-b1SB7hXXv1K6CmXkcy5Y7mb0YRWkyvulyhK8VW5O5hIAPuGxOTx70psmVeZbmVzhjdORCiro9jKx8Ec0LfolFg==} peerDependencies: react: '>=18.0.0' @@ -6529,8 +6431,8 @@ packages: '@zag-js/store': 0.32.1 '@zag-js/types': 0.32.1 proxy-compare: 2.5.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false /@zag-js/rect-utils@0.32.1: @@ -6693,18 +6595,6 @@ packages: resolution: {integrity: sha512-Vzieo4vNulzY/0zqmVfeYW/LcFJp5xtEoyUgR1FBctH8uBPBRhTIEXxKtoMablW6/vccOVo7zcu0UrR5Vx+eYQ==} dev: false - /@zkochan/retry@0.2.0: - resolution: {integrity: sha512-WhB+2B/ZPlW2Xy/kMJBrMbqecWXcbDDgn0K0wKBAgO2OlBTz1iLJrRWduo+DGGn0Akvz1Lu4Xvls7dJojximWw==} - engines: {node: '>=10'} - dev: true - - /@zkochan/rimraf@2.1.3: - resolution: {integrity: sha512-mCfR3gylCzPC+iqdxEA6z5SxJeOgzgbwmyxanKriIne5qZLswDe/M43aD3p5MNzwzXRhbZg/OX+MpES6Zk1a6A==} - engines: {node: '>=12.10'} - dependencies: - rimraf: 3.0.2 - dev: true - /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -6874,7 +6764,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-object-atoms: 1.0.0 get-intrinsic: 1.2.4 is-string: 1.0.7 @@ -6898,7 +6788,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 es-object-atoms: 1.0.0 es-shim-unscopables: 1.0.2 @@ -6910,7 +6800,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 es-object-atoms: 1.0.0 es-shim-unscopables: 1.0.2 @@ -6922,7 +6812,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-shim-unscopables: 1.0.2 dev: true @@ -6932,7 +6822,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-shim-unscopables: 1.0.2 dev: true @@ -6941,7 +6831,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-shim-unscopables: 1.0.2 dev: true @@ -6950,7 +6840,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 es-shim-unscopables: 1.0.2 dev: true @@ -6962,7 +6852,7 @@ packages: array-buffer-byte-length: 1.0.1 call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 get-intrinsic: 1.2.4 is-array-buffer: 3.0.4 @@ -7006,55 +6896,55 @@ packages: possible-typed-array-names: 1.0.0 dev: true - /babel-core@7.0.0-bridge.0(@babel/core@7.24.3): + /babel-core@7.0.0-bridge.0(@babel/core@7.24.5): resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.24.3 + '@babel/core': 7.24.5 dev: true /babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 cosmiconfig: 7.1.0 resolve: 1.22.8 dev: false - /babel-plugin-polyfill-corejs2@0.4.10(@babel/core@7.24.3): - resolution: {integrity: sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==} + /babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.5): + resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/compat-data': 7.24.1 - '@babel/core': 7.24.3 - '@babel/helper-define-polyfill-provider': 0.6.1(@babel/core@7.24.3) + '@babel/compat-data': 7.24.4 + '@babel/core': 7.24.5 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.5) semver: 6.3.1 transitivePeerDependencies: - supports-color dev: true - /babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.3): + /babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.5): resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-define-polyfill-provider': 0.6.1(@babel/core@7.24.3) - core-js-compat: 3.36.1 + '@babel/core': 7.24.5 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.5) + core-js-compat: 3.37.0 transitivePeerDependencies: - supports-color dev: true - /babel-plugin-polyfill-regenerator@0.6.1(@babel/core@7.24.3): - resolution: {integrity: sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==} + /babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.5): + resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.24.3 - '@babel/helper-define-polyfill-provider': 0.6.1(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.5) transitivePeerDependencies: - supports-color dev: true @@ -7117,13 +7007,6 @@ packages: - supports-color dev: true - /bole@5.0.11: - resolution: {integrity: sha512-KB0Ye0iMAW5BnNbnLfMSQcnI186hKUzE2fpkZWqcxsoTR7eqzlTidSOMYPHJOn/yR7VGH7uSZp37qH9q2Et0zQ==} - dependencies: - fast-safe-stringify: 2.1.1 - individual: 3.0.0 - dev: true - /boolean@3.2.0: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} dev: false @@ -7170,10 +7053,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001600 - electron-to-chromium: 1.4.719 + caniuse-lite: 1.0.30001616 + electron-to-chromium: 1.4.757 node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.23.0) + update-browserslist-db: 1.0.15(browserslist@4.23.0) dev: true /buffer-from@1.1.2: @@ -7187,12 +7070,6 @@ packages: ieee754: 1.2.1 dev: true - /builtins@5.0.1: - resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} - dependencies: - semver: 7.6.0 - dev: true - /bytes@3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -7223,8 +7100,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - /caniuse-lite@1.0.30001600: - resolution: {integrity: sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==} + /caniuse-lite@1.0.30001616: + resolution: {integrity: sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==} dev: true /chai@4.4.1: @@ -7240,7 +7117,7 @@ packages: type-detect: 4.0.8 dev: true - /chakra-react-select@4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.4)(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): + /chakra-react-select@4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.4)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-ZL43hyXPnWf1g/HjsZDecbeJ4F2Q6tTPYJozlKWkrQ7lIX7ORP0aZYwmc5/Wly4UNzMimj2Vuosl6MmIXH+G2g==} peerDependencies: '@chakra-ui/form-control': ^2.0.0 @@ -7254,17 +7131,17 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.22)(react@18.2.0) - '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-select: 5.7.7(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.1.8)(react@18.3.1) + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.3.1) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.3.1) + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-select: 5.7.7(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false @@ -7334,8 +7211,8 @@ packages: consola: 3.2.3 dev: true - /classcat@5.0.4: - resolution: {integrity: sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==} + /classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} dev: false /clean-stack@2.2.0: @@ -7388,11 +7265,6 @@ packages: requiresBuild: true dev: true - /clsx@1.2.1: - resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} - engines: {node: '>=6'} - dev: false - /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -7501,6 +7373,10 @@ packages: yargs: 17.7.2 dev: true + /confbox@0.1.7: + resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} + dev: true + /consola@3.2.3: resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} @@ -7541,8 +7417,8 @@ packages: toggle-selection: 1.0.6 dev: false - /core-js-compat@3.36.1: - resolution: {integrity: sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==} + /core-js-compat@3.37.0: + resolution: {integrity: sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA==} dependencies: browserslist: 4.23.0 dev: true @@ -7676,11 +7552,6 @@ packages: d3-transition: 3.0.1(d3-selection@3.0.0) dev: false - /data-uri-to-buffer@3.0.1: - resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} - engines: {node: '>= 6'} - dev: true - /data-view-buffer@1.0.1: resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} engines: {node: '>= 0.4'} @@ -7712,7 +7583,7 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 dev: true /dateformat@5.0.3: @@ -7941,7 +7812,7 @@ packages: /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 csstype: 3.1.3 dev: false @@ -7961,10 +7832,10 @@ packages: dependencies: chalk: 4.1.2 fs-extra: 11.2.0 - glob: 10.3.10 + glob: 10.3.12 ora: 5.4.1 tslib: 2.6.2 - typescript: 5.4.3 + typescript: 5.4.5 yargs: 17.7.2 dev: true @@ -7993,16 +7864,16 @@ packages: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: true - /ejs@3.1.9: - resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} + /ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} hasBin: true dependencies: - jake: 10.8.7 + jake: 10.9.1 dev: true - /electron-to-chromium@1.4.719: - resolution: {integrity: sha512-FbWy2Q2YgdFzkFUW/W5jBjE9dj+804+98E4Pup78JBPnbdb3pv6IneY2JCPKdeKLh3AOKHQeYf+KwLr7mxGh6Q==} + /electron-to-chromium@1.4.757: + resolution: {integrity: sha512-jftDaCknYSSt/+KKeXzH3LX5E2CvRLm75P3Hj+J/dv3CL0qUYcOt13d5FN1NiL5IJbbhzHrb3BomeG2tkSlZmw==} dev: true /emoji-regex@8.0.0: @@ -8013,13 +7884,6 @@ packages: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true - /encode-registry@3.0.1: - resolution: {integrity: sha512-6qOwkl1g0fv0DN3Y3ggr2EaZXN71aoAqPp3p/pVaWSBSIo+YjLOWN61Fva43oVyQNPf7kgm8lkudzlzojwE2jw==} - engines: {node: '>=10'} - dependencies: - mem: 8.1.1 - dev: true - /encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -8034,7 +7898,7 @@ packages: /engine.io-client@6.5.3: resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} dependencies: - '@socket.io/component-emitter': 3.1.0 + '@socket.io/component-emitter': 3.1.2 debug: 4.3.4 engine.io-parser: 5.2.2 ws: 8.11.0 @@ -8055,16 +7919,12 @@ packages: engines: {node: '>=0.12'} dev: true - /envinfo@7.11.1: - resolution: {integrity: sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==} + /envinfo@7.13.0: + resolution: {integrity: sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==} engines: {node: '>=4'} hasBin: true dev: true - /err-code@2.0.3: - resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - dev: true - /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -8076,8 +7936,8 @@ packages: stackframe: 1.3.4 dev: false - /es-abstract@1.23.2: - resolution: {integrity: sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==} + /es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} engines: {node: '>= 0.4'} dependencies: array-buffer-byte-length: 1.0.1 @@ -8095,7 +7955,7 @@ packages: function.prototype.name: 1.1.6 get-intrinsic: 1.2.4 get-symbol-description: 1.0.2 - globalthis: 1.0.3 + globalthis: 1.0.4 gopd: 1.0.1 has-property-descriptors: 1.0.2 has-proto: 1.0.3 @@ -8152,18 +8012,18 @@ packages: stop-iteration-iterator: 1.0.0 dev: true - /es-iterator-helpers@1.0.18: - resolution: {integrity: sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==} + /es-iterator-helpers@1.0.19: + resolution: {integrity: sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 es-set-tostringtag: 2.0.3 function-bind: 1.1.2 get-intrinsic: 1.2.4 - globalthis: 1.0.3 + globalthis: 1.0.4 has-property-descriptors: 1.0.2 has-proto: 1.0.3 has-symbols: 1.0.3 @@ -8301,7 +8161,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.1(@typescript-eslint/parser@7.4.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): + /eslint-module-utils@2.8.1(@typescript-eslint/parser@7.8.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} engines: {node: '>=4'} peerDependencies: @@ -8322,7 +8182,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) debug: 3.2.7 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -8338,7 +8198,7 @@ packages: requireindex: 1.1.0 dev: true - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.4.0)(eslint@8.57.0): + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.8.0)(eslint@8.57.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} peerDependencies: @@ -8348,7 +8208,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 7.4.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -8357,7 +8217,7 @@ packages: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.4.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.8.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -8383,8 +8243,8 @@ packages: load-tsconfig: 0.2.5 dev: true - /eslint-plugin-react-hooks@4.6.0(eslint@8.57.0): - resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + /eslint-plugin-react-hooks@4.6.2(eslint@8.57.0): + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 @@ -8412,7 +8272,7 @@ packages: array.prototype.toreversed: 1.1.2 array.prototype.tosorted: 1.1.3 doctrine: 2.1.0 - es-iterator-helpers: 1.0.18 + es-iterator-helpers: 1.0.19 eslint: 8.57.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.5 @@ -8427,22 +8287,22 @@ packages: string.prototype.matchall: 4.0.11 dev: true - /eslint-plugin-simple-import-sort@12.0.0(eslint@8.57.0): - resolution: {integrity: sha512-8o0dVEdAkYap0Cn5kNeklaKcT1nUsa3LITWEuFk3nJifOoD+5JQGoyDUW2W/iPWwBsNBJpyJS9y4je/BgxLcyQ==} + /eslint-plugin-simple-import-sort@12.1.0(eslint@8.57.0): + resolution: {integrity: sha512-Y2fqAfC11TcG/WP3TrI1Gi3p3nc8XJyEOJYHyEPEGI/UAgNx6akxxlX74p7SbAQdLcgASKhj8M0GKvH3vq/+ig==} peerDependencies: eslint: '>=5.0.0' dependencies: eslint: 8.57.0 dev: true - /eslint-plugin-storybook@0.8.0(eslint@8.57.0)(typescript@5.4.3): + /eslint-plugin-storybook@0.8.0(eslint@8.57.0)(typescript@5.4.5): resolution: {integrity: sha512-CZeVO5EzmPY7qghO2t64oaFM+8FTaD4uzOEjHKp516exyTKo+skKAL9GI3QALS2BXhyALJjNtwbmr1XinGE8bA==} engines: {node: '>= 18'} peerDependencies: eslint: '>=6' dependencies: '@storybook/csf': 0.0.1 - '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 requireindex: 1.2.0 ts-dedent: 2.2.0 @@ -8451,8 +8311,8 @@ packages: - typescript dev: true - /eslint-plugin-unused-imports@3.1.0(@typescript-eslint/eslint-plugin@7.4.0)(eslint@8.57.0): - resolution: {integrity: sha512-9l1YFCzXKkw1qtAru1RWUtG2EVDZY0a0eChKXcL+EZ5jitG7qxdctu4RnvhOJHv4xfmUf7h+JJPINlVpGhZMrw==} + /eslint-plugin-unused-imports@3.2.0(@typescript-eslint/eslint-plugin@7.8.0)(eslint@8.57.0): + resolution: {integrity: sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: '@typescript-eslint/eslint-plugin': 6 - 7 @@ -8461,7 +8321,7 @@ packages: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 7.4.0(@typescript-eslint/parser@7.4.0)(eslint@8.57.0)(typescript@5.4.3) + '@typescript-eslint/eslint-plugin': 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 eslint-rule-composer: 0.3.0 dev: true @@ -8532,7 +8392,7 @@ packages: lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.3 + optionator: 0.9.4 strip-ansi: 6.0.1 text-table: 0.2.0 transitivePeerDependencies: @@ -8700,10 +8560,6 @@ packages: boolean: 3.2.0 dev: false - /fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - dev: true - /fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} dev: false @@ -8718,20 +8574,14 @@ packages: reusify: 1.0.4 dev: true - /fetch-blob@2.1.2: - resolution: {integrity: sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==} - engines: {node: ^10.17.0 || >=12.3.0} - peerDependencies: - domexception: '*' - peerDependenciesMeta: - domexception: - optional: true - dev: true - /fetch-retry@5.0.6: resolution: {integrity: sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==} dev: true + /fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + dev: true + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -8739,6 +8589,13 @@ packages: flat-cache: 3.2.0 dev: true + /file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + dependencies: + flat-cache: 4.0.1 + dev: true + /file-selector@0.6.0: resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} engines: {node: '>= 12'} @@ -8849,12 +8706,20 @@ packages: rimraf: 3.0.2 dev: true + /flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + dev: true + /flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} dev: true - /flow-parser@0.232.0: - resolution: {integrity: sha512-U8vcKyYdM+Kb0tPzfPJ5JyPMU0uXKwHxp0L6BcEc+wBlbTW9qRhOqV5DeGXclgclVvtqQNGEG8Strj/b6c/IxA==} + /flow-parser@0.235.1: + resolution: {integrity: sha512-s04193L4JE+ntEcQXbD6jxRRlyj9QXcgEl2W6xSjH4l9x4b0eHoCHfbYHjqf9LdZFUiM5LhgpiqsvLj/AyOyYQ==} engines: {node: '>=0.4.0'} dev: true @@ -8890,7 +8755,11 @@ packages: engines: {node: '>= 0.6'} dev: true - /framer-motion@10.18.0(react-dom@18.2.0)(react@18.2.0): + /fracturedjsonjs@4.0.1: + resolution: {integrity: sha512-KMhSx7o45aPVj4w27dwdQyKJkNU8oBqw8UiK/s3VzsQB3+pKQ/3AqG/YOEQblV2BDuYE5dKp0OMf8RDsshrjTA==} + dev: false + + /framer-motion@10.18.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==} peerDependencies: react: ^18.0.0 @@ -8901,15 +8770,15 @@ packages: react-dom: optional: true dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) tslib: 2.6.2 optionalDependencies: '@emotion/is-prop-valid': 0.8.8 dev: false - /framer-motion@11.0.22(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-kWyldNJLyKDvLWjPYFmgngQYLiU8973BtAeVBc83r2cnil/NBUQJb1ff/6/EweNQYb5BW3PaXFjZa4D3pn/W2Q==} + /framer-motion@11.1.8(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-W2OGZmNfUarhh6A/rLXernq/JthjekbgeRWqzigPpbaShe/+HfQKUDSjiEdL302XOlINtO+SCFCiR1hlqN3uOA==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 @@ -8922,8 +8791,8 @@ packages: react-dom: optional: true dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) tslib: 2.6.2 dev: false @@ -8942,15 +8811,6 @@ packages: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} dev: true - /fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - dev: true - /fs-extra@11.1.1: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} engines: {node: '>=14.14'} @@ -9006,7 +8866,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 functions-have-names: 1.2.3 dev: true @@ -9113,16 +8973,16 @@ packages: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} dev: true - /glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + /glob@10.3.12: + resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true dependencies: foreground-child: 3.1.1 jackspeak: 2.3.6 - minimatch: 9.0.3 - minipass: 7.0.4 - path-scurry: 1.10.1 + minimatch: 9.0.4 + minipass: 7.1.0 + path-scurry: 1.10.2 dev: true /glob@7.2.3: @@ -9148,11 +9008,12 @@ packages: type-fest: 0.20.2 dev: true - /globalthis@1.0.3: - resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + /globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} dependencies: define-properties: 1.2.1 + gopd: 1.0.1 /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} @@ -9284,18 +9145,8 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true - /hosted-git-info@4.1.0: - resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} - engines: {node: '>=10'} - dependencies: - lru-cache: 6.0.0 - dev: true - - /hosted-git-info@7.0.1: - resolution: {integrity: sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - lru-cache: 10.2.0 + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true /html-parse-stringify@3.0.1: @@ -9334,18 +9185,18 @@ packages: resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} dev: false - /i18next-http-backend@2.5.0: - resolution: {integrity: sha512-Z/aQsGZk1gSxt2/DztXk92DuDD20J+rNudT7ZCdTrNOiK8uQppfvdjq9+DFQfpAnFPn3VZS+KQIr1S/W1KxhpQ==} + /i18next-http-backend@2.5.1: + resolution: {integrity: sha512-+rNX1tghdVxdfjfPt0bI1sNg5ahGW9kA7OboG7b4t03Fp69NdDlRIze6yXhIbN8rbHxJ8IP4dzRm/okZ15lkQg==} dependencies: cross-fetch: 4.0.0 transitivePeerDependencies: - encoding dev: false - /i18next@23.10.1: - resolution: {integrity: sha512-NDiIzFbcs3O9PXpfhkjyf7WdqFn5Vq6mhzhtkXzj51aOcNuPNcTwuYNuXCpHsanZGHlHKL35G7huoFeVic1hng==} + /i18next@23.11.3: + resolution: {integrity: sha512-Pq/aSKowir7JM0rj+Wa23Kb6KKDUGno/HjG+wRQu0PxoTbpQ4N89MAT0rFGvXmLkRLNMb1BbBOKGozl01dabzg==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 dev: false /iconv-lite@0.4.24: @@ -9372,8 +9223,8 @@ packages: engines: {node: '>= 4'} dev: true - /immer@10.0.4: - resolution: {integrity: sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==} + /immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} dev: false /import-fresh@3.3.0: @@ -9398,10 +9249,6 @@ packages: engines: {node: '>=8'} dev: true - /individual@3.0.0: - resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} - dev: true - /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: @@ -9726,16 +9573,44 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /isexe@3.1.1: - resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} - engines: {node: '>=16'} - dev: true - /isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} dev: true + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@5.0.4: + resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==} + engines: {node: '>=10'} + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + /iterable-lookahead@1.0.0: resolution: {integrity: sha512-hJnEP2Xk4+44DDwJqUQGdXal5VbyeWLaPyDl2AQc242Zr7iqz4DgpQOrEzglWVMGHMDCkguLHEKxd1+rOsmgSQ==} engines: {node: '>=4'} @@ -9751,13 +9626,13 @@ packages: set-function-name: 2.0.2 dev: true - /its-fine@1.1.3(react@18.2.0): - resolution: {integrity: sha512-mncCA+yb6tuh5zK26cHqKlsSyxm4zdm4YgJpxycyx6p9fgxgK5PLu3iDVpKhzTn57Yrv3jk/r0aK0RFTT1OjFw==} + /its-fine@1.2.5(react@18.3.1): + resolution: {integrity: sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==} peerDependencies: react: '>=18.0' dependencies: '@types/react-reconciler': 0.28.8 - react: 18.2.0 + react: 18.3.1 dev: false /jackspeak@2.3.6: @@ -9769,8 +9644,8 @@ packages: '@pkgjs/parseargs': 0.11.0 dev: true - /jake@10.8.7: - resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==} + /jake@10.9.1: + resolution: {integrity: sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==} engines: {node: '>=10'} hasBin: true dependencies: @@ -9796,8 +9671,8 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - /js-tokens@8.0.3: - resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} + /js-tokens@9.0.0: + resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} dev: true /js-yaml@4.1.0: @@ -9807,7 +9682,7 @@ packages: argparse: 2.0.1 dev: true - /jscodeshift@0.15.2(@babel/preset-env@7.24.3): + /jscodeshift@0.15.2(@babel/preset-env@7.24.5): resolution: {integrity: sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==} hasBin: true peerDependencies: @@ -9816,20 +9691,20 @@ packages: '@babel/preset-env': optional: true dependencies: - '@babel/core': 7.24.3 - '@babel/parser': 7.24.1 - '@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-optional-chaining': 7.24.1(@babel/core@7.24.3) - '@babel/plugin-transform-private-methods': 7.24.1(@babel/core@7.24.3) - '@babel/preset-env': 7.24.3(@babel/core@7.24.3) - '@babel/preset-flow': 7.24.1(@babel/core@7.24.3) - '@babel/preset-typescript': 7.24.1(@babel/core@7.24.3) - '@babel/register': 7.23.7(@babel/core@7.24.3) - babel-core: 7.0.0-bridge.0(@babel/core@7.24.3) + '@babel/core': 7.24.5 + '@babel/parser': 7.24.5 + '@babel/plugin-transform-class-properties': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-nullish-coalescing-operator': 7.24.1(@babel/core@7.24.5) + '@babel/plugin-transform-optional-chaining': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-private-methods': 7.24.1(@babel/core@7.24.5) + '@babel/preset-env': 7.24.5(@babel/core@7.24.5) + '@babel/preset-flow': 7.24.1(@babel/core@7.24.5) + '@babel/preset-typescript': 7.24.1(@babel/core@7.24.5) + '@babel/register': 7.23.7(@babel/core@7.24.5) + babel-core: 7.0.0-bridge.0(@babel/core@7.24.5) chalk: 4.1.2 - flow-parser: 0.232.0 + flow-parser: 0.235.1 graceful-fs: 4.2.11 micromatch: 4.0.5 neo-async: 2.6.2 @@ -9859,11 +9734,6 @@ packages: /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - /json-parse-even-better-errors@3.0.1: - resolution: {integrity: sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true @@ -9872,10 +9742,6 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true - /json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - dev: true - /json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -9889,10 +9755,6 @@ packages: hasBin: true dev: true - /jsonc-parser@3.2.1: - resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} - dev: true - /jsondiffpatch@0.6.0: resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -9948,8 +9810,8 @@ packages: engines: {node: '>= 8'} dev: false - /knip@5.6.1(@types/node@20.11.30)(typescript@5.4.3): - resolution: {integrity: sha512-occwYqHrV6KSyM1DbpWj8qQ8pCQzsdxVxYbjhYcryoXxWmHG2scyxxB4HyxVmp3Xdora4Px+3ZV5QQDi2ArerA==} + /knip@5.12.3(@types/node@20.12.10)(typescript@5.4.5): + resolution: {integrity: sha512-LL+NsE+3H0TkUnQW6icHQ+5qSrPENmjHJyMHgzjiZPmunstrIsaRG+QjahnzoH/FjMjVJwrdwVOSvksa8ixFbw==} engines: {node: '>=18.6.0'} hasBin: true peerDependencies: @@ -9958,31 +9820,24 @@ packages: dependencies: '@ericcornelissen/bash-parser': 0.5.2 '@nodelib/fs.walk': 2.0.0 - '@npmcli/map-workspaces': 3.0.4 - '@npmcli/package-json': 5.0.0 - '@pnpm/logger': 5.0.0 - '@pnpm/workspace.pkgs-graph': 2.0.15(@pnpm/logger@5.0.0) '@snyk/github-codeowners': 1.1.0 - '@types/node': 20.11.30 - '@types/picomatch': 2.3.3 + '@types/node': 20.12.10 easy-table: 1.2.0 fast-glob: 3.3.2 + file-entry-cache: 8.0.0 jiti: 1.21.0 js-yaml: 4.1.0 - micromatch: 4.0.5 minimist: 1.2.8 picocolors: 1.0.0 - picomatch: 4.0.1 + picomatch: 4.0.2 pretty-ms: 9.0.0 + resolve: 1.22.8 smol-toml: 1.1.4 strip-json-comments: 5.0.1 summary: 2.1.0 - typescript: 5.4.3 - zod: 3.22.4 - zod-validation-error: 3.0.3(zod@3.22.4) - transitivePeerDependencies: - - bluebird - - domexception + typescript: 5.4.5 + zod: 3.23.6 + zod-validation-error: 3.2.0(zod@3.23.6) dev: true /kolorist@1.8.0: @@ -10026,16 +9881,6 @@ packages: ts-error: 1.0.6 dev: false - /load-json-file@6.2.0: - resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} - engines: {node: '>=8'} - dependencies: - graceful-fs: 4.2.11 - parse-json: 5.2.0 - strip-bom: 4.0.0 - type-fest: 0.6.0 - dev: true - /load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -10045,8 +9890,8 @@ packages: resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} engines: {node: '>=14'} dependencies: - mlly: 1.6.1 - pkg-types: 1.0.3 + mlly: 1.7.0 + pkg-types: 1.1.0 dev: true /locate-path@3.0.0: @@ -10123,8 +9968,8 @@ packages: get-func-name: 2.0.2 dev: true - /lru-cache@10.2.0: - resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} + /lru-cache@10.2.2: + resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} dev: true @@ -10159,13 +10004,20 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /magic-string@0.30.8: - resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} - engines: {node: '>=12'} + /magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /magicast@0.3.4: + resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==} + dependencies: + '@babel/parser': 7.24.5 + '@babel/types': 7.24.5 + source-map-js: 1.2.0 + dev: true + /make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} @@ -10181,11 +10033,11 @@ packages: semver: 6.3.1 dev: true - /map-age-cleaner@0.1.3: - resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} - engines: {node: '>=6'} + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} dependencies: - p-defer: 1.0.0 + semver: 7.6.0 dev: true /map-obj@2.0.0: @@ -10197,13 +10049,13 @@ packages: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} dev: true - /markdown-to-jsx@7.3.2(react@18.2.0): + /markdown-to-jsx@7.3.2(react@18.3.1): resolution: {integrity: sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q==} engines: {node: '>= 10'} peerDependencies: react: '>= 0.14.0' dependencies: - react: 18.2.0 + react: 18.3.1 dev: true /mdn-data@2.0.14: @@ -10215,22 +10067,6 @@ packages: engines: {node: '>= 0.6'} dev: true - /mem@6.1.1: - resolution: {integrity: sha512-Ci6bIfq/UgcxPTYa8dQQ5FY3BzKkT894bwXWXxC/zqs0XgMO2cT20CGkOqda7gZNkmK5VP4x89IGZ6K7hfbn3Q==} - engines: {node: '>=8'} - dependencies: - map-age-cleaner: 0.1.3 - mimic-fn: 3.1.0 - dev: true - - /mem@8.1.1: - resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} - engines: {node: '>=10'} - dependencies: - map-age-cleaner: 0.1.3 - mimic-fn: 3.1.0 - dev: true - /memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} dev: false @@ -10290,11 +10126,6 @@ packages: engines: {node: '>=6'} dev: true - /mimic-fn@3.1.0: - resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} - engines: {node: '>=8'} - dev: true - /mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} @@ -10324,8 +10155,8 @@ packages: brace-expansion: 2.0.1 dev: true - /minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + /minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 @@ -10347,8 +10178,8 @@ packages: engines: {node: '>=8'} dev: true - /minipass@7.0.4: - resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + /minipass@7.1.0: + resolution: {integrity: sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig==} engines: {node: '>=16 || 14 >=14.17'} dev: true @@ -10370,12 +10201,12 @@ packages: hasBin: true dev: true - /mlly@1.6.1: - resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} + /mlly@1.7.0: + resolution: {integrity: sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==} dependencies: acorn: 8.11.3 pathe: 1.1.2 - pkg-types: 1.0.3 + pkg-types: 1.1.0 ufo: 1.5.3 dev: true @@ -10383,6 +10214,11 @@ packages: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} dev: false + /mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + dev: true + /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: true @@ -10398,7 +10234,7 @@ packages: resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} dev: true - /nano-css@5.6.1(react-dom@18.2.0)(react@18.2.0): + /nano-css@5.6.1(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==} peerDependencies: react: '*' @@ -10409,11 +10245,11 @@ packages: csstype: 3.1.3 fastest-stable-stringify: 2.0.2 inline-style-prefixer: 7.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) rtl-css-js: 1.16.1 stacktrace-js: 2.0.2 - stylis: 4.3.1 + stylis: 4.3.2 dev: false /nanoid@3.3.7: @@ -10422,8 +10258,8 @@ packages: hasBin: true dev: true - /nanostores@0.10.0: - resolution: {integrity: sha512-Poy5+9wFXOD0jAstn4kv9n686U2BFw48z/W8lms8cS8lcbRz7BU20JxZ3e/kkKQVfRrkm4yLWCUA6GQINdvJCQ==} + /nanostores@0.10.3: + resolution: {integrity: sha512-Nii8O1XqmawqSCf9o2aWqVxhKRN01+iue9/VEd1TiJCr9VT5XxgPFbF1Edl1XN6pwJcZRsl8Ki+z01yb/T/C2g==} engines: {node: ^18.0.0 || >=20.0.0} dev: false @@ -10436,18 +10272,6 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true - /ndjson@2.0.0: - resolution: {integrity: sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==} - engines: {node: '>=10'} - hasBin: true - dependencies: - json-stringify-safe: 5.0.1 - minimist: 1.2.8 - readable-stream: 3.6.2 - split2: 3.2.2 - through2: 4.0.2 - dev: true - /nearley@2.20.1: resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} hasBin: true @@ -10494,16 +10318,6 @@ packages: dependencies: whatwg-url: 5.0.0 - /node-fetch@3.0.0-beta.9: - resolution: {integrity: sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==} - engines: {node: ^10.17 || >=12.3} - dependencies: - data-uri-to-buffer: 3.0.1 - fetch-blob: 2.1.2 - transitivePeerDependencies: - - domexception - dev: true - /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true @@ -10517,53 +10331,11 @@ packages: validate-npm-package-license: 3.0.4 dev: true - /normalize-package-data@6.0.0: - resolution: {integrity: sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - hosted-git-info: 7.0.1 - is-core-module: 2.13.1 - semver: 7.6.0 - validate-npm-package-license: 3.0.4 - dev: true - /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} dev: true - /npm-install-checks@6.3.0: - resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - semver: 7.6.0 - dev: true - - /npm-normalize-package-bin@3.0.1: - resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - - /npm-package-arg@11.0.1: - resolution: {integrity: sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - hosted-git-info: 7.0.1 - proc-log: 3.0.0 - semver: 7.6.0 - validate-npm-package-name: 5.0.0 - dev: true - - /npm-pick-manifest@9.0.0: - resolution: {integrity: sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - npm-install-checks: 6.3.0 - npm-normalize-package-bin: 3.0.1 - npm-package-arg: 11.0.1 - semver: 7.6.0 - dev: true - /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -10644,7 +10416,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-object-atoms: 1.0.0 dev: true @@ -10654,7 +10426,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 dev: true /object.hasown@1.1.4: @@ -10662,7 +10434,7 @@ packages: engines: {node: '>= 0.4'} dependencies: define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-object-atoms: 1.0.0 dev: true @@ -10732,20 +10504,20 @@ packages: fast-glob: 3.3.2 js-yaml: 4.1.0 supports-color: 9.4.0 - undici: 5.28.3 + undici: 5.28.4 yargs-parser: 21.1.1 dev: true - /optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + /optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} dependencies: - '@aashutoshrathi/word-wrap': 1.2.6 deep-is: 0.1.4 fast-levenshtein: 2.0.6 levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 + word-wrap: 1.2.5 dev: true /ora@5.4.1: @@ -10763,25 +10535,20 @@ packages: wcwidth: 1.0.1 dev: true - /overlayscrollbars-react@0.5.5(overlayscrollbars@2.6.1)(react@18.2.0): - resolution: {integrity: sha512-PakK1QEV/PAi4XniiTykcSeyoBmfDvgv2uBQ290IaY5ThrwvWg3Zk3Z39hosJYkyrS4mJ0zuIWtlHX4AKd2nZQ==} + /overlayscrollbars-react@0.5.6(overlayscrollbars@2.7.3)(react@18.3.1): + resolution: {integrity: sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==} peerDependencies: overlayscrollbars: ^2.0.0 react: '>=16.8.0' dependencies: - overlayscrollbars: 2.6.1 - react: 18.2.0 + overlayscrollbars: 2.7.3 + react: 18.3.1 dev: false - /overlayscrollbars@2.6.1: - resolution: {integrity: sha512-V+ZAqWMYMyGBJNRDEcdRC7Ch+WT9RBx9hY8bfJSMyFObQeJoecs1Vqg7ZAzBVcpN6sCUXFAZldCbeySwmmD0RA==} + /overlayscrollbars@2.7.3: + resolution: {integrity: sha512-HmNo8RPtuGUjBhUbVpZBHH7SHci5iSAdg5zSekCZVsjzaM6z8MIr3F9RXrzf4y7m+fOY0nx0+y0emr1fqQmfoA==} dev: false - /p-defer@1.0.0: - resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} - engines: {node: '>=4'} - dev: true - /p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -10831,14 +10598,6 @@ packages: aggregate-error: 3.1.0 dev: true - /p-memoize@4.0.1: - resolution: {integrity: sha512-km0sP12uE0dOZ5qP+s7kGVf07QngxyG0gS8sYFvFWhqlgzOsSy+m71aUejf/0akxj5W7gE//2G74qTv6b4iMog==} - engines: {node: '>=10'} - dependencies: - mem: 6.1.1 - mimic-fn: 3.1.0 - dev: true - /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -10868,13 +10627,6 @@ packages: engines: {node: '>=18'} dev: true - /parse-npm-tarball-url@3.0.0: - resolution: {integrity: sha512-InpdgIdNe5xWMEUcrVQUniQKwnggBtJ7+SCwh7zQAZwbbIYZV9XdgJyhtmDSSvykFyQXoe4BINnzKTfCwWLs5g==} - engines: {node: '>=8.15'} - dependencies: - semver: 6.3.1 - dev: true - /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -10912,19 +10664,12 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - /path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + /path-scurry@1.10.2: + resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} engines: {node: '>=16 || 14 >=14.17'} dependencies: - lru-cache: 10.2.0 - minipass: 7.0.4 - dev: true - - /path-temp@2.1.0: - resolution: {integrity: sha512-cMMJTAZlion/RWRRC48UbrDymEIt+/YSD/l8NqjneyDw2rDOBQcP5yRkMB4CYGn47KMhZvbblBP7Z79OsMw72w==} - engines: {node: '>=8.15'} - dependencies: - unique-string: 2.0.0 + lru-cache: 10.2.2 + minipass: 7.1.0 dev: true /path-to-regexp@0.1.7: @@ -10959,8 +10704,8 @@ packages: engines: {node: '>=8.6'} dev: true - /picomatch@4.0.1: - resolution: {integrity: sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==} + /picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} dev: true @@ -10995,11 +10740,11 @@ packages: find-up: 5.0.0 dev: true - /pkg-types@1.0.3: - resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + /pkg-types@1.1.0: + resolution: {integrity: sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==} dependencies: - jsonc-parser: 3.2.1 - mlly: 1.6.1 + confbox: 0.1.7 + mlly: 1.7.0 pathe: 1.1.2 dev: true @@ -11007,7 +10752,7 @@ packages: resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} engines: {node: '>=10'} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 dev: true /possible-typed-array-names@1.0.0: @@ -11050,7 +10795,7 @@ packages: dependencies: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 - react-is: 18.2.0 + react-is: 18.3.1 dev: true /pretty-hrtime@1.0.3: @@ -11065,11 +10810,6 @@ packages: parse-ms: 4.0.0 dev: true - /proc-log@3.0.0: - resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true - /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true @@ -11079,23 +10819,6 @@ packages: engines: {node: '>= 0.6.0'} dev: true - /promise-inflight@1.0.1: - resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} - peerDependencies: - bluebird: '*' - peerDependenciesMeta: - bluebird: - optional: true - dev: true - - /promise-retry@2.0.1: - resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} - engines: {node: '>=10'} - dependencies: - err-code: 2.0.3 - retry: 0.12.0 - dev: true - /prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -11157,8 +10880,8 @@ packages: side-channel: 1.0.6 dev: true - /qs@6.12.0: - resolution: {integrity: sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==} + /qs@6.12.1: + resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==} engines: {node: '>=0.6'} dependencies: side-channel: 1.0.6 @@ -11208,49 +10931,39 @@ packages: unpipe: 1.0.0 dev: true - /re-resizable@6.9.14(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-2UbPrpezMr6gkHKNCRA/N6QGGU237SKOZ78yMHId204A/oXWSAREAIuGZNQ9qlrJosewzcsv2CphZH3u7hC6ng==} - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /react-clientside-effect@1.2.6(react@18.2.0): + /react-clientside-effect@1.2.6(react@18.3.1): resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} peerDependencies: react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 dependencies: '@babel/runtime': 7.24.1 - react: 18.2.0 + react: 18.3.1 dev: false - /react-colorful@5.6.1(react-dom@18.2.0)(react@18.2.0): + /react-colorful@5.6.1(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) - /react-docgen-typescript@2.2.2(typescript@5.4.3): + /react-docgen-typescript@2.2.2(typescript@5.4.5): resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} peerDependencies: typescript: '>= 4.3.x' dependencies: - typescript: 5.4.3 + typescript: 5.4.5 dev: true /react-docgen@7.0.3: resolution: {integrity: sha512-i8aF1nyKInZnANZ4uZrH49qn1paRgBZ7wZiCNBMnenlPzEv0mRl+ShpTVEI6wZNl8sSc79xZkivtgLKQArcanQ==} engines: {node: '>=16.14.0'} dependencies: - '@babel/core': 7.24.3 - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 + '@babel/core': 7.24.5 + '@babel/traverse': 7.24.5 + '@babel/types': 7.24.5 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.5 '@types/doctrine': 0.0.9 @@ -11262,28 +10975,16 @@ packages: - supports-color dev: true - /react-dom@18.2.0(react@18.2.0): - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + /react-dom@18.3.1(react@18.3.1): + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: - react: ^18.2.0 + react: ^18.3.1 dependencies: loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.0 + react: 18.3.1 + scheduler: 0.23.2 - /react-draggable@4.4.6(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==} - peerDependencies: - react: '>= 16.3.0' - react-dom: '>= 16.3.0' - dependencies: - clsx: 1.2.1 - prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /react-dropzone@14.2.3(react@18.2.0): + /react-dropzone@14.2.3(react@18.3.1): resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==} engines: {node: '>= 10.13'} peerDependencies: @@ -11292,10 +10993,10 @@ packages: attr-accept: 2.2.2 file-selector: 0.6.0 prop-types: 15.8.1 - react: 18.2.0 + react: 18.3.1 dev: false - /react-element-to-jsx-string@15.0.0(react-dom@18.2.0)(react@18.2.0): + /react-element-to-jsx-string@15.0.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==} peerDependencies: react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 @@ -11303,25 +11004,25 @@ packages: dependencies: '@base2/pretty-print-object': 1.0.1 is-plain-object: 5.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) react-is: 18.1.0 dev: true - /react-error-boundary@4.0.13(react@18.2.0): + /react-error-boundary@4.0.13(react@18.3.1): resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==} peerDependencies: react: '>=16.13.1' dependencies: - '@babel/runtime': 7.24.1 - react: 18.2.0 + '@babel/runtime': 7.24.5 + react: 18.3.1 dev: false /react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false - /react-focus-lock@2.11.1(@types/react@18.2.73)(react@18.2.0): + /react-focus-lock@2.11.1(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-IXLwnTBrLTlKTpASZXqqXJ8oymWrgAlOfuuDYN4XCuN1YJ72dwX198UCaF1QqGUk5C3QOnlMik//n3ufcfe8Ig==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -11331,36 +11032,36 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.9 - '@types/react': 18.2.73 + '@types/react': 18.3.1 focus-lock: 1.3.3 prop-types: 15.8.1 - react: 18.2.0 - react-clientside-effect: 1.2.6(react@18.2.0) - use-callback-ref: 1.3.1(@types/react@18.2.73)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-clientside-effect: 1.2.6(react@18.3.1) + use-callback-ref: 1.3.1(@types/react@18.3.1)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.1)(react@18.3.1) dev: false - /react-hook-form@7.51.2(react@18.2.0): - resolution: {integrity: sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==} + /react-hook-form@7.51.4(react@18.3.1): + resolution: {integrity: sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==} engines: {node: '>=12.22.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /react-hotkeys-hook@4.5.0(react-dom@18.2.0)(react@18.2.0): + /react-hotkeys-hook@4.5.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==} peerDependencies: react: '>=16.8.1' react-dom: '>=16.8.1' dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /react-i18next@14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==} + /react-i18next@14.1.1(i18next@23.11.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-QSiKw+ihzJ/CIeIYWrarCmXJUySHDwQr5y8uaNIkbxoGRm/5DukkxZs+RPla79IKyyDPzC/DRlgQCABHtrQuQQ==} peerDependencies: i18next: '>= 23.2.3' react: '>= 16.8.0' @@ -11372,19 +11073,19 @@ packages: react-native: optional: true dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 html-parse-stringify: 3.0.1 - i18next: 23.10.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + i18next: 23.11.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /react-icons@5.0.1(react@18.2.0): - resolution: {integrity: sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==} + /react-icons@5.2.0(react@18.3.1): + resolution: {integrity: sha512-n52Y7Eb4MgQZHsSZOhSXv1zs2668/hBYKfSRIvKh42yExjyhZu0d1IK2CLLZ3BZB1oo13lDfwx2vOh2z9FTV6Q==} peerDependencies: react: '*' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false /react-is@16.13.1: @@ -11398,11 +11099,11 @@ packages: resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==} dev: true - /react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + /react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} dev: true - /react-konva@18.2.10(konva@9.3.6)(react-dom@18.2.0)(react@18.2.0): + /react-konva@18.2.10(konva@9.3.6)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==} peerDependencies: konva: ^8.0.1 || ^7.2.5 || ^9.0.0 @@ -11410,48 +11111,45 @@ packages: react-dom: '>=18.0.0' dependencies: '@types/react-reconciler': 0.28.8 - its-fine: 1.1.3(react@18.2.0) + its-fine: 1.2.5(react@18.3.1) konva: 9.3.6 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-reconciler: 0.29.0(react@18.2.0) - scheduler: 0.23.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-reconciler: 0.29.2(react@18.3.1) + scheduler: 0.23.2 dev: false - /react-reconciler@0.29.0(react@18.2.0): - resolution: {integrity: sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==} + /react-reconciler@0.29.2(react@18.3.1): + resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==} engines: {node: '>=0.10.0'} peerDependencies: - react: ^18.2.0 + react: ^18.3.1 dependencies: loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.0 + react: 18.3.1 + scheduler: 0.23.2 dev: false - /react-redux@9.1.0(@types/react@18.2.73)(react@18.2.0)(redux@5.0.1): - resolution: {integrity: sha512-6qoDzIO+gbrza8h3hjMA9aq4nwVFCKFtY2iLxCtVT38Swyy2C/dJCGBXHeHLtx6qlg/8qzc2MrhOeduf5K32wQ==} + /react-redux@9.1.2(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1): + resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==} peerDependencies: '@types/react': ^18.2.25 react: ^18.0 - react-native: '>=0.69' redux: ^5.0.0 peerDependenciesMeta: '@types/react': optional: true - react-native: - optional: true redux: optional: true dependencies: - '@types/react': 18.2.73 + '@types/react': 18.3.1 '@types/use-sync-external-store': 0.0.3 - react: 18.2.0 + react: 18.3.1 redux: 5.0.1 - use-sync-external-store: 1.2.0(react@18.2.0) + use-sync-external-store: 1.2.2(react@18.3.1) dev: false - /react-remove-scroll-bar@2.3.5(@types/react@18.2.73)(react@18.2.0): + /react-remove-scroll-bar@2.3.5(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==} engines: {node: '>=10'} peerDependencies: @@ -11461,13 +11159,13 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 - react: 18.2.0 - react-style-singleton: 2.2.1(@types/react@18.2.73)(react@18.2.0) + '@types/react': 18.3.1 + react: 18.3.1 + react-style-singleton: 2.2.1(@types/react@18.3.1)(react@18.3.1) tslib: 2.6.2 dev: false - /react-remove-scroll@2.5.7(@types/react@18.2.73)(react@18.2.0): + /react-remove-scroll@2.5.7(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} engines: {node: '>=10'} peerDependencies: @@ -11477,81 +11175,68 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 - react: 18.2.0 - react-remove-scroll-bar: 2.3.5(@types/react@18.2.73)(react@18.2.0) - react-style-singleton: 2.2.1(@types/react@18.2.73)(react@18.2.0) + '@types/react': 18.3.1 + react: 18.3.1 + react-remove-scroll-bar: 2.3.5(@types/react@18.3.1)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.1)(react@18.3.1) tslib: 2.6.2 - use-callback-ref: 1.3.1(@types/react@18.2.73)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.73)(react@18.2.0) + use-callback-ref: 1.3.1(@types/react@18.3.1)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.1)(react@18.3.1) dev: false - /react-resizable-panels@2.0.16(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-UrnxmTZaTnbCl/xIOX38ig35RicqGfLuqt2x5fytpNlQvCRuxyXZwIBEhmF+pmrEGxfajyXFBoCplNxLvhF0CQ==} + /react-resizable-panels@2.0.19(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-v3E41kfKSuCPIvJVb4nL4mIZjjKIn/gh6YqZF/gDfQDolv/8XnhJBek4EiV2gOr3hhc5A3kOGOayk3DhanpaQw==} peerDependencies: react: ^16.14.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /react-rnd@10.4.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-YjQAgEeSbNUoOXSD9ZBvIiLVizFb+bNhpDk8DbIRHA557NW02CXbwsAeOTpJQnsdhEL+NP2I+Ssrwejqcodtjg==} - peerDependencies: - react: '>=16.3.0' - react-dom: '>=16.3.0' - dependencies: - re-resizable: 6.9.14(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-draggable: 4.4.6(react-dom@18.2.0)(react@18.2.0) - tslib: 2.6.2 - dev: false - - /react-select@5.7.7(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): + /react-select@5.7.7(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@floating-ui/dom': 1.6.3 + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@floating-ui/dom': 1.6.5 '@types/react-transition-group': 4.4.10 memoize-one: 6.0.0 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) - use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1)(react@18.3.1) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /react-select@5.8.0(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): + /react-select@5.8.0(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.4(@types/react@18.2.73)(react@18.2.0) - '@floating-ui/dom': 1.6.3 + '@emotion/react': 11.11.4(@types/react@18.3.1)(react@18.3.1) + '@floating-ui/dom': 1.6.5 '@types/react-transition-group': 4.4.10 memoize-one: 6.0.0 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) - use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.73)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1)(react@18.3.1) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/react' dev: false - /react-style-singleton@2.2.1(@types/react@18.2.73)(react@18.2.0): + /react-style-singleton@2.2.1(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} peerDependencies: @@ -11561,38 +11246,38 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 + '@types/react': 18.3.1 get-nonce: 1.0.1 invariant: 2.2.4 - react: 18.2.0 + react: 18.3.1 tslib: 2.6.2 dev: false - /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + /react-transition-group@4.4.5(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: react: '>=16.6.0' react-dom: '>=16.6.0' dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /react-universal-interface@0.6.2(react@18.2.0)(tslib@2.6.2): + /react-universal-interface@0.6.2(react@18.3.1)(tslib@2.6.2): resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} peerDependencies: react: '*' tslib: '*' dependencies: - react: 18.2.0 + react: 18.3.1 tslib: 2.6.2 dev: false - /react-use@17.5.0(react-dom@18.2.0)(react@18.2.0): + /react-use@17.5.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==} peerDependencies: react: '*' @@ -11604,10 +11289,10 @@ packages: fast-deep-equal: 3.1.3 fast-shallow-equal: 1.0.0 js-cookie: 2.2.1 - nano-css: 5.6.1(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-universal-interface: 0.6.2(react@18.2.0)(tslib@2.6.2) + nano-css: 5.6.1(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-universal-interface: 0.6.2(react@18.3.1)(tslib@2.6.2) resize-observer-polyfill: 1.5.1 screenfull: 5.2.0 set-harmonic-interval: 1.0.1 @@ -11616,50 +11301,42 @@ packages: tslib: 2.6.2 dev: false - /react-virtuoso@4.7.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-sYRQ1dHGiLCA/4ngq86U4fjO5SubEbbR53+mmcgcQZjzTK2E+9M300C3nXr54Zgr1ewZfdr9SKt6wpha0CsYUQ==} + /react-virtuoso@4.7.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-l+fnBf/G1Fp6pHCnhFq2Ra4lkZtT6c5XrS9rCS0OA6de7WGLZviCo0y61CUZZG79TeAw3L7O4czeNPiqh9CIrg==} engines: {node: '>=10'} peerDependencies: react: '>=16 || >=17 || >= 18' react-dom: '>=16 || >=17 || >= 18' dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /react@18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + /react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - /reactflow@11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-0CApYhtYicXEDg/x2kvUHiUk26Qur8lAtTtiSlptNKuyEuGti6P1y5cS32YGaUoDMoCqkm/m+jcKkfMOvSCVRA==} + /reactflow@11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-wusd1Xpn1wgsSEv7UIa4NNraCwH9syBtubBy4xVNXg3b+CDKM+sFaF3hnMx0tr0et4km9urIDdNvwm34QiZong==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/background': 11.3.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/controls': 11.2.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/core': 11.10.4(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/minimap': 11.7.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-resizer': 2.2.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-toolbar': 1.3.9(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@reactflow/background': 11.3.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@reactflow/controls': 11.2.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@reactflow/core': 11.11.3(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@reactflow/minimap': 11.7.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@reactflow/node-resizer': 2.2.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + '@reactflow/node-toolbar': 1.3.13(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@types/react' - immer dev: false - /read-package-json-fast@3.0.2: - resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - json-parse-even-better-errors: 3.0.1 - npm-normalize-package-bin: 3.0.1 - dev: true - /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -11760,10 +11437,10 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 get-intrinsic: 1.2.4 - globalthis: 1.0.3 + globalthis: 1.0.4 which-builtin-type: 1.1.3 dev: true @@ -11784,7 +11461,7 @@ packages: /regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 dev: true /regexp.prototype.flags@1.5.2: @@ -11837,14 +11514,6 @@ packages: unist-util-visit: 5.0.0 dev: true - /rename-overwrite@5.0.0: - resolution: {integrity: sha512-vSxE5Ww7Jnyotvaxi3Dj0vOMoojH8KMkBfs9xYeW/qNfJiLTcC1fmwTjrbGUq3mQSOCxkG0DbdcvwTUrpvBN4w==} - engines: {node: '>=12.10'} - dependencies: - '@zkochan/rimraf': 2.1.3 - fs-extra: 10.1.0 - dev: true - /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -11914,11 +11583,6 @@ packages: engines: {node: '>=0.12'} dev: false - /retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - dev: true - /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -11979,34 +11643,36 @@ packages: fsevents: 2.3.3 dev: true - /rollup@4.13.1: - resolution: {integrity: sha512-hFi+fU132IvJ2ZuihN56dwgpltpmLZHZWsx27rMCTZ2sYwrqlgL5sECGy1eeV2lAihD8EzChBVVhsXci0wD4Tg==} + /rollup@4.17.2: + resolution: {integrity: sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.13.1 - '@rollup/rollup-android-arm64': 4.13.1 - '@rollup/rollup-darwin-arm64': 4.13.1 - '@rollup/rollup-darwin-x64': 4.13.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.13.1 - '@rollup/rollup-linux-arm64-gnu': 4.13.1 - '@rollup/rollup-linux-arm64-musl': 4.13.1 - '@rollup/rollup-linux-riscv64-gnu': 4.13.1 - '@rollup/rollup-linux-s390x-gnu': 4.13.1 - '@rollup/rollup-linux-x64-gnu': 4.13.1 - '@rollup/rollup-linux-x64-musl': 4.13.1 - '@rollup/rollup-win32-arm64-msvc': 4.13.1 - '@rollup/rollup-win32-ia32-msvc': 4.13.1 - '@rollup/rollup-win32-x64-msvc': 4.13.1 + '@rollup/rollup-android-arm-eabi': 4.17.2 + '@rollup/rollup-android-arm64': 4.17.2 + '@rollup/rollup-darwin-arm64': 4.17.2 + '@rollup/rollup-darwin-x64': 4.17.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.17.2 + '@rollup/rollup-linux-arm-musleabihf': 4.17.2 + '@rollup/rollup-linux-arm64-gnu': 4.17.2 + '@rollup/rollup-linux-arm64-musl': 4.17.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.17.2 + '@rollup/rollup-linux-riscv64-gnu': 4.17.2 + '@rollup/rollup-linux-s390x-gnu': 4.17.2 + '@rollup/rollup-linux-x64-gnu': 4.17.2 + '@rollup/rollup-linux-x64-musl': 4.17.2 + '@rollup/rollup-win32-arm64-msvc': 4.17.2 + '@rollup/rollup-win32-ia32-msvc': 4.17.2 + '@rollup/rollup-win32-x64-msvc': 4.17.2 fsevents: 2.3.3 dev: true /rtl-css-js@1.16.1: resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} dependencies: - '@babel/runtime': 7.24.1 + '@babel/runtime': 7.24.5 dev: false /run-parallel@1.2.0: @@ -12057,8 +11723,8 @@ packages: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true - /scheduler@0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + /scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} dependencies: loose-envify: 1.4.0 @@ -12218,6 +11884,15 @@ packages: engines: {node: '>=14'} dev: true + /sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.25 + mrmime: 2.0.0 + totalist: 3.0.1 + dev: true + /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -12236,7 +11911,7 @@ packages: resolution: {integrity: sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==} engines: {node: '>=10.0.0'} dependencies: - '@socket.io/component-emitter': 3.1.0 + '@socket.io/component-emitter': 3.1.2 debug: 4.3.4 engine.io-client: 6.5.3 socket.io-parser: 4.2.4 @@ -12250,7 +11925,7 @@ packages: resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} engines: {node: '>=10.0.0'} dependencies: - '@socket.io/component-emitter': 3.1.0 + '@socket.io/component-emitter': 3.1.2 debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -12322,23 +11997,10 @@ packages: engines: {node: '>=12'} dev: false - /split2@3.2.2: - resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} - dependencies: - readable-stream: 3.6.2 - dev: true - /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true - /ssri@10.0.5: - resolution: {integrity: sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - minipass: 7.0.4 - dev: true - /stack-generator@2.0.10: resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} dependencies: @@ -12388,11 +12050,11 @@ packages: resolution: {integrity: sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==} dev: true - /storybook@8.0.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-FUr3Uc2dSAQ80jINH5fSXz7zD7Ncn08OthROjwRtHAH+jMf4wxyZ+RhF3heFy9xLot2/HXOLIWyHyzZZMtGhxg==} + /storybook@8.0.10(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-9/4oxISopLyr5xz7Du27mmQgcIfB7UTLlNzkK4IklWTiSgsOgYgZpsmIwymoXNtkrvh+QsqskdcUP1C7nNiEtw==} hasBin: true dependencies: - '@storybook/cli': 8.0.4(react-dom@18.2.0)(react@18.2.0) + '@storybook/cli': 8.0.10(react-dom@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@babel/preset-env' - bufferutil @@ -12440,7 +12102,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-errors: 1.3.0 es-object-atoms: 1.0.0 get-intrinsic: 1.2.4 @@ -12458,7 +12120,7 @@ packages: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.23.2 + es-abstract: 1.23.3 es-object-atoms: 1.0.0 dev: true @@ -12510,11 +12172,6 @@ packages: engines: {node: '>=4'} dev: true - /strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - dev: true - /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -12549,18 +12206,18 @@ packages: engines: {node: '>=14.16'} dev: true - /strip-literal@2.0.0: - resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} + /strip-literal@2.1.0: + resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} dependencies: - js-tokens: 8.0.3 + js-tokens: 9.0.0 dev: true /stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} dev: false - /stylis@4.3.1: - resolution: {integrity: sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==} + /stylis@4.3.2: + resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==} dev: false /summary@2.1.0: @@ -12661,6 +12318,15 @@ packages: unique-string: 2.0.0 dev: true + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -12677,21 +12343,15 @@ packages: xtend: 4.0.2 dev: true - /through2@4.0.2: - resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} - dependencies: - readable-stream: 3.6.2 - dev: true - /tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - /tinybench@2.6.0: - resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + /tinybench@2.8.0: + resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} dev: true - /tinypool@0.8.3: - resolution: {integrity: sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==} + /tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} engines: {node: '>=14.0.0'} dev: true @@ -12727,8 +12387,8 @@ packages: to-no-case: 1.0.2 dev: true - /tocbot@4.25.0: - resolution: {integrity: sha512-kE5wyCQJ40hqUaRVkyQ4z5+4juzYsv/eK+aqD97N62YH0TxFhzJvo22RUQQZdO3YnXAk42ZOfOpjVdy+Z0YokA==} + /tocbot@4.27.19: + resolution: {integrity: sha512-0yu8k0L3gCQ1OVNZnKqpbZp+kLd6qtlNEBxsb+e0G/bS0EXMl2tWqWi1Oy9knRX8rTPYfOxd/sI/OzAj3JowGg==} dev: true /toggle-selection@1.0.6: @@ -12740,6 +12400,11 @@ packages: engines: {node: '>=0.6'} dev: true + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: true + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -12748,13 +12413,13 @@ packages: hasBin: true dev: true - /ts-api-utils@1.3.0(typescript@5.4.3): + /ts-api-utils@1.3.0(typescript@5.4.5): resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.4.3 + typescript: 5.4.5 dev: true /ts-dedent@2.2.0: @@ -12778,7 +12443,7 @@ packages: resolution: {integrity: sha512-gzkapsdbMNwBnTIjgO758GujLCj031IgHK/PKr2mrmkCSJMhSOR5FeOuSxKLMUoYc0vAA4RGEYYbjt/v6afD3g==} dev: true - /tsconfck@3.0.3(typescript@5.4.3): + /tsconfck@3.0.3(typescript@5.4.5): resolution: {integrity: sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==} engines: {node: ^18 || >=20} hasBin: true @@ -12788,7 +12453,7 @@ packages: typescript: optional: true dependencies: - typescript: 5.4.3 + typescript: 5.4.5 dev: true /tsconfig-paths@3.15.0: @@ -12820,14 +12485,14 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - /tsutils@3.21.0(typescript@5.4.3): + /tsutils@3.21.0(typescript@5.4.5): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.4.3 + typescript: 5.4.5 dev: true /type-check@0.4.0: @@ -12924,8 +12589,8 @@ packages: hasBin: true dev: true - /typescript@5.4.3: - resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==} + /typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -12955,8 +12620,8 @@ packages: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: true - /undici@5.28.3: - resolution: {integrity: sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==} + /undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} dependencies: '@fastify/busboy': 2.1.1 @@ -13034,8 +12699,8 @@ packages: engines: {node: '>= 0.8'} dev: true - /unplugin@1.10.0: - resolution: {integrity: sha512-CuZtvvO8ua2Wl+9q2jEaqH6m3DoQ38N7pvBYQbbaeNlWGvK2l6GHiKi29aIHDPoSxdUzQ7Unevf1/ugil5X6Pg==} + /unplugin@1.10.1: + resolution: {integrity: sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg==} engines: {node: '>=14.0.0'} dependencies: acorn: 8.11.3 @@ -13049,8 +12714,8 @@ packages: engines: {node: '>=8'} dev: true - /update-browserslist-db@1.0.13(browserslist@4.23.0): - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + /update-browserslist-db@1.0.15(browserslist@4.23.0): + resolution: {integrity: sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -13066,7 +12731,7 @@ packages: punycode: 2.3.1 dev: true - /use-callback-ref@1.3.1(@types/react@18.2.73)(react@18.2.0): + /use-callback-ref@1.3.1(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==} engines: {node: '>=10'} peerDependencies: @@ -13076,39 +12741,39 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 - react: 18.2.0 + '@types/react': 18.3.1 + react: 18.3.1 tslib: 2.6.2 dev: false - /use-debounce@10.0.0(react@18.2.0): + /use-debounce@10.0.0(react@18.3.1): resolution: {integrity: sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==} engines: {node: '>= 16.0.0'} peerDependencies: react: '>=16.8.0' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /use-device-pixel-ratio@1.1.2(react@18.2.0): + /use-device-pixel-ratio@1.1.2(react@18.3.1): resolution: {integrity: sha512-nFxV0HwLdRUt20kvIgqHYZe6PK/v4mU1X8/eLsT1ti5ck0l2ob0HDRziaJPx+YWzBo6dMm4cTac3mcyk68Gh+A==} peerDependencies: react: '>=16.8.0' dependencies: - react: 18.2.0 + react: 18.3.1 dev: false - /use-image@1.1.1(react-dom@18.2.0)(react@18.2.0): + /use-image@1.1.1(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-n4YO2k8AJG/BcDtxmBx8Aa+47kxY5m335dJiCQA5tTeVU4XdhrhqR6wT0WISRXwdMEOv5CSjqekDZkEMiiWaYQ==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) dev: false - /use-isomorphic-layout-effect@1.1.2(@types/react@18.2.73)(react@18.2.0): + /use-isomorphic-layout-effect@1.1.2(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} peerDependencies: '@types/react': '*' @@ -13117,11 +12782,11 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 - react: 18.2.0 + '@types/react': 18.3.1 + react: 18.3.1 dev: false - /use-sidecar@1.1.2(@types/react@18.2.73)(react@18.2.0): + /use-sidecar@1.1.2(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} peerDependencies: @@ -13131,18 +12796,26 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.73 + '@types/react': 18.3.1 detect-node-es: 1.1.0 - react: 18.2.0 + react: 18.3.1 tslib: 2.6.2 dev: false - /use-sync-external-store@1.2.0(react@18.2.0): + /use-sync-external-store@1.2.0(react@18.3.1): resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - react: 18.2.0 + react: 18.3.1 + dev: false + + /use-sync-external-store@1.2.2(react@18.3.1): + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.3.1 dev: false /util-deprecate@1.0.2: @@ -13175,20 +12848,6 @@ packages: spdx-expression-parse: 3.0.1 dev: true - /validate-npm-package-name@4.0.0: - resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dependencies: - builtins: 5.0.1 - dev: true - - /validate-npm-package-name@5.0.0: - resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - builtins: 5.0.1 - dev: true - /validator@13.11.0: resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} engines: {node: '>= 0.10'} @@ -13199,15 +12858,8 @@ packages: engines: {node: '>= 0.8'} dev: true - /version-selector-type@3.0.0: - resolution: {integrity: sha512-PSvMIZS7C1MuVNBXl/CDG2pZq8EXy/NW2dHIdm3bVP5N0PC8utDK8ttXLXj44Gn3J0lQE3U7Mpm1estAOd+eiA==} - engines: {node: '>=10.13'} - dependencies: - semver: 7.6.0 - dev: true - - /vite-node@1.4.0(@types/node@20.11.30): - resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} + /vite-node@1.6.0(@types/node@20.12.10): + resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: @@ -13215,7 +12867,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.2.6(@types/node@20.11.30) + vite: 5.2.11(@types/node@20.12.10) transitivePeerDependencies: - '@types/node' - less @@ -13227,16 +12879,16 @@ packages: - terser dev: true - /vite-plugin-css-injected-by-js@3.5.0(vite@5.2.6): - resolution: {integrity: sha512-d0QaHH9kS93J25SwRqJNEfE29PSuQS5jn51y9N9i2Yoq0FRO7rjuTeLvjM5zwklZlRrIn6SUdtOEDKyHokgJZg==} + /vite-plugin-css-injected-by-js@3.5.1(vite@5.2.11): + resolution: {integrity: sha512-9ioqwDuEBxW55gNoWFEDhfLTrVKXEEZgl5adhWmmqa88EQGKfTmexy4v1Rh0pAS6RhKQs2bUYQArprB32JpUZQ==} peerDependencies: vite: '>2.0.0-0' dependencies: - vite: 5.2.6(@types/node@20.11.30) + vite: 5.2.11(@types/node@20.12.10) dev: true - /vite-plugin-dts@3.8.0(@types/node@20.11.30)(typescript@5.4.3)(vite@5.2.6): - resolution: {integrity: sha512-wt9ST1MwS5lkxHtA3M30+lSA3TO8RnaUu3YUPmGgY1iKm+vWZmB7KBss6qspyUlto9ynLNHYG2eJ09d2Q4/7Qg==} + /vite-plugin-dts@3.9.1(@types/node@20.12.10)(typescript@5.4.5)(vite@5.2.11): + resolution: {integrity: sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: typescript: '*' @@ -13245,35 +12897,35 @@ packages: vite: optional: true dependencies: - '@microsoft/api-extractor': 7.43.0(@types/node@20.11.30) + '@microsoft/api-extractor': 7.43.0(@types/node@20.12.10) '@rollup/pluginutils': 5.1.0 - '@vue/language-core': 1.8.27(typescript@5.4.3) + '@vue/language-core': 1.8.27(typescript@5.4.5) debug: 4.3.4 kolorist: 1.8.0 - magic-string: 0.30.8 - typescript: 5.4.3 - vite: 5.2.6(@types/node@20.11.30) - vue-tsc: 1.8.27(typescript@5.4.3) + magic-string: 0.30.10 + typescript: 5.4.5 + vite: 5.2.11(@types/node@20.12.10) + vue-tsc: 1.8.27(typescript@5.4.5) transitivePeerDependencies: - '@types/node' - rollup - supports-color dev: true - /vite-plugin-eslint@1.8.1(eslint@8.57.0)(vite@5.2.6): + /vite-plugin-eslint@1.8.1(eslint@8.57.0)(vite@5.2.11): resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} peerDependencies: eslint: '>=7' vite: '>=2' dependencies: '@rollup/pluginutils': 4.2.1 - '@types/eslint': 8.56.6 + '@types/eslint': 8.56.10 eslint: 8.57.0 rollup: 2.79.1 - vite: 5.2.6(@types/node@20.11.30) + vite: 5.2.11(@types/node@20.12.10) dev: true - /vite-tsconfig-paths@4.3.2(typescript@5.4.3)(vite@5.2.6): + /vite-tsconfig-paths@4.3.2(typescript@5.4.5)(vite@5.2.11): resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} peerDependencies: vite: '*' @@ -13283,15 +12935,15 @@ packages: dependencies: debug: 4.3.4 globrex: 0.1.2 - tsconfck: 3.0.3(typescript@5.4.3) - vite: 5.2.6(@types/node@20.11.30) + tsconfck: 3.0.3(typescript@5.4.5) + vite: 5.2.11(@types/node@20.12.10) transitivePeerDependencies: - supports-color - typescript dev: true - /vite@5.2.6(@types/node@20.11.30): - resolution: {integrity: sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==} + /vite@5.2.11(@types/node@20.12.10): + resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -13318,23 +12970,23 @@ packages: terser: optional: true dependencies: - '@types/node': 20.11.30 + '@types/node': 20.12.10 esbuild: 0.20.2 postcss: 8.4.38 - rollup: 4.13.1 + rollup: 4.17.2 optionalDependencies: fsevents: 2.3.3 dev: true - /vitest@1.4.0(@types/node@20.11.30): - resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} + /vitest@1.6.0(@types/node@20.12.10)(@vitest/ui@1.6.0): + resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.4.0 - '@vitest/ui': 1.4.0 + '@vitest/browser': 1.6.0 + '@vitest/ui': 1.6.0 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -13351,26 +13003,27 @@ packages: jsdom: optional: true dependencies: - '@types/node': 20.11.30 - '@vitest/expect': 1.4.0 - '@vitest/runner': 1.4.0 - '@vitest/snapshot': 1.4.0 - '@vitest/spy': 1.4.0 - '@vitest/utils': 1.4.0 + '@types/node': 20.12.10 + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/ui': 1.6.0(vitest@1.6.0) + '@vitest/utils': 1.6.0 acorn-walk: 8.3.2 chai: 4.4.1 debug: 4.3.4 execa: 8.0.1 local-pkg: 0.5.0 - magic-string: 0.30.8 + magic-string: 0.30.10 pathe: 1.1.2 picocolors: 1.0.0 std-env: 3.7.0 - strip-literal: 2.0.0 - tinybench: 2.6.0 - tinypool: 0.8.3 - vite: 5.2.6(@types/node@20.11.30) - vite-node: 1.4.0(@types/node@20.11.30) + strip-literal: 2.1.0 + tinybench: 2.8.0 + tinypool: 0.8.4 + vite: 5.2.11(@types/node@20.12.10) + vite-node: 1.6.0(@types/node@20.12.10) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -13398,16 +13051,16 @@ packages: he: 1.2.0 dev: true - /vue-tsc@1.8.27(typescript@5.4.3): + /vue-tsc@1.8.27(typescript@5.4.5): resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} hasBin: true peerDependencies: typescript: '*' dependencies: '@volar/typescript': 1.11.1 - '@vue/language-core': 1.8.27(typescript@5.4.3) + '@vue/language-core': 1.8.27(typescript@5.4.5) semver: 7.6.0 - typescript: 5.4.3 + typescript: 5.4.5 dev: true /watchpack@2.4.1: @@ -13499,14 +13152,6 @@ packages: isexe: 2.0.0 dev: true - /which@4.0.0: - resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} - engines: {node: ^16.13.0 || >=18.0.0} - hasBin: true - dependencies: - isexe: 3.1.1 - dev: true - /why-is-node-running@2.2.2: resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} engines: {node: '>=8'} @@ -13516,6 +13161,11 @@ packages: stackback: 0.0.2 dev: true + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + dev: true + /wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} dev: true @@ -13563,8 +13213,8 @@ packages: optional: true dev: false - /ws@8.16.0: - resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + /ws@8.17.0: + resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -13644,18 +13294,18 @@ packages: commander: 9.5.0 dev: true - /zod-validation-error@3.0.3(zod@3.22.4): - resolution: {integrity: sha512-cETTrcMq3Ze58vhdR0zD37uJm/694I6mAxcf/ei5bl89cC++fBNxrC2z8lkFze/8hVMPwrbtrwXHR2LB50fpHw==} + /zod-validation-error@3.2.0(zod@3.23.6): + resolution: {integrity: sha512-cYlPR6zuyrgmu2wRTdumEAJGuwI7eHVHGT+VyneAQxmRAKtGRL1/7pjz4wfLhz4J05f5qoSZc3rGacswgyTjjw==} engines: {node: '>=18.0.0'} peerDependencies: zod: ^3.18.0 dependencies: - zod: 3.22.4 + zod: 3.23.6 - /zod@3.22.4: - resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + /zod@3.23.6: + resolution: {integrity: sha512-RTHJlZhsRbuA8Hmp/iNL7jnfc4nZishjsanDAfEY1QpDQZCahUp3xDzl+zfweE9BklxMUcgBgS1b7Lvie/ZVwA==} - /zustand@4.5.2(@types/react@18.2.73)(react@18.2.0): + /zustand@4.5.2(@types/react@18.3.1)(react@18.3.1): resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==} engines: {node: '>=12.7.0'} peerDependencies: @@ -13670,7 +13320,7 @@ packages: react: optional: true dependencies: - '@types/react': 18.2.73 - react: 18.2.0 - use-sync-external-store: 1.2.0(react@18.2.0) + '@types/react': 18.3.1 + react: 18.3.1 + use-sync-external-store: 1.2.0(react@18.3.1) dev: false diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index 0a104c083b..1db283aabd 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -76,7 +76,9 @@ "aboutHeading": "Nutzen Sie Ihre kreative Energie", "toResolve": "Lösen", "add": "Hinzufügen", - "loglevel": "Protokoll Stufe" + "loglevel": "Protokoll Stufe", + "selected": "Ausgewählt", + "beta": "Beta" }, "gallery": { "galleryImageSize": "Bildgröße", @@ -86,7 +88,7 @@ "noImagesInGallery": "Keine Bilder in der Galerie", "loading": "Lade", "deleteImage_one": "Lösche Bild", - "deleteImage_other": "", + "deleteImage_other": "Lösche {{count}} Bilder", "copy": "Kopieren", "download": "Runterladen", "setCurrentImage": "Setze aktuelle Bild", @@ -397,7 +399,14 @@ "cancel": "Stornieren", "defaultSettingsSaved": "Standardeinstellungen gespeichert", "addModels": "Model hinzufügen", - "deleteModelImage": "Lösche Model Bild" + "deleteModelImage": "Lösche Model Bild", + "hfTokenInvalidErrorMessage": "Falscher oder fehlender HuggingFace Schlüssel.", + "huggingFaceRepoID": "HuggingFace Repo ID", + "hfToken": "HuggingFace Schlüssel", + "hfTokenInvalid": "Falscher oder fehlender HF Schlüssel", + "huggingFacePlaceholder": "besitzer/model-name", + "hfTokenSaved": "HF Schlüssel gespeichert", + "hfTokenUnableToVerify": "Konnte den HF Schlüssel nicht validieren" }, "parameters": { "images": "Bilder", @@ -686,7 +695,11 @@ "hands": "Hände", "dwOpenpose": "DW Openpose", "dwOpenposeDescription": "Posenschätzung mit DW Openpose", - "selectCLIPVisionModel": "Wähle ein CLIP Vision Model aus" + "selectCLIPVisionModel": "Wähle ein CLIP Vision Model aus", + "ipAdapterMethod": "Methode", + "composition": "Nur Komposition", + "full": "Voll", + "style": "Nur Style" }, "queue": { "status": "Status", @@ -717,7 +730,6 @@ "resume": "Wieder aufnehmen", "item": "Auftrag", "notReady": "Warteschlange noch nicht bereit", - "queueCountPrediction": "{{promptsCount}} Prompts × {{iterations}} Iterationen -> {{count}} Generationen", "clearQueueAlertDialog": "\"Die Warteschlange leeren\" stoppt den aktuellen Prozess und leert die Warteschlange komplett.", "completedIn": "Fertig in", "cancelBatchSucceeded": "Stapel abgebrochen", diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 83e80e8a81..306151984a 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2,6 +2,7 @@ "accessibility": { "about": "About", "createIssue": "Create Issue", + "submitSupportTicket": "Submit Support Ticket", "invokeProgressBar": "Invoke progress bar", "menu": "Menu", "mode": "Mode", @@ -142,9 +143,15 @@ "blue": "Blue", "alpha": "Alpha", "selected": "Selected", - "viewer": "Viewer", "tab": "Tab", - "close": "Close" + "viewing": "Viewing", + "viewingDesc": "Review images in a large gallery view", + "editing": "Editing", + "editingDesc": "Edit on the Control Layers canvas", + "comparing": "Comparing", + "comparingDesc": "Comparing two images", + "enabled": "Enabled", + "disabled": "Disabled" }, "controlnet": { "controlAdapter_one": "Control Adapter", @@ -259,7 +266,6 @@ "queue": "Queue", "queueFront": "Add to Front of Queue", "queueBack": "Add to Queue", - "queueCountPrediction": "{{promptsCount}} prompts \u00d7 {{iterations}} iterations -> {{count}} generations", "queueEmpty": "Queue Empty", "enqueueing": "Queueing Batch", "resume": "Resume", @@ -312,7 +318,13 @@ "batchFailedToQueue": "Failed to Queue Batch", "graphQueued": "Graph queued", "graphFailedToQueue": "Failed to queue graph", - "openQueue": "Open Queue" + "openQueue": "Open Queue", + "prompts_one": "Prompt", + "prompts_other": "Prompts", + "iterations_one": "Iteration", + "iterations_other": "Iterations", + "generations_one": "Generation", + "generations_other": "Generations" }, "invocationCache": { "invocationCache": "Invocation Cache", @@ -366,9 +378,22 @@ "bulkDownloadFailed": "Download Failed", "problemDeletingImages": "Problem Deleting Images", "problemDeletingImagesDesc": "One or more images could not be deleted", - "switchTo": "Switch to {{ tab }} (Z)", - "openFloatingViewer": "Open Floating Viewer", - "closeFloatingViewer": "Close Floating Viewer" + "viewerImage": "Viewer Image", + "compareImage": "Compare Image", + "openInViewer": "Open in Viewer", + "selectForCompare": "Select for Compare", + "selectAnImageToCompare": "Select an Image to Compare", + "slider": "Slider", + "sideBySide": "Side-by-Side", + "hover": "Hover", + "swapImages": "Swap Images", + "compareOptions": "Comparison Options", + "stretchToFit": "Stretch to Fit", + "exitCompare": "Exit Compare", + "compareHelp1": "Hold Alt while clicking a gallery image or using the arrow keys to change the compare image.", + "compareHelp2": "Press M to cycle through comparison modes.", + "compareHelp3": "Press C to swap the compared images.", + "compareHelp4": "Press Z or Esc to exit." }, "hotkeys": { "searchHotkeys": "Search Hotkeys", @@ -770,10 +795,15 @@ "cannotConnectOutputToOutput": "Cannot connect output to output", "cannotConnectToSelf": "Cannot connect to self", "cannotDuplicateConnection": "Cannot create duplicate connections", + "cannotMixAndMatchCollectionItemTypes": "Cannot mix and match collection item types", + "missingNode": "Missing invocation node", + "missingInvocationTemplate": "Missing invocation template", + "missingFieldTemplate": "Missing field template", "nodePack": "Node pack", "collection": "Collection", - "collectionFieldType": "{{name}} Collection", - "collectionOrScalarFieldType": "{{name}} Collection|Scalar", + "singleFieldType": "{{name}} (Single)", + "collectionFieldType": "{{name}} (Collection)", + "collectionOrScalarFieldType": "{{name}} (Single or Collection)", "colorCodeEdges": "Color-Code Edges", "colorCodeEdgesHelp": "Color-code edges according to their connected fields", "connectionWouldCreateCycle": "Connection would create a cycle", @@ -875,6 +905,7 @@ "versionUnknown": " Version Unknown", "workflow": "Workflow", "graph": "Graph", + "noGraph": "No Graph", "workflowAuthor": "Author", "workflowContact": "Contact", "workflowDescription": "Short Description", @@ -887,7 +918,10 @@ "zoomInNodes": "Zoom In", "zoomOutNodes": "Zoom Out", "betaDesc": "This invocation is in beta. Until it is stable, it may have breaking changes during app updates. We plan to support this invocation long-term.", - "prototypeDesc": "This invocation is a prototype. It may have breaking changes during app updates and may be removed at any time." + "prototypeDesc": "This invocation is a prototype. It may have breaking changes during app updates and may be removed at any time.", + "imageAccessError": "Unable to find image {{image_name}}, resetting to default", + "boardAccessError": "Unable to find board {{board_id}}, resetting to default", + "modelAccessError": "Unable to find model {{key}}, resetting to default" }, "parameters": { "aspect": "Aspect", @@ -935,17 +969,30 @@ "noModelSelected": "No model selected", "noPrompts": "No prompts generated", "noNodesInGraph": "No nodes in graph", - "systemDisconnected": "System disconnected" + "systemDisconnected": "System disconnected", + "layer": { + "initialImageNoImageSelected": "no initial image selected", + "controlAdapterNoModelSelected": "no Control Adapter model selected", + "controlAdapterIncompatibleBaseModel": "incompatible Control Adapter base model", + "controlAdapterNoImageSelected": "no Control Adapter image selected", + "controlAdapterImageNotProcessed": "Control Adapter image not processed", + "t2iAdapterIncompatibleDimensions": "T2I Adapter requires image dimension to be multiples of {{multiple}}", + "ipAdapterNoModelSelected": "no IP adapter selected", + "ipAdapterIncompatibleBaseModel": "incompatible IP Adapter base model", + "ipAdapterNoImageSelected": "no IP Adapter image selected", + "rgNoPromptsOrIPAdapters": "no text prompts or IP Adapters", + "rgNoRegion": "no region selected" + } }, "maskBlur": "Mask Blur", "negativePromptPlaceholder": "Negative Prompt", + "globalNegativePromptPlaceholder": "Global Negative Prompt", "noiseThreshold": "Noise Threshold", "patchmatchDownScaleSize": "Downscale", "perlinNoise": "Perlin Noise", "positivePromptPlaceholder": "Positive Prompt", + "globalPositivePromptPlaceholder": "Global Positive Prompt", "iterations": "Iterations", - "iterationsWithCount_one": "{{count}} Iteration", - "iterationsWithCount_other": "{{count}} Iterations", "scale": "Scale", "scaleBeforeProcessing": "Scale Before Processing", "scaledHeight": "Scaled H", @@ -1047,8 +1094,9 @@ }, "toast": { "addedToBoard": "Added to board", - "baseModelChangedCleared_one": "Base model changed, cleared or disabled {{count}} incompatible submodel", - "baseModelChangedCleared_other": "Base model changed, cleared or disabled {{count}} incompatible submodels", + "baseModelChanged": "Base Model Changed", + "baseModelChangedCleared_one": "Cleared or disabled {{count}} incompatible submodel", + "baseModelChangedCleared_other": "Cleared or disabled {{count}} incompatible submodels", "canceled": "Processing Canceled", "canvasCopiedClipboard": "Canvas Copied to Clipboard", "canvasDownloaded": "Canvas Downloaded", @@ -1069,10 +1117,17 @@ "metadataLoadFailed": "Failed to load metadata", "modelAddedSimple": "Model Added to Queue", "modelImportCanceled": "Model Import Canceled", + "outOfMemoryError": "Out of Memory Error", + "outOfMemoryErrorDesc": "Your current generation settings exceed system capacity. Please adjust your settings and try again.", "parameters": "Parameters", - "parameterNotSet": "{{parameter}} not set", - "parameterSet": "{{parameter}} set", - "parametersNotSet": "Parameters Not Set", + "parameterSet": "Parameter Recalled", + "parameterSetDesc": "Recalled {{parameter}}", + "parameterNotSet": "Parameter Not Recalled", + "parameterNotSetDesc": "Unable to recall {{parameter}}", + "parameterNotSetDescWithMessage": "Unable to recall {{parameter}}: {{message}}", + "parametersSet": "Parameters Recalled", + "parametersNotSet": "Parameters Not Recalled", + "errorCopied": "Error Copied", "problemCopyingCanvas": "Problem Copying Canvas", "problemCopyingCanvasDesc": "Unable to export base layer", "problemCopyingImage": "Unable to Copy Image", @@ -1092,11 +1147,13 @@ "sentToImageToImage": "Sent To Image To Image", "sentToUnifiedCanvas": "Sent to Unified Canvas", "serverError": "Server Error", + "sessionRef": "Session: {{sessionId}}", "setAsCanvasInitialImage": "Set as canvas initial image", "setCanvasInitialImage": "Set canvas initial image", "setControlImage": "Set as control image", "setInitialImage": "Set as initial image", "setNodeField": "Set as node field", + "somethingWentWrong": "Something Went Wrong", "uploadFailed": "Upload failed", "uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image", "uploadInitialImage": "Upload Initial Image", @@ -1536,7 +1593,6 @@ "controlLayers": "Control Layers", "globalMaskOpacity": "Global Mask Opacity", "autoNegative": "Auto Negative", - "toggleVisibility": "Toggle Layer Visibility", "deletePrompt": "Delete Prompt", "resetRegion": "Reset Region", "debugLayers": "Debug Layers", @@ -1547,8 +1603,6 @@ "addIPAdapter": "Add $t(common.ipAdapter)", "regionalGuidance": "Regional Guidance", "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", - "controlNetLayer": "$t(common.controlNet) $t(unifiedCanvas.layer)", - "ipAdapterLayer": "$t(common.ipAdapter) $t(unifiedCanvas.layer)", "opacity": "Opacity", "globalControlAdapter": "Global $t(controlnet.controlAdapter_one)", "globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", @@ -1559,7 +1613,9 @@ "opacityFilter": "Opacity Filter", "clearProcessor": "Clear Processor", "resetProcessor": "Reset Processor to Defaults", - "noLayersAdded": "No Layers Added" + "noLayersAdded": "No Layers Added", + "layers_one": "Layer", + "layers_other": "Layers" }, "ui": { "tabs": { diff --git a/invokeai/frontend/web/public/locales/es.json b/invokeai/frontend/web/public/locales/es.json index 6b410cd0bf..169bfdb066 100644 --- a/invokeai/frontend/web/public/locales/es.json +++ b/invokeai/frontend/web/public/locales/es.json @@ -25,7 +25,24 @@ "areYouSure": "¿Estas seguro?", "batch": "Administrador de lotes", "modelManager": "Administrador de modelos", - "communityLabel": "Comunidad" + "communityLabel": "Comunidad", + "direction": "Dirección", + "ai": "Ia", + "add": "Añadir", + "auto": "Automático", + "copyError": "Error $t(gallery.copy)", + "details": "Detalles", + "or": "o", + "checkpoint": "Punto de control", + "controlNet": "ControlNet", + "aboutHeading": "Sea dueño de su poder creativo", + "advanced": "Avanzado", + "data": "Fecha", + "delete": "Borrar", + "copy": "Copiar", + "beta": "Beta", + "on": "En", + "aboutDesc": "¿Utilizas Invoke para trabajar? Mira aquí:" }, "gallery": { "galleryImageSize": "Tamaño de la imagen", @@ -365,7 +382,7 @@ "canvasMerged": "Lienzo consolidado", "sentToImageToImage": "Enviar hacia Imagen a Imagen", "sentToUnifiedCanvas": "Enviar hacia Lienzo Consolidado", - "parametersNotSet": "Parámetros no establecidos", + "parametersNotSet": "Parámetros no recuperados", "metadataLoadFailed": "Error al cargar metadatos", "serverError": "Error en el servidor", "canceled": "Procesando la cancelación", @@ -373,7 +390,8 @@ "uploadFailedInvalidUploadDesc": "Debe ser una sola imagen PNG o JPEG", "parameterSet": "Conjunto de parámetros", "parameterNotSet": "Parámetro no configurado", - "problemCopyingImage": "No se puede copiar la imagen" + "problemCopyingImage": "No se puede copiar la imagen", + "errorCopied": "Error al copiar" }, "tooltip": { "feature": { @@ -443,7 +461,13 @@ "previousImage": "Imagen anterior", "nextImage": "Siguiente imagen", "showOptionsPanel": "Mostrar el panel lateral", - "menu": "Menú" + "menu": "Menú", + "showGalleryPanel": "Mostrar panel de galería", + "loadMore": "Cargar más", + "about": "Acerca de", + "createIssue": "Crear un problema", + "resetUI": "Interfaz de usuario $t(accessibility.reset)", + "mode": "Modo" }, "nodes": { "zoomInNodes": "Acercar", @@ -456,5 +480,68 @@ "reloadNodeTemplates": "Recargar las plantillas de nodos", "loadWorkflow": "Cargar el flujo de trabajo", "downloadWorkflow": "Descargar el flujo de trabajo en un archivo JSON" + }, + "boards": { + "autoAddBoard": "Agregar panel automáticamente", + "changeBoard": "Cambiar el panel", + "clearSearch": "Borrar la búsqueda", + "deleteBoard": "Borrar el panel", + "selectBoard": "Seleccionar un panel", + "uncategorized": "Sin categoría", + "cancel": "Cancelar", + "addBoard": "Agregar un panel", + "movingImagesToBoard_one": "Moviendo {{count}} imagen al panel:", + "movingImagesToBoard_many": "Moviendo {{count}} imágenes al panel:", + "movingImagesToBoard_other": "Moviendo {{count}} imágenes al panel:", + "bottomMessage": "Al eliminar este panel y las imágenes que contiene, se restablecerán las funciones que los estén utilizando actualmente.", + "deleteBoardAndImages": "Borrar el panel y las imágenes", + "loading": "Cargando...", + "deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar", + "move": "Mover", + "menuItemAutoAdd": "Agregar automáticamente a este panel", + "searchBoard": "Buscando paneles…", + "topMessage": "Este panel contiene imágenes utilizadas en las siguientes funciones:", + "downloadBoard": "Descargar panel", + "deleteBoardOnly": "Borrar solo el panel", + "myBoard": "Mi panel", + "noMatching": "No hay paneles que coincidan" + }, + "accordions": { + "compositing": { + "title": "Composición", + "infillTab": "Relleno" + }, + "generation": { + "title": "Generación" + }, + "image": { + "title": "Imagen" + }, + "control": { + "title": "Control" + }, + "advanced": { + "options": "$t(accordions.advanced.title) opciones", + "title": "Avanzado" + } + }, + "ui": { + "tabs": { + "generationTab": "$t(ui.tabs.generation) $t(common.tab)", + "canvas": "Lienzo", + "generation": "Generación", + "queue": "Cola", + "queueTab": "$t(ui.tabs.queue) $t(common.tab)", + "workflows": "Flujos de trabajo", + "models": "Modelos", + "modelsTab": "$t(ui.tabs.models) $t(common.tab)", + "canvasTab": "$t(ui.tabs.canvas) $t(common.tab)", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)" + } + }, + "controlLayers": { + "layers_one": "Capa", + "layers_many": "Capas", + "layers_other": "Capas" } } diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 491b31907b..bd82dd9a5b 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -5,7 +5,7 @@ "reportBugLabel": "Segnala un errore", "settingsLabel": "Impostazioni", "img2img": "Immagine a Immagine", - "unifiedCanvas": "Tela unificata", + "unifiedCanvas": "Tela", "nodes": "Flussi di lavoro", "upload": "Caricamento", "load": "Carica", @@ -74,7 +74,18 @@ "file": "File", "toResolve": "Da risolvere", "add": "Aggiungi", - "loglevel": "Livello di log" + "loglevel": "Livello di log", + "beta": "Beta", + "positivePrompt": "Prompt positivo", + "negativePrompt": "Prompt negativo", + "selected": "Selezionato", + "goTo": "Vai a", + "editor": "Editor", + "tab": "Scheda", + "viewing": "Visualizza", + "viewingDesc": "Rivedi le immagini in un'ampia vista della galleria", + "editing": "Modifica", + "editingDesc": "Modifica nell'area Livelli di controllo" }, "gallery": { "galleryImageSize": "Dimensione dell'immagine", @@ -180,8 +191,8 @@ "desc": "Mostra le informazioni sui metadati dell'immagine corrente" }, "sendToImageToImage": { - "title": "Invia a Immagine a Immagine", - "desc": "Invia l'immagine corrente a da Immagine a Immagine" + "title": "Invia a Generazione da immagine", + "desc": "Invia l'immagine corrente a Generazione da immagine" }, "deleteImage": { "title": "Elimina immagine", @@ -334,6 +345,10 @@ "remixImage": { "desc": "Utilizza tutti i parametri tranne il seme dell'immagine corrente", "title": "Remixa l'immagine" + }, + "toggleViewer": { + "title": "Attiva/disattiva il visualizzatore di immagini", + "desc": "Passa dal Visualizzatore immagini all'area di lavoro per la scheda corrente." } }, "modelManager": { @@ -471,8 +486,8 @@ "scaledHeight": "Altezza ridimensionata", "infillMethod": "Metodo di riempimento", "tileSize": "Dimensione piastrella", - "sendToImg2Img": "Invia a Immagine a Immagine", - "sendToUnifiedCanvas": "Invia a Tela Unificata", + "sendToImg2Img": "Invia a Generazione da immagine", + "sendToUnifiedCanvas": "Invia alla Tela", "downloadImage": "Scarica l'immagine", "usePrompt": "Usa Prompt", "useSeed": "Usa Seme", @@ -508,13 +523,24 @@ "incompatibleBaseModelForControlAdapter": "Il modello dell'adattatore di controllo #{{number}} non è compatibile con il modello principale.", "missingNodeTemplate": "Modello di nodo mancante", "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} ingresso mancante", - "missingFieldTemplate": "Modello di campo mancante" + "missingFieldTemplate": "Modello di campo mancante", + "imageNotProcessedForControlAdapter": "L'immagine dell'adattatore di controllo #{{number}} non è stata elaborata", + "layer": { + "initialImageNoImageSelected": "Nessuna immagine iniziale selezionata", + "t2iAdapterIncompatibleDimensions": "L'adattatore T2I richiede che la dimensione dell'immagine sia un multiplo di {{multiple}}", + "controlAdapterNoModelSelected": "Nessun modello di Adattatore di Controllo selezionato", + "controlAdapterIncompatibleBaseModel": "Il modello base dell'adattatore di controllo non è compatibile", + "controlAdapterNoImageSelected": "Nessuna immagine dell'adattatore di controllo selezionata", + "controlAdapterImageNotProcessed": "Immagine dell'adattatore di controllo non elaborata", + "ipAdapterNoModelSelected": "Nessun adattatore IP selezionato", + "ipAdapterIncompatibleBaseModel": "Il modello base dell'adattatore IP non è compatibile", + "ipAdapterNoImageSelected": "Nessuna immagine dell'adattatore IP selezionata", + "rgNoPromptsOrIPAdapters": "Nessun prompt o adattatore IP", + "rgNoRegion": "Nessuna regione selezionata" + } }, "useCpuNoise": "Usa la CPU per generare rumore", "iterations": "Iterazioni", - "iterationsWithCount_one": "{{count}} Iterazione", - "iterationsWithCount_many": "{{count}} Iterazioni", - "iterationsWithCount_other": "{{count}} Iterazioni", "isAllowedToUpscale": { "useX2Model": "L'immagine è troppo grande per l'ampliamento con il modello x4, utilizza il modello x2", "tooLarge": "L'immagine è troppo grande per l'ampliamento, seleziona un'immagine più piccola" @@ -534,7 +560,10 @@ "infillMosaicMinColor": "Colore minimo", "infillMosaicMaxColor": "Colore massimo", "infillMosaicTileHeight": "Altezza piastrella", - "infillColorValue": "Colore di riempimento" + "infillColorValue": "Colore di riempimento", + "globalSettings": "Impostazioni globali", + "globalPositivePromptPlaceholder": "Prompt positivo globale", + "globalNegativePromptPlaceholder": "Prompt negativo globale" }, "settings": { "models": "Modelli", @@ -559,7 +588,7 @@ "intermediatesCleared_one": "Cancellata {{count}} immagine intermedia", "intermediatesCleared_many": "Cancellate {{count}} immagini intermedie", "intermediatesCleared_other": "Cancellate {{count}} immagini intermedie", - "clearIntermediatesDesc1": "La cancellazione delle immagini intermedie ripristinerà lo stato di Tela Unificata e ControlNet.", + "clearIntermediatesDesc1": "La cancellazione delle immagini intermedie ripristinerà lo stato della Tela e degli Adattatori di Controllo.", "intermediatesClearedFailed": "Problema con la cancellazione delle immagini intermedie", "clearIntermediatesWithCount_one": "Cancella {{count}} immagine intermedia", "clearIntermediatesWithCount_many": "Cancella {{count}} immagini intermedie", @@ -575,8 +604,8 @@ "imageCopied": "Immagine copiata", "imageNotLoadedDesc": "Impossibile trovare l'immagine", "canvasMerged": "Tela unita", - "sentToImageToImage": "Inviato a Immagine a Immagine", - "sentToUnifiedCanvas": "Inviato a Tela Unificata", + "sentToImageToImage": "Inviato a Generazione da immagine", + "sentToUnifiedCanvas": "Inviato alla Tela", "parametersNotSet": "Parametri non impostati", "metadataLoadFailed": "Impossibile caricare i metadati", "serverError": "Errore del Server", @@ -795,7 +824,7 @@ "float": "In virgola mobile", "currentImageDescription": "Visualizza l'immagine corrente nell'editor dei nodi", "fieldTypesMustMatch": "I tipi di campo devono corrispondere", - "edge": "Bordo", + "edge": "Collegamento", "currentImage": "Immagine corrente", "integer": "Numero Intero", "inputMayOnlyHaveOneConnection": "L'ingresso può avere solo una connessione", @@ -808,8 +837,8 @@ "unableToUpdateNodes_other": "Impossibile aggiornare {{count}} nodi", "addLinearView": "Aggiungi alla vista Lineare", "unknownErrorValidatingWorkflow": "Errore sconosciuto durante la convalida del flusso di lavoro", - "collectionFieldType": "{{name}} Raccolta", - "collectionOrScalarFieldType": "{{name}} Raccolta|Scalare", + "collectionFieldType": "{{name}} (Raccolta)", + "collectionOrScalarFieldType": "{{name}} (Singola o Raccolta)", "nodeVersion": "Versione Nodo", "inputFieldTypeParseError": "Impossibile analizzare il tipo di campo di input {{node}}.{{field}} ({{message}})", "unsupportedArrayItemType": "Tipo di elemento dell'array non supportato \"{{type}}\"", @@ -845,7 +874,15 @@ "resetToDefaultValue": "Ripristina il valore predefinito", "noFieldsViewMode": "Questo flusso di lavoro non ha campi selezionati da visualizzare. Visualizza il flusso di lavoro completo per configurare i valori.", "edit": "Modifica", - "graph": "Grafico" + "graph": "Grafico", + "showEdgeLabelsHelp": "Mostra etichette sui collegamenti, che indicano i nodi collegati", + "showEdgeLabels": "Mostra le etichette del collegamento", + "cannotMixAndMatchCollectionItemTypes": "Impossibile combinare e abbinare i tipi di elementi della raccolta", + "noGraph": "Nessun grafico", + "missingNode": "Nodo di invocazione mancante", + "missingInvocationTemplate": "Modello di invocazione mancante", + "missingFieldTemplate": "Modello di campo mancante", + "singleFieldType": "{{name}} (Singola)" }, "boards": { "autoAddBoard": "Aggiungi automaticamente bacheca", @@ -922,7 +959,7 @@ "colorMapTileSize": "Dimensione piastrella", "mediapipeFaceDescription": "Rilevamento dei volti tramite Mediapipe", "hedDescription": "Rilevamento dei bordi nidificati olisticamente", - "setControlImageDimensions": "Imposta le dimensioni dell'immagine di controllo su L/A", + "setControlImageDimensions": "Copia le dimensioni in L/A (ottimizza per il modello)", "maxFaces": "Numero massimo di volti", "addT2IAdapter": "Aggiungi $t(common.t2iAdapter)", "addControlNet": "Aggiungi $t(common.controlNet)", @@ -951,12 +988,17 @@ "mediapipeFace": "Mediapipe Volto", "ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))", "t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))", - "selectCLIPVisionModel": "Seleziona un modello CLIP Vision" + "selectCLIPVisionModel": "Seleziona un modello CLIP Vision", + "ipAdapterMethod": "Metodo", + "full": "Completo", + "composition": "Solo la composizione", + "style": "Solo lo stile", + "beginEndStepPercentShort": "Inizio/Fine %", + "setControlImageDimensionsForce": "Copia le dimensioni in L/A (ignora il modello)" }, "queue": { "queueFront": "Aggiungi all'inizio della coda", "queueBack": "Aggiungi alla coda", - "queueCountPrediction": "{{promptsCount}} prompt × {{iterations}} iterazioni -> {{count}} generazioni", "queue": "Coda", "status": "Stato", "pruneSucceeded": "Rimossi {{item_count}} elementi completati dalla coda", @@ -993,7 +1035,7 @@ "cancelBatchSucceeded": "Lotto annullato", "clearTooltip": "Annulla e cancella tutti gli elementi", "current": "Attuale", - "pauseTooltip": "Sospende l'elaborazione", + "pauseTooltip": "Sospendi l'elaborazione", "failed": "Falliti", "cancelItem": "Annulla l'elemento", "next": "Prossimo", @@ -1011,7 +1053,16 @@ "graphFailedToQueue": "Impossibile mettere in coda il grafico", "batchFieldValues": "Valori Campi Lotto", "time": "Tempo", - "openQueue": "Apri coda" + "openQueue": "Apri coda", + "iterations_one": "Iterazione", + "iterations_many": "Iterazioni", + "iterations_other": "Iterazioni", + "prompts_one": "Prompt", + "prompts_many": "Prompt", + "prompts_other": "Prompt", + "generations_one": "Generazione", + "generations_many": "Generazioni", + "generations_other": "Generazioni" }, "models": { "noMatchingModels": "Nessun modello corrispondente", @@ -1394,6 +1445,12 @@ "paragraphs": [ "La dimensione del bordo del passaggio di coerenza." ] + }, + "ipAdapterMethod": { + "heading": "Metodo", + "paragraphs": [ + "Metodo con cui applicare l'adattatore IP corrente." + ] } }, "sdxl": { @@ -1522,5 +1579,55 @@ "compatibleEmbeddings": "Incorporamenti compatibili", "addPromptTrigger": "Aggiungi Trigger nel prompt", "noMatchingTriggers": "Nessun Trigger corrispondente" + }, + "controlLayers": { + "opacityFilter": "Filtro opacità", + "deleteAll": "Cancella tutto", + "addLayer": "Aggiungi Livello", + "moveToFront": "Sposta in primo piano", + "moveToBack": "Sposta in fondo", + "moveForward": "Sposta avanti", + "moveBackward": "Sposta indietro", + "brushSize": "Dimensioni del pennello", + "globalMaskOpacity": "Opacità globale della maschera", + "autoNegative": "Auto Negativo", + "deletePrompt": "Cancella il prompt", + "debugLayers": "Debug dei Livelli", + "rectangle": "Rettangolo", + "maskPreviewColor": "Colore anteprima maschera", + "addPositivePrompt": "Aggiungi $t(common.positivePrompt)", + "addNegativePrompt": "Aggiungi $t(common.negativePrompt)", + "addIPAdapter": "Aggiungi $t(common.ipAdapter)", + "regionalGuidance": "Guida regionale", + "regionalGuidanceLayer": "$t(unifiedCanvas.layer) $t(controlLayers.regionalGuidance)", + "opacity": "Opacità", + "globalControlAdapter": "$t(controlnet.controlAdapter_one) Globale", + "globalControlAdapterLayer": "$t(controlnet.controlAdapter_one) - $t(unifiedCanvas.layer) Globale", + "globalIPAdapter": "$t(common.ipAdapter) Globale", + "globalIPAdapterLayer": "$t(common.ipAdapter) - $t(unifiedCanvas.layer) Globale", + "globalInitialImage": "Immagine iniziale", + "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) - $t(unifiedCanvas.layer) Globale", + "clearProcessor": "Cancella processore", + "resetProcessor": "Ripristina il processore alle impostazioni predefinite", + "noLayersAdded": "Nessun livello aggiunto", + "resetRegion": "Reimposta la regione", + "controlLayers": "Livelli di controllo", + "layers_one": "Livello", + "layers_many": "Livelli", + "layers_other": "Livelli" + }, + "ui": { + "tabs": { + "generation": "Generazione", + "generationTab": "$t(ui.tabs.generation) $t(common.tab)", + "canvas": "Tela", + "canvasTab": "$t(ui.tabs.canvas) $t(common.tab)", + "workflows": "Flussi di lavoro", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", + "models": "Modelli", + "modelsTab": "$t(ui.tabs.models) $t(common.tab)", + "queue": "Coda", + "queueTab": "$t(ui.tabs.queue) $t(common.tab)" + } } } diff --git a/invokeai/frontend/web/public/locales/ja.json b/invokeai/frontend/web/public/locales/ja.json index 264593153a..e953944c44 100644 --- a/invokeai/frontend/web/public/locales/ja.json +++ b/invokeai/frontend/web/public/locales/ja.json @@ -570,7 +570,6 @@ "pauseSucceeded": "処理が一時停止されました", "queueFront": "キューの先頭へ追加", "queueBack": "キューに追加", - "queueCountPrediction": "{{promptsCount}} プロンプト × {{iterations}} イテレーション -> {{count}} 枚生成", "pause": "一時停止", "queue": "キュー", "pauseTooltip": "処理を一時停止", diff --git a/invokeai/frontend/web/public/locales/ko.json b/invokeai/frontend/web/public/locales/ko.json index 1c02d86105..db9cd0ca67 100644 --- a/invokeai/frontend/web/public/locales/ko.json +++ b/invokeai/frontend/web/public/locales/ko.json @@ -505,7 +505,6 @@ "completed": "완성된", "queueBack": "Queue에 추가", "cancelFailed": "항목 취소 중 발생한 문제", - "queueCountPrediction": "Queue에 {{predicted}} 추가", "batchQueued": "Batch Queued", "pauseFailed": "프로세서 중지 중 발생한 문제", "clearFailed": "Queue 제거 중 발생한 문제", diff --git a/invokeai/frontend/web/public/locales/nl.json b/invokeai/frontend/web/public/locales/nl.json index 29ceb3227b..afcce62163 100644 --- a/invokeai/frontend/web/public/locales/nl.json +++ b/invokeai/frontend/web/public/locales/nl.json @@ -6,7 +6,7 @@ "settingsLabel": "Instellingen", "img2img": "Afbeelding naar afbeelding", "unifiedCanvas": "Centraal canvas", - "nodes": "Werkstroom-editor", + "nodes": "Werkstromen", "upload": "Upload", "load": "Laad", "statusDisconnected": "Niet verbonden", @@ -34,7 +34,60 @@ "controlNet": "ControlNet", "imageFailedToLoad": "Kan afbeelding niet laden", "learnMore": "Meer informatie", - "advanced": "Uitgebreid" + "advanced": "Uitgebreid", + "file": "Bestand", + "installed": "Geïnstalleerd", + "notInstalled": "Niet $t(common.installed)", + "simple": "Eenvoudig", + "somethingWentWrong": "Er ging iets mis", + "add": "Voeg toe", + "checkpoint": "Checkpoint", + "details": "Details", + "outputs": "Uitvoeren", + "save": "Bewaar", + "nextPage": "Volgende pagina", + "blue": "Blauw", + "alpha": "Alfa", + "red": "Rood", + "editor": "Editor", + "folder": "Map", + "format": "structuur", + "goTo": "Ga naar", + "template": "Sjabloon", + "input": "Invoer", + "loglevel": "Logboekniveau", + "safetensors": "Safetensors", + "saveAs": "Bewaar als", + "created": "Gemaakt", + "green": "Groen", + "tab": "Tab", + "positivePrompt": "Positieve prompt", + "negativePrompt": "Negatieve prompt", + "selected": "Geselecteerd", + "orderBy": "Sorteer op", + "prevPage": "Vorige pagina", + "beta": "Bèta", + "copyError": "$t(gallery.copy) Fout", + "toResolve": "Op te lossen", + "aboutDesc": "Gebruik je Invoke voor het werk? Kijk dan naar:", + "aboutHeading": "Creatieve macht voor jou", + "copy": "Kopieer", + "data": "Gegevens", + "or": "of", + "updated": "Bijgewerkt", + "outpaint": "outpainten", + "viewing": "Bekijken", + "viewingDesc": "Beoordeel afbeelding in een grote galerijweergave", + "editing": "Bewerken", + "editingDesc": "Bewerk op het canvas Stuurlagen", + "ai": "ai", + "inpaint": "inpainten", + "unknown": "Onbekend", + "delete": "Verwijder", + "direction": "Richting", + "error": "Fout", + "localSystem": "Lokaal systeem", + "unknownError": "Onbekende fout" }, "gallery": { "galleryImageSize": "Afbeeldingsgrootte", @@ -310,10 +363,41 @@ "modelSyncFailed": "Synchronisatie modellen mislukt", "modelDeleteFailed": "Model kon niet verwijderd worden", "convertingModelBegin": "Model aan het converteren. Even geduld.", - "predictionType": "Soort voorspelling (voor Stable Diffusion 2.x-modellen en incidentele Stable Diffusion 1.x-modellen)", + "predictionType": "Soort voorspelling", "advanced": "Uitgebreid", "modelType": "Soort model", - "vaePrecision": "Nauwkeurigheid VAE" + "vaePrecision": "Nauwkeurigheid VAE", + "loraTriggerPhrases": "LoRA-triggerzinnen", + "urlOrLocalPathHelper": "URL's zouden moeten wijzen naar een los bestand. Lokale paden kunnen wijzen naar een los bestand of map voor een individueel Diffusers-model.", + "modelName": "Modelnaam", + "path": "Pad", + "triggerPhrases": "Triggerzinnen", + "typePhraseHere": "Typ zin hier in", + "useDefaultSettings": "Gebruik standaardinstellingen", + "modelImageDeleteFailed": "Fout bij verwijderen modelafbeelding", + "modelImageUpdated": "Modelafbeelding bijgewerkt", + "modelImageUpdateFailed": "Fout bij bijwerken modelafbeelding", + "noMatchingModels": "Geen overeenkomende modellen", + "scanPlaceholder": "Pad naar een lokale map", + "noModelsInstalled": "Geen modellen geïnstalleerd", + "noModelsInstalledDesc1": "Installeer modellen met de", + "noModelSelected": "Geen model geselecteerd", + "starterModels": "Beginnermodellen", + "textualInversions": "Tekstuele omkeringen", + "upcastAttention": "Upcast-aandacht", + "uploadImage": "Upload afbeelding", + "mainModelTriggerPhrases": "Triggerzinnen hoofdmodel", + "urlOrLocalPath": "URL of lokaal pad", + "scanFolderHelper": "De map zal recursief worden ingelezen voor modellen. Dit kan enige tijd in beslag nemen voor erg grote mappen.", + "simpleModelPlaceholder": "URL of pad naar een lokaal pad of Diffusers-map", + "modelSettings": "Modelinstellingen", + "pathToConfig": "Pad naar configuratie", + "prune": "Snoei", + "pruneTooltip": "Snoei voltooide importeringen uit wachtrij", + "repoVariant": "Repovariant", + "scanFolder": "Lees map in", + "scanResults": "Resultaten inlezen", + "source": "Bron" }, "parameters": { "images": "Afbeeldingen", @@ -353,13 +437,13 @@ "copyImage": "Kopieer afbeelding", "denoisingStrength": "Sterkte ontruisen", "scheduler": "Planner", - "seamlessXAxis": "X-as", - "seamlessYAxis": "Y-as", + "seamlessXAxis": "Naadloze tegels in x-as", + "seamlessYAxis": "Naadloze tegels in y-as", "clipSkip": "Overslaan CLIP", "negativePromptPlaceholder": "Negatieve prompt", "controlNetControlMode": "Aansturingsmodus", "positivePromptPlaceholder": "Positieve prompt", - "maskBlur": "Vervaag", + "maskBlur": "Vervaging van masker", "invoke": { "noNodesInGraph": "Geen knooppunten in graaf", "noModelSelected": "Geen model ingesteld", @@ -369,11 +453,25 @@ "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} invoer ontbreekt", "noControlImageForControlAdapter": "Controle-adapter #{{number}} heeft geen controle-afbeelding", "noModelForControlAdapter": "Control-adapter #{{number}} heeft geen model ingesteld staan.", - "incompatibleBaseModelForControlAdapter": "Model van controle-adapter #{{number}} is ongeldig in combinatie met het hoofdmodel.", + "incompatibleBaseModelForControlAdapter": "Model van controle-adapter #{{number}} is niet compatibel met het hoofdmodel.", "systemDisconnected": "Systeem is niet verbonden", "missingNodeTemplate": "Knooppuntsjabloon ontbreekt", "missingFieldTemplate": "Veldsjabloon ontbreekt", - "addingImagesTo": "Bezig met toevoegen van afbeeldingen aan" + "addingImagesTo": "Bezig met toevoegen van afbeeldingen aan", + "layer": { + "initialImageNoImageSelected": "geen initiële afbeelding geselecteerd", + "controlAdapterNoModelSelected": "geen controle-adaptermodel geselecteerd", + "controlAdapterIncompatibleBaseModel": "niet-compatibele basismodel voor controle-adapter", + "controlAdapterNoImageSelected": "geen afbeelding voor controle-adapter geselecteerd", + "controlAdapterImageNotProcessed": "Afbeelding voor controle-adapter niet verwerkt", + "ipAdapterIncompatibleBaseModel": "niet-compatibele basismodel voor IP-adapter", + "ipAdapterNoImageSelected": "geen afbeelding voor IP-adapter geselecteerd", + "rgNoRegion": "geen gebied geselecteerd", + "rgNoPromptsOrIPAdapters": "geen tekstprompts of IP-adapters", + "t2iAdapterIncompatibleDimensions": "T2I-adapter vereist een afbeelding met afmetingen met een veelvoud van 64", + "ipAdapterNoModelSelected": "geen IP-adapter geselecteerd" + }, + "imageNotProcessedForControlAdapter": "De afbeelding van controle-adapter #{{number}} is niet verwerkt" }, "isAllowedToUpscale": { "useX2Model": "Afbeelding is te groot om te vergroten met het x4-model. Gebruik hiervoor het x2-model", @@ -383,9 +481,26 @@ "useCpuNoise": "Gebruik CPU-ruis", "imageActions": "Afbeeldingshandeling", "iterations": "Iteraties", - "iterationsWithCount_one": "{{count}} iteratie", - "iterationsWithCount_other": "{{count}} iteraties", - "coherenceMode": "Modus" + "coherenceMode": "Modus", + "infillColorValue": "Vulkleur", + "remixImage": "Meng afbeelding opnieuw", + "setToOptimalSize": "Optimaliseer grootte voor het model", + "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (is mogelijk te klein)", + "aspect": "Beeldverhouding", + "infillMosaicTileWidth": "Breedte tegel", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (is mogelijk te groot)", + "lockAspectRatio": "Zet beeldverhouding vast", + "infillMosaicTileHeight": "Hoogte tegel", + "globalNegativePromptPlaceholder": "Globale negatieve prompt", + "globalPositivePromptPlaceholder": "Globale positieve prompt", + "useSize": "Gebruik grootte", + "swapDimensions": "Wissel afmetingen om", + "globalSettings": "Globale instellingen", + "coherenceEdgeSize": "Randgrootte", + "coherenceMinDenoise": "Min. ontruising", + "infillMosaicMinColor": "Min. kleur", + "infillMosaicMaxColor": "Max. kleur", + "cfgRescaleMultiplier": "Vermenigvuldiger voor CFG-herschaling" }, "settings": { "models": "Modellen", @@ -412,7 +527,12 @@ "intermediatesCleared_one": "{{count}} tussentijdse afbeelding gewist", "intermediatesCleared_other": "{{count}} tussentijdse afbeeldingen gewist", "clearIntermediatesDesc1": "Als je tussentijdse afbeeldingen wist, dan wordt de staat hersteld van je canvas en van ControlNet.", - "intermediatesClearedFailed": "Fout bij wissen van tussentijdse afbeeldingen" + "intermediatesClearedFailed": "Fout bij wissen van tussentijdse afbeeldingen", + "clearIntermediatesDisabled": "Wachtrij moet leeg zijn om tussentijdse afbeeldingen te kunnen leegmaken", + "enableInformationalPopovers": "Schakel informatieve hulpballonnen in", + "enableInvisibleWatermark": "Schakel onzichtbaar watermerk in", + "enableNSFWChecker": "Schakel NSFW-controle in", + "reloadingIn": "Opnieuw laden na" }, "toast": { "uploadFailed": "Upload mislukt", @@ -427,8 +547,8 @@ "connected": "Verbonden met server", "canceled": "Verwerking geannuleerd", "uploadFailedInvalidUploadDesc": "Moet een enkele PNG- of JPEG-afbeelding zijn", - "parameterNotSet": "Parameter niet ingesteld", - "parameterSet": "Instellen parameters", + "parameterNotSet": "{{parameter}} niet ingesteld", + "parameterSet": "{{parameter}} ingesteld", "problemCopyingImage": "Kan Afbeelding Niet Kopiëren", "baseModelChangedCleared_one": "Basismodel is gewijzigd: {{count}} niet-compatibel submodel weggehaald of uitgeschakeld", "baseModelChangedCleared_other": "Basismodel is gewijzigd: {{count}} niet-compatibele submodellen weggehaald of uitgeschakeld", @@ -445,11 +565,11 @@ "maskSavedAssets": "Masker bewaard in Assets", "problemDownloadingCanvas": "Fout bij downloaden van canvas", "problemMergingCanvas": "Fout bij samenvoegen canvas", - "setCanvasInitialImage": "Ingesteld als initiële canvasafbeelding", + "setCanvasInitialImage": "Initiële canvasafbeelding ingesteld", "imageUploaded": "Afbeelding geüpload", "addedToBoard": "Toegevoegd aan bord", "workflowLoaded": "Werkstroom geladen", - "modelAddedSimple": "Model toegevoegd", + "modelAddedSimple": "Model toegevoegd aan wachtrij", "problemImportingMaskDesc": "Kan masker niet exporteren", "problemCopyingCanvas": "Fout bij kopiëren canvas", "problemSavingCanvas": "Fout bij bewaren canvas", @@ -461,7 +581,18 @@ "maskSentControlnetAssets": "Masker gestuurd naar ControlNet en Assets", "canvasSavedGallery": "Canvas bewaard in galerij", "imageUploadFailed": "Fout bij uploaden afbeelding", - "problemImportingMask": "Fout bij importeren masker" + "problemImportingMask": "Fout bij importeren masker", + "workflowDeleted": "Werkstroom verwijderd", + "invalidUpload": "Ongeldige upload", + "uploadInitialImage": "Initiële afbeelding uploaden", + "setAsCanvasInitialImage": "Ingesteld als initiële afbeelding voor canvas", + "problemRetrievingWorkflow": "Fout bij ophalen van werkstroom", + "parameters": "Parameters", + "modelImportCanceled": "Importeren model geannuleerd", + "problemDeletingWorkflow": "Fout bij verwijderen van werkstroom", + "prunedQueue": "Wachtrij gesnoeid", + "problemDownloadingImage": "Fout bij downloaden afbeelding", + "resetInitialImage": "Initiële afbeelding hersteld" }, "tooltip": { "feature": { @@ -535,7 +666,11 @@ "showOptionsPanel": "Toon zijscherm", "menu": "Menu", "showGalleryPanel": "Toon deelscherm Galerij", - "loadMore": "Laad meer" + "loadMore": "Laad meer", + "about": "Over", + "mode": "Modus", + "resetUI": "$t(accessibility.reset) UI", + "createIssue": "Maak probleem aan" }, "nodes": { "zoomOutNodes": "Uitzoomen", @@ -549,7 +684,7 @@ "loadWorkflow": "Laad werkstroom", "downloadWorkflow": "Download JSON van werkstroom", "scheduler": "Planner", - "missingTemplate": "Ontbrekende sjabloon", + "missingTemplate": "Ongeldig knooppunt: knooppunt {{node}} van het soort {{type}} heeft een ontbrekend sjabloon (niet geïnstalleerd?)", "workflowDescription": "Korte beschrijving", "versionUnknown": " Versie onbekend", "noNodeSelected": "Geen knooppunt gekozen", @@ -565,7 +700,7 @@ "integer": "Geheel getal", "nodeTemplate": "Sjabloon knooppunt", "nodeOpacity": "Dekking knooppunt", - "unableToLoadWorkflow": "Kan werkstroom niet valideren", + "unableToLoadWorkflow": "Fout bij laden werkstroom", "snapToGrid": "Lijn uit op raster", "noFieldsLinearview": "Geen velden toegevoegd aan lineaire weergave", "nodeSearch": "Zoek naar knooppunten", @@ -616,11 +751,56 @@ "unknownField": "Onbekend veld", "colorCodeEdges": "Kleurgecodeerde randen", "unknownNode": "Onbekend knooppunt", - "mismatchedVersion": "Heeft niet-overeenkomende versie", + "mismatchedVersion": "Ongeldig knooppunt: knooppunt {{node}} van het soort {{type}} heeft een niet-overeenkomende versie (probeer het bij te werken?)", "addNodeToolTip": "Voeg knooppunt toe (Shift+A, spatie)", "loadingNodes": "Bezig met laden van knooppunten...", "snapToGridHelp": "Lijn knooppunten uit op raster bij verplaatsing", - "workflowSettings": "Instellingen werkstroomeditor" + "workflowSettings": "Instellingen werkstroomeditor", + "addLinearView": "Voeg toe aan lineaire weergave", + "nodePack": "Knooppuntpakket", + "unknownInput": "Onbekende invoer: {{name}}", + "sourceNodeFieldDoesNotExist": "Ongeldige rand: bron-/uitvoerveld {{node}}.{{field}} bestaat niet", + "collectionFieldType": "Verzameling {{name}}", + "deletedInvalidEdge": "Ongeldige hoek {{source}} -> {{target}} verwijderd", + "graph": "Grafiek", + "targetNodeDoesNotExist": "Ongeldige rand: doel-/invoerknooppunt {{node}} bestaat niet", + "resetToDefaultValue": "Herstel naar standaardwaarden", + "editMode": "Bewerk in Werkstroom-editor", + "showEdgeLabels": "Toon randlabels", + "showEdgeLabelsHelp": "Toon labels aan randen, waarmee de verbonden knooppunten mee worden aangegeven", + "clearWorkflowDesc2": "Je huidige werkstroom heeft niet-bewaarde wijzigingen.", + "unableToParseFieldType": "fout bij bepalen soort veld", + "sourceNodeDoesNotExist": "Ongeldige rand: bron-/uitvoerknooppunt {{node}} bestaat niet", + "unsupportedArrayItemType": "niet-ondersteunde soort van het array-onderdeel \"{{type}}\"", + "targetNodeFieldDoesNotExist": "Ongeldige rand: doel-/invoerveld {{node}}.{{field}} bestaat niet", + "reorderLinearView": "Herorden lineaire weergave", + "newWorkflowDesc": "Een nieuwe werkstroom aanmaken?", + "collectionOrScalarFieldType": "Verzameling|scalair {{name}}", + "newWorkflow": "Nieuwe werkstroom", + "unknownErrorValidatingWorkflow": "Onbekende fout bij valideren werkstroom", + "unsupportedAnyOfLength": "te veel union-leden ({{count}})", + "unknownOutput": "Onbekende uitvoer: {{name}}", + "viewMode": "Gebruik in lineaire weergave", + "unableToExtractSchemaNameFromRef": "fout bij het extraheren van de schemanaam via de ref", + "unsupportedMismatchedUnion": "niet-overeenkomende soort CollectionOrScalar met basissoorten {{firstType}} en {{secondType}}", + "unknownNodeType": "Onbekend soort knooppunt", + "edit": "Bewerk", + "updateAllNodes": "Werk knooppunten bij", + "allNodesUpdated": "Alle knooppunten bijgewerkt", + "nodeVersion": "Knooppuntversie", + "newWorkflowDesc2": "Je huidige werkstroom heeft niet-bewaarde wijzigingen.", + "clearWorkflow": "Maak werkstroom leeg", + "clearWorkflowDesc": "Deze werkstroom leegmaken en met een nieuwe beginnen?", + "inputFieldTypeParseError": "Fout bij bepalen van het soort invoerveld {{node}}.{{field}} ({{message}})", + "outputFieldTypeParseError": "Fout bij het bepalen van het soort uitvoerveld {{node}}.{{field}} ({{message}})", + "unableToExtractEnumOptions": "fout bij extraheren enumeratie-opties", + "unknownFieldType": "Soort $t(nodes.unknownField): {{type}}", + "unableToGetWorkflowVersion": "Fout bij ophalen schemaversie van werkstroom", + "betaDesc": "Deze uitvoering is in bèta. Totdat deze stabiel is kunnen er wijzigingen voorkomen gedurende app-updates die zaken kapotmaken. We zijn van plan om deze uitvoering op lange termijn te gaan ondersteunen.", + "prototypeDesc": "Deze uitvoering is een prototype. Er kunnen wijzigingen voorkomen gedurende app-updates die zaken kapotmaken. Deze kunnen op een willekeurig moment verwijderd worden.", + "noFieldsViewMode": "Deze werkstroom heeft geen geselecteerde velden om te tonen. Bekijk de volledige werkstroom om de waarden te configureren.", + "unableToUpdateNodes_one": "Fout bij bijwerken van {{count}} knooppunt", + "unableToUpdateNodes_other": "Fout bij bijwerken van {{count}} knooppunten" }, "controlnet": { "amult": "a_mult", @@ -693,9 +873,28 @@ "canny": "Canny", "depthZoeDescription": "Genereer diepteblad via Zoe", "hedDescription": "Herkenning van holistisch-geneste randen", - "setControlImageDimensions": "Stel afmetingen controle-afbeelding in op B/H", + "setControlImageDimensions": "Kopieer grootte naar B/H (optimaliseer voor model)", "scribble": "Krabbel", - "maxFaces": "Max. gezichten" + "maxFaces": "Max. gezichten", + "dwOpenpose": "DW Openpose", + "depthAnything": "Depth Anything", + "base": "Basis", + "hands": "Handen", + "selectCLIPVisionModel": "Selecteer een CLIP Vision-model", + "modelSize": "Modelgrootte", + "small": "Klein", + "large": "Groot", + "resizeSimple": "Wijzig grootte (eenvoudig)", + "beginEndStepPercentShort": "Begin-/eind-%", + "depthAnythingDescription": "Genereren dieptekaart d.m.v. de techniek Depth Anything", + "face": "Gezicht", + "body": "Lichaam", + "dwOpenposeDescription": "Schatting menselijke pose d.m.v. DW Openpose", + "ipAdapterMethod": "Methode", + "full": "Volledig", + "style": "Alleen stijl", + "composition": "Alleen samenstelling", + "setControlImageDimensionsForce": "Kopieer grootte naar B/H (negeer model)" }, "dynamicPrompts": { "seedBehaviour": { @@ -708,7 +907,10 @@ "maxPrompts": "Max. prompts", "promptsWithCount_one": "{{count}} prompt", "promptsWithCount_other": "{{count}} prompts", - "dynamicPrompts": "Dynamische prompts" + "dynamicPrompts": "Dynamische prompts", + "showDynamicPrompts": "Toon dynamische prompts", + "loading": "Genereren van dynamische prompts...", + "promptsPreview": "Voorvertoning prompts" }, "popovers": { "noiseUseCPU": { @@ -721,7 +923,7 @@ }, "paramScheduler": { "paragraphs": [ - "De planner bepaalt hoe ruis per iteratie wordt toegevoegd aan een afbeelding of hoe een monster wordt bijgewerkt op basis van de uitvoer van een model." + "De planner gebruikt gedurende het genereringsproces." ], "heading": "Planner" }, @@ -808,8 +1010,8 @@ }, "clipSkip": { "paragraphs": [ - "Kies hoeveel CLIP-modellagen je wilt overslaan.", - "Bepaalde modellen werken beter met bepaalde Overslaan CLIP-instellingen." + "Aantal over te slaan CLIP-modellagen.", + "Bepaalde modellen zijn beter geschikt met bepaalde Overslaan CLIP-instellingen." ], "heading": "Overslaan CLIP" }, @@ -940,7 +1142,6 @@ "completed": "Voltooid", "queueBack": "Voeg toe aan wachtrij", "cancelFailed": "Fout bij annuleren onderdeel", - "queueCountPrediction": "Voeg {{predicted}} toe aan wachtrij", "batchQueued": "Reeks in wachtrij geplaatst", "pauseFailed": "Fout bij onderbreken verwerker", "clearFailed": "Fout bij wissen van wachtrij", @@ -994,17 +1195,26 @@ "denoisingStrength": "Sterkte ontruising", "refinermodel": "Verfijningsmodel", "posAestheticScore": "Positieve esthetische score", - "concatPromptStyle": "Plak prompt- en stijltekst aan elkaar", + "concatPromptStyle": "Koppelen van prompt en stijl", "loading": "Bezig met laden...", "steps": "Stappen", - "posStylePrompt": "Positieve-stijlprompt" + "posStylePrompt": "Positieve-stijlprompt", + "freePromptStyle": "Handmatige stijlprompt", + "refinerSteps": "Aantal stappen verfijner" }, "models": { "noMatchingModels": "Geen overeenkomend modellen", "loading": "bezig met laden", "noMatchingLoRAs": "Geen overeenkomende LoRA's", "noModelsAvailable": "Geen modellen beschikbaar", - "selectModel": "Kies een model" + "selectModel": "Kies een model", + "noLoRAsInstalled": "Geen LoRA's geïnstalleerd", + "noRefinerModelsInstalled": "Geen SDXL-verfijningsmodellen geïnstalleerd", + "defaultVAE": "Standaard-VAE", + "lora": "LoRA", + "esrganModel": "ESRGAN-model", + "addLora": "Voeg LoRA toe", + "concepts": "Concepten" }, "boards": { "autoAddBoard": "Voeg automatisch bord toe", @@ -1022,7 +1232,13 @@ "downloadBoard": "Download bord", "changeBoard": "Wijzig bord", "loading": "Bezig met laden...", - "clearSearch": "Maak zoekopdracht leeg" + "clearSearch": "Maak zoekopdracht leeg", + "deleteBoard": "Verwijder bord", + "deleteBoardAndImages": "Verwijder bord en afbeeldingen", + "deleteBoardOnly": "Verwijder alleen bord", + "deletedBoardsCannotbeRestored": "Verwijderde borden kunnen niet worden hersteld", + "movingImagesToBoard_one": "Verplaatsen van {{count}} afbeelding naar bord:", + "movingImagesToBoard_other": "Verplaatsen van {{count}} afbeeldingen naar bord:" }, "invocationCache": { "disable": "Schakel uit", @@ -1039,5 +1255,39 @@ "clear": "Wis", "maxCacheSize": "Max. grootte cache", "cacheSize": "Grootte cache" + }, + "accordions": { + "generation": { + "title": "Genereren" + }, + "image": { + "title": "Afbeelding" + }, + "advanced": { + "title": "Geavanceerd", + "options": "$t(accordions.advanced.title) Opties" + }, + "control": { + "title": "Besturing" + }, + "compositing": { + "title": "Samenstellen", + "coherenceTab": "Coherentiefase", + "infillTab": "Invullen" + } + }, + "hrf": { + "upscaleMethod": "Opschaalmethode", + "metadata": { + "strength": "Sterkte oplossing voor hoge resolutie", + "method": "Methode oplossing voor hoge resolutie", + "enabled": "Oplossing voor hoge resolutie ingeschakeld" + }, + "hrf": "Oplossing voor hoge resolutie", + "enableHrf": "Schakel oplossing in voor hoge resolutie" + }, + "prompt": { + "addPromptTrigger": "Voeg prompttrigger toe", + "compatibleEmbeddings": "Compatibele embeddings" } } diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index f254b7faa5..03ff7eb706 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -76,7 +76,18 @@ "localSystem": "Локальная система", "aboutDesc": "Используя Invoke для работы? Проверьте это:", "add": "Добавить", - "loglevel": "Уровень логов" + "loglevel": "Уровень логов", + "beta": "Бета", + "selected": "Выбрано", + "positivePrompt": "Позитивный запрос", + "negativePrompt": "Негативный запрос", + "editor": "Редактор", + "goTo": "Перейти к", + "tab": "Вкладка", + "viewing": "Просмотр", + "editing": "Редактирование", + "viewingDesc": "Просмотр изображений в режиме большой галереи", + "editingDesc": "Редактировать на холсте слоёв управления" }, "gallery": { "galleryImageSize": "Размер изображений", @@ -87,8 +98,8 @@ "deleteImagePermanent": "Удаленные изображения невозможно восстановить.", "deleteImageBin": "Удаленные изображения будут отправлены в корзину вашей операционной системы.", "deleteImage_one": "Удалить изображение", - "deleteImage_few": "", - "deleteImage_many": "", + "deleteImage_few": "Удалить {{count}} изображения", + "deleteImage_many": "Удалить {{count}} изображений", "assets": "Ресурсы", "autoAssignBoardOnClick": "Авто-назначение доски по клику", "deleteSelection": "Удалить выделенное", @@ -336,6 +347,10 @@ "remixImage": { "desc": "Используйте все параметры, кроме сида из текущего изображения", "title": "Ремикс изображения" + }, + "toggleViewer": { + "title": "Переключить просмотр изображений", + "desc": "Переключение между средством просмотра изображений и рабочей областью для текущей вкладки." } }, "modelManager": { @@ -512,7 +527,8 @@ "missingNodeTemplate": "Отсутствует шаблон узла", "missingFieldTemplate": "Отсутствует шаблон поля", "addingImagesTo": "Добавление изображений в", - "invoke": "Создать" + "invoke": "Создать", + "imageNotProcessedForControlAdapter": "Изображение адаптера контроля №{{number}} не обрабатывается" }, "isAllowedToUpscale": { "useX2Model": "Изображение слишком велико для увеличения с помощью модели x4. Используйте модель x2", @@ -523,9 +539,6 @@ "useCpuNoise": "Использовать шум CPU", "imageActions": "Действия с изображениями", "iterations": "Кол-во", - "iterationsWithCount_one": "{{count}} Интеграция", - "iterationsWithCount_few": "{{count}} Итерации", - "iterationsWithCount_many": "{{count}} Итераций", "useSize": "Использовать размер", "coherenceMode": "Режим", "aspect": "Соотношение", @@ -541,7 +554,10 @@ "infillMosaicTileHeight": "Высота плиток", "infillMosaicMinColor": "Мин цвет", "infillMosaicMaxColor": "Макс цвет", - "infillColorValue": "Цвет заливки" + "infillColorValue": "Цвет заливки", + "globalSettings": "Глобальные настройки", + "globalNegativePromptPlaceholder": "Глобальный негативный запрос", + "globalPositivePromptPlaceholder": "Глобальный запрос" }, "settings": { "models": "Модели", @@ -706,7 +722,9 @@ "coherenceModeBoxBlur": "коробчатое размытие", "discardCurrent": "Отбросить текущее", "invertBrushSizeScrollDirection": "Инвертировать прокрутку для размера кисти", - "initialFitImageSize": "Подогнать размер изображения при перебросе" + "initialFitImageSize": "Подогнать размер изображения при перебросе", + "hideBoundingBox": "Скрыть ограничительную рамку", + "showBoundingBox": "Показать ограничительную рамку" }, "accessibility": { "uploadImage": "Загрузить изображение", @@ -849,7 +867,10 @@ "editMode": "Открыть в редакторе узлов", "resetToDefaultValue": "Сбросить к стандартному значкнию", "edit": "Редактировать", - "noFieldsViewMode": "В этом рабочем процессе нет выбранных полей для отображения. Просмотрите полный рабочий процесс для настройки значений." + "noFieldsViewMode": "В этом рабочем процессе нет выбранных полей для отображения. Просмотрите полный рабочий процесс для настройки значений.", + "graph": "График", + "showEdgeLabels": "Показать метки на ребрах", + "showEdgeLabelsHelp": "Показать метки на ребрах, указывающие на соединенные узлы" }, "controlnet": { "amult": "a_mult", @@ -917,8 +938,8 @@ "lineartAnime": "Контурный рисунок в стиле аниме", "mediapipeFaceDescription": "Обнаружение лиц с помощью Mediapipe", "hedDescription": "Целостное обнаружение границ", - "setControlImageDimensions": "Установите размеры контрольного изображения на Ш/В", - "scribble": "каракули", + "setControlImageDimensions": "Скопируйте размер в Ш/В (оптимизируйте для модели)", + "scribble": "Штрихи", "maxFaces": "Макс Лица", "mlsdDescription": "Минималистичный детектор отрезков линии", "resizeSimple": "Изменить размер (простой)", @@ -933,7 +954,18 @@ "small": "Маленький", "body": "Тело", "hands": "Руки", - "selectCLIPVisionModel": "Выбрать модель CLIP Vision" + "selectCLIPVisionModel": "Выбрать модель CLIP Vision", + "ipAdapterMethod": "Метод", + "full": "Всё", + "mlsd": "M-LSD", + "h": "H", + "style": "Только стиль", + "dwOpenpose": "DW Openpose", + "pidi": "PIDI", + "composition": "Только композиция", + "hed": "HED", + "beginEndStepPercentShort": "Начало/конец %", + "setControlImageDimensionsForce": "Скопируйте размер в Ш/В (игнорируйте модель)" }, "boards": { "autoAddBoard": "Авто добавление Доски", @@ -1312,6 +1344,12 @@ "paragraphs": [ "Плавно укладывайте изображение вдоль вертикальной оси." ] + }, + "ipAdapterMethod": { + "heading": "Метод", + "paragraphs": [ + "Метод, с помощью которого применяется текущий IP-адаптер." + ] } }, "metadata": { @@ -1359,7 +1397,6 @@ "completed": "Выполнено", "queueBack": "Добавить в очередь", "cancelFailed": "Проблема с отменой элемента", - "queueCountPrediction": "{{promptsCount}} запросов × {{iterations}} изображений -> {{count}} генераций", "batchQueued": "Пакетная очередь", "pauseFailed": "Проблема с приостановкой рендеринга", "clearFailed": "Проблема с очисткой очереди", @@ -1475,7 +1512,11 @@ "projectWorkflows": "Рабочие процессы проекта", "defaultWorkflows": "Стандартные рабочие процессы", "name": "Имя", - "noRecentWorkflows": "Нет последних рабочих процессов" + "noRecentWorkflows": "Нет последних рабочих процессов", + "loadWorkflow": "Рабочий процесс $t(common.load)", + "convertGraph": "Конвертировать график", + "loadFromGraph": "Загрузка рабочего процесса из графика", + "autoLayout": "Автоматическое расположение" }, "hrf": { "enableHrf": "Включить исправление высокого разрешения", @@ -1528,5 +1569,55 @@ "addPromptTrigger": "Добавить триггер запроса", "compatibleEmbeddings": "Совместимые встраивания", "noMatchingTriggers": "Нет соответствующих триггеров" + }, + "controlLayers": { + "moveToBack": "На задний план", + "moveForward": "Переместить вперёд", + "moveBackward": "Переместить назад", + "brushSize": "Размер кисти", + "controlLayers": "Слои управления", + "globalMaskOpacity": "Глобальная непрозрачность маски", + "autoNegative": "Авто негатив", + "deletePrompt": "Удалить запрос", + "resetRegion": "Сбросить регион", + "debugLayers": "Слои отладки", + "rectangle": "Прямоугольник", + "maskPreviewColor": "Цвет предпросмотра маски", + "addNegativePrompt": "Добавить $t(common.negativePrompt)", + "regionalGuidance": "Региональная точность", + "opacity": "Непрозрачность", + "globalControlAdapter": "Глобальный $t(controlnet.controlAdapter_one)", + "globalControlAdapterLayer": "Глобальный $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)", + "globalIPAdapter": "Глобальный $t(common.ipAdapter)", + "globalIPAdapterLayer": "Глобальный $t(common.ipAdapter) $t(unifiedCanvas.layer)", + "opacityFilter": "Фильтр непрозрачности", + "deleteAll": "Удалить всё", + "addLayer": "Добавить слой", + "moveToFront": "На передний план", + "addPositivePrompt": "Добавить $t(common.positivePrompt)", + "addIPAdapter": "Добавить $t(common.ipAdapter)", + "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", + "resetProcessor": "Сброс процессора по умолчанию", + "clearProcessor": "Чистый процессор", + "globalInitialImage": "Глобальное исходное изображение", + "globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)", + "noLayersAdded": "Без слоев", + "layers_one": "Слой", + "layers_few": "Слоя", + "layers_many": "Слоев" + }, + "ui": { + "tabs": { + "generation": "Генерация", + "canvas": "Холст", + "workflowsTab": "$t(ui.tabs.workflows) $t(common.tab)", + "models": "Модели", + "generationTab": "$t(ui.tabs.generation) $t(common.tab)", + "workflows": "Рабочие процессы", + "canvasTab": "$t(ui.tabs.canvas) $t(common.tab)", + "queueTab": "$t(ui.tabs.queue) $t(common.tab)", + "modelsTab": "$t(ui.tabs.models) $t(common.tab)", + "queue": "Очередь" + } } } diff --git a/invokeai/frontend/web/public/locales/zh_CN.json b/invokeai/frontend/web/public/locales/zh_CN.json index 8aff73d2a1..45bab5c6da 100644 --- a/invokeai/frontend/web/public/locales/zh_CN.json +++ b/invokeai/frontend/web/public/locales/zh_CN.json @@ -66,7 +66,7 @@ "saveAs": "保存为", "ai": "ai", "or": "或", - "aboutDesc": "使用 Invoke 工作?查看:", + "aboutDesc": "使用 Invoke 工作?来看看:", "add": "添加", "loglevel": "日志级别", "copy": "复制", @@ -445,7 +445,6 @@ "useX2Model": "图像太大,无法使用 x4 模型,使用 x2 模型作为替代", "tooLarge": "图像太大无法进行放大,请选择更小的图像" }, - "iterationsWithCount_other": "{{count}} 次迭代生成", "cfgRescaleMultiplier": "CFG 重缩放倍数", "useSize": "使用尺寸", "setToOptimalSize": "优化模型大小", @@ -853,7 +852,6 @@ "pruneSucceeded": "从队列修剪 {{item_count}} 个已完成的项目", "notReady": "无法排队", "batchFailedToQueue": "批次加入队列失败", - "queueCountPrediction": "{{promptsCount}} 提示词 × {{iterations}} 迭代次数 -> {{count}} 次生成", "batchQueued": "加入队列的批次", "front": "前", "pruneTooltip": "修剪 {{item_count}} 个已完成的项目", diff --git a/invokeai/frontend/web/scripts/typegen.js b/invokeai/frontend/web/scripts/typegen.js index 758a0ef4f5..fa2d791350 100644 --- a/invokeai/frontend/web/scripts/typegen.js +++ b/invokeai/frontend/web/scripts/typegen.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import fs from 'node:fs'; import openapiTS from 'openapi-typescript'; diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 03c854bb48..2d878d96e7 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -12,7 +12,6 @@ import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys'; import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal'; import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal'; import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal'; -import { FloatingImageViewer } from 'features/gallery/components/ImageViewer/FloatingImageViewer'; import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast'; import { configChanged } from 'features/system/store/configSlice'; import { languageSelector } from 'features/system/store/systemSelectors'; @@ -22,10 +21,10 @@ import i18n from 'i18n'; import { size } from 'lodash-es'; import { memo, useCallback, useEffect } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; +import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; import AppErrorBoundaryFallback from './AppErrorBoundaryFallback'; import PreselectedImage from './PreselectedImage'; -import Toaster from './Toaster'; const DEFAULT_CONFIG = {}; @@ -47,6 +46,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => { useSocketIO(); useGlobalModifiersInit(); useGlobalHotkeys(); + useGetOpenAPISchemaQuery(); const { dropzone, isHandlingUpload, setIsHandlingUpload } = useFullscreenDropzone(); @@ -95,9 +95,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => { - - ); }; diff --git a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx index d2992a8cd9..ced3037a40 100644 --- a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx +++ b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx @@ -1,5 +1,8 @@ -import { Button, Flex, Heading, Link, Text, useToast } from '@invoke-ai/ui-library'; +import { Button, Flex, Heading, Image, Link, Text } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { toast } from 'features/toast/toast'; import newGithubIssueUrl from 'new-github-issue-url'; +import InvokeLogoYellow from 'public/assets/images/invoke-symbol-ylw-lrg.svg'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowCounterClockwiseBold, PiArrowSquareOutBold, PiCopyBold } from 'react-icons/pi'; @@ -11,31 +14,39 @@ type Props = { }; const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => { - const toast = useToast(); const { t } = useTranslation(); + const isLocal = useAppSelector((s) => s.config.isLocal); const handleCopy = useCallback(() => { const text = JSON.stringify(serializeError(error), null, 2); navigator.clipboard.writeText(`\`\`\`\n${text}\n\`\`\``); toast({ - title: 'Error Copied', + id: 'ERROR_COPIED', + title: t('toast.errorCopied'), }); - }, [error, toast]); + }, [error, t]); - const url = useMemo( - () => - newGithubIssueUrl({ + const url = useMemo(() => { + if (isLocal) { + return newGithubIssueUrl({ user: 'invoke-ai', repo: 'InvokeAI', template: 'BUG_REPORT.yml', title: `[bug]: ${error.name}: ${error.message}`, - }), - [error.message, error.name] - ); + }); + } else { + return 'https://support.invoke.ai/support/tickets/new'; + } + }, [error.message, error.name, isLocal]); + return ( - {t('common.somethingWentWrong')} + + invoke-logo + {t('common.somethingWentWrong')} + + { {t('common.copyError')} - + diff --git a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx index 0b4ca90933..aa3a24209c 100644 --- a/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx +++ b/invokeai/frontend/web/src/app/components/ThemeLocaleProvider.tsx @@ -19,6 +19,13 @@ function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) { return extendTheme({ ..._theme, direction, + shadows: { + ..._theme.shadows, + selectedForCompare: + '0px 0px 0px 1px var(--invoke-colors-base-900), 0px 0px 0px 4px var(--invoke-colors-green-400)', + hoverSelectedForCompare: + '0px 0px 0px 1px var(--invoke-colors-base-900), 0px 0px 0px 4px var(--invoke-colors-green-300)', + }, }); }, [direction]); diff --git a/invokeai/frontend/web/src/app/components/Toaster.ts b/invokeai/frontend/web/src/app/components/Toaster.ts deleted file mode 100644 index c86fd5060d..0000000000 --- a/invokeai/frontend/web/src/app/components/Toaster.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useToast } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { addToast, clearToastQueue } from 'features/system/store/systemSlice'; -import type { MakeToastArg } from 'features/system/util/makeToast'; -import { makeToast } from 'features/system/util/makeToast'; -import { memo, useCallback, useEffect } from 'react'; - -/** - * Logical component. Watches the toast queue and makes toasts when the queue is not empty. - * @returns null - */ -const Toaster = () => { - const dispatch = useAppDispatch(); - const toastQueue = useAppSelector((s) => s.system.toastQueue); - const toast = useToast(); - useEffect(() => { - toastQueue.forEach((t) => { - toast(t); - }); - toastQueue.length > 0 && dispatch(clearToastQueue()); - }, [dispatch, toast, toastQueue]); - - return null; -}; - -/** - * Returns a function that can be used to make a toast. - * @example - * const toaster = useAppToaster(); - * toaster('Hello world!'); - * toaster({ title: 'Hello world!', status: 'success' }); - * @returns A function that can be used to make a toast. - * @see makeToast - * @see MakeToastArg - * @see UseToastOptions - */ -export const useAppToaster = () => { - const dispatch = useAppDispatch(); - const toaster = useCallback((arg: MakeToastArg) => dispatch(addToast(makeToast(arg))), [dispatch]); - - return toaster; -}; - -export default memo(Toaster); diff --git a/invokeai/frontend/web/src/app/hooks/useSocketIO.ts b/invokeai/frontend/web/src/app/hooks/useSocketIO.ts index e1c4cebdb9..d3baf5f452 100644 --- a/invokeai/frontend/web/src/app/hooks/useSocketIO.ts +++ b/invokeai/frontend/web/src/app/hooks/useSocketIO.ts @@ -6,8 +6,8 @@ import { useAppDispatch } from 'app/store/storeHooks'; import type { MapStore } from 'nanostores'; import { atom, map } from 'nanostores'; import { useEffect, useMemo } from 'react'; +import { setEventListeners } from 'services/events/setEventListeners'; import type { ClientToServerEvents, ServerToClientEvents } from 'services/events/types'; -import { setEventListeners } from 'services/events/util/setEventListeners'; import type { ManagerOptions, Socket, SocketOptions } from 'socket.io-client'; import { io } from 'socket.io-client'; @@ -67,6 +67,8 @@ export const useSocketIO = () => { if ($isDebugging.get() || import.meta.env.MODE === 'development') { window.$socketOptions = $socketOptions; + // This is only enabled manually for debugging, console is allowed. + /* eslint-disable-next-line no-console */ console.log('Socket initialized', socket); } @@ -75,6 +77,8 @@ export const useSocketIO = () => { return () => { if ($isDebugging.get() || import.meta.env.MODE === 'development') { window.$socketOptions = undefined; + // This is only enabled manually for debugging, console is allowed. + /* eslint-disable-next-line no-console */ console.log('Socket teardown', socket); } socket.disconnect(); diff --git a/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts b/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts index b6df6dab94..89010275d1 100644 --- a/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts +++ b/invokeai/frontend/web/src/app/store/middleware/debugLoggerMiddleware.ts @@ -1,3 +1,6 @@ +/* eslint-disable no-console */ +// This is only enabled manually for debugging, console is allowed. + import type { Middleware, MiddlewareAPI } from '@reduxjs/toolkit'; import { diff } from 'jsondiffpatch'; diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts index 508109caf5..f0ea175aec 100644 --- a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts @@ -1,7 +1,6 @@ import type { UnknownAction } from '@reduxjs/toolkit'; import { deepClone } from 'common/util/deepClone'; import { isAnyGraphBuilt } from 'features/nodes/store/actions'; -import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice'; import { appInfoApi } from 'services/api/endpoints/appInfo'; import type { Graph } from 'services/api/types'; import { socketGeneratorProgress } from 'services/events/actions'; @@ -25,13 +24,6 @@ export const actionSanitizer = (action: A): A => { }; } - if (nodeTemplatesBuilt.match(action)) { - return { - ...action, - payload: '', - }; - } - if (socketGeneratorProgress.match(action)) { const sanitized = deepClone(action); if (sanitized.payload.data.progress_image) { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index 0c0c8ed2bc..0fd2f1b79c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -35,28 +35,22 @@ import { addImageUploadedFulfilledListener } from 'app/store/middleware/listener import { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected'; import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded'; import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged'; +import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMiddleware/listeners/setDefaultSettings'; import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected'; import { addSocketDisconnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected'; import { addGeneratorProgressEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress'; -import { addGraphExecutionStateCompleteEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketGraphExecutionStateComplete'; import { addInvocationCompleteEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete'; import { addInvocationErrorEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError'; -import { addInvocationRetrievalErrorEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationRetrievalError'; import { addInvocationStartedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted'; import { addModelInstallEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall'; import { addModelLoadEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad'; import { addSocketQueueItemStatusChangedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged'; -import { addSessionRetrievalErrorEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketSessionRetrievalError'; -import { addSocketSubscribedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketSubscribed'; -import { addSocketUnsubscribedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketUnsubscribed'; import { addStagingAreaImageSavedListener } from 'app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved'; 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 type { AppDispatch, RootState } from 'app/store/store'; -import { addSetDefaultSettingsListener } from './listeners/setDefaultSettings'; - export const listenerMiddleware = createListenerMiddleware(); export type AppStartListening = TypedStartListening; @@ -104,18 +98,13 @@ addCommitStagingAreaImageListener(startAppListening); // Socket.IO addGeneratorProgressEventListener(startAppListening); -addGraphExecutionStateCompleteEventListener(startAppListening); addInvocationCompleteEventListener(startAppListening); addInvocationErrorEventListener(startAppListening); addInvocationStartedEventListener(startAppListening); addSocketConnectedEventListener(startAppListening); addSocketDisconnectedEventListener(startAppListening); -addSocketSubscribedEventListener(startAppListening); -addSocketUnsubscribedEventListener(startAppListening); addModelLoadEventListener(startAppListening); addModelInstallEventListener(startAppListening); -addSessionRetrievalErrorEventListener(startAppListening); -addInvocationRetrievalErrorEventListener(startAppListening); addSocketQueueItemStatusChangedEventListener(startAppListening); addBulkDownloadListeners(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index ae26531722..9095a08431 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -8,7 +8,7 @@ import { resetCanvas, setInitialCanvasImage, } from 'features/canvas/store/canvasSlice'; -import { addToast } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { queueApi } from 'services/api/endpoints/queue'; @@ -30,22 +30,20 @@ export const addCommitStagingAreaImageListener = (startAppListening: AppStartLis req.reset(); if (canceled > 0) { log.debug(`Canceled ${canceled} canvas batches`); - dispatch( - addToast({ - title: t('queue.cancelBatchSucceeded'), - status: 'success', - }) - ); + toast({ + id: 'CANCEL_BATCH_SUCCEEDED', + title: t('queue.cancelBatchSucceeded'), + status: 'success', + }); } dispatch(canvasBatchIdsReset()); } catch { log.error('Failed to cancel canvas batches'); - dispatch( - addToast({ - title: t('queue.cancelBatchFailed'), - status: 'error', - }) - ); + toast({ + id: 'CANCEL_BATCH_FAILED', + title: t('queue.cancelBatchFailed'), + status: 'error', + }); } }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts index 68eda997b7..3f74bf9b61 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts @@ -1,8 +1,8 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { parseify } from 'common/util/serialize'; -import { toast } from 'common/util/toast'; import { zPydanticValidationError } from 'features/system/store/zodSchemas'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { truncate, upperFirst } from 'lodash-es'; import { queueApi } from 'services/api/endpoints/queue'; @@ -16,18 +16,15 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) = const arg = action.meta.arg.originalArgs; logger('queue').debug({ enqueueResult: parseify(response) }, 'Batch enqueued'); - if (!toast.isActive('batch-queued')) { - toast({ - id: 'batch-queued', - title: t('queue.batchQueued'), - description: t('queue.batchQueuedDesc', { - count: response.enqueued, - direction: arg.prepend ? t('queue.front') : t('queue.back'), - }), - duration: 1000, - status: 'success', - }); - } + toast({ + id: 'QUEUE_BATCH_SUCCEEDED', + title: t('queue.batchQueued'), + status: 'success', + description: t('queue.batchQueuedDesc', { + count: response.enqueued, + direction: arg.prepend ? t('queue.front') : t('queue.back'), + }), + }); }, }); @@ -40,9 +37,10 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) = if (!response) { toast({ + id: 'QUEUE_BATCH_FAILED', title: t('queue.batchFailedToQueue'), status: 'error', - description: 'Unknown Error', + description: t('common.unknownError'), }); logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue')); return; @@ -52,7 +50,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) = if (result.success) { result.data.data.detail.map((e) => { toast({ - id: 'batch-failed-to-queue', + id: 'QUEUE_BATCH_FAILED', title: truncate(upperFirst(e.msg), { length: 128 }), status: 'error', description: truncate( @@ -64,9 +62,10 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) = }); } else if (response.status !== 403) { toast({ + id: 'QUEUE_BATCH_FAILED', title: t('queue.batchFailedToQueue'), - description: t('common.unknownError'), status: 'error', + description: t('common.unknownError'), }); } logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue')); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index a0b07b9419..244e0cdf8a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -21,7 +21,7 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS const { canvas, nodes, controlAdapters, controlLayers } = getState(); deleted_images.forEach((image_name) => { - const imageUsage = getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, image_name); + const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, controlLayers.present, image_name); if (imageUsage.isCanvasImage && !wasCanvasReset) { dispatch(resetCanvas()); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx index 38a0fd7911..489f218370 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx @@ -1,13 +1,12 @@ -import type { UseToastOptions } from '@invoke-ai/ui-library'; import { ExternalLink } from '@invoke-ai/ui-library'; import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { toast } from 'common/util/toast'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; import { - socketBulkDownloadCompleted, - socketBulkDownloadFailed, + socketBulkDownloadComplete, + socketBulkDownloadError, socketBulkDownloadStarted, } from 'services/events/actions'; @@ -28,7 +27,6 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) = // Show the response message if it exists, otherwise show the default message description: action.payload.response || t('gallery.bulkDownloadRequestedDesc'), duration: null, - isClosable: true, }); }, }); @@ -40,9 +38,9 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) = // There isn't any toast to update if we get this event. toast({ + id: 'BULK_DOWNLOAD_REQUEST_FAILED', title: t('gallery.bulkDownloadRequestFailed'), - status: 'success', - isClosable: true, + status: 'error', }); }, }); @@ -56,7 +54,7 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) = }); startAppListening({ - actionCreator: socketBulkDownloadCompleted, + actionCreator: socketBulkDownloadComplete, effect: async (action) => { log.debug(action.payload.data, 'Bulk download preparation completed'); @@ -65,7 +63,7 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) = // TODO(psyche): This URL may break in in some environments (e.g. Nvidia workbench) but we need to test it first const url = `/api/v1/images/download/${bulk_download_item_name}`; - const toastOptions: UseToastOptions = { + toast({ id: bulk_download_item_name, title: t('gallery.bulkDownloadReady', 'Download ready'), status: 'success', @@ -77,38 +75,24 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) = /> ), duration: null, - isClosable: true, - }; - - if (toast.isActive(bulk_download_item_name)) { - toast.update(bulk_download_item_name, toastOptions); - } else { - toast(toastOptions); - } + }); }, }); startAppListening({ - actionCreator: socketBulkDownloadFailed, + actionCreator: socketBulkDownloadError, effect: async (action) => { log.debug(action.payload.data, 'Bulk download preparation failed'); const { bulk_download_item_name } = action.payload.data; - const toastOptions: UseToastOptions = { + toast({ id: bulk_download_item_name, title: t('gallery.bulkDownloadFailed'), status: 'error', description: action.payload.data.error, duration: null, - isClosable: true, - }; - - if (toast.isActive(bulk_download_item_name)) { - toast.update(bulk_download_item_name, toastOptions); - } else { - toast(toastOptions); - } + }); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts index e1f4804d56..311dda3e2e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts @@ -2,14 +2,14 @@ import { $logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { canvasCopiedToClipboard } from 'features/canvas/store/actions'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; -import { addToast } from 'features/system/store/systemSlice'; import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; export const addCanvasCopiedToClipboardListener = (startAppListening: AppStartListening) => { startAppListening({ actionCreator: canvasCopiedToClipboard, - effect: async (action, { dispatch, getState }) => { + effect: async (action, { getState }) => { const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' }); const state = getState(); @@ -19,22 +19,20 @@ export const addCanvasCopiedToClipboardListener = (startAppListening: AppStartLi copyBlobToClipboard(blob); } catch (err) { moduleLog.error(String(err)); - dispatch( - addToast({ - title: t('toast.problemCopyingCanvas'), - description: t('toast.problemCopyingCanvasDesc'), - status: 'error', - }) - ); + toast({ + id: 'CANVAS_COPY_FAILED', + title: t('toast.problemCopyingCanvas'), + description: t('toast.problemCopyingCanvasDesc'), + status: 'error', + }); return; } - dispatch( - addToast({ - title: t('toast.canvasCopiedClipboard'), - status: 'success', - }) - ); + toast({ + id: 'CANVAS_COPY_SUCCEEDED', + title: t('toast.canvasCopiedClipboard'), + status: 'success', + }); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts index 5b8150bd20..71e616b9ea 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts @@ -3,13 +3,13 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { canvasDownloadedAsImage } from 'features/canvas/store/actions'; import { downloadBlob } from 'features/canvas/util/downloadBlob'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; -import { addToast } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; export const addCanvasDownloadedAsImageListener = (startAppListening: AppStartListening) => { startAppListening({ actionCreator: canvasDownloadedAsImage, - effect: async (action, { dispatch, getState }) => { + effect: async (action, { getState }) => { const moduleLog = $logger.get().child({ namespace: 'canvasSavedToGalleryListener' }); const state = getState(); @@ -18,18 +18,17 @@ export const addCanvasDownloadedAsImageListener = (startAppListening: AppStartLi blob = await getBaseLayerBlob(state); } catch (err) { moduleLog.error(String(err)); - dispatch( - addToast({ - title: t('toast.problemDownloadingCanvas'), - description: t('toast.problemDownloadingCanvasDesc'), - status: 'error', - }) - ); + toast({ + id: 'CANVAS_DOWNLOAD_FAILED', + title: t('toast.problemDownloadingCanvas'), + description: t('toast.problemDownloadingCanvasDesc'), + status: 'error', + }); return; } downloadBlob(blob, 'canvas.png'); - dispatch(addToast({ title: t('toast.canvasDownloaded'), status: 'success' })); + toast({ id: 'CANVAS_DOWNLOAD_SUCCEEDED', title: t('toast.canvasDownloaded'), status: 'success' }); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts index 55392ebff4..2aa1f52d6c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts @@ -3,7 +3,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { canvasImageToControlAdapter } from 'features/canvas/store/actions'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { addToast } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; @@ -20,13 +20,12 @@ export const addCanvasImageToControlNetListener = (startAppListening: AppStartLi blob = await getBaseLayerBlob(state, true); } catch (err) { log.error(String(err)); - dispatch( - addToast({ - title: t('toast.problemSavingCanvas'), - description: t('toast.problemSavingCanvasDesc'), - status: 'error', - }) - ); + toast({ + id: 'PROBLEM_SAVING_CANVAS', + title: t('toast.problemSavingCanvas'), + description: t('toast.problemSavingCanvasDesc'), + status: 'error', + }); return; } @@ -43,7 +42,7 @@ export const addCanvasImageToControlNetListener = (startAppListening: AppStartLi crop_visible: false, postUploadAction: { type: 'TOAST', - toastOptions: { title: t('toast.canvasSentControlnetAssets') }, + title: t('toast.canvasSentControlnetAssets'), }, }) ).unwrap(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts index af0c3878fc..454342b997 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts @@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { canvasMaskSavedToGallery } from 'features/canvas/store/actions'; import { getCanvasData } from 'features/canvas/util/getCanvasData'; -import { addToast } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; @@ -29,13 +29,12 @@ export const addCanvasMaskSavedToGalleryListener = (startAppListening: AppStartL if (!maskBlob) { log.error('Problem getting mask layer blob'); - dispatch( - addToast({ - title: t('toast.problemSavingMask'), - description: t('toast.problemSavingMaskDesc'), - status: 'error', - }) - ); + toast({ + id: 'PROBLEM_SAVING_MASK', + title: t('toast.problemSavingMask'), + description: t('toast.problemSavingMaskDesc'), + status: 'error', + }); return; } @@ -52,7 +51,7 @@ export const addCanvasMaskSavedToGalleryListener = (startAppListening: AppStartL crop_visible: true, postUploadAction: { type: 'TOAST', - toastOptions: { title: t('toast.maskSavedAssets') }, + title: t('toast.maskSavedAssets'), }, }) ); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts index 569b4badc7..2e6ca61d8a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet.ts @@ -3,7 +3,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { canvasMaskToControlAdapter } from 'features/canvas/store/actions'; import { getCanvasData } from 'features/canvas/util/getCanvasData'; import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { addToast } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; @@ -30,13 +30,12 @@ export const addCanvasMaskToControlNetListener = (startAppListening: AppStartLis if (!maskBlob) { log.error('Problem getting mask layer blob'); - dispatch( - addToast({ - title: t('toast.problemImportingMask'), - description: t('toast.problemImportingMaskDesc'), - status: 'error', - }) - ); + toast({ + id: 'PROBLEM_IMPORTING_MASK', + title: t('toast.problemImportingMask'), + description: t('toast.problemImportingMaskDesc'), + status: 'error', + }); return; } @@ -53,7 +52,7 @@ export const addCanvasMaskToControlNetListener = (startAppListening: AppStartLis crop_visible: false, postUploadAction: { type: 'TOAST', - toastOptions: { title: t('toast.maskSentControlnetAssets') }, + title: t('toast.maskSentControlnetAssets'), }, }) ).unwrap(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts index 71b0e62b44..9ae6de2e76 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts @@ -4,7 +4,7 @@ import { canvasMerged } from 'features/canvas/store/actions'; import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore'; import { setMergedCanvas } from 'features/canvas/store/canvasSlice'; import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob'; -import { addToast } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; @@ -17,13 +17,12 @@ export const addCanvasMergedListener = (startAppListening: AppStartListening) => if (!blob) { moduleLog.error('Problem getting base layer blob'); - dispatch( - addToast({ - title: t('toast.problemMergingCanvas'), - description: t('toast.problemMergingCanvasDesc'), - status: 'error', - }) - ); + toast({ + id: 'PROBLEM_MERGING_CANVAS', + title: t('toast.problemMergingCanvas'), + description: t('toast.problemMergingCanvasDesc'), + status: 'error', + }); return; } @@ -31,13 +30,12 @@ export const addCanvasMergedListener = (startAppListening: AppStartListening) => if (!canvasBaseLayer) { moduleLog.error('Problem getting canvas base layer'); - dispatch( - addToast({ - title: t('toast.problemMergingCanvas'), - description: t('toast.problemMergingCanvasDesc'), - status: 'error', - }) - ); + toast({ + id: 'PROBLEM_MERGING_CANVAS', + title: t('toast.problemMergingCanvas'), + description: t('toast.problemMergingCanvasDesc'), + status: 'error', + }); return; } @@ -54,7 +52,7 @@ export const addCanvasMergedListener = (startAppListening: AppStartListening) => is_intermediate: true, postUploadAction: { type: 'TOAST', - toastOptions: { title: t('toast.canvasMerged') }, + title: t('toast.canvasMerged'), }, }) ).unwrap(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts index e3ba988886..71586b5f6e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts @@ -1,8 +1,9 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { parseify } from 'common/util/serialize'; import { canvasSavedToGallery } from 'features/canvas/store/actions'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; -import { addToast } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; @@ -18,13 +19,12 @@ export const addCanvasSavedToGalleryListener = (startAppListening: AppStartListe blob = await getBaseLayerBlob(state); } catch (err) { log.error(String(err)); - dispatch( - addToast({ - title: t('toast.problemSavingCanvas'), - description: t('toast.problemSavingCanvasDesc'), - status: 'error', - }) - ); + toast({ + id: 'CANVAS_SAVE_FAILED', + title: t('toast.problemSavingCanvas'), + description: t('toast.problemSavingCanvasDesc'), + status: 'error', + }); return; } @@ -41,7 +41,10 @@ export const addCanvasSavedToGalleryListener = (startAppListening: AppStartListe crop_visible: true, postUploadAction: { type: 'TOAST', - toastOptions: { title: t('toast.canvasSavedGallery') }, + title: t('toast.canvasSavedGallery'), + }, + metadata: { + _canvas_objects: parseify(state.canvas.layerState.objects), }, }) ); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index 7d5aa27f20..ba04947a2d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -1,60 +1,56 @@ import { isAnyOf } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import type { AppDispatch } from 'app/store/store'; import { parseify } from 'common/util/serialize'; import { caLayerImageChanged, - caLayerIsProcessingImageChanged, caLayerModelChanged, caLayerProcessedImageChanged, caLayerProcessorConfigChanged, + caLayerProcessorPendingBatchIdChanged, + caLayerRecalled, isControlAdapterLayer, } from 'features/controlLayers/store/controlLayersSlice'; import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; -import { isImageOutput } from 'features/nodes/types/common'; -import { addToast } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { isEqual } from 'lodash-es'; -import { imagesApi } from 'services/api/endpoints/images'; +import { getImageDTO } from 'services/api/endpoints/images'; import { queueApi } from 'services/api/endpoints/queue'; -import type { BatchConfig, ImageDTO } from 'services/api/types'; +import type { BatchConfig } from 'services/api/types'; import { socketInvocationComplete } from 'services/events/actions'; +import { assert } from 'tsafe'; -const matcher = isAnyOf(caLayerImageChanged, caLayerProcessorConfigChanged, caLayerModelChanged); +const matcher = isAnyOf(caLayerImageChanged, caLayerProcessorConfigChanged, caLayerModelChanged, caLayerRecalled); const DEBOUNCE_MS = 300; const log = logger('session'); +/** + * Simple helper to cancel a batch and reset the pending batch ID + */ +const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batchId: string) => { + const req = dispatch(queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: [batchId] })); + log.trace({ batchId }, 'Cancelling existing preprocessor batch'); + try { + await req.unwrap(); + } catch { + // no-op + } finally { + req.reset(); + // Always reset the pending batch ID - the cancel req could fail if the batch doesn't exist + dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null })); + } +}; + export const addControlAdapterPreprocessor = (startAppListening: AppStartListening) => { startAppListening({ matcher, - effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take }) => { - const { layerId } = action.payload; - const precheckLayerOriginal = getOriginalState() - .controlLayers.present.layers.filter(isControlAdapterLayer) - .find((l) => l.id === layerId); - const precheckLayer = getState() - .controlLayers.present.layers.filter(isControlAdapterLayer) - .find((l) => l.id === layerId); - - // Conditions to bail - const layerDoesNotExist = !precheckLayer; - const layerHasNoImage = !precheckLayer?.controlAdapter.image; - const layerHasNoProcessorConfig = !precheckLayer?.controlAdapter.processorConfig; - const layerIsAlreadyProcessingImage = precheckLayer?.controlAdapter.isProcessingImage; - const areImageAndProcessorUnchanged = - isEqual(precheckLayer?.controlAdapter.image, precheckLayerOriginal?.controlAdapter.image) && - isEqual(precheckLayer?.controlAdapter.processorConfig, precheckLayerOriginal?.controlAdapter.processorConfig); - - if ( - layerDoesNotExist || - layerHasNoImage || - layerHasNoProcessorConfig || - areImageAndProcessorUnchanged || - layerIsAlreadyProcessingImage - ) { - return; - } + effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take, signal }) => { + const layerId = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId; + const state = getState(); + const originalState = getOriginalState(); // Cancel any in-progress instances of this listener cancelActiveListeners(); @@ -62,27 +58,55 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni // Delay before starting actual work await delay(DEBOUNCE_MS); - dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: true })); - // Double-check that we are still eligible for processing - const state = getState(); const layer = state.controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); - const image = layer?.controlAdapter.image; - const config = layer?.controlAdapter.processorConfig; - // If we have no image or there is no processor config, bail - if (!layer || !image || !config) { + if (!layer) { return; } - // @ts-expect-error: TS isn't able to narrow the typing of buildNode and `config` will error... - const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config); + // We should only process if the processor settings or image have changed + const originalLayer = originalState.controlLayers.present.layers + .filter(isControlAdapterLayer) + .find((l) => l.id === layerId); + const originalImage = originalLayer?.controlAdapter.image; + const originalConfig = originalLayer?.controlAdapter.processorConfig; + + const image = layer.controlAdapter.image; + const config = layer.controlAdapter.processorConfig; + + if (isEqual(config, originalConfig) && isEqual(image, originalImage)) { + // Neither config nor image have changed, we can bail + return; + } + + if (!image || !config) { + // - If we have no image, we have nothing to process + // - If we have no processor config, we have nothing to process + // Clear the processed image and bail + dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null })); + return; + } + + // At this point, the user has stopped fiddling with the processor settings and there is a processor selected. + + // If there is a pending processor batch, cancel it. + if (layer.controlAdapter.processorPendingBatchId) { + cancelProcessorBatch(dispatch, layerId, layer.controlAdapter.processorPendingBatchId); + } + + // TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now + const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config as never); const enqueueBatchArg: BatchConfig = { prepend: true, batch: { graph: { nodes: { - [processorNode.id]: { ...processorNode, is_intermediate: true }, + [processorNode.id]: { + ...processorNode, + // Control images are always intermediate - do not save to gallery + is_intermediate: true, + }, }, edges: [], }, @@ -90,66 +114,73 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni }, }; + // Kick off the processor batch + const req = dispatch( + queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, { + fixedCacheKey: 'enqueueBatch', + }) + ); + try { - const req = dispatch( - queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, { - fixedCacheKey: 'enqueueBatch', - }) - ); const enqueueResult = await req.unwrap(); - req.reset(); + // TODO(psyche): Update the pydantic models, pretty sure we will _always_ have a batch_id here, but the model says it's optional + assert(enqueueResult.batch.batch_id, 'Batch ID not returned from queue'); + dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id })); log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued')); + // Wait for the processor node to complete const [invocationCompleteAction] = await take( (action): action is ReturnType => socketInvocationComplete.match(action) && - action.payload.data.queue_batch_id === enqueueResult.batch.batch_id && - action.payload.data.source_node_id === processorNode.id + action.payload.data.batch_id === enqueueResult.batch.batch_id && + action.payload.data.invocation_source_id === processorNode.id ); // We still have to check the output type - if (isImageOutput(invocationCompleteAction.payload.data.result)) { - const { image_name } = invocationCompleteAction.payload.data.result.image; + assert( + invocationCompleteAction.payload.data.result.type === 'image_output', + `Processor did not return an image output, got: ${invocationCompleteAction.payload.data.result}` + ); + const { image_name } = invocationCompleteAction.payload.data.result.image; - // Wait for the ImageDTO to be received - const [{ payload }] = await take( - (action) => - imagesApi.endpoints.getImageDTO.matchFulfilled(action) && action.payload.image_name === image_name - ); + const imageDTO = await getImageDTO(image_name); + assert(imageDTO, "Failed to fetch processor output's image DTO"); - const imageDTO = payload as ImageDTO; - - log.debug({ layerId, imageDTO }, 'ControlNet image processed'); - - // Update the processed image in the store - dispatch( - caLayerProcessedImageChanged({ - layerId, - imageDTO, - }) - ); - dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: false })); - } + // Whew! We made it. Update the layer with the processed image + log.debug({ layerId, imageDTO }, 'ControlNet image processed'); + dispatch(caLayerProcessedImageChanged({ layerId, imageDTO })); + dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null })); } catch (error) { - console.log(error); - log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue')); - dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: false })); + if (signal.aborted) { + // The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now). + const pendingBatchId = getState() + .controlLayers.present.layers.filter(isControlAdapterLayer) + .find((l) => l.id === layerId)?.controlAdapter.processorPendingBatchId; + if (pendingBatchId) { + cancelProcessorBatch(dispatch, layerId, pendingBatchId); + } + log.trace('Control Adapter preprocessor cancelled'); + } else { + // Some other error condition... + log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue')); - if (error instanceof Object) { - if ('data' in error && 'status' in error) { - if (error.status === 403) { - dispatch(caLayerImageChanged({ layerId, imageDTO: null })); - return; + if (error instanceof Object) { + if ('data' in error && 'status' in error) { + if (error.status === 403) { + dispatch(caLayerImageChanged({ layerId, imageDTO: null })); + return; + } } } - } - dispatch( - addToast({ + toast({ + id: 'GRAPH_QUEUE_FAILED', title: t('queue.graphFailedToQueue'), status: 'error', - }) - ); + }); + } + } finally { + req.reset(); } }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts index 0055866aa7..574dad00eb 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts @@ -9,8 +9,7 @@ import { selectControlAdapterById, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { isImageOutput } from 'features/nodes/types/common'; -import { addToast } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; import { queueApi } from 'services/api/endpoints/queue'; @@ -69,12 +68,12 @@ export const addControlNetImageProcessedListener = (startAppListening: AppStartL const [invocationCompleteAction] = await take( (action): action is ReturnType => socketInvocationComplete.match(action) && - action.payload.data.queue_batch_id === enqueueResult.batch.batch_id && - action.payload.data.source_node_id === nodeId + action.payload.data.batch_id === enqueueResult.batch.batch_id && + action.payload.data.invocation_source_id === nodeId ); // We still have to check the output type - if (isImageOutput(invocationCompleteAction.payload.data.result)) { + if (invocationCompleteAction.payload.data.result.type === 'image_output') { const { image_name } = invocationCompleteAction.payload.data.result.image; // Wait for the ImageDTO to be received @@ -108,12 +107,11 @@ export const addControlNetImageProcessedListener = (startAppListening: AppStartL } } - dispatch( - addToast({ - title: t('queue.graphFailedToQueue'), - status: 'error', - }) - ); + toast({ + id: 'GRAPH_QUEUE_FAILED', + title: t('queue.graphFailedToQueue'), + status: 'error', + }); } }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts index cdcc99ade2..a7491ab01b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts @@ -8,8 +8,8 @@ import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; import { getCanvasData } from 'features/canvas/util/getCanvasData'; import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode'; import { canvasGraphBuilt } from 'features/nodes/store/actions'; -import { buildCanvasGraph } from 'features/nodes/util/graph/buildCanvasGraph'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; +import { buildCanvasGraph } from 'features/nodes/util/graph/canvas/buildCanvasGraph'; import { imagesApi } from 'services/api/endpoints/images'; import { queueApi } from 'services/api/endpoints/queue'; import type { ImageDTO } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 557220c449..6ca7ee7ffa 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -1,8 +1,9 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { buildGenerationTabGraph } from 'features/nodes/util/graph/buildGenerationTabGraph'; -import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph'; +import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; +import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph'; +import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph'; import { queueApi } from 'services/api/endpoints/queue'; export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => { @@ -11,12 +12,13 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) enqueueRequested.match(action) && action.payload.tabName === 'generation', effect: async (action, { getState, dispatch }) => { const state = getState(); + const { shouldShowProgressInViewer } = state.ui; const model = state.generation.model; const { prepend } = action.payload; let graph; - if (model && model.base === 'sdxl') { + if (model?.base === 'sdxl') { graph = await buildGenerationTabSDXLGraph(state); } else { graph = await buildGenerationTabGraph(state); @@ -29,7 +31,14 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) fixedCacheKey: 'enqueueBatch', }) ); - req.reset(); + try { + await req.unwrap(); + if (shouldShowProgressInViewer) { + dispatch(isImageViewerOpenChanged(true)); + } + } finally { + req.reset(); + } }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index 8d39daaef8..c4087aacde 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -11,9 +11,9 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) = enqueueRequested.match(action) && action.payload.tabName === 'workflows', effect: async (action, { getState, dispatch }) => { const state = getState(); - const { nodes, edges } = state.nodes; + const { nodes, edges } = state.nodes.present; const workflow = state.workflow; - const graph = buildNodesGraph(state.nodes); + const graph = buildNodesGraph(state.nodes.present); const builtWorkflow = buildWorkflowWithValidation({ nodes, edges, @@ -39,7 +39,11 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) = fixedCacheKey: 'enqueueBatch', }) ); - req.reset(); + try { + await req.unwrap(); + } finally { + req.reset(); + } }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts index 6b8c9b4ea3..43f9355125 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/galleryImageClicked.ts @@ -1,7 +1,7 @@ import { createAction } from '@reduxjs/toolkit'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; -import { isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice'; +import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice'; import { imagesApi } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import { imagesSelectors } from 'services/api/util'; @@ -11,6 +11,7 @@ export const galleryImageClicked = createAction<{ shiftKey: boolean; ctrlKey: boolean; metaKey: boolean; + altKey: boolean; }>('gallery/imageClicked'); /** @@ -28,7 +29,7 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen startAppListening({ actionCreator: galleryImageClicked, effect: async (action, { dispatch, getState }) => { - const { imageDTO, shiftKey, ctrlKey, metaKey } = action.payload; + const { imageDTO, shiftKey, ctrlKey, metaKey, altKey } = action.payload; const state = getState(); const queryArgs = selectListImagesQueryArgs(state); const { data: listImagesData } = imagesApi.endpoints.listImages.select(queryArgs)(state); @@ -41,7 +42,13 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen const imageDTOs = imagesSelectors.selectAll(listImagesData); const selection = state.gallery.selection; - if (shiftKey) { + if (altKey) { + if (state.gallery.imageToCompare?.image_name === imageDTO.image_name) { + dispatch(imageToCompareChanged(null)); + } else { + dispatch(imageToCompareChanged(imageDTO)); + } + } else if (shiftKey) { const rangeEndImageName = imageDTO.image_name; const lastSelectedImage = selection[selection.length - 1]?.image_name; const lastClickedIndex = imageDTOs.findIndex((n) => n.image_name === lastSelectedImage); @@ -62,7 +69,6 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen } else { dispatch(selectionChanged([imageDTO])); } - dispatch(isImageViewerOpenChanged(true)); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts index acb2bdb698..923b2c0197 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts @@ -1,7 +1,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { parseify } from 'common/util/serialize'; -import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { parseSchema } from 'features/nodes/util/schema/parseSchema'; import { size } from 'lodash-es'; import { appInfoApi } from 'services/api/endpoints/appInfo'; @@ -9,7 +9,7 @@ import { appInfoApi } from 'services/api/endpoints/appInfo'; export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening) => { startAppListening({ matcher: appInfoApi.endpoints.getOpenAPISchema.matchFulfilled, - effect: (action, { dispatch, getState }) => { + effect: (action, { getState }) => { const log = logger('system'); const schemaJSON = action.payload; @@ -20,7 +20,7 @@ export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening log.debug({ nodeTemplates: parseify(nodeTemplates) }, `Built ${size(nodeTemplates)} node templates`); - dispatch(nodeTemplatesBuilt(nodeTemplates)); + $templates.set(nodeTemplates); }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts index 95d17da653..8c24badc76 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts @@ -29,7 +29,7 @@ import type { ImageDTO } from 'services/api/types'; import { imagesSelectors } from 'services/api/util'; const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { - state.nodes.nodes.forEach((node) => { + state.nodes.present.nodes.forEach((node) => { if (!isInvocationNode(node)) { return; } @@ -73,25 +73,25 @@ const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, ima const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => { state.controlLayers.present.layers.forEach((l) => { if (isRegionalGuidanceLayer(l)) { - if (l.ipAdapters.some((ipa) => ipa.image?.imageName === imageDTO.image_name)) { + if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) { dispatch(layerDeleted(l.id)); } } if (isControlAdapterLayer(l)) { if ( - l.controlAdapter.image?.imageName === imageDTO.image_name || - l.controlAdapter.processedImage?.imageName === imageDTO.image_name + l.controlAdapter.image?.name === imageDTO.image_name || + l.controlAdapter.processedImage?.name === imageDTO.image_name ) { dispatch(layerDeleted(l.id)); } } if (isIPAdapterLayer(l)) { - if (l.ipAdapter.image?.imageName === imageDTO.image_name) { + if (l.ipAdapter.image?.name === imageDTO.image_name) { dispatch(layerDeleted(l.id)); } } if (isInitialImageLayer(l)) { - if (l.image?.imageName === imageDTO.image_name) { + if (l.image?.name === imageDTO.image_name) { dispatch(layerDeleted(l.id)); } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 9bc9635299..7cb0703af8 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -14,7 +14,8 @@ import { rgLayerIPAdapterImageChanged, } from 'features/controlLayers/store/controlLayersSlice'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -import { imageSelected } from 'features/gallery/store/gallerySlice'; +import { isValidDrop } from 'features/dnd/util/isValidDrop'; +import { imageSelected, imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { imagesApi } from 'services/api/endpoints/images'; @@ -30,6 +31,9 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => effect: async (action, { dispatch, getState }) => { const log = logger('dnd'); const { activeData, overData } = action.payload; + if (!isValidDrop(overData, activeData)) { + return; + } if (activeData.payloadType === 'IMAGE_DTO') { log.debug({ activeData, overData }, 'Image dropped'); @@ -50,6 +54,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => activeData.payload.imageDTO ) { dispatch(imageSelected(activeData.payload.imageDTO)); + dispatch(isImageViewerOpenChanged(true)); return; } @@ -182,24 +187,18 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => } /** - * TODO - * Image selection dropped on node image collection field + * Image selected for compare */ - // if ( - // overData.actionType === 'SET_MULTI_NODES_IMAGE' && - // activeData.payloadType === 'IMAGE_DTO' && - // activeData.payload.imageDTO - // ) { - // const { fieldName, nodeId } = overData.context; - // dispatch( - // fieldValueChanged({ - // nodeId, - // fieldName, - // value: [activeData.payload.imageDTO], - // }) - // ); - // return; - // } + if ( + overData.actionType === 'SELECT_FOR_COMPARE' && + activeData.payloadType === 'IMAGE_DTO' && + activeData.payload.imageDTO + ) { + const { imageDTO } = activeData.payload; + dispatch(imageToCompareChanged(imageDTO)); + dispatch(isImageViewerOpenChanged(true)); + return; + } /** * Image dropped on user board diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index d5d74bf668..cd5304c32b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -1,4 +1,3 @@ -import type { UseToastOptions } from '@invoke-ai/ui-library'; import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; @@ -14,7 +13,7 @@ import { } from 'features/controlLayers/store/controlLayersSlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; -import { addToast } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { omit } from 'lodash-es'; import { boardsApi } from 'services/api/endpoints/boards'; @@ -42,16 +41,17 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis return; } - const DEFAULT_UPLOADED_TOAST: UseToastOptions = { + const DEFAULT_UPLOADED_TOAST = { + id: 'IMAGE_UPLOADED', title: t('toast.imageUploaded'), status: 'success', - }; + } as const; // default action - just upload and alert user if (postUploadAction?.type === 'TOAST') { - const { toastOptions } = postUploadAction; if (!autoAddBoardId || autoAddBoardId === 'none') { - dispatch(addToast({ ...DEFAULT_UPLOADED_TOAST, ...toastOptions })); + const title = postUploadAction.title || DEFAULT_UPLOADED_TOAST.title; + toast({ ...DEFAULT_UPLOADED_TOAST, title }); } else { // Add this image to the board dispatch( @@ -70,24 +70,20 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis ? `${t('toast.addedToBoard')} ${board.board_name}` : `${t('toast.addedToBoard')} ${autoAddBoardId}`; - dispatch( - addToast({ - ...DEFAULT_UPLOADED_TOAST, - description, - }) - ); + toast({ + ...DEFAULT_UPLOADED_TOAST, + description, + }); } return; } if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') { dispatch(setInitialCanvasImage(imageDTO, selectOptimalDimension(state))); - dispatch( - addToast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setAsCanvasInitialImage'), - }) - ); + toast({ + ...DEFAULT_UPLOADED_TOAST, + description: t('toast.setAsCanvasInitialImage'), + }); return; } @@ -105,68 +101,56 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis controlImage: imageDTO.image_name, }) ); - dispatch( - addToast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }) - ); + toast({ + ...DEFAULT_UPLOADED_TOAST, + description: t('toast.setControlImage'), + }); return; } if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') { const { layerId } = postUploadAction; dispatch(caLayerImageChanged({ layerId, imageDTO })); - dispatch( - addToast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }) - ); + toast({ + ...DEFAULT_UPLOADED_TOAST, + description: t('toast.setControlImage'), + }); } if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') { const { layerId } = postUploadAction; dispatch(ipaLayerImageChanged({ layerId, imageDTO })); - dispatch( - addToast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }) - ); + toast({ + ...DEFAULT_UPLOADED_TOAST, + description: t('toast.setControlImage'), + }); } if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') { const { layerId, ipAdapterId } = postUploadAction; dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); - dispatch( - addToast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }) - ); + toast({ + ...DEFAULT_UPLOADED_TOAST, + description: t('toast.setControlImage'), + }); } if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') { const { layerId } = postUploadAction; dispatch(iiLayerImageChanged({ layerId, imageDTO })); - dispatch( - addToast({ - ...DEFAULT_UPLOADED_TOAST, - description: t('toast.setControlImage'), - }) - ); + toast({ + ...DEFAULT_UPLOADED_TOAST, + description: t('toast.setControlImage'), + }); } if (postUploadAction?.type === 'SET_NODES_IMAGE') { const { nodeId, fieldName } = postUploadAction; dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO })); - dispatch( - addToast({ - ...DEFAULT_UPLOADED_TOAST, - description: `${t('toast.setNodeField')} ${fieldName}`, - }) - ); + toast({ + ...DEFAULT_UPLOADED_TOAST, + description: `${t('toast.setNodeField')} ${fieldName}`, + }); return; } }, @@ -174,7 +158,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis startAppListening({ matcher: imagesApi.endpoints.uploadImage.matchRejected, - effect: (action, { dispatch }) => { + effect: (action) => { const log = logger('images'); const sanitizedData = { arg: { @@ -183,13 +167,11 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis }, }; log.error({ ...sanitizedData }, 'Image upload failed'); - dispatch( - addToast({ - title: t('toast.imageUploadFailed'), - description: action.error.message, - status: 'error', - }) - ); + toast({ + title: t('toast.imageUploadFailed'), + description: action.error.message, + status: 'error', + }); }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index bc049cf498..239a5b863d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -8,8 +8,7 @@ import { loraRemoved } from 'features/lora/store/loraSlice'; import { modelSelected } from 'features/parameters/store/actions'; import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice'; import { zParameterModel } from 'features/parameters/types/parameterSchemas'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { forEach } from 'lodash-es'; @@ -60,16 +59,14 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = }); if (modelsCleared > 0) { - dispatch( - addToast( - makeToast({ - title: t('toast.baseModelChangedCleared', { - count: modelsCleared, - }), - status: 'warning', - }) - ) - ); + toast({ + id: 'BASE_MODEL_CHANGED', + title: t('toast.baseModelChanged'), + description: t('toast.baseModelChangedCleared', { + count: modelsCleared, + }), + status: 'warning', + }); } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts index 61a978d576..415c359d70 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -19,8 +19,7 @@ import { isParameterWidth, zParameterVAEModel, } from 'features/parameters/types/parameterSchemas'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; import { isNonRefinerMainModelConfig } from 'services/api/types'; @@ -109,7 +108,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni } } - dispatch(addToast(makeToast({ title: t('toast.parameterSet', { parameter: 'Default settings' }) }))); + toast({ id: 'PARAMETER_SET', title: t('toast.parameterSet', { parameter: 'Default settings' }) }); } }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts index bb113a09ee..08ad830ba4 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress.ts @@ -1,5 +1,9 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { deepClone } from 'common/util/deepClone'; +import { parseify } from 'common/util/serialize'; +import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; +import { zNodeStatus } from 'features/nodes/types/invocation'; import { socketGeneratorProgress } from 'services/events/actions'; const log = logger('socketio'); @@ -8,7 +12,15 @@ export const addGeneratorProgressEventListener = (startAppListening: AppStartLis startAppListening({ actionCreator: socketGeneratorProgress, effect: (action) => { - log.trace(action.payload, `Generator progress`); + log.trace(parseify(action.payload), `Generator progress`); + const { invocation_source_id, step, total_steps, progress_image } = action.payload.data; + const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); + if (nes) { + nes.status = zNodeStatus.enum.IN_PROGRESS; + nes.progress = (step + 1) / total_steps; + nes.progressImage = progress_image ?? null; + upsertExecutionState(nes.nodeId, nes); + } }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGraphExecutionStateComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGraphExecutionStateComplete.ts deleted file mode 100644 index 5221679232..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketGraphExecutionStateComplete.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { socketGraphExecutionStateComplete } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addGraphExecutionStateCompleteEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketGraphExecutionStateComplete, - effect: (action) => { - log.debug(action.payload, 'Session complete'); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index 279f9aac5b..2841493ca6 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -1,10 +1,17 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; -import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice'; +import { + boardIdSelected, + galleryViewChanged, + imageSelected, + isImageViewerOpenChanged, +} from 'features/gallery/store/gallerySlice'; import { IMAGE_CATEGORIES } from 'features/gallery/store/types'; -import { isImageOutput } from 'features/nodes/types/common'; +import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; +import { zNodeStatus } from 'features/nodes/types/invocation'; import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants'; import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; @@ -21,12 +28,12 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi actionCreator: socketInvocationComplete, effect: async (action, { dispatch, getState }) => { const { data } = action.payload; - log.debug({ data: parseify(data) }, `Invocation complete (${action.payload.data.node.type})`); + log.debug({ data: parseify(data) }, `Invocation complete (${data.invocation.type})`); - const { result, node, queue_batch_id } = data; + const { result, invocation_source_id } = data; // This complete event has an associated image output - if (isImageOutput(result) && !nodeTypeDenylist.includes(node.type)) { - const { image_name } = result.image; + if (data.result.type === 'image_output' && !nodeTypeDenylist.includes(data.invocation.type)) { + const { image_name } = data.result.image; const { canvas, gallery } = getState(); // This populates the `getImageDTO` cache @@ -40,7 +47,7 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi imageDTORequest.unsubscribe(); // Add canvas images to the staging area - if (canvas.batchIds.includes(queue_batch_id) && data.source_node_id === CANVAS_OUTPUT) { + if (canvas.batchIds.includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) { dispatch(addImageToStagingArea(imageDTO)); } @@ -101,9 +108,20 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi } dispatch(imageSelected(imageDTO)); + dispatch(isImageViewerOpenChanged(true)); } } } + + const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); + if (nes) { + nes.status = zNodeStatus.enum.COMPLETED; + if (nes.progress !== null) { + nes.progress = 1; + } + nes.outputs.push(result); + upsertExecutionState(nes.nodeId, nes); + } }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts index fb898b4c7a..b34f34a079 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError.ts @@ -1,5 +1,9 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { deepClone } from 'common/util/deepClone'; +import { parseify } from 'common/util/serialize'; +import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; +import { zNodeStatus } from 'features/nodes/types/invocation'; import { socketInvocationError } from 'services/events/actions'; const log = logger('socketio'); @@ -8,7 +12,20 @@ export const addInvocationErrorEventListener = (startAppListening: AppStartListe startAppListening({ actionCreator: socketInvocationError, effect: (action) => { - log.error(action.payload, `Invocation error (${action.payload.data.node.type})`); + const { invocation_source_id, invocation, error_type, error_message, error_traceback } = action.payload.data; + log.error(parseify(action.payload), `Invocation error (${invocation.type})`); + const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); + if (nes) { + nes.status = zNodeStatus.enum.FAILED; + nes.progress = null; + nes.progressImage = null; + nes.error = { + error_type, + error_message, + error_traceback, + }; + upsertExecutionState(nes.nodeId, nes); + } }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationRetrievalError.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationRetrievalError.ts deleted file mode 100644 index 44da4c0ddb..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationRetrievalError.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { socketInvocationRetrievalError } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addInvocationRetrievalErrorEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketInvocationRetrievalError, - effect: (action) => { - log.error(action.payload, `Invocation retrieval error (${action.payload.data.graph_execution_state_id})`); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts index baf476a66b..7dae869ce2 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted.ts @@ -1,5 +1,9 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { deepClone } from 'common/util/deepClone'; +import { parseify } from 'common/util/serialize'; +import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; +import { zNodeStatus } from 'features/nodes/types/invocation'; import { socketInvocationStarted } from 'services/events/actions'; const log = logger('socketio'); @@ -8,7 +12,13 @@ export const addInvocationStartedEventListener = (startAppListening: AppStartLis startAppListening({ actionCreator: socketInvocationStarted, effect: (action) => { - log.debug(action.payload, `Invocation started (${action.payload.data.node.type})`); + log.debug(parseify(action.payload), `Invocation started (${action.payload.data.invocation.type})`); + const { invocation_source_id } = action.payload.data; + const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]); + if (nes) { + nes.status = zNodeStatus.enum.IN_PROGRESS; + upsertExecutionState(nes.nodeId, nes); + } }, }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall.ts index f474c2736b..7fafb8302c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall.ts @@ -3,14 +3,14 @@ import { api, LIST_TAG } from 'services/api'; import { modelsApi } from 'services/api/endpoints/models'; import { socketModelInstallCancelled, - socketModelInstallCompleted, - socketModelInstallDownloading, + socketModelInstallComplete, + socketModelInstallDownloadProgress, socketModelInstallError, } from 'services/events/actions'; export const addModelInstallEventListener = (startAppListening: AppStartListening) => { startAppListening({ - actionCreator: socketModelInstallDownloading, + actionCreator: socketModelInstallDownloadProgress, effect: async (action, { dispatch }) => { const { bytes, total_bytes, id } = action.payload.data; @@ -29,7 +29,7 @@ export const addModelInstallEventListener = (startAppListening: AppStartListenin }); startAppListening({ - actionCreator: socketModelInstallCompleted, + actionCreator: socketModelInstallComplete, effect: (action, { dispatch }) => { const { id } = action.payload.data; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad.ts index 4f4ec7635e..0240fe219a 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad.ts @@ -1,6 +1,6 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { socketModelLoadCompleted, socketModelLoadStarted } from 'services/events/actions'; +import { socketModelLoadComplete, socketModelLoadStarted } from 'services/events/actions'; const log = logger('socketio'); @@ -8,10 +8,11 @@ export const addModelLoadEventListener = (startAppListening: AppStartListening) startAppListening({ actionCreator: socketModelLoadStarted, effect: (action) => { - const { model_config, submodel_type } = action.payload.data; - const { name, base, type } = model_config; + const { config, submodel_type } = action.payload.data; + const { name, base, type } = config; const extras: string[] = [base, type]; + if (submodel_type) { extras.push(submodel_type); } @@ -23,10 +24,10 @@ export const addModelLoadEventListener = (startAppListening: AppStartListening) }); startAppListening({ - actionCreator: socketModelLoadCompleted, + actionCreator: socketModelLoadComplete, effect: (action) => { - const { model_config, submodel_type } = action.payload.data; - const { name, base, type } = model_config; + const { config, submodel_type } = action.payload.data; + const { name, base, type } = config; const extras: string[] = [base, type]; if (submodel_type) { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.ts deleted file mode 100644 index 84073bb427..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue'; -import { socketQueueItemStatusChanged } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addSocketQueueItemStatusChangedEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketQueueItemStatusChanged, - effect: async (action, { dispatch }) => { - // we've got new status for the queue item, batch and queue - const { queue_item, batch_status, queue_status } = action.payload.data; - - log.debug(action.payload, `Queue item ${queue_item.item_id} status updated: ${queue_item.status}`); - - // Update this specific queue item in the list of queue items (this is the queue item DTO, without the session) - dispatch( - queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => { - queueItemsAdapter.updateOne(draft, { - id: String(queue_item.item_id), - changes: queue_item, - }); - }) - ); - - // Update the queue status (we do not get the processor status here) - dispatch( - queueApi.util.updateQueryData('getQueueStatus', undefined, (draft) => { - if (!draft) { - return; - } - Object.assign(draft.queue, queue_status); - }) - ); - - // Update the batch status - dispatch( - queueApi.util.updateQueryData('getBatchStatus', { batch_id: batch_status.batch_id }, () => batch_status) - ); - - // Update the queue item status (this is the full queue item, including the session) - dispatch( - queueApi.util.updateQueryData('getQueueItem', queue_item.item_id, (draft) => { - if (!draft) { - return; - } - Object.assign(draft, queue_item); - }) - ); - - // Invalidate caches for things we cannot update - // TODO: technically, we could possibly update the current session queue item, but feels safer to just request it again - dispatch( - queueApi.util.invalidateTags(['CurrentSessionQueueItem', 'NextSessionQueueItem', 'InvocationCacheStatus']) - ); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.tsx b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.tsx new file mode 100644 index 0000000000..8a83609b3c --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged.tsx @@ -0,0 +1,114 @@ +import { logger } from 'app/logging/logger'; +import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { deepClone } from 'common/util/deepClone'; +import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState'; +import { zNodeStatus } from 'features/nodes/types/invocation'; +import ErrorToastDescription, { getTitleFromErrorType } from 'features/toast/ErrorToastDescription'; +import { toast } from 'features/toast/toast'; +import { forEach } from 'lodash-es'; +import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue'; +import { socketQueueItemStatusChanged } from 'services/events/actions'; + +const log = logger('socketio'); + +export const addSocketQueueItemStatusChangedEventListener = (startAppListening: AppStartListening) => { + startAppListening({ + actionCreator: socketQueueItemStatusChanged, + effect: async (action, { dispatch, getState }) => { + // we've got new status for the queue item, batch and queue + const { + item_id, + session_id, + status, + started_at, + updated_at, + completed_at, + batch_status, + queue_status, + error_type, + error_message, + error_traceback, + } = action.payload.data; + + log.debug(action.payload, `Queue item ${item_id} status updated: ${status}`); + + // Update this specific queue item in the list of queue items (this is the queue item DTO, without the session) + dispatch( + queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => { + queueItemsAdapter.updateOne(draft, { + id: String(item_id), + changes: { + status, + started_at, + updated_at: updated_at ?? undefined, + completed_at: completed_at ?? undefined, + error_type, + error_message, + error_traceback, + }, + }); + }) + ); + + // Update the queue status (we do not get the processor status here) + dispatch( + queueApi.util.updateQueryData('getQueueStatus', undefined, (draft) => { + if (!draft) { + return; + } + Object.assign(draft.queue, queue_status); + }) + ); + + // Update the batch status + dispatch( + queueApi.util.updateQueryData('getBatchStatus', { batch_id: batch_status.batch_id }, () => batch_status) + ); + + // Invalidate caches for things we cannot update + // TODO: technically, we could possibly update the current session queue item, but feels safer to just request it again + dispatch( + queueApi.util.invalidateTags([ + 'CurrentSessionQueueItem', + 'NextSessionQueueItem', + 'InvocationCacheStatus', + { type: 'SessionQueueItem', id: item_id }, + ]) + ); + + if (status === 'in_progress') { + forEach($nodeExecutionStates.get(), (nes) => { + if (!nes) { + return; + } + const clone = deepClone(nes); + clone.status = zNodeStatus.enum.PENDING; + clone.error = null; + clone.progress = null; + clone.progressImage = null; + clone.outputs = []; + $nodeExecutionStates.setKey(clone.nodeId, clone); + }); + } else if (status === 'failed' && error_type) { + const isLocal = getState().config.isLocal ?? true; + const sessionId = session_id; + + toast({ + id: `INVOCATION_ERROR_${error_type}`, + title: getTitleFromErrorType(error_type), + status: 'error', + duration: null, + updateDescription: isLocal, + description: ( + + ), + }); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketSessionRetrievalError.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketSessionRetrievalError.ts deleted file mode 100644 index a1a497dc08..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketSessionRetrievalError.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { socketSessionRetrievalError } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addSessionRetrievalErrorEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketSessionRetrievalError, - effect: (action) => { - log.error(action.payload, `Session retrieval error (${action.payload.data.graph_execution_state_id})`); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketSubscribed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketSubscribed.ts deleted file mode 100644 index 48324cb652..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketSubscribed.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { socketSubscribedSession } from 'services/events/actions'; - -const log = logger('socketio'); - -export const addSocketSubscribedEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketSubscribedSession, - effect: (action) => { - log.debug(action.payload, 'Subscribed'); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketUnsubscribed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketUnsubscribed.ts deleted file mode 100644 index 7a76a809d6..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketUnsubscribed.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { logger } from 'app/logging/logger'; -import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { socketUnsubscribedSession } from 'services/events/actions'; -const log = logger('socketio'); - -export const addSocketUnsubscribedEventListener = (startAppListening: AppStartListening) => { - startAppListening({ - actionCreator: socketUnsubscribedSession, - effect: (action) => { - log.debug(action.payload, 'Unsubscribed'); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts index 6816e25bc1..6c4c2a9df1 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts @@ -1,6 +1,6 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { stagingAreaImageSaved } from 'features/canvas/store/actions'; -import { addToast } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { imagesApi } from 'services/api/endpoints/images'; @@ -29,15 +29,14 @@ export const addStagingAreaImageSavedListener = (startAppListening: AppStartList }) ); } - dispatch(addToast({ title: t('toast.imageSaved'), status: 'success' })); + toast({ id: 'IMAGE_SAVED', title: t('toast.imageSaved'), status: 'success' }); } catch (error) { - dispatch( - addToast({ - title: t('toast.imageSavingFailed'), - description: (error as Error)?.message, - status: 'error', - }) - ); + toast({ + id: 'IMAGE_SAVE_FAILED', + title: t('toast.imageSavingFailed'), + description: (error as Error)?.message, + status: 'error', + }); } }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts index 5ee9de3c11..07df2a4f42 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts @@ -1,12 +1,11 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { updateAllNodesRequested } from 'features/nodes/store/actions'; -import { nodeReplaced } from 'features/nodes/store/nodesSlice'; +import { $templates, nodesChanged } from 'features/nodes/store/nodesSlice'; import { NodeUpdateError } from 'features/nodes/types/error'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartListening) => { @@ -14,7 +13,8 @@ export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartLi actionCreator: updateAllNodesRequested, effect: (action, { dispatch, getState }) => { const log = logger('nodes'); - const { nodes, templates } = getState().nodes; + const { nodes } = getState().nodes.present; + const templates = $templates.get(); let unableToUpdateCount = 0; @@ -24,13 +24,18 @@ export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartLi unableToUpdateCount++; return; } - if (!getNeedsUpdate(node, template)) { + if (!getNeedsUpdate(node.data, template)) { // No need to increment the count here, since we're not actually updating return; } try { const updatedNode = updateNode(node, template); - dispatch(nodeReplaced({ nodeId: updatedNode.id, node: updatedNode })); + dispatch( + nodesChanged([ + { type: 'remove', id: updatedNode.id }, + { type: 'add', item: updatedNode }, + ]) + ); } catch (e) { if (e instanceof NodeUpdateError) { unableToUpdateCount++; @@ -44,24 +49,18 @@ export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartLi count: unableToUpdateCount, }) ); - dispatch( - addToast( - makeToast({ - title: t('nodes.unableToUpdateNodes', { - count: unableToUpdateCount, - }), - }) - ) - ); + toast({ + id: 'UNABLE_TO_UPDATE_NODES', + title: t('nodes.unableToUpdateNodes', { + count: unableToUpdateCount, + }), + }); } else { - dispatch( - addToast( - makeToast({ - title: t('nodes.allNodesUpdated'), - status: 'success', - }) - ) - ); + toast({ + id: 'ALL_NODES_UPDATED', + title: t('nodes.allNodesUpdated'), + status: 'success', + }); } }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts index ff5d5f24be..ce480a3573 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts @@ -4,7 +4,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import { parseify } from 'common/util/serialize'; import { buildAdHocUpscaleGraph } from 'features/nodes/util/graph/buildAdHocUpscaleGraph'; import { createIsAllowedToUpscaleSelector } from 'features/parameters/hooks/useIsAllowedToUpscale'; -import { addToast } from 'features/system/store/systemSlice'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { queueApi } from 'services/api/endpoints/queue'; import type { BatchConfig, ImageDTO } from 'services/api/types'; @@ -29,12 +29,11 @@ export const addUpscaleRequestedListener = (startAppListening: AppStartListening { imageDTO }, t(detailTKey ?? 'parameters.isAllowedToUpscale.tooLarge') // should never coalesce ); - dispatch( - addToast({ - title: t(detailTKey ?? 'parameters.isAllowedToUpscale.tooLarge'), // should never coalesce - status: 'error', - }) - ); + toast({ + id: 'NOT_ALLOWED_TO_UPSCALE', + title: t(detailTKey ?? 'parameters.isAllowedToUpscale.tooLarge'), // should never coalesce + status: 'error', + }); return; } @@ -65,12 +64,11 @@ export const addUpscaleRequestedListener = (startAppListening: AppStartListening if (error instanceof Object && 'status' in error && error.status === 403) { return; } else { - dispatch( - addToast({ - title: t('queue.graphFailedToQueue'), - status: 'error', - }) - ); + toast({ + id: 'GRAPH_QUEUE_FAILED', + title: t('queue.graphFailedToQueue'), + status: 'error', + }); } } }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts index 0227597fe9..2c0caa0ec9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts @@ -2,110 +2,110 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { parseify } from 'common/util/serialize'; import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions'; -import { $flow } from 'features/nodes/store/reactFlowInstance'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { $needsFit } from 'features/nodes/store/reactFlowInstance'; +import type { Templates } from 'features/nodes/store/types'; import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error'; +import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow'; import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; +import { checkBoardAccess, checkImageAccess, checkModelAccess } from 'services/api/hooks/accessChecks'; +import type { GraphAndWorkflowResponse, NonNullableGraph } from 'services/api/types'; import { z } from 'zod'; import { fromZodError } from 'zod-validation-error'; +const getWorkflow = async (data: GraphAndWorkflowResponse, templates: Templates) => { + if (data.workflow) { + // Prefer to load the workflow if it's available - it has more information + const parsed = JSON.parse(data.workflow); + return await validateWorkflow(parsed, templates, checkImageAccess, checkBoardAccess, checkModelAccess); + } else if (data.graph) { + // Else we fall back on the graph, using the graphToWorkflow function to convert and do layout + const parsed = JSON.parse(data.graph); + const workflow = graphToWorkflow(parsed as NonNullableGraph, true); + return await validateWorkflow(workflow, templates, checkImageAccess, checkBoardAccess, checkModelAccess); + } else { + throw new Error('No workflow or graph provided'); + } +}; + export const addWorkflowLoadRequestedListener = (startAppListening: AppStartListening) => { startAppListening({ actionCreator: workflowLoadRequested, - effect: (action, { dispatch, getState }) => { + effect: async (action, { dispatch }) => { const log = logger('nodes'); - const { workflow, asCopy } = action.payload; - const nodeTemplates = getState().nodes.templates; + const { data, asCopy } = action.payload; + const nodeTemplates = $templates.get(); try { - const { workflow: validatedWorkflow, warnings } = validateWorkflow(workflow, nodeTemplates); + const { workflow, warnings } = await getWorkflow(data, nodeTemplates); if (asCopy) { // If we're loading a copy, we need to remove the ID so that the backend will create a new workflow - delete validatedWorkflow.id; + delete workflow.id; } - dispatch(workflowLoaded(validatedWorkflow)); + dispatch(workflowLoaded(workflow)); if (!warnings.length) { - dispatch( - addToast( - makeToast({ - title: t('toast.workflowLoaded'), - status: 'success', - }) - ) - ); + toast({ + id: 'WORKFLOW_LOADED', + title: t('toast.workflowLoaded'), + status: 'success', + }); } else { - dispatch( - addToast( - makeToast({ - title: t('toast.loadedWithWarnings'), - status: 'warning', - }) - ) - ); + toast({ + id: 'WORKFLOW_LOADED', + title: t('toast.loadedWithWarnings'), + status: 'warning', + }); + warnings.forEach(({ message, ...rest }) => { log.warn(rest, message); }); } - requestAnimationFrame(() => { - $flow.get()?.fitView(); - }); + $needsFit.set(true); } catch (e) { if (e instanceof WorkflowVersionError) { // The workflow version was not recognized in the valid list of versions log.error({ error: parseify(e) }, e.message); - dispatch( - addToast( - makeToast({ - title: t('nodes.unableToValidateWorkflow'), - status: 'error', - description: e.message, - }) - ) - ); + toast({ + id: 'UNABLE_TO_VALIDATE_WORKFLOW', + title: t('nodes.unableToValidateWorkflow'), + status: 'error', + description: e.message, + }); } else if (e instanceof WorkflowMigrationError) { // There was a problem migrating the workflow to the latest version log.error({ error: parseify(e) }, e.message); - dispatch( - addToast( - makeToast({ - title: t('nodes.unableToValidateWorkflow'), - status: 'error', - description: e.message, - }) - ) - ); + toast({ + id: 'UNABLE_TO_VALIDATE_WORKFLOW', + title: t('nodes.unableToValidateWorkflow'), + status: 'error', + description: e.message, + }); } else if (e instanceof z.ZodError) { // There was a problem validating the workflow itself const { message } = fromZodError(e, { prefix: t('nodes.workflowValidation'), }); log.error({ error: parseify(e) }, message); - dispatch( - addToast( - makeToast({ - title: t('nodes.unableToValidateWorkflow'), - status: 'error', - description: message, - }) - ) - ); + toast({ + id: 'UNABLE_TO_VALIDATE_WORKFLOW', + title: t('nodes.unableToValidateWorkflow'), + status: 'error', + description: message, + }); } else { // Some other error occurred log.error({ error: parseify(e) }, t('nodes.unknownErrorValidatingWorkflow')); - dispatch( - addToast( - makeToast({ - title: t('nodes.unableToValidateWorkflow'), - status: 'error', - description: t('nodes.unknownErrorValidatingWorkflow'), - }) - ) - ); + toast({ + id: 'UNABLE_TO_VALIDATE_WORKFLOW', + title: t('nodes.unableToValidateWorkflow'), + status: 'error', + description: t('nodes.unknownErrorValidatingWorkflow'), + }); } } }, diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 9661f57f99..062cdc1cbf 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -21,7 +21,8 @@ import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/galle import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice'; import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice'; import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice'; -import { nodesPersistConfig, nodesSlice } from 'features/nodes/store/nodesSlice'; +import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice'; +import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice'; import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice'; import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice'; @@ -50,7 +51,7 @@ const allReducers = { [canvasSlice.name]: canvasSlice.reducer, [gallerySlice.name]: gallerySlice.reducer, [generationSlice.name]: generationSlice.reducer, - [nodesSlice.name]: nodesSlice.reducer, + [nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig), [postprocessingSlice.name]: postprocessingSlice.reducer, [systemSlice.name]: systemSlice.reducer, [configSlice.name]: configSlice.reducer, @@ -66,6 +67,7 @@ const allReducers = { [workflowSlice.name]: workflowSlice.reducer, [hrfSlice.name]: hrfSlice.reducer, [controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig), + [workflowSettingsSlice.name]: workflowSettingsSlice.reducer, [api.reducerPath]: api.reducer, }; @@ -111,6 +113,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig, [hrfPersistConfig.name]: hrfPersistConfig, [controlLayersPersistConfig.name]: controlLayersPersistConfig, + [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 4982dbb83f..21636ada49 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -74,6 +74,7 @@ export type AppConfig = { maxUpscalePixels?: number; metadataFetchDebounce?: number; workflowFetchDebounce?: number; + isLocal?: boolean; sd: { defaultModel?: string; disabledControlNetModels: string[]; diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index 01107c21b4..f16aa3d4b4 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -35,6 +35,7 @@ type IAIDndImageProps = FlexProps & { draggableData?: TypesafeDraggableData; dropLabel?: ReactNode; isSelected?: boolean; + isSelectedForCompare?: boolean; thumbnail?: boolean; noContentFallback?: ReactElement; useThumbailFallback?: boolean; @@ -61,6 +62,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { draggableData, dropLabel, isSelected = false, + isSelectedForCompare = false, thumbnail = false, noContentFallback = defaultNoContentFallback, uploadElement = defaultUploadElement, @@ -70,6 +72,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { onMouseOver, onMouseOut, dataTestId, + ...rest } = props; const [isHovered, setIsHovered] = useState(false); @@ -138,6 +141,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { minH={minSize ? minSize : undefined} userSelect="none" cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'} + {...rest} > {imageDTO && ( { data-testid={dataTestId} /> {withMetadataOverlay && } - + )} {!imageDTO && !isUploadDisabled && ( diff --git a/invokeai/frontend/web/src/common/components/IAIDroppable.tsx b/invokeai/frontend/web/src/common/components/IAIDroppable.tsx index 258a6e9004..ef331c4377 100644 --- a/invokeai/frontend/web/src/common/components/IAIDroppable.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDroppable.tsx @@ -36,7 +36,7 @@ const IAIDroppable = (props: IAIDroppableProps) => { pointerEvents={active ? 'auto' : 'none'} > - {isValidDrop(data, active) && } + {isValidDrop(data, active?.data.current) && } ); diff --git a/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx b/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx index eb50a6b9d4..3e2ecca4ae 100644 --- a/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/SelectionOverlay.tsx @@ -3,10 +3,17 @@ import { memo, useMemo } from 'react'; type Props = { isSelected: boolean; + isSelectedForCompare: boolean; isHovered: boolean; }; -const SelectionOverlay = ({ isSelected, isHovered }: Props) => { +const SelectionOverlay = ({ isSelected, isSelectedForCompare, isHovered }: Props) => { const shadow = useMemo(() => { + if (isSelectedForCompare && isHovered) { + return 'hoverSelectedForCompare'; + } + if (isSelectedForCompare && !isHovered) { + return 'selectedForCompare'; + } if (isSelected && isHovered) { return 'hoverSelected'; } @@ -17,7 +24,7 @@ const SelectionOverlay = ({ isSelected, isHovered }: Props) => { return 'hoverUnselected'; } return undefined; - }, [isHovered, isSelected]); + }, [isHovered, isSelected, isSelectedForCompare]); return ( { bottom={0} insetInlineStart={0} borderRadius="base" - opacity={isSelected ? 1 : 0.7} + opacity={isSelected || isSelectedForCompare ? 1 : 0.7} transitionProperty="common" transitionDuration="0.1s" pointerEvents="none" diff --git a/invokeai/frontend/web/src/common/hooks/useBoolean.ts b/invokeai/frontend/web/src/common/hooks/useBoolean.ts new file mode 100644 index 0000000000..123e48cd75 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useBoolean.ts @@ -0,0 +1,21 @@ +import { useCallback, useMemo, useState } from 'react'; + +export const useBoolean = (initialValue: boolean) => { + const [isTrue, set] = useState(initialValue); + const setTrue = useCallback(() => set(true), []); + const setFalse = useCallback(() => set(false), []); + const toggle = useCallback(() => set((v) => !v), []); + + const api = useMemo( + () => ({ + isTrue, + set, + setTrue, + setFalse, + toggle, + }), + [isTrue, set, setTrue, setFalse, toggle] + ); + + return api; +}; diff --git a/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts b/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts index ef9db44a9d..233b841034 100644 --- a/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts +++ b/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts @@ -1,11 +1,10 @@ -import { useAppToaster } from 'app/components/Toaster'; import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob'; import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard'; +import { toast } from 'features/toast/toast'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const useCopyImageToClipboard = () => { - const toaster = useAppToaster(); const { t } = useTranslation(); const imageUrlToBlob = useImageUrlToBlob(); @@ -16,12 +15,11 @@ export const useCopyImageToClipboard = () => { const copyImageToClipboard = useCallback( async (image_url: string) => { if (!isClipboardAPIAvailable) { - toaster({ + toast({ + id: 'PROBLEM_COPYING_IMAGE', title: t('toast.problemCopyingImage'), description: "Your browser doesn't support the Clipboard API.", status: 'error', - duration: 2500, - isClosable: true, }); } try { @@ -33,23 +31,21 @@ export const useCopyImageToClipboard = () => { copyBlobToClipboard(blob); - toaster({ + toast({ + id: 'IMAGE_COPIED', title: t('toast.imageCopied'), status: 'success', - duration: 2500, - isClosable: true, }); } catch (err) { - toaster({ + toast({ + id: 'PROBLEM_COPYING_IMAGE', title: t('toast.problemCopyingImage'), description: String(err), status: 'error', - duration: 2500, - isClosable: true, }); } }, - [imageUrlToBlob, isClipboardAPIAvailable, t, toaster] + [imageUrlToBlob, isClipboardAPIAvailable, t] ); return { isClipboardAPIAvailable, copyImageToClipboard }; diff --git a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts index 26a17e1d0c..ede247b9fb 100644 --- a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts +++ b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts @@ -1,13 +1,12 @@ import { useStore } from '@nanostores/react'; -import { useAppToaster } from 'app/components/Toaster'; import { $authToken } from 'app/store/nanostores/authToken'; import { useAppDispatch } from 'app/store/storeHooks'; import { imageDownloaded } from 'features/gallery/store/actions'; +import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const useDownloadImage = () => { - const toaster = useAppToaster(); const { t } = useTranslation(); const dispatch = useAppDispatch(); const authToken = useStore($authToken); @@ -37,16 +36,15 @@ export const useDownloadImage = () => { window.URL.revokeObjectURL(url); dispatch(imageDownloaded()); } catch (err) { - toaster({ + toast({ + id: 'PROBLEM_DOWNLOADING_IMAGE', title: t('toast.problemDownloadingImage'), description: String(err), status: 'error', - duration: 2500, - isClosable: true, }); } }, - [t, toaster, dispatch, authToken] + [t, dispatch, authToken] ); return { downloadImage }; diff --git a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts index 0334294e98..5b1bf1f5b3 100644 --- a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts +++ b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts @@ -1,6 +1,6 @@ -import { useAppToaster } from 'app/components/Toaster'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { toast } from 'features/toast/toast'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { useCallback, useEffect, useState } from 'react'; import type { Accept, FileRejection } from 'react-dropzone'; @@ -26,7 +26,6 @@ const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (ac export const useFullscreenDropzone = () => { const { t } = useTranslation(); - const toaster = useAppToaster(); const postUploadAction = useAppSelector(selectPostUploadAction); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const [isHandlingUpload, setIsHandlingUpload] = useState(false); @@ -37,13 +36,14 @@ export const useFullscreenDropzone = () => { (rejection: FileRejection) => { setIsHandlingUpload(true); - toaster({ + toast({ + id: 'UPLOAD_FAILED', title: t('toast.uploadFailed'), description: rejection.errors.map((error) => error.message).join('\n'), status: 'error', }); }, - [t, toaster] + [t] ); const fileAcceptedCallback = useCallback( @@ -62,7 +62,8 @@ export const useFullscreenDropzone = () => { const onDrop = useCallback( (acceptedFiles: Array, fileRejections: Array) => { if (fileRejections.length > 1) { - toaster({ + toast({ + id: 'UPLOAD_FAILED', title: t('toast.uploadFailed'), description: t('toast.uploadFailedInvalidUploadDesc'), status: 'error', @@ -78,7 +79,7 @@ export const useFullscreenDropzone = () => { fileAcceptedCallback(file); }); }, - [t, toaster, fileAcceptedCallback, fileRejectionCallback] + [t, fileAcceptedCallback, fileRejectionCallback] ); const onDragOver = useCallback(() => { diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index 55887eb3be..5b57fcd2bb 100644 --- a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -13,6 +13,7 @@ type UseGroupedModelComboboxArg = { onChange: (value: T | null) => void; getIsDisabled?: (model: T) => boolean; isLoading?: boolean; + groupByType?: boolean; }; type UseGroupedModelComboboxReturn = { @@ -23,17 +24,21 @@ type UseGroupedModelComboboxReturn = { noOptionsMessage: () => string; }; +const groupByBaseFunc = (model: T) => model.base.toUpperCase(); +const groupByBaseAndTypeFunc = (model: T) => + `${model.base.toUpperCase()} / ${model.type.replaceAll('_', ' ').toUpperCase()}`; + export const useGroupedModelCombobox = ( arg: UseGroupedModelComboboxArg ): UseGroupedModelComboboxReturn => { const { t } = useTranslation(); const base_model = useAppSelector((s) => s.generation.model?.base ?? 'sdxl'); - const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading } = arg; + const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg; const options = useMemo[]>(() => { if (!modelConfigs) { return []; } - const groupedModels = groupBy(modelConfigs, 'base'); + const groupedModels = groupBy(modelConfigs, groupByType ? groupByBaseAndTypeFunc : groupByBaseFunc); const _options = reduce( groupedModels, (acc, val, label) => { @@ -49,9 +54,9 @@ export const useGroupedModelCombobox = ( }, [] as GroupBase[] ); - _options.sort((a) => (a.label === base_model ? -1 : 1)); + _options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base_model) ? -1 : 1)); return _options; - }, [getIsDisabled, modelConfigs, base_model]); + }, [modelConfigs, groupByType, getIsDisabled, base_model]); const value = useMemo( () => diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 2aac5b8e72..dbf3c41480 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -1,3 +1,4 @@ +import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { @@ -6,187 +7,230 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import type { Layer } from 'features/controlLayers/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import type { Templates } from 'features/nodes/store/types'; +import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { selectSystemSlice } from 'features/system/store/systemSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import i18n from 'i18next'; -import { forEach } from 'lodash-es'; +import { forEach, upperFirst } from 'lodash-es'; +import { useMemo } from 'react'; import { getConnectedEdges } from 'reactflow'; -const selector = createMemoizedSelector( - [ - selectControlAdaptersSlice, - selectGenerationSlice, - selectSystemSlice, - selectNodesSlice, - selectDynamicPromptsSlice, - selectControlLayersSlice, - activeTabNameSelector, - ], - (controlAdapters, generation, system, nodes, dynamicPrompts, controlLayers, activeTabName) => { - const { model } = generation; - const { positivePrompt } = controlLayers.present; +const LAYER_TYPE_TO_TKEY: Record = { + initial_image_layer: 'controlLayers.globalInitialImage', + control_adapter_layer: 'controlLayers.globalControlAdapter', + ip_adapter_layer: 'controlLayers.globalIPAdapter', + regional_guidance_layer: 'controlLayers.regionalGuidance', +}; - const { isConnected } = system; +const createSelector = (templates: Templates) => + createMemoizedSelector( + [ + selectControlAdaptersSlice, + selectGenerationSlice, + selectSystemSlice, + selectNodesSlice, + selectWorkflowSettingsSlice, + selectDynamicPromptsSlice, + selectControlLayersSlice, + activeTabNameSelector, + ], + (controlAdapters, generation, system, nodes, workflowSettings, dynamicPrompts, controlLayers, activeTabName) => { + const { model } = generation; + const { size } = controlLayers.present; + const { positivePrompt } = controlLayers.present; - const reasons: string[] = []; + const { isConnected } = system; - // Cannot generate if not connected - if (!isConnected) { - reasons.push(i18n.t('parameters.invoke.systemDisconnected')); - } + const reasons: { prefix?: string; content: string }[] = []; - if (activeTabName === 'workflows') { - if (nodes.shouldValidateGraph) { - if (!nodes.nodes.length) { - reasons.push(i18n.t('parameters.invoke.noNodesInGraph')); + // Cannot generate if not connected + if (!isConnected) { + reasons.push({ content: i18n.t('parameters.invoke.systemDisconnected') }); + } + + if (activeTabName === 'workflows') { + if (workflowSettings.shouldValidateGraph) { + if (!nodes.nodes.length) { + reasons.push({ content: i18n.t('parameters.invoke.noNodesInGraph') }); + } + + nodes.nodes.forEach((node) => { + if (!isInvocationNode(node)) { + return; + } + + const nodeTemplate = templates[node.data.type]; + + if (!nodeTemplate) { + // Node type not found + reasons.push({ content: i18n.t('parameters.invoke.missingNodeTemplate') }); + return; + } + + const connectedEdges = getConnectedEdges([node], nodes.edges); + + forEach(node.data.inputs, (field) => { + const fieldTemplate = nodeTemplate.inputs[field.name]; + const hasConnection = connectedEdges.some( + (edge) => edge.target === node.id && edge.targetHandle === field.name + ); + + if (!fieldTemplate) { + reasons.push({ content: i18n.t('parameters.invoke.missingFieldTemplate') }); + return; + } + + if (fieldTemplate.required && field.value === undefined && !hasConnection) { + reasons.push({ + content: i18n.t('parameters.invoke.missingInputForField', { + nodeLabel: node.data.label || nodeTemplate.title, + fieldLabel: field.label || fieldTemplate.title, + }), + }); + return; + } + }); + }); + } + } else { + if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) { + reasons.push({ content: i18n.t('parameters.invoke.noPrompts') }); } - nodes.nodes.forEach((node) => { - if (!isInvocationNode(node)) { - return; - } + if (!model) { + reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); + } - const nodeTemplate = nodes.templates[node.data.type]; + if (activeTabName === 'generation') { + // Handling for generation tab + controlLayers.present.layers + .filter((l) => l.isEnabled) + .forEach((l, i) => { + const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerNumber = i + 1; + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]); + const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; + const problems: string[] = []; + if (l.type === 'control_adapter_layer') { + // Must have model + if (!l.controlAdapter.model) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); + } + // Model base must match + if (l.controlAdapter.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); + } + // Must have a control image OR, if it has a processor, it must have a processed image + if (!l.controlAdapter.image) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected')); + } else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); + } + // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL) + if (l.controlAdapter.type === 't2i_adapter') { + const multiple = model?.base === 'sdxl' ? 32 : 64; + if (size.width % multiple !== 0 || size.height % multiple !== 0) { + problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple })); + } + } + } - if (!nodeTemplate) { - // Node type not found - reasons.push(i18n.t('parameters.invoke.missingNodeTemplate')); - return; - } + if (l.type === 'ip_adapter_layer') { + // Must have model + if (!l.ipAdapter.model) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); + } + // Model base must match + if (l.ipAdapter.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); + } + // Must have an image + if (!l.ipAdapter.image) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); + } + } - const connectedEdges = getConnectedEdges([node], nodes.edges); + if (l.type === 'initial_image_layer') { + // Must have an image + if (!l.image) { + problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected')); + } + } - forEach(node.data.inputs, (field) => { - const fieldTemplate = nodeTemplate.inputs[field.name]; - const hasConnection = connectedEdges.some( - (edge) => edge.target === node.id && edge.targetHandle === field.name - ); + if (l.type === 'regional_guidance_layer') { + // Must have a region + if (l.maskObjects.length === 0) { + problems.push(i18n.t('parameters.invoke.layer.rgNoRegion')); + } + // Must have at least 1 prompt or IP Adapter + if (l.positivePrompt === null && l.negativePrompt === null && l.ipAdapters.length === 0) { + problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters')); + } + l.ipAdapters.forEach((ipAdapter) => { + // Must have model + if (!ipAdapter.model) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); + } + // Model base must match + if (ipAdapter.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); + } + // Must have an image + if (!ipAdapter.image) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); + } + }); + } - if (!fieldTemplate) { - reasons.push(i18n.t('parameters.invoke.missingFieldTemplate')); - return; - } + if (problems.length) { + const content = upperFirst(problems.join(', ')); + reasons.push({ prefix, content }); + } + }); + } else { + // Handling for all other tabs + selectControlAdapterAll(controlAdapters) + .filter((ca) => ca.isEnabled) + .forEach((ca, i) => { + if (!ca.isEnabled) { + return; + } - if (fieldTemplate.required && field.value === undefined && !hasConnection) { - reasons.push( - i18n.t('parameters.invoke.missingInputForField', { - nodeLabel: node.data.label || nodeTemplate.title, - fieldLabel: field.label || fieldTemplate.title, - }) - ); - return; - } - }); - }); - } - } else { - if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) { - reasons.push(i18n.t('parameters.invoke.noPrompts')); + if (!ca.model) { + reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) }); + } else if (ca.model.base !== model?.base) { + // This should never happen, just a sanity check + reasons.push({ + content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }), + }); + } + + if ( + !ca.controlImage || + (isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none') + ) { + reasons.push({ + content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }), + }); + } + }); + } } - if (!model) { - reasons.push(i18n.t('parameters.invoke.noModelSelected')); - } - - if (activeTabName === 'generation') { - // Handling for generation tab - controlLayers.present.layers - .filter((l) => l.isEnabled) - .flatMap((l) => { - if (l.type === 'control_adapter_layer') { - return l.controlAdapter; - } else if (l.type === 'ip_adapter_layer') { - return l.ipAdapter; - } else if (l.type === 'regional_guidance_layer') { - return l.ipAdapters; - } - return []; - }) - .forEach((ca, i) => { - const hasNoModel = !ca.model; - const mismatchedModelBase = ca.model?.base !== model?.base; - const hasNoImage = !ca.image; - const imageNotProcessed = - (ca.type === 'controlnet' || ca.type === 't2i_adapter') && !ca.processedImage && ca.processorConfig; - - if (hasNoModel) { - reasons.push( - i18n.t('parameters.invoke.noModelForControlAdapter', { - number: i + 1, - }) - ); - } - if (mismatchedModelBase) { - // This should never happen, just a sanity check - reasons.push( - i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { - number: i + 1, - }) - ); - } - if (hasNoImage) { - reasons.push( - i18n.t('parameters.invoke.noControlImageForControlAdapter', { - number: i + 1, - }) - ); - } - if (imageNotProcessed) { - reasons.push( - i18n.t('parameters.invoke.imageNotProcessedForControlAdapter', { - number: i + 1, - }) - ); - } - }); - } else { - // Handling for all other tabs - selectControlAdapterAll(controlAdapters) - .filter((ca) => ca.isEnabled) - .forEach((ca, i) => { - if (!ca.isEnabled) { - return; - } - - if (!ca.model) { - reasons.push( - i18n.t('parameters.invoke.noModelForControlAdapter', { - number: i + 1, - }) - ); - } else if (ca.model.base !== model?.base) { - // This should never happen, just a sanity check - reasons.push( - i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { - number: i + 1, - }) - ); - } - - if ( - !ca.controlImage || - (isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none') - ) { - reasons.push( - i18n.t('parameters.invoke.noControlImageForControlAdapter', { - number: i + 1, - }) - ); - } - }); - } + return { isReady: !reasons.length, reasons }; } - - return { isReady: !reasons.length, reasons }; - } -); + ); export const useIsReadyToEnqueue = () => { - const { isReady, reasons } = useAppSelector(selector); - return { isReady, reasons }; + const templates = useStore($templates); + const selector = useMemo(() => createSelector(templates), [templates]); + const value = useAppSelector(selector); + return value; }; diff --git a/invokeai/frontend/web/src/common/util/stopPropagation.ts b/invokeai/frontend/web/src/common/util/stopPropagation.ts index b3481b7c0e..0c6a1fc507 100644 --- a/invokeai/frontend/web/src/common/util/stopPropagation.ts +++ b/invokeai/frontend/web/src/common/util/stopPropagation.ts @@ -1,3 +1,7 @@ export const stopPropagation = (e: React.MouseEvent) => { e.stopPropagation(); }; + +export const preventDefault = (e: React.MouseEvent) => { + e.preventDefault(); +}; diff --git a/invokeai/frontend/web/src/common/util/toast.ts b/invokeai/frontend/web/src/common/util/toast.ts deleted file mode 100644 index ac61a4a12d..0000000000 --- a/invokeai/frontend/web/src/common/util/toast.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createStandaloneToast, theme, TOAST_OPTIONS } from '@invoke-ai/ui-library'; - -export const { toast } = createStandaloneToast({ - theme: theme, - defaultOptions: TOAST_OPTIONS.defaultOptions, -}); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx index 15d38b9f76..5ed5ffe573 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx @@ -21,8 +21,6 @@ import { setShouldShowBoundingBox, } from 'features/canvas/store/canvasSlice'; import type { CanvasLayer } from 'features/canvas/store/canvasTypes'; -import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes'; -import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton'; import { memo, useCallback, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -217,110 +215,107 @@ const IAICanvasToolbar = () => { [dispatch, isMaskEnabled] ); - const value = useMemo(() => LAYER_NAMES_DICT.filter((o) => o.value === layer)[0], [layer]); + const layerOptions = useMemo<{ label: string; value: CanvasLayer }[]>( + () => [ + { label: t('unifiedCanvas.base'), value: 'base' }, + { label: t('unifiedCanvas.mask'), value: 'mask' }, + ], + [t] + ); + const layerValue = useMemo(() => layerOptions.filter((o) => o.value === layer)[0] ?? null, [layer, layerOptions]); return ( - - - - - - - - - - + + + + + + - - + + - - } - isChecked={tool === 'move' || isStaging} - onClick={handleSelectMoveTool} - /> - : } - onClick={handleSetShouldShowBoundingBox} - isDisabled={isStaging} - /> - } - onClick={handleClickResetCanvasView} - /> - + + } + isChecked={tool === 'move' || isStaging} + onClick={handleSelectMoveTool} + /> + : } + onClick={handleSetShouldShowBoundingBox} + isDisabled={isStaging} + /> + } + onClick={handleClickResetCanvasView} + /> + - + + } + onClick={handleMergeVisible} + isDisabled={isStaging} + /> + } + onClick={handleSaveToGallery} + isDisabled={isStaging} + /> + {isClipboardAPIAvailable && ( } - onClick={handleMergeVisible} + aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`} + tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`} + icon={} + onClick={handleCopyImageToClipboard} isDisabled={isStaging} /> - } - onClick={handleSaveToGallery} - isDisabled={isStaging} - /> - {isClipboardAPIAvailable && ( - } - onClick={handleCopyImageToClipboard} - isDisabled={isStaging} - /> - )} - } - onClick={handleDownloadAsImage} - isDisabled={isStaging} - /> - - - - - + )} + } + onClick={handleDownloadAsImage} + isDisabled={isStaging} + /> + + + + + - - } - isDisabled={isStaging} - {...getUploadButtonProps()} - /> - - } - onClick={handleResetCanvas} - colorScheme="error" - isDisabled={isStaging} - /> - - - - - - - - - - + + } + isDisabled={isStaging} + {...getUploadButtonProps()} + /> + + } + onClick={handleResetCanvas} + colorScheme="error" + isDisabled={isStaging} + /> + + + + ); }; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index a22f23d9d3..fbb6378166 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -613,7 +613,7 @@ export const canvasSlice = createSlice({ state.batchIds = state.batchIds.filter((id) => id !== batch_status.batch_id); } - const queueItemStatus = action.payload.data.queue_item.status; + const queueItemStatus = action.payload.data.status; if (queueItemStatus === 'canceled' || queueItemStatus === 'failed') { resetStagingAreaIfEmpty(state); } diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts index 2d30e18760..c41c6f329f 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts @@ -5,11 +5,6 @@ import { z } from 'zod'; export type CanvasLayer = 'base' | 'mask'; -export const LAYER_NAMES_DICT: { label: string; value: CanvasLayer }[] = [ - { label: 'Base', value: 'base' }, - { label: 'Mask', value: 'mask' }, -]; - const zBoundingBoxScaleMethod = z.enum(['none', 'auto', 'manual']); export type BoundingBoxScaleMethod = z.infer; export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod => diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/types.ts b/invokeai/frontend/web/src/features/controlAdapters/store/types.ts index 7e2f18af5c..b76a729263 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/store/types.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/store/types.ts @@ -5,22 +5,7 @@ import type { ParameterT2IAdapterModel, } from 'features/parameters/types/parameterSchemas'; import type { components } from 'services/api/schema'; -import type { - CannyImageProcessorInvocation, - ColorMapImageProcessorInvocation, - ContentShuffleImageProcessorInvocation, - DepthAnythingImageProcessorInvocation, - DWOpenposeImageProcessorInvocation, - HedImageProcessorInvocation, - LineartAnimeImageProcessorInvocation, - LineartImageProcessorInvocation, - MediapipeFaceProcessorInvocation, - MidasDepthImageProcessorInvocation, - MlsdImageProcessorInvocation, - NormalbaeImageProcessorInvocation, - PidiImageProcessorInvocation, - ZoeDepthImageProcessorInvocation, -} from 'services/api/types'; +import type { Invocation } from 'services/api/types'; import type { O } from 'ts-toolbelt'; import { z } from 'zod'; @@ -28,20 +13,20 @@ import { z } from 'zod'; * Any ControlNet processor node */ export type ControlAdapterProcessorNode = - | CannyImageProcessorInvocation - | ColorMapImageProcessorInvocation - | ContentShuffleImageProcessorInvocation - | DepthAnythingImageProcessorInvocation - | HedImageProcessorInvocation - | LineartAnimeImageProcessorInvocation - | LineartImageProcessorInvocation - | MediapipeFaceProcessorInvocation - | MidasDepthImageProcessorInvocation - | MlsdImageProcessorInvocation - | NormalbaeImageProcessorInvocation - | DWOpenposeImageProcessorInvocation - | PidiImageProcessorInvocation - | ZoeDepthImageProcessorInvocation; + | Invocation<'canny_image_processor'> + | Invocation<'color_map_image_processor'> + | Invocation<'content_shuffle_image_processor'> + | Invocation<'depth_anything_image_processor'> + | Invocation<'hed_image_processor'> + | Invocation<'lineart_anime_image_processor'> + | Invocation<'lineart_image_processor'> + | Invocation<'mediapipe_face_processor'> + | Invocation<'midas_depth_image_processor'> + | Invocation<'mlsd_image_processor'> + | Invocation<'normalbae_image_processor'> + | Invocation<'dw_openpose_image_processor'> + | Invocation<'pidi_image_processor'> + | Invocation<'zoe_depth_image_processor'>; /** * Any ControlNet processor type @@ -71,7 +56,7 @@ export const isControlAdapterProcessorType = (v: unknown): v is ControlAdapterPr * The Canny processor node, with parameters flagged as required */ export type RequiredCannyImageProcessorInvocation = O.Required< - CannyImageProcessorInvocation, + Invocation<'canny_image_processor'>, 'type' | 'low_threshold' | 'high_threshold' | 'image_resolution' | 'detect_resolution' >; @@ -79,7 +64,7 @@ export type RequiredCannyImageProcessorInvocation = O.Required< * The Color Map processor node, with parameters flagged as required */ export type RequiredColorMapImageProcessorInvocation = O.Required< - ColorMapImageProcessorInvocation, + Invocation<'color_map_image_processor'>, 'type' | 'color_map_tile_size' >; @@ -87,7 +72,7 @@ export type RequiredColorMapImageProcessorInvocation = O.Required< * The ContentShuffle processor node, with parameters flagged as required */ export type RequiredContentShuffleImageProcessorInvocation = O.Required< - ContentShuffleImageProcessorInvocation, + Invocation<'content_shuffle_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' | 'w' | 'h' | 'f' >; @@ -95,7 +80,7 @@ export type RequiredContentShuffleImageProcessorInvocation = O.Required< * The DepthAnything processor node, with parameters flagged as required */ export type RequiredDepthAnythingImageProcessorInvocation = O.Required< - DepthAnythingImageProcessorInvocation, + Invocation<'depth_anything_image_processor'>, 'type' | 'model_size' | 'resolution' | 'offload' >; @@ -108,7 +93,7 @@ export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSiz * The HED processor node, with parameters flagged as required */ export type RequiredHedImageProcessorInvocation = O.Required< - HedImageProcessorInvocation, + Invocation<'hed_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' | 'scribble' >; @@ -116,7 +101,7 @@ export type RequiredHedImageProcessorInvocation = O.Required< * The Lineart Anime processor node, with parameters flagged as required */ export type RequiredLineartAnimeImageProcessorInvocation = O.Required< - LineartAnimeImageProcessorInvocation, + Invocation<'lineart_anime_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' >; @@ -124,7 +109,7 @@ export type RequiredLineartAnimeImageProcessorInvocation = O.Required< * The Lineart processor node, with parameters flagged as required */ export type RequiredLineartImageProcessorInvocation = O.Required< - LineartImageProcessorInvocation, + Invocation<'lineart_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' | 'coarse' >; @@ -132,7 +117,7 @@ export type RequiredLineartImageProcessorInvocation = O.Required< * The MediapipeFace processor node, with parameters flagged as required */ export type RequiredMediapipeFaceProcessorInvocation = O.Required< - MediapipeFaceProcessorInvocation, + Invocation<'mediapipe_face_processor'>, 'type' | 'max_faces' | 'min_confidence' | 'image_resolution' | 'detect_resolution' >; @@ -140,7 +125,7 @@ export type RequiredMediapipeFaceProcessorInvocation = O.Required< * The MidasDepth processor node, with parameters flagged as required */ export type RequiredMidasDepthImageProcessorInvocation = O.Required< - MidasDepthImageProcessorInvocation, + Invocation<'midas_depth_image_processor'>, 'type' | 'a_mult' | 'bg_th' | 'image_resolution' | 'detect_resolution' >; @@ -148,7 +133,7 @@ export type RequiredMidasDepthImageProcessorInvocation = O.Required< * The MLSD processor node, with parameters flagged as required */ export type RequiredMlsdImageProcessorInvocation = O.Required< - MlsdImageProcessorInvocation, + Invocation<'mlsd_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' | 'thr_v' | 'thr_d' >; @@ -156,7 +141,7 @@ export type RequiredMlsdImageProcessorInvocation = O.Required< * The NormalBae processor node, with parameters flagged as required */ export type RequiredNormalbaeImageProcessorInvocation = O.Required< - NormalbaeImageProcessorInvocation, + Invocation<'normalbae_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' >; @@ -164,7 +149,7 @@ export type RequiredNormalbaeImageProcessorInvocation = O.Required< * The DW Openpose processor node, with parameters flagged as required */ export type RequiredDWOpenposeImageProcessorInvocation = O.Required< - DWOpenposeImageProcessorInvocation, + Invocation<'dw_openpose_image_processor'>, 'type' | 'image_resolution' | 'draw_body' | 'draw_face' | 'draw_hands' >; @@ -172,14 +157,14 @@ export type RequiredDWOpenposeImageProcessorInvocation = O.Required< * The Pidi processor node, with parameters flagged as required */ export type RequiredPidiImageProcessorInvocation = O.Required< - PidiImageProcessorInvocation, + Invocation<'pidi_image_processor'>, 'type' | 'detect_resolution' | 'image_resolution' | 'safe' | 'scribble' >; /** * The ZoeDepth processor node, with parameters flagged as required */ -export type RequiredZoeDepthImageProcessorInvocation = O.Required; +export type RequiredZoeDepthImageProcessorInvocation = O.Required, 'type'>; /** * Any ControlNet Processor node, with its parameters flagged as required diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index 3102e4afa8..c7a49da8c7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -18,7 +18,12 @@ export const AddLayerButton = memo(() => { return ( - } variant="ghost"> + } + variant="ghost" + data-testid="control-layers-add-layer-menu-button" + > {t('controlLayers.addLayer')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx index 984331a050..9e71ad943c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayer.tsx @@ -4,7 +4,7 @@ import { CALayerControlAdapterWrapper } from 'features/controlLayers/components/ import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { layerSelected, selectCALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; @@ -19,7 +19,6 @@ export const CALayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected); const onClick = useCallback(() => { - // Must be capture so that the layer is selected before deleting/resetting/etc dispatch(layerSelected(layerId)); }, [dispatch, layerId]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); @@ -27,7 +26,7 @@ export const CALayer = memo(({ layerId }: Props) => { return ( - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx index e6c6aae286..4d93eb12ec 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterImagePreview.tsx @@ -42,10 +42,10 @@ export const ControlAdapterImagePreview = memo( const [isMouseOverImage, setIsMouseOverImage] = useState(false); const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery( - controlAdapter.image?.imageName ?? skipToken + controlAdapter.image?.name ?? skipToken ); const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery( - controlAdapter.processedImage?.imageName ?? skipToken + controlAdapter.processedImage?.name ?? skipToken ); const [changeIsIntermediate] = useChangeImageIsIntermediateMutation(); @@ -124,7 +124,7 @@ export const ControlAdapterImagePreview = memo( controlImage && processedControlImage && !isMouseOverImage && - !controlAdapter.isProcessingImage && + !controlAdapter.processorPendingBatchId && controlAdapter.processorConfig !== null; useEffect(() => { @@ -190,7 +190,7 @@ export const ControlAdapterImagePreview = memo( /> - {controlAdapter.isProcessingImage && ( + {controlAdapter.processorPendingBatchId !== null && ( { onChangeImage(null); }, [onChangeImage]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx index 00993789b1..b5da990b6f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/processors/DepthAnythingProcessor.tsx @@ -2,14 +2,13 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAndIPAdapter/processors/types'; import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/util/controlAdapters'; -import { CA_PROCESSOR_DATA, isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters'; +import { isDepthAnythingModelSize } from 'features/controlLayers/util/controlAdapters'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import ProcessorWrapper from './ProcessorWrapper'; type Props = ProcessorComponentProps; -const DEFAULTS = CA_PROCESSOR_DATA['depth_anything_image_processor'].buildDefaults(); export const DepthAnythingProcessor = memo(({ onChange, config }: Props) => { const { t } = useTranslation(); @@ -38,12 +37,7 @@ export const DepthAnythingProcessor = memo(({ onChange, config }: Props) => { {t('controlnet.modelSize')} - + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 1dd79d0220..d3ddc07139 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -32,7 +32,7 @@ export const ControlLayersPanelContent = memo(() => { {layerIdTypePairs.length > 0 && ( - + {layerIdTypePairs.map(({ id, type }) => ( ))} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index 89032b7c76..1f3307bdf1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -1,12 +1,30 @@ -import { Flex, IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library'; +import { + Checkbox, + Flex, + FormControl, + FormLabel, + IconButton, + Popover, + PopoverBody, + PopoverContent, + PopoverTrigger, +} from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { setShouldInvertBrushSizeScrollDirection } from 'features/canvas/store/canvasSlice'; import { GlobalMaskLayerOpacity } from 'features/controlLayers/components/GlobalMaskLayerOpacity'; -import { memo } from 'react'; +import type { ChangeEvent } from 'react'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { RiSettings4Fill } from 'react-icons/ri'; const ControlLayersSettingsPopover = () => { const { t } = useTranslation(); - + const dispatch = useAppDispatch(); + const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); + const handleChangeShouldInvertBrushSizeScrollDirection = useCallback( + (e: ChangeEvent) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)), + [dispatch] + ); return ( @@ -16,6 +34,13 @@ const ControlLayersSettingsPopover = () => { + + {t('unifiedCanvas.invertBrushSizeScrollDirection')} + + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index b78910700d..8cc3aa93fe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -4,14 +4,17 @@ import { BrushSize } from 'features/controlLayers/components/BrushSize'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; -import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton'; +import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; +import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu'; import { memo } from 'react'; export const ControlLayersToolbar = memo(() => { return ( - + + + @@ -21,7 +24,7 @@ export const ControlLayersToolbar = memo(() => { - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx index dad102b470..00487fcc43 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/DeleteAllLayersButton.tsx @@ -20,6 +20,7 @@ export const DeleteAllLayersButton = memo(() => { variant="ghost" colorScheme="error" isDisabled={isDisabled} + data-testid="control-layers-delete-all-layers-button" > {t('controlLayers.deleteAll')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx index 772dbd7332..c53c4c7631 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx @@ -5,9 +5,10 @@ import { InitialImagePreview } from 'features/controlLayers/components/IILayer/I import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { + iiLayerDenoisingStrengthChanged, iiLayerImageChanged, layerSelected, selectIILayerOrThrow, @@ -36,6 +37,13 @@ export const IILayer = memo(({ layerId }: Props) => { [dispatch, layerId] ); + const onChangeDenoisingStrength = useCallback( + (denoisingStrength: number) => { + dispatch(iiLayerDenoisingStrengthChanged({ layerId, denoisingStrength })); + }, + [dispatch, layerId] + ); + const droppableData = useMemo( () => ({ actionType: 'SET_II_LAYER_IMAGE', @@ -58,7 +66,7 @@ export const IILayer = memo(({ layerId }: Props) => { return ( - + @@ -67,7 +75,7 @@ export const IILayer = memo(({ layerId }: Props) => { {isOpen && ( - + { onChangeImage(null); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx index 02a161608d..e8f60c8d07 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayer.tsx @@ -1,21 +1,28 @@ import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { memo } from 'react'; +import { layerSelected, selectIPALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; +import { memo, useCallback } from 'react'; type Props = { layerId: string; }; export const IPALayer = memo(({ layerId }: Props) => { + const dispatch = useAppDispatch(); + const isSelected = useAppSelector((s) => selectIPALayerOrThrow(s.controlLayers.present, layerId).isSelected); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); + const onClick = useCallback(() => { + dispatch(layerSelected(layerId)); + }, [dispatch, layerId]); return ( - + - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerVisibilityToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerVisibilityToggle.tsx index d2dab39e36..227d74c35a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerVisibilityToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerVisibilityToggle.tsx @@ -1,8 +1,8 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { useLayerIsVisible } from 'features/controlLayers/hooks/layerStateHooks'; -import { layerVisibilityToggled } from 'features/controlLayers/store/controlLayersSlice'; +import { useLayerIsEnabled } from 'features/controlLayers/hooks/layerStateHooks'; +import { layerIsEnabledToggled } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCheckBold } from 'react-icons/pi'; @@ -11,21 +11,21 @@ type Props = { layerId: string; }; -export const LayerVisibilityToggle = memo(({ layerId }: Props) => { +export const LayerIsEnabledToggle = memo(({ layerId }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const isVisible = useLayerIsVisible(layerId); + const isEnabled = useLayerIsEnabled(layerId); const onClick = useCallback(() => { - dispatch(layerVisibilityToggled(layerId)); + dispatch(layerIsEnabledToggled(layerId)); }, [dispatch, layerId]); return ( : undefined} + icon={isEnabled ? : undefined} onClick={onClick} colorScheme="base" onDoubleClick={stopPropagation} // double click expands the layer @@ -33,4 +33,4 @@ export const LayerVisibilityToggle = memo(({ layerId }: Props) => { ); }); -LayerVisibilityToggle.displayName = 'LayerVisibilityToggle'; +LayerIsEnabledToggle.displayName = 'LayerVisibilityToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx index a6bce5316e..cc331017d3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -6,7 +6,7 @@ import { AddPromptButtons } from 'features/controlLayers/components/AddPromptBut import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; -import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; +import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { isRegionalGuidanceLayer, @@ -55,7 +55,7 @@ export const RGLayer = memo(({ layerId }: Props) => { return ( - + {autoNegative === 'invert' && ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index d0d693a5f2..08956e73dc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -130,11 +130,11 @@ const useStageRenderer = ( }, [stage, state.size.width, state.size.height, wrapper]); useLayoutEffect(() => { - log.trace('Rendering tool preview'); if (asPreview) { // Preview should not display tool return; } + log.trace('Rendering tool preview'); renderers.renderToolPreview( stage, tool, @@ -178,15 +178,24 @@ const useStageRenderer = ( // Preview should not display bboxes return; } - renderers.renderBbox(stage, state.layers, tool, onBboxChanged); + renderers.renderBboxes(stage, state.layers, tool); }, [stage, asPreview, state.layers, tool, onBboxChanged, renderers]); useLayoutEffect(() => { - log.trace('Rendering background'); + if (asPreview) { + // Preview should not check for transparency + return; + } + log.trace('Updating bboxes'); + debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged); + }, [stage, asPreview, state.layers, onBboxChanged]); + + useLayoutEffect(() => { if (asPreview) { // The preview should not have a background return; } + log.trace('Rendering background'); renderers.renderBackground(stage, state.size.width, state.size.height); }, [stage, asPreview, state.size.width, state.size.height, renderers]); @@ -196,11 +205,11 @@ const useStageRenderer = ( }, [stage, layerIds, renderers]); useLayoutEffect(() => { - log.trace('Rendering no layers message'); if (asPreview) { // The preview should not display the no layers message return; } + log.trace('Rendering no layers message'); renderers.renderNoLayersMessage(stage, layerCount, state.size.width, state.size.height); }, [stage, layerCount, renderers, asPreview, state.size.width, state.size.height]); @@ -233,7 +242,14 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { return ( - + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts index f2054779d4..21e49ba15e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts @@ -39,7 +39,7 @@ export const useLayerNegativePrompt = (layerId: string) => { return prompt; }; -export const useLayerIsVisible = (layerId: string) => { +export const useLayerIsEnabled = (layerId: string) => { const selectLayer = useMemo( () => createSelector(selectControlLayersSlice, (controlLayers) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts index fa49fdb473..514e8c35ff 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/mouseEventHooks.ts @@ -224,5 +224,10 @@ export const useMouseEvents = () => { [selectedLayerType, tool, shouldInvertBrushSizeScrollDirection, dispatch, brushSize] ); - return { onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel }; + const handlers = useMemo( + () => ({ onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel }), + [onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel] + ); + + return handlers; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts deleted file mode 100644 index bf0fa661a9..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useControlLayersTitle.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { - isControlAdapterLayer, - isInitialImageLayer, - isIPAdapterLayer, - isRegionalGuidanceLayer, - selectControlLayersSlice, -} from 'features/controlLayers/store/controlLayersSlice'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlLayers) => { - let count = 0; - controlLayers.present.layers.forEach((l) => { - if (isRegionalGuidanceLayer(l)) { - const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt); - const hasAtLeastOneImagePrompt = l.ipAdapters.filter((ipa) => Boolean(ipa.image)).length > 0; - if (hasTextPrompt || hasAtLeastOneImagePrompt) { - count += 1; - } - } - if (isControlAdapterLayer(l)) { - if (l.controlAdapter.image || l.controlAdapter.processedImage) { - count += 1; - } - } - if (isIPAdapterLayer(l)) { - if (l.ipAdapter.image) { - count += 1; - } - } - if (isInitialImageLayer(l)) { - if (l.image) { - count += 1; - } - } - }); - - return count; -}); - -export const useControlLayersTitle = () => { - const { t } = useTranslation(); - const validLayerCount = useAppSelector(selectValidLayerCount); - const title = useMemo(() => { - const suffix = validLayerCount > 0 ? ` (${validLayerCount})` : ''; - return `${t('controlLayers.controlLayers')}${suffix}`; - }, [t, validLayerCount]); - return title; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 1ef90ead3a..5fa8cc3dfb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -27,7 +27,7 @@ import { modelChanged } from 'features/parameters/store/generationSlice'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect, Vector2d } from 'konva/lib/types'; -import { isEqual, partition } from 'lodash-es'; +import { isEqual, partition, unset } from 'lodash-es'; import { atom } from 'nanostores'; import type { RgbColor } from 'react-colorful'; import type { UndoableOptions } from 'redux-undo'; @@ -49,7 +49,7 @@ import type { } from './types'; export const initialControlLayersState: ControlLayersState = { - _version: 1, + _version: 3, selectedLayerId: null, brushSize: 100, layers: [], @@ -124,6 +124,12 @@ const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => { const lastColor = rgLayers[rgLayers.length - 1]?.previewColor; return LayerColors.next(lastColor); }; +const exclusivelySelectLayer = (state: ControlLayersState, layerId: string) => { + for (const layer of state.layers) { + layer.isSelected = layer.id === layerId; + } + state.selectedLayerId = layerId; +}; export const controlLayersSlice = createSlice({ name: 'controlLayers', @@ -131,16 +137,9 @@ export const controlLayersSlice = createSlice({ reducers: { //#region Any Layer Type layerSelected: (state, action: PayloadAction) => { - for (const layer of state.layers.filter(isRenderableLayer)) { - if (layer.id === action.payload) { - layer.isSelected = true; - state.selectedLayerId = action.payload; - } else { - layer.isSelected = false; - } - } + exclusivelySelectLayer(state, action.payload); }, - layerVisibilityToggled: (state, action: PayloadAction) => { + layerIsEnabledToggled: (state, action: PayloadAction) => { const layer = state.layers.find((l) => l.id === action.payload); if (layer) { layer.isEnabled = !layer.isEnabled; @@ -167,7 +166,6 @@ export const controlLayersSlice = createSlice({ // The layer was fully erased, empty its objects to prevent accumulation of invisible objects layer.maskObjects = []; layer.uploadedMaskImage = null; - layer.needsPixelBbox = false; } } }, @@ -178,7 +176,6 @@ export const controlLayersSlice = createSlice({ layer.maskObjects = []; layer.bbox = null; layer.isEnabled = true; - layer.needsPixelBbox = false; layer.bboxNeedsUpdate = false; layer.uploadedMaskImage = null; } @@ -244,17 +241,16 @@ export const controlLayersSlice = createSlice({ controlAdapter, }; state.layers.push(layer); - state.selectedLayerId = layer.id; - for (const layer of state.layers.filter(isRenderableLayer)) { - if (layer.id !== layerId) { - layer.isSelected = false; - } - } + exclusivelySelectLayer(state, layer.id); }, prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({ payload: { layerId: uuidv4(), controlAdapter }, }), }, + caLayerRecalled: (state, action: PayloadAction) => { + state.layers.push({ ...action.payload, isSelected: true }); + exclusivelySelectLayer(state, action.payload.id); + }, caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; const layer = selectCALayerOrThrow(state, layerId); @@ -338,19 +334,13 @@ export const controlLayersSlice = createSlice({ const layer = selectCALayerOrThrow(state, layerId); layer.opacity = opacity; }, - caLayerIsProcessingImageChanged: ( + caLayerProcessorPendingBatchIdChanged: ( state, - action: PayloadAction<{ layerId: string; isProcessingImage: boolean }> + action: PayloadAction<{ layerId: string; batchId: string | null }> ) => { - const { layerId, isProcessingImage } = action.payload; + const { layerId, batchId } = action.payload; const layer = selectCALayerOrThrow(state, layerId); - layer.controlAdapter.isProcessingImage = isProcessingImage; - }, - caLayerControlNetsDeleted: (state) => { - state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 'controlnet'); - }, - caLayerT2IAdaptersDeleted: (state) => { - state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 't2i_adapter'); + layer.controlAdapter.processorPendingBatchId = batchId; }, //#endregion @@ -362,12 +352,17 @@ export const controlLayersSlice = createSlice({ id: getIPALayerId(layerId), type: 'ip_adapter_layer', isEnabled: true, + isSelected: true, ipAdapter, }; state.layers.push(layer); + exclusivelySelectLayer(state, layer.id); }, prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }), }, + ipaLayerRecalled: (state, action: PayloadAction) => { + state.layers.push(action.payload); + }, ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; const layer = selectIPALayerOrThrow(state, layerId); @@ -401,9 +396,6 @@ export const controlLayersSlice = createSlice({ const layer = selectIPALayerOrThrow(state, layerId); layer.ipAdapter.clipVisionModel = clipVisionModel; }, - ipaLayersDeleted: (state) => { - state.layers = state.layers.filter((l) => !isIPAdapterLayer(l)); - }, //#endregion //#region CA or IPA Layers @@ -445,7 +437,6 @@ export const controlLayersSlice = createSlice({ x: 0, y: 0, autoNegative: 'invert', - needsPixelBbox: false, positivePrompt: '', negativePrompt: null, ipAdapters: [], @@ -453,15 +444,14 @@ export const controlLayersSlice = createSlice({ uploadedMaskImage: null, }; state.layers.push(layer); - state.selectedLayerId = layer.id; - for (const layer of state.layers.filter(isRenderableLayer)) { - if (layer.id !== layerId) { - layer.isSelected = false; - } - } + exclusivelySelectLayer(state, layer.id); }, prepare: () => ({ payload: { layerId: uuidv4() } }), }, + rgLayerRecalled: (state, action: PayloadAction) => { + state.layers.push({ ...action.payload, isSelected: true }); + exclusivelySelectLayer(state, action.payload.id); + }, rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { const { layerId, prompt } = action.payload; const layer = selectRGLayerOrThrow(state, layerId); @@ -501,9 +491,6 @@ export const controlLayersSlice = createSlice({ }); layer.bboxNeedsUpdate = true; layer.uploadedMaskImage = null; - if (!layer.needsPixelBbox && tool === 'eraser') { - layer.needsPixelBbox = true; - } }, prepare: (payload: { layerId: string; points: [number, number, number, number]; tool: DrawingTool }) => ({ payload: { ...payload, lineUuid: uuidv4() }, @@ -629,12 +616,24 @@ export const controlLayersSlice = createSlice({ iiLayerAdded: { reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; + + // Retain opacity and denoising strength of existing initial image layer if exists + let opacity = 1; + let denoisingStrength = 0.75; + const iiLayer = state.layers.find((l) => l.id === layerId); + if (iiLayer) { + assert(isInitialImageLayer(iiLayer)); + opacity = iiLayer.opacity; + denoisingStrength = iiLayer.denoisingStrength; + } + // Highlander! There can be only one! state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); + const layer: InitialImageLayer = { id: layerId, type: 'initial_image_layer', - opacity: 1, + opacity, x: 0, y: 0, bbox: null, @@ -642,16 +641,17 @@ export const controlLayersSlice = createSlice({ isEnabled: true, image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, isSelected: true, + denoisingStrength, }; state.layers.push(layer); - state.selectedLayerId = layer.id; - for (const layer of state.layers.filter(isRenderableLayer)) { - if (layer.id !== layerId) { - layer.isSelected = false; - } - } + exclusivelySelectLayer(state, layer.id); }, - prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: 'initial_image_layer', imageDTO } }), + prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: INITIAL_IMAGE_LAYER_ID, imageDTO } }), + }, + iiLayerRecalled: (state, action: PayloadAction) => { + state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); + state.layers.push({ ...action.payload, isSelected: true }); + exclusivelySelectLayer(state, action.payload.id); }, iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { const { layerId, imageDTO } = action.payload; @@ -666,6 +666,11 @@ export const controlLayersSlice = createSlice({ const layer = selectIILayerOrThrow(state, layerId); layer.opacity = opacity; }, + iiLayerDenoisingStrengthChanged: (state, action: PayloadAction<{ layerId: string; denoisingStrength: number }>) => { + const { layerId, denoisingStrength } = action.payload; + const layer = selectIILayerOrThrow(state, layerId); + layer.denoisingStrength = denoisingStrength; + }, //#endregion //#region Globals @@ -786,7 +791,7 @@ class LayerColors { export const { // Any Layer Type layerSelected, - layerVisibilityToggled, + layerIsEnabledToggled, layerTranslated, layerBboxChanged, layerReset, @@ -799,6 +804,7 @@ export const { allLayersDeleted, // CA Layers caLayerAdded, + caLayerRecalled, caLayerImageChanged, caLayerProcessedImageChanged, caLayerModelChanged, @@ -806,21 +812,20 @@ export const { caLayerProcessorConfigChanged, caLayerIsFilterEnabledChanged, caLayerOpacityChanged, - caLayerIsProcessingImageChanged, - caLayerControlNetsDeleted, - caLayerT2IAdaptersDeleted, + caLayerProcessorPendingBatchIdChanged, // IPA Layers ipaLayerAdded, + ipaLayerRecalled, ipaLayerImageChanged, ipaLayerMethodChanged, ipaLayerModelChanged, ipaLayerCLIPVisionModelChanged, - ipaLayersDeleted, // CA or IPA Layers caOrIPALayerWeightChanged, caOrIPALayerBeginEndStepPctChanged, // RG Layers rgLayerAdded, + rgLayerRecalled, rgLayerPositivePromptChanged, rgLayerNegativePromptChanged, rgLayerPreviewColorChanged, @@ -839,8 +844,10 @@ export const { rgLayerIPAdapterCLIPVisionModelChanged, // II Layer iiLayerAdded, + iiLayerRecalled, iiLayerImageChanged, iiLayerOpacityChanged, + iiLayerDenoisingStrengthChanged, // Globals positivePromptChanged, negativePromptChanged, @@ -860,6 +867,19 @@ export const selectControlLayersSlice = (state: RootState) => state.controlLayer /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateControlLayersState = (state: any): any => { + if (state._version === 1) { + // Reset state for users on v1 (e.g. beta users), some changes could cause + state = deepClone(initialControlLayersState); + } + if (state._version === 2) { + // The CA `isProcessingImage` flag was replaced with a `processorPendingBatchId` property, fix up CA layers + for (const layer of (state as ControlLayersState).layers) { + if (layer.type === 'control_adapter_layer') { + layer.controlAdapter.processorPendingBatchId = null; + unset(layer.controlAdapter, 'isProcessingImage'); + } + } + } return state; }; @@ -886,21 +906,22 @@ export const RG_LAYER_NAME = 'regional_guidance_layer'; export const RG_LAYER_LINE_NAME = 'regional_guidance_layer.line'; export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group'; export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect'; +export const INITIAL_IMAGE_LAYER_ID = 'singleton_initial_image_layer'; export const INITIAL_IMAGE_LAYER_NAME = 'initial_image_layer'; export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image'; export const LAYER_BBOX_NAME = 'layer.bbox'; export const COMPOSITING_RECT_NAME = 'compositing-rect'; // Getters for non-singleton layer and object IDs -const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`; +export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`; const getRGLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`; const getRGLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`; export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`; export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`; -const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`; +export const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`; export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; export const getIILayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`; -const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`; +export const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`; export const controlLayersPersistConfig: PersistConfig = { name: controlLayersSlice.name, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index afb04aae37..771e5060e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,90 +1,119 @@ -import type { - ControlNetConfigV2, - ImageWithDims, - IPAdapterConfigV2, - T2IAdapterConfigV2, +import { + zControlNetConfigV2, + zImageWithDims, + zIPAdapterConfigV2, + zT2IAdapterConfigV2, } from 'features/controlLayers/util/controlAdapters'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; -import type { - ParameterAutoNegative, - ParameterHeight, - ParameterNegativePrompt, - ParameterNegativeStylePromptSDXL, - ParameterPositivePrompt, - ParameterPositiveStylePromptSDXL, - ParameterWidth, +import { + type ParameterHeight, + type ParameterNegativePrompt, + type ParameterNegativeStylePromptSDXL, + type ParameterPositivePrompt, + type ParameterPositiveStylePromptSDXL, + type ParameterWidth, + zAutoNegative, + zParameterNegativePrompt, + zParameterPositivePrompt, + zParameterStrength, } from 'features/parameters/types/parameterSchemas'; -import type { IRect } from 'konva/lib/types'; -import type { RgbColor } from 'react-colorful'; +import { z } from 'zod'; -export type DrawingTool = 'brush' | 'eraser'; +const zTool = z.enum(['brush', 'eraser', 'move', 'rect']); +export type Tool = z.infer; +const zDrawingTool = zTool.extract(['brush', 'eraser']); +export type DrawingTool = z.infer; -export type Tool = DrawingTool | 'move' | 'rect'; +const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, { + message: 'Must have an even number of points', +}); +const zVectorMaskLine = z.object({ + id: z.string(), + type: z.literal('vector_mask_line'), + tool: zDrawingTool, + strokeWidth: z.number().min(1), + points: zPoints, +}); +export type VectorMaskLine = z.infer; -export type VectorMaskLine = { - id: string; - type: 'vector_mask_line'; - tool: DrawingTool; - strokeWidth: number; - points: number[]; -}; +const zVectorMaskRect = z.object({ + id: z.string(), + type: z.literal('vector_mask_rect'), + x: z.number(), + y: z.number(), + width: z.number().min(1), + height: z.number().min(1), +}); +export type VectorMaskRect = z.infer; -export type VectorMaskRect = { - id: string; - type: 'vector_mask_rect'; - x: number; - y: number; - width: number; - height: number; -}; +const zLayerBase = z.object({ + id: z.string(), + isEnabled: z.boolean().default(true), + isSelected: z.boolean().default(true), +}); -type LayerBase = { - id: string; - isEnabled: boolean; -}; +const zRect = z.object({ + x: z.number(), + y: z.number(), + width: z.number().min(1), + height: z.number().min(1), +}); +const zRenderableLayerBase = zLayerBase.extend({ + x: z.number(), + y: z.number(), + bbox: zRect.nullable(), + bboxNeedsUpdate: z.boolean(), +}); -type RenderableLayerBase = LayerBase & { - x: number; - y: number; - bbox: IRect | null; - bboxNeedsUpdate: boolean; - isSelected: boolean; -}; +const zControlAdapterLayer = zRenderableLayerBase.extend({ + type: z.literal('control_adapter_layer'), + opacity: z.number().gte(0).lte(1), + isFilterEnabled: z.boolean(), + controlAdapter: z.discriminatedUnion('type', [zControlNetConfigV2, zT2IAdapterConfigV2]), +}); +export type ControlAdapterLayer = z.infer; -export type ControlAdapterLayer = RenderableLayerBase & { - type: 'control_adapter_layer'; // technically, also t2i adapter layer - opacity: number; - isFilterEnabled: boolean; - controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2; -}; +const zIPAdapterLayer = zLayerBase.extend({ + type: z.literal('ip_adapter_layer'), + ipAdapter: zIPAdapterConfigV2, +}); +export type IPAdapterLayer = z.infer; -export type IPAdapterLayer = LayerBase & { - type: 'ip_adapter_layer'; - ipAdapter: IPAdapterConfigV2; -}; +const zRgbColor = z.object({ + r: z.number().int().min(0).max(255), + g: z.number().int().min(0).max(255), + b: z.number().int().min(0).max(255), +}); +const zRegionalGuidanceLayer = zRenderableLayerBase.extend({ + type: z.literal('regional_guidance_layer'), + maskObjects: z.array(z.discriminatedUnion('type', [zVectorMaskLine, zVectorMaskRect])), + positivePrompt: zParameterPositivePrompt.nullable(), + negativePrompt: zParameterNegativePrompt.nullable(), + ipAdapters: z.array(zIPAdapterConfigV2), + previewColor: zRgbColor, + autoNegative: zAutoNegative, + uploadedMaskImage: zImageWithDims.nullable(), +}); +export type RegionalGuidanceLayer = z.infer; -export type RegionalGuidanceLayer = RenderableLayerBase & { - type: 'regional_guidance_layer'; - maskObjects: (VectorMaskLine | VectorMaskRect)[]; - positivePrompt: ParameterPositivePrompt | null; - negativePrompt: ParameterNegativePrompt | null; // Up to one text prompt per mask - ipAdapters: IPAdapterConfigV2[]; // Any number of image prompts - previewColor: RgbColor; - autoNegative: ParameterAutoNegative; - needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object - uploadedMaskImage: ImageWithDims | null; -}; +const zInitialImageLayer = zRenderableLayerBase.extend({ + type: z.literal('initial_image_layer'), + opacity: z.number().gte(0).lte(1), + image: zImageWithDims.nullable(), + denoisingStrength: zParameterStrength, +}); +export type InitialImageLayer = z.infer; -export type InitialImageLayer = RenderableLayerBase & { - type: 'initial_image_layer'; - opacity: number; - image: ImageWithDims | null; -}; - -export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer | InitialImageLayer; +export const zLayer = z.discriminatedUnion('type', [ + zRegionalGuidanceLayer, + zControlAdapterLayer, + zIPAdapterLayer, + zInitialImageLayer, +]); +export type Layer = z.infer; export type ControlLayersState = { - _version: 1; + _version: 3; selectedLayerId: string | null; layers: Layer[]; brushSize: number; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/util/bbox.ts index 72aefe1eb4..3b037863c9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/bbox.ts @@ -2,7 +2,6 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL'; import { RG_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/store/controlLayersSlice'; import Konva from 'konva'; -import type { Layer as KonvaLayerType } from 'konva/lib/Layer'; import type { IRect } from 'konva/lib/types'; import { assert } from 'tsafe'; @@ -54,34 +53,30 @@ const getImageDataBbox = (imageData: ImageData): Extents | null => { }; /** - * Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers. - * @param layer The konva layer to get the bounding box of. - * @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox. + * Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer + * to be captured, manipulated or analyzed without interference from other layers. + * @param layer The konva layer to clone. + * @returns The cloned stage and layer. */ -export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = false): IRect | null => { - // To calculate the layer's bounding box, we must first export it to a pixel array, then do some math. - // - // Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect - // by calculating the extents of individual shapes from their "vector" shape data. - // - // This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines. - // These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large. +const getIsolatedRGLayerClone = (layer: Konva.Layer): { stageClone: Konva.Stage; layerClone: Konva.Layer } => { const stage = layer.getStage(); - // Construct and offscreen canvas on which we will do the bbox calculations. + // Construct an offscreen canvas with the same dimensions as the layer's stage. const offscreenStageContainer = document.createElement('div'); - const offscreenStage = new Konva.Stage({ + const stageClone = new Konva.Stage({ container: offscreenStageContainer, + x: stage.x(), + y: stage.y(), width: stage.width(), height: stage.height(), }); // Clone the layer and filter out unwanted children. const layerClone = layer.clone(); - offscreenStage.add(layerClone); + stageClone.add(layerClone); for (const child of layerClone.getChildren()) { - if (child.name() === RG_LAYER_OBJECT_GROUP_NAME) { + if (child.name() === RG_LAYER_OBJECT_GROUP_NAME && child.hasChildren()) { // We need to cache the group to ensure it composites out eraser strokes correctly child.opacity(1); child.cache(); @@ -91,11 +86,31 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal } } + return { stageClone, layerClone }; +}; + +/** + * Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers. + * @param layer The konva layer to get the bounding box of. + * @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox. + */ +export const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false): IRect | null => { + // To calculate the layer's bounding box, we must first export it to a pixel array, then do some math. + // + // Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect + // by calculating the extents of individual shapes from their "vector" shape data. + // + // This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines. + // These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large. + const { stageClone, layerClone } = getIsolatedRGLayerClone(layer); + // Get a worst-case rect using the relatively fast `getClientRect`. const layerRect = layerClone.getClientRect(); - + if (layerRect.width === 0 || layerRect.height === 0) { + return null; + } // Capture the image data with the above rect. - const layerImageData = offscreenStage + const layerImageData = stageClone .toCanvas(layerRect) .getContext('2d') ?.getImageData(0, 0, layerRect.width, layerRect.height); @@ -114,8 +129,8 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal // Correct the bounding box to be relative to the layer's position. const correctedLayerBbox = { - x: layerBbox.minX - Math.floor(stage.x()) + layerRect.x - Math.floor(layer.x()), - y: layerBbox.minY - Math.floor(stage.y()) + layerRect.y - Math.floor(layer.y()), + x: layerBbox.minX - Math.floor(stageClone.x()) + layerRect.x - Math.floor(layer.x()), + y: layerBbox.minY - Math.floor(stageClone.y()) + layerRect.y - Math.floor(layer.y()), width: layerBbox.maxX - layerBbox.minX, height: layerBbox.maxY - layerBbox.minY, }; @@ -123,7 +138,13 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal return correctedLayerBbox; }; -export const getLayerBboxFast = (layer: KonvaLayerType): IRect => { +/** + * Get the bounding box of a konva layer. This function is faster than `getLayerBboxPixels` but less accurate. It + * should only be used when there are no eraser strokes or shapes in the layer. + * @param layer The konva layer to get the bounding box of. + * @returns The bounding box of the layer. + */ +export const getLayerBboxFast = (layer: Konva.Layer): IRect => { const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG); return { x: Math.floor(bbox.x), diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts index 880514bf7c..22f54d622c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.test.ts @@ -1,23 +1,93 @@ -import type { S } from 'services/api/types'; +import type { Invocation } from 'services/api/types'; import type { Equals } from 'tsafe'; import { assert } from 'tsafe'; import { describe, test } from 'vitest'; import type { + CannyProcessorConfig, CLIPVisionModelV2, + ColorMapProcessorConfig, + ContentShuffleProcessorConfig, ControlModeV2, DepthAnythingModelSize, + DepthAnythingProcessorConfig, + DWOpenposeProcessorConfig, + HedProcessorConfig, IPMethodV2, + LineartAnimeProcessorConfig, + LineartProcessorConfig, + MediapipeFaceProcessorConfig, + MidasDepthProcessorConfig, + MlsdProcessorConfig, + NormalbaeProcessorConfig, + PidiProcessorConfig, ProcessorConfig, ProcessorTypeV2, + ZoeDepthProcessorConfig, } from './controlAdapters'; describe('Control Adapter Types', () => { - test('ProcessorType', () => assert>()); - test('IP Adapter Method', () => assert, IPMethodV2>>()); - test('CLIP Vision Model', () => - assert, CLIPVisionModelV2>>()); - test('Control Mode', () => assert, ControlModeV2>>()); - test('DepthAnything Model Size', () => - assert, DepthAnythingModelSize>>()); + test('ProcessorType', () => { + assert>(); + }); + test('IP Adapter Method', () => { + assert['method']>, IPMethodV2>>(); + }); + test('CLIP Vision Model', () => { + assert['clip_vision_model']>, CLIPVisionModelV2>>(); + }); + test('Control Mode', () => { + assert['control_mode']>, ControlModeV2>>(); + }); + test('DepthAnything Model Size', () => { + assert['model_size']>, DepthAnythingModelSize>>(); + }); + test('Processor Configs', () => { + // The processor configs are manually modeled zod schemas. This test ensures that the inferred types are correct. + // The types prefixed with `_` are types generated from OpenAPI, while the types without the prefix are manually modeled. + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + assert>(); + }); }); + +// Types derived from OpenAPI +type _CannyProcessorConfig = Required< + Pick, 'id' | 'type' | 'low_threshold' | 'high_threshold'> +>; +type _ColorMapProcessorConfig = Required< + Pick, 'id' | 'type' | 'color_map_tile_size'> +>; +type _ContentShuffleProcessorConfig = Required< + Pick, 'id' | 'type' | 'w' | 'h' | 'f'> +>; +type _DepthAnythingProcessorConfig = Required< + Pick, 'id' | 'type' | 'model_size'> +>; +type _HedProcessorConfig = Required, 'id' | 'type' | 'scribble'>>; +type _LineartAnimeProcessorConfig = Required, 'id' | 'type'>>; +type _LineartProcessorConfig = Required, 'id' | 'type' | 'coarse'>>; +type _MediapipeFaceProcessorConfig = Required< + Pick, 'id' | 'type' | 'max_faces' | 'min_confidence'> +>; +type _MidasDepthProcessorConfig = Required< + Pick, 'id' | 'type' | 'a_mult' | 'bg_th'> +>; +type _MlsdProcessorConfig = Required, 'id' | 'type' | 'thr_v' | 'thr_d'>>; +type _NormalbaeProcessorConfig = Required, 'id' | 'type'>>; +type _DWOpenposeProcessorConfig = Required< + Pick, 'id' | 'type' | 'draw_body' | 'draw_face' | 'draw_hands'> +>; +type _PidiProcessorConfig = Required, 'id' | 'type' | 'safe' | 'scribble'>>; +type _ZoeDepthProcessorConfig = Required, 'id' | 'type'>>; diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 2964a2eb6c..13435bdb7c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -1,117 +1,182 @@ import { deepClone } from 'common/util/deepClone'; -import type { - ParameterControlNetModel, - ParameterIPAdapterModel, - ParameterT2IAdapterModel, -} from 'features/parameters/types/parameterSchemas'; +import { zModelIdentifierField } from 'features/nodes/types/common'; import { merge, omit } from 'lodash-es'; import type { + AnyInvocation, BaseModelType, - CannyImageProcessorInvocation, - ColorMapImageProcessorInvocation, - ContentShuffleImageProcessorInvocation, ControlNetModelConfig, - DepthAnythingImageProcessorInvocation, - DWOpenposeImageProcessorInvocation, - Graph, - HedImageProcessorInvocation, ImageDTO, - LineartAnimeImageProcessorInvocation, - LineartImageProcessorInvocation, - MediapipeFaceProcessorInvocation, - MidasDepthImageProcessorInvocation, - MlsdImageProcessorInvocation, - NormalbaeImageProcessorInvocation, - PidiImageProcessorInvocation, T2IAdapterModelConfig, - ZoeDepthImageProcessorInvocation, } from 'services/api/types'; import { z } from 'zod'; +const zId = z.string().min(1); + +const zCannyProcessorConfig = z.object({ + id: zId, + type: z.literal('canny_image_processor'), + low_threshold: z.number().int().gte(0).lte(255), + high_threshold: z.number().int().gte(0).lte(255), +}); +export type CannyProcessorConfig = z.infer; + +const zColorMapProcessorConfig = z.object({ + id: zId, + type: z.literal('color_map_image_processor'), + color_map_tile_size: z.number().int().gte(1), +}); +export type ColorMapProcessorConfig = z.infer; + +const zContentShuffleProcessorConfig = z.object({ + id: zId, + type: z.literal('content_shuffle_image_processor'), + w: z.number().int().gte(0), + h: z.number().int().gte(0), + f: z.number().int().gte(0), +}); +export type ContentShuffleProcessorConfig = z.infer; + const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']); export type DepthAnythingModelSize = z.infer; export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize => zDepthAnythingModelSize.safeParse(v).success; +const zDepthAnythingProcessorConfig = z.object({ + id: zId, + type: z.literal('depth_anything_image_processor'), + model_size: zDepthAnythingModelSize, +}); +export type DepthAnythingProcessorConfig = z.infer; -export type CannyProcessorConfig = Required< - Pick ->; -export type ColorMapProcessorConfig = Required< - Pick ->; -export type ContentShuffleProcessorConfig = Required< - Pick ->; -export type DepthAnythingProcessorConfig = Required< - Pick ->; -export type HedProcessorConfig = Required>; -type LineartAnimeProcessorConfig = Required>; -export type LineartProcessorConfig = Required>; -export type MediapipeFaceProcessorConfig = Required< - Pick ->; -export type MidasDepthProcessorConfig = Required< - Pick ->; -export type MlsdProcessorConfig = Required>; -type NormalbaeProcessorConfig = Required>; -export type DWOpenposeProcessorConfig = Required< - Pick ->; -export type PidiProcessorConfig = Required>; -type ZoeDepthProcessorConfig = Required>; +const zHedProcessorConfig = z.object({ + id: zId, + type: z.literal('hed_image_processor'), + scribble: z.boolean(), +}); +export type HedProcessorConfig = z.infer; -export type ProcessorConfig = - | CannyProcessorConfig - | ColorMapProcessorConfig - | ContentShuffleProcessorConfig - | DepthAnythingProcessorConfig - | HedProcessorConfig - | LineartAnimeProcessorConfig - | LineartProcessorConfig - | MediapipeFaceProcessorConfig - | MidasDepthProcessorConfig - | MlsdProcessorConfig - | NormalbaeProcessorConfig - | DWOpenposeProcessorConfig - | PidiProcessorConfig - | ZoeDepthProcessorConfig; +const zLineartAnimeProcessorConfig = z.object({ + id: zId, + type: z.literal('lineart_anime_image_processor'), +}); +export type LineartAnimeProcessorConfig = z.infer; -export type ImageWithDims = { - imageName: string; - width: number; - height: number; -}; +const zLineartProcessorConfig = z.object({ + id: zId, + type: z.literal('lineart_image_processor'), + coarse: z.boolean(), +}); +export type LineartProcessorConfig = z.infer; -type ControlAdapterBase = { - id: string; - weight: number; - image: ImageWithDims | null; - processedImage: ImageWithDims | null; - isProcessingImage: boolean; - processorConfig: ProcessorConfig | null; - beginEndStepPct: [number, number]; -}; +const zMediapipeFaceProcessorConfig = z.object({ + id: zId, + type: z.literal('mediapipe_face_processor'), + max_faces: z.number().int().gte(1), + min_confidence: z.number().gte(0).lte(1), +}); +export type MediapipeFaceProcessorConfig = z.infer; + +const zMidasDepthProcessorConfig = z.object({ + id: zId, + type: z.literal('midas_depth_image_processor'), + a_mult: z.number().gte(0), + bg_th: z.number().gte(0), +}); +export type MidasDepthProcessorConfig = z.infer; + +const zMlsdProcessorConfig = z.object({ + id: zId, + type: z.literal('mlsd_image_processor'), + thr_v: z.number().gte(0), + thr_d: z.number().gte(0), +}); +export type MlsdProcessorConfig = z.infer; + +const zNormalbaeProcessorConfig = z.object({ + id: zId, + type: z.literal('normalbae_image_processor'), +}); +export type NormalbaeProcessorConfig = z.infer; + +const zDWOpenposeProcessorConfig = z.object({ + id: zId, + type: z.literal('dw_openpose_image_processor'), + draw_body: z.boolean(), + draw_face: z.boolean(), + draw_hands: z.boolean(), +}); +export type DWOpenposeProcessorConfig = z.infer; + +const zPidiProcessorConfig = z.object({ + id: zId, + type: z.literal('pidi_image_processor'), + safe: z.boolean(), + scribble: z.boolean(), +}); +export type PidiProcessorConfig = z.infer; + +const zZoeDepthProcessorConfig = z.object({ + id: zId, + type: z.literal('zoe_depth_image_processor'), +}); +export type ZoeDepthProcessorConfig = z.infer; + +const zProcessorConfig = z.discriminatedUnion('type', [ + zCannyProcessorConfig, + zColorMapProcessorConfig, + zContentShuffleProcessorConfig, + zDepthAnythingProcessorConfig, + zHedProcessorConfig, + zLineartAnimeProcessorConfig, + zLineartProcessorConfig, + zMediapipeFaceProcessorConfig, + zMidasDepthProcessorConfig, + zMlsdProcessorConfig, + zNormalbaeProcessorConfig, + zDWOpenposeProcessorConfig, + zPidiProcessorConfig, + zZoeDepthProcessorConfig, +]); +export type ProcessorConfig = z.infer; + +export const zImageWithDims = z.object({ + name: z.string(), + width: z.number().int().positive(), + height: z.number().int().positive(), +}); +export type ImageWithDims = z.infer; + +const zBeginEndStepPct = z + .tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)]) + .refine(([begin, end]) => begin < end, { + message: 'Begin must be less than end', + }); + +const zControlAdapterBase = z.object({ + id: zId, + weight: z.number().gte(-1).lte(2), + image: zImageWithDims.nullable(), + processedImage: zImageWithDims.nullable(), + processorConfig: zProcessorConfig.nullable(), + processorPendingBatchId: z.string().nullable().default(null), + beginEndStepPct: zBeginEndStepPct, +}); const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); export type ControlModeV2 = z.infer; export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success; -export type ControlNetConfigV2 = ControlAdapterBase & { - type: 'controlnet'; - model: ParameterControlNetModel | null; - controlMode: ControlModeV2; -}; -export const isControlNetConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is ControlNetConfigV2 => - ca.type === 'controlnet'; +export const zControlNetConfigV2 = zControlAdapterBase.extend({ + type: z.literal('controlnet'), + model: zModelIdentifierField.nullable(), + controlMode: zControlModeV2, +}); +export type ControlNetConfigV2 = z.infer; -export type T2IAdapterConfigV2 = ControlAdapterBase & { - type: 't2i_adapter'; - model: ParameterT2IAdapterModel | null; -}; -export const isT2IAdapterConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is T2IAdapterConfigV2 => - ca.type === 't2i_adapter'; +export const zT2IAdapterConfigV2 = zControlAdapterBase.extend({ + type: z.literal('t2i_adapter'), + model: zModelIdentifierField.nullable(), +}); +export type T2IAdapterConfigV2 = z.infer; const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']); export type CLIPVisionModelV2 = z.infer; @@ -121,16 +186,17 @@ const zIPMethodV2 = z.enum(['full', 'style', 'composition']); export type IPMethodV2 = z.infer; export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success; -export type IPAdapterConfigV2 = { - id: string; - type: 'ip_adapter'; - weight: number; - method: IPMethodV2; - image: ImageWithDims | null; - model: ParameterIPAdapterModel | null; - clipVisionModel: CLIPVisionModelV2; - beginEndStepPct: [number, number]; -}; +export const zIPAdapterConfigV2 = z.object({ + id: zId, + type: z.literal('ip_adapter'), + weight: z.number().gte(-1).lte(2), + method: zIPMethodV2, + image: zImageWithDims.nullable(), + model: zModelIdentifierField.nullable(), + clipVisionModel: zCLIPVisionModelV2, + beginEndStepPct: zBeginEndStepPct, +}); +export type IPAdapterConfigV2 = z.infer; const zProcessorTypeV2 = z.enum([ 'canny_image_processor', @@ -156,10 +222,7 @@ type ProcessorData = { labelTKey: string; descriptionTKey: string; buildDefaults(baseModel?: BaseModelType): Extract; - buildNode( - image: ImageWithDims, - config: Extract - ): Extract; + buildNode(image: ImageWithDims, config: Extract): Extract; }; const minDim = (image: ImageWithDims): number => Math.min(image.width, image.height); @@ -190,7 +253,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { buildNode: (image, config) => ({ ...config, type: 'canny_image_processor', - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -207,7 +270,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { buildNode: (image, config) => ({ ...config, type: 'color_map_image_processor', - image: { image_name: image.imageName }, + image: { image_name: image.name }, }), }, content_shuffle_image_processor: { @@ -223,7 +286,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -239,7 +302,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, resolution: minDim(image), }), }, @@ -254,7 +317,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -269,7 +332,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -285,7 +348,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -302,7 +365,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -319,7 +382,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -336,7 +399,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -351,7 +414,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -369,7 +432,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, image_resolution: minDim(image), }), }, @@ -385,7 +448,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, detect_resolution: minDim(image), image_resolution: minDim(image), }), @@ -400,7 +463,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = { }), buildNode: (image, config) => ({ ...config, - image: { image_name: image.imageName }, + image: { image_name: image.name }, }), }, }; @@ -413,8 +476,8 @@ export const initialControlNetV2: Omit = { controlMode: 'balanced', image: null, processedImage: null, - isProcessingImage: false, processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), + processorPendingBatchId: null, }; export const initialT2IAdapterV2: Omit = { @@ -424,8 +487,8 @@ export const initialT2IAdapterV2: Omit = { beginEndStepPct: [0, 1], image: null, processedImage: null, - isProcessingImage: false, processorConfig: CA_PROCESSOR_DATA.canny_image_processor.buildDefaults(), + processorPendingBatchId: null, }; export const initialIPAdapterV2: Omit = { @@ -462,7 +525,7 @@ export const buildControlAdapterProcessorV2 = ( }; export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({ - imageName: image_name, + name: image_name, width, height, }); diff --git a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts index f58b1e3b74..79933e6b00 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/renderers.ts @@ -54,7 +54,7 @@ const BBOX_SELECTED_STROKE = 'rgba(78, 190, 255, 1)'; const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)'; const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)'; // This is invokeai/frontend/web/public/assets/images/transparent_bg.png as a dataURL -const STAGE_BG_DATAURL = +export const STAGE_BG_DATAURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAEsmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjIwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMjAiCiAgIGV4aWY6Q29sb3JTcGFjZT0iMSIKICAgdGlmZjpJbWFnZVdpZHRoPSIyMCIKICAgdGlmZjpJbWFnZUxlbmd0aD0iMjAiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjMwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIzMDAvMSIKICAgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIKICAgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InByb2R1Y2VkIgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZmZpbml0eSBQaG90byAxLjEwLjgiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDQtMjNUMDg6MjA6NDcrMTA6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/Pn9pdVgAAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWR3yuDURjHP5uJmKghFy6WxpVpqMWNMgm1tGbKr5vt3S+1d3t73y3JrXKrKHHj1wV/AbfKtVJESq53TdywXs9rakv2nJ7zfM73nOfpnOeAPZJRVMPhAzWb18NTAffC4pK7oYiDTjpw4YgqhjYeCgWpaR8P2Kx457Vq1T73rzXHE4YCtkbhMUXT88LTwsG1vGbxrnC7ko7Ghc+F+3W5oPC9pcfKXLQ4VeYvi/VIeALsbcLuVBXHqlhJ66qwvByPmikov/exXuJMZOfnJPaId2MQZooAbmaYZAI/g4zK7MfLEAOyoka+7yd/lpzkKjJrrKOzSoo0efpFLUj1hMSk6AkZGdat/v/tq5EcHipXdwag/sU033qhYQdK26b5eWyapROoe4arbCU/dwQj76JvVzTPIbRuwsV1RYvtweUWdD1pUT36I9WJ25NJeD2DlkVw3ULTcrlnv/ucPkJkQ77qBvYPoE/Ot658AxagZ8FoS/a7AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAL0lEQVQ4jWM8ffo0A25gYmKCR5YJjxxBMKp5ZGhm/P//Px7pM2fO0MrmUc0jQzMAB2EIhZC3pUYAAAAASUVORK5CYII='; const mapId = (object: { id: string }) => object.id; @@ -437,8 +437,8 @@ const renderRegionalGuidanceLayer = ( konvaObjectGroup.opacity(1); compositingRect.setAttrs({ - // The rect should be the size of the layer - use the fast method bc it's OK if the rect is larger - ...getLayerBboxFast(konvaLayer), + // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already + ...(!reduxLayer.bboxNeedsUpdate && reduxLayer.bbox ? reduxLayer.bbox : getLayerBboxFast(konvaLayer)), fill: rgbColor, opacity: globalMaskLayerOpacity, // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) @@ -464,6 +464,7 @@ const createInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLay id: reduxLayer.id, name: INITIAL_IMAGE_LAYER_NAME, imageSmoothingEnabled: true, + listening: false, }); stage.add(konvaLayer); return konvaLayer; @@ -483,6 +484,9 @@ const updateInitialImageLayerImageAttrs = ( konvaImage: Konva.Image, reduxLayer: InitialImageLayer ) => { + // Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching, + // but it doesn't seem to break anything. + // TODO(psyche): Investigate and report upstream. const newWidth = stage.width() / stage.scaleX(); const newHeight = stage.height() / stage.scaleY(); if ( @@ -510,7 +514,7 @@ const updateInitialImageLayerImageSource = async ( reduxLayer: InitialImageLayer ) => { if (reduxLayer.image) { - const { imageName } = reduxLayer.image; + const imageName = reduxLayer.image.name; const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName)); const imageDTO = await req.unwrap(); req.unsubscribe(); @@ -543,7 +547,7 @@ const renderInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLay let imageSourceNeedsUpdate = false; if (canvasImageSource instanceof HTMLImageElement) { const image = reduxLayer.image; - if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.imageName)) { + if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.name)) { imageSourceNeedsUpdate = true; } else if (!image) { imageSourceNeedsUpdate = true; @@ -564,6 +568,7 @@ const createControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay id: reduxLayer.id, name: CA_LAYER_NAME, imageSmoothingEnabled: true, + listening: false, }); stage.add(konvaLayer); return konvaLayer; @@ -585,7 +590,7 @@ const updateControlNetLayerImageSource = async ( ) => { const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image; if (image) { - const { imageName } = image; + const imageName = image.name; const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName)); const imageDTO = await req.unwrap(); req.unsubscribe(); @@ -618,6 +623,9 @@ const updateControlNetLayerImageAttrs = ( reduxLayer: ControlAdapterLayer ) => { let needsCache = false; + // Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching, + // but it doesn't seem to break anything. + // TODO(psyche): Investigate and report upstream. const newWidth = stage.width() / stage.scaleX(); const newHeight = stage.height() / stage.scaleY(); const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0; @@ -653,7 +661,7 @@ const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay let imageSourceNeedsUpdate = false; if (canvasImageSource instanceof HTMLImageElement) { const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image; - if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.imageName)) { + if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.name)) { imageSourceNeedsUpdate = true; } else if (!image) { imageSourceNeedsUpdate = true; @@ -702,6 +710,7 @@ const renderLayers = ( if (isInitialImageLayer(reduxLayer)) { renderInitialImageLayer(stage, reduxLayer); } + // IP Adapter layers are not rendered } }; @@ -716,6 +725,7 @@ const createBboxRect = (reduxLayer: Layer, konvaLayer: Konva.Layer) => { id: getLayerBboxId(reduxLayer.id), name: LAYER_BBOX_NAME, strokeWidth: 1, + visible: false, }); konvaLayer.add(rect); return rect; @@ -725,18 +735,10 @@ const createBboxRect = (reduxLayer: Layer, konvaLayer: Konva.Layer) => { * Renders the bounding boxes for the layers. * @param stage The konva stage to render on * @param reduxLayers An array of all redux layers to draw bboxes for - * @param selectedLayerId The selected layer's id * @param tool The current tool - * @param onBboxChanged Callback for when the bbox is changed - * @param onBboxMouseDown Callback for when the bbox is clicked * @returns */ -const renderBbox = ( - stage: Konva.Stage, - reduxLayers: Layer[], - tool: Tool, - onBboxChanged: (layerId: string, bbox: IRect | null) => void -) => { +const renderBboxes = (stage: Konva.Stage, reduxLayers: Layer[], tool: Tool) => { // Hide all bboxes so they don't interfere with getClientRect for (const bboxRect of stage.find(`.${LAYER_BBOX_NAME}`)) { bboxRect.visible(false); @@ -747,36 +749,59 @@ const renderBbox = ( return; } - for (const reduxLayer of reduxLayers) { - if (reduxLayer.type === 'regional_guidance_layer') { - const konvaLayer = stage.findOne(`#${reduxLayer.id}`); - assert(konvaLayer, `Layer ${reduxLayer.id} not found in stage`); + for (const reduxLayer of reduxLayers.filter(isRegionalGuidanceLayer)) { + if (!reduxLayer.bbox) { + continue; + } + const konvaLayer = stage.findOne(`#${reduxLayer.id}`); + assert(konvaLayer, `Layer ${reduxLayer.id} not found in stage`); - let bbox = reduxLayer.bbox; + const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(reduxLayer, konvaLayer); - // We only need to recalculate the bbox if the layer has changed and it has objects - if (reduxLayer.bboxNeedsUpdate && reduxLayer.maskObjects.length) { - // We only need to use the pixel-perfect bounding box if the layer has eraser strokes - bbox = reduxLayer.needsPixelBbox ? getLayerBboxPixels(konvaLayer) : getLayerBboxFast(konvaLayer); - // Update the layer's bbox in the redux store - onBboxChanged(reduxLayer.id, bbox); + bboxRect.setAttrs({ + visible: !reduxLayer.bboxNeedsUpdate, + listening: reduxLayer.isSelected, + x: reduxLayer.bbox.x, + y: reduxLayer.bbox.y, + width: reduxLayer.bbox.width, + height: reduxLayer.bbox.height, + stroke: reduxLayer.isSelected ? BBOX_SELECTED_STROKE : '', + }); + } +}; + +/** + * Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed. + * @param stage The konva stage to render on. + * @param reduxLayers An array of redux layers to calculate bboxes for + * @param onBboxChanged Callback for when the bounding box changes + */ +const updateBboxes = ( + stage: Konva.Stage, + reduxLayers: Layer[], + onBboxChanged: (layerId: string, bbox: IRect | null) => void +) => { + for (const rgLayer of reduxLayers.filter(isRegionalGuidanceLayer)) { + const konvaLayer = stage.findOne(`#${rgLayer.id}`); + assert(konvaLayer, `Layer ${rgLayer.id} not found in stage`); + // We only need to recalculate the bbox if the layer has changed + if (rgLayer.bboxNeedsUpdate) { + const bboxRect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rgLayer, konvaLayer); + + // Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation + const visible = bboxRect.visible(); + bboxRect.visible(false); + + if (rgLayer.maskObjects.length === 0) { + // No objects - no bbox to calculate + onBboxChanged(rgLayer.id, null); + } else { + // Calculate the bbox by rendering the layer and checking its pixels + onBboxChanged(rgLayer.id, getLayerBboxPixels(konvaLayer)); } - if (!bbox) { - continue; - } - - const rect = konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(reduxLayer, konvaLayer); - - rect.setAttrs({ - visible: true, - listening: reduxLayer.isSelected, - x: bbox.x, - y: bbox.y, - width: bbox.width, - height: bbox.height, - stroke: reduxLayer.isSelected ? BBOX_SELECTED_STROKE : '', - }); + // Restore the visibility of the bbox + bboxRect.visible(visible); } } }; @@ -893,10 +918,11 @@ const renderNoLayersMessage = (stage: Konva.Stage, layerCount: number, width: nu export const renderers = { renderToolPreview, renderLayers, - renderBbox, + renderBboxes, renderBackground, renderNoLayersMessage, arrangeLayers, + updateBboxes, }; const DEBOUNCE_MS = 300; @@ -904,10 +930,11 @@ const DEBOUNCE_MS = 300; export const debouncedRenderers = { renderToolPreview: debounce(renderToolPreview, DEBOUNCE_MS), renderLayers: debounce(renderLayers, DEBOUNCE_MS), - renderBbox: debounce(renderBbox, DEBOUNCE_MS), + renderBboxes: debounce(renderBboxes, DEBOUNCE_MS), renderBackground: debounce(renderBackground, DEBOUNCE_MS), renderNoLayersMessage: debounce(renderNoLayersMessage, DEBOUNCE_MS), arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS), + updateBboxes: debounce(updateBboxes, DEBOUNCE_MS), }; /** diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index ce989de7b1..a7934f72d2 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -46,18 +46,16 @@ export const getImageUsage = ( const isControlLayerImage = controlLayers.layers.some((l) => { if (isRegionalGuidanceLayer(l)) { - return l.ipAdapters.some((ipa) => ipa.image?.imageName === image_name); + return l.ipAdapters.some((ipa) => ipa.image?.name === image_name); } if (isControlAdapterLayer(l)) { - return ( - l.controlAdapter.image?.imageName === image_name || l.controlAdapter.processedImage?.imageName === image_name - ); + return l.controlAdapter.image?.name === image_name || l.controlAdapter.processedImage?.name === image_name; } if (isIPAdapterLayer(l)) { - return l.ipAdapter.image?.imageName === image_name; + return l.ipAdapter.image?.name === image_name; } if (isInitialImageLayer(l)) { - return l.image?.imageName === image_name; + return l.image?.name === image_name; } return false; }); diff --git a/invokeai/frontend/web/src/features/dnd/hooks/useScaledCenteredModifer.ts b/invokeai/frontend/web/src/features/dnd/hooks/useScaledCenteredModifer.ts index f3f0c50f03..ce93611ff4 100644 --- a/invokeai/frontend/web/src/features/dnd/hooks/useScaledCenteredModifer.ts +++ b/invokeai/frontend/web/src/features/dnd/hooks/useScaledCenteredModifer.ts @@ -1,23 +1,21 @@ import type { Modifier } from '@dnd-kit/core'; import { getEventCoordinates } from '@dnd-kit/utilities'; -import { createSelector } from '@reduxjs/toolkit'; +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $viewport } from 'features/nodes/store/nodesSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { useCallback } from 'react'; -const selectZoom = createSelector([selectNodesSlice, activeTabNameSelector], (nodes, activeTabName) => - activeTabName === 'workflows' ? nodes.viewport.zoom : 1 -); - /** * Applies scaling to the drag transform (if on node editor tab) and centers it on cursor. */ export const useScaledModifer = () => { - const zoom = useAppSelector(selectZoom); + const activeTabName = useAppSelector(activeTabNameSelector); + const workflowsViewport = useStore($viewport); const modifier: Modifier = useCallback( ({ activatorEvent, draggingNodeRect, transform }) => { if (draggingNodeRect && activatorEvent) { + const zoom = activeTabName === 'workflows' ? workflowsViewport.zoom : 1; const activatorCoordinates = getEventCoordinates(activatorEvent); if (!activatorCoordinates) { @@ -42,7 +40,7 @@ export const useScaledModifer = () => { return transform; }, - [zoom] + [activeTabName, workflowsViewport.zoom] ); return modifier; diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 4d09c759eb..6fcf18421e 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -18,7 +18,7 @@ type BaseDropData = { id: string; }; -type CurrentImageDropData = BaseDropData & { +export type CurrentImageDropData = BaseDropData & { actionType: 'SET_CURRENT_IMAGE'; }; @@ -79,6 +79,14 @@ export type RemoveFromBoardDropData = BaseDropData & { actionType: 'REMOVE_FROM_BOARD'; }; +export type SelectForCompareDropData = BaseDropData & { + actionType: 'SELECT_FOR_COMPARE'; + context: { + firstImageName?: string | null; + secondImageName?: string | null; + }; +}; + export type TypesafeDroppableData = | CurrentImageDropData | ControlAdapterDropData @@ -89,7 +97,8 @@ export type TypesafeDroppableData = | CALayerImageDropData | IPALayerImageDropData | RGLayerIPAdapterImageDropData - | IILayerImageDropData; + | IILayerImageDropData + | SelectForCompareDropData; type BaseDragData = { id: string; @@ -134,7 +143,7 @@ export type UseDraggableTypesafeReturnValue = Omit { +interface TypesafeActive extends Omit { data: React.MutableRefObject; } diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index b701c72947..6dec862345 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -1,14 +1,14 @@ -import type { TypesafeActive, TypesafeDroppableData } from 'features/dnd/types'; +import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; -export const isValidDrop = (overData: TypesafeDroppableData | undefined, active: TypesafeActive | null) => { - if (!overData || !active?.data.current) { +export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData?: TypesafeDraggableData | null) => { + if (!overData || !activeData) { return false; } const { actionType } = overData; - const { payloadType } = active.data.current; + const { payloadType } = activeData; - if (overData.id === active.data.current.id) { + if (overData.id === activeData.id) { return false; } @@ -29,6 +29,8 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active: return payloadType === 'IMAGE_DTO'; case 'SET_NODES_IMAGE': return payloadType === 'IMAGE_DTO'; + case 'SELECT_FOR_COMPARE': + return payloadType === 'IMAGE_DTO'; case 'ADD_TO_BOARD': { // If the board is the same, don't allow the drop @@ -40,7 +42,7 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active: // Check if the image's board is the board we are dragging onto if (payloadType === 'IMAGE_DTO') { - const { imageDTO } = active.data.current.payload; + const { imageDTO } = activeData.payload; const currentBoard = imageDTO.board_id ?? 'none'; const destinationBoard = overData.context.boardId; @@ -49,7 +51,7 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active: if (payloadType === 'GALLERY_SELECTION') { // Assume all images are on the same board - this is true for the moment - const currentBoard = active.data.current.payload.boardId; + const currentBoard = activeData.payload.boardId; const destinationBoard = overData.context.boardId; return currentBoard !== destinationBoard; } @@ -67,14 +69,14 @@ export const isValidDrop = (overData: TypesafeDroppableData | undefined, active: // Check if the image's board is the board we are dragging onto if (payloadType === 'IMAGE_DTO') { - const { imageDTO } = active.data.current.payload; + const { imageDTO } = activeData.payload; const currentBoard = imageDTO.board_id ?? 'none'; return currentBoard !== 'none'; } if (payloadType === 'GALLERY_SELECTION') { - const currentBoard = active.data.current.payload.boardId; + const currentBoard = activeData.payload.boardId; return currentBoard !== 'none'; } diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx index 0509305192..f8c4f5ebcf 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx @@ -162,7 +162,7 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps )} {isSelectedForAutoAdd && } - + { > {boardName} - + {t('unifiedCanvas.move')}} /> diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 7bfb4050fb..31df113115 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -1,6 +1,5 @@ import { Flex, MenuDivider, MenuItem, Spinner } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; -import { useAppToaster } from 'app/components/Toaster'; import { $customStarUI } from 'app/store/nanostores/customStarUI'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard'; @@ -11,10 +10,14 @@ import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import { useImageActions } from 'features/gallery/hooks/useImageActions'; import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions'; +import { imageToCompareChanged } from 'features/gallery/store/gallerySlice'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; +import { toast } from 'features/toast/toast'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; +import { size } from 'lodash-es'; import { memo, useCallback } from 'react'; import { flushSync } from 'react-dom'; import { useTranslation } from 'react-i18next'; @@ -25,6 +28,7 @@ import { PiDownloadSimpleBold, PiFlowArrowBold, PiFoldersBold, + PiImagesBold, PiPlantBold, PiQuotesBold, PiShareFatBold, @@ -42,12 +46,13 @@ type SingleSelectionMenuItemsProps = { const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const { imageDTO } = props; const optimalDimension = useAppSelector(selectOptimalDimension); + const maySelectForCompare = useAppSelector((s) => s.gallery.imageToCompare?.image_name !== imageDTO.image_name); const dispatch = useAppDispatch(); const { t } = useTranslation(); - const toaster = useAppToaster(); const isCanvasEnabled = useFeatureStatus('canvas'); const customStarUi = useStore($customStarUI); const { downloadImage } = useDownloadImage(); + const templates = useStore($templates); const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } = useImageActions(imageDTO?.image_name); @@ -73,6 +78,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const handleSendToImageToImage = useCallback(() => { dispatch(sentImageToImg2Img()); dispatch(iiLayerAdded(imageDTO)); + dispatch(setActiveTab('generation')); }, [dispatch, imageDTO]); const handleSendToCanvas = useCallback(() => { @@ -82,13 +88,12 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { }); dispatch(setInitialCanvasImage(imageDTO, optimalDimension)); - toaster({ + toast({ + id: 'SENT_TO_CANVAS', title: t('toast.sentToUnifiedCanvas'), status: 'success', - duration: 2500, - isClosable: true, }); - }, [dispatch, imageDTO, t, toaster, optimalDimension]); + }, [dispatch, imageDTO, t, optimalDimension]); const handleChangeBoard = useCallback(() => { dispatch(imagesToChangeSelected([imageDTO])); @@ -115,6 +120,10 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { downloadImage(imageDTO.image_url, imageDTO.image_name); }, [downloadImage, imageDTO.image_name, imageDTO.image_url]); + const handleSelectImageForCompare = useCallback(() => { + dispatch(imageToCompareChanged(imageDTO)); + }, [dispatch, imageDTO]); + return ( <> }> @@ -128,11 +137,14 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { } onClickCapture={handleDownloadImage}> {t('parameters.downloadImage')} + } isDisabled={!maySelectForCompare} onClick={handleSelectImageForCompare}> + {t('gallery.selectForCompare')} + : } onClickCapture={handleLoadWorkflow} - isDisabled={!imageDTO.has_workflow} + isDisabled={!imageDTO.has_workflow || !size(templates)} > {t('nodes.loadWorkflow')} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx index 2788b1095d..e5e216c97c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -11,6 +11,7 @@ import type { GallerySelectionDraggableData, ImageDraggableData, TypesafeDraggab import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId'; import { useMultiselect } from 'features/gallery/hooks/useMultiselect'; import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView'; +import { imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import type { MouseEvent } from 'react'; import { memo, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -45,6 +46,7 @@ const GalleryImage = (props: HoverableImageProps) => { const { t } = useTranslation(); const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId); const alwaysShowImageSizeBadge = useAppSelector((s) => s.gallery.alwaysShowImageSizeBadge); + const isSelectedForCompare = useAppSelector((s) => s.gallery.imageToCompare?.image_name === imageName); const { handleClick, isSelected, areMultiplesSelected } = useMultiselect(imageDTO); const customStarUi = useStore($customStarUI); @@ -102,6 +104,11 @@ const GalleryImage = (props: HoverableImageProps) => { setIsHovered(true); }, []); + const onDoubleClick = useCallback(() => { + dispatch(isImageViewerOpenChanged(true)); + dispatch(imageToCompareChanged(null)); + }, [dispatch]); + const handleMouseOut = useCallback(() => { setIsHovered(false); }, []); @@ -143,9 +150,11 @@ const GalleryImage = (props: HoverableImageProps) => { > { const { label, data, fileName, withDownload = true, withCopy = true, extraCopyActions } = props; - const dataString = useMemo(() => (isString(data) ? data : JSON.stringify(data, null, 2)), [data]); + const dataString = useMemo(() => (isString(data) ? data : formatter.Serialize(data)) ?? '', [data]); const shift = useShiftModifier(); const handleCopy = useCallback(() => { navigator.clipboard.writeText(dataString); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index c73f5b1817..a192ff4fbb 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -1,12 +1,10 @@ import { useAppSelector } from 'app/store/storeHooks'; import { MetadataControlNets } from 'features/metadata/components/MetadataControlNets'; -import { MetadataControlNetsV2 } from 'features/metadata/components/MetadataControlNetsV2'; import { MetadataIPAdapters } from 'features/metadata/components/MetadataIPAdapters'; -import { MetadataIPAdaptersV2 } from 'features/metadata/components/MetadataIPAdaptersV2'; import { MetadataItem } from 'features/metadata/components/MetadataItem'; +import { MetadataLayers } from 'features/metadata/components/MetadataLayers'; import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs'; import { MetadataT2IAdapters } from 'features/metadata/components/MetadataT2IAdapters'; -import { MetadataT2IAdaptersV2 } from 'features/metadata/components/MetadataT2IAdaptersV2'; import { handlers } from 'features/metadata/util/handlers'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { memo } from 'react'; @@ -39,8 +37,7 @@ const ImageMetadataActions = (props: Props) => { - - + {activeTabName !== 'generation' && } @@ -52,12 +49,10 @@ const ImageMetadataActions = (props: Props) => { + {activeTabName === 'generation' && } {activeTabName !== 'generation' && } {activeTabName !== 'generation' && } {activeTabName !== 'generation' && } - {activeTabName === 'generation' && } - {activeTabName === 'generation' && } - {activeTabName === 'generation' && } ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent.tsx new file mode 100644 index 0000000000..9f7cac4a3e --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent.tsx @@ -0,0 +1,34 @@ +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDebouncedImageWorkflow } from 'services/api/hooks/useDebouncedImageWorkflow'; +import type { ImageDTO } from 'services/api/types'; + +import DataViewer from './DataViewer'; + +type Props = { + image: ImageDTO; +}; + +const ImageMetadataGraphTabContent = ({ image }: Props) => { + const { t } = useTranslation(); + const { currentData } = useDebouncedImageWorkflow(image); + const graph = useMemo(() => { + if (currentData?.graph) { + try { + return JSON.parse(currentData.graph); + } catch { + return null; + } + } + return null; + }, [currentData]); + + if (!graph) { + return ; + } + + return ; +}; + +export default memo(ImageMetadataGraphTabContent); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx index ccc4436452..46121f9724 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataViewer.tsx @@ -1,6 +1,7 @@ import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@invoke-ai/ui-library'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import ImageMetadataGraphTabContent from 'features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent'; import { useMetadataItem } from 'features/metadata/hooks/useMetadataItem'; import { handlers } from 'features/metadata/util/handlers'; import { memo } from 'react'; @@ -52,6 +53,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { {t('metadata.metadata')} {t('metadata.imageDetails')} {t('metadata.workflow')} + {t('nodes.graph')} @@ -81,6 +83,9 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { + + + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataWorkflowTabContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataWorkflowTabContent.tsx index 60cda21028..fe4ce3e701 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataWorkflowTabContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataWorkflowTabContent.tsx @@ -1,5 +1,5 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useDebouncedImageWorkflow } from 'services/api/hooks/useDebouncedImageWorkflow'; import type { ImageDTO } from 'services/api/types'; @@ -12,7 +12,17 @@ type Props = { const ImageMetadataWorkflowTabContent = ({ image }: Props) => { const { t } = useTranslation(); - const { workflow } = useDebouncedImageWorkflow(image); + const { currentData } = useDebouncedImageWorkflow(image); + const workflow = useMemo(() => { + if (currentData?.workflow) { + try { + return JSON.parse(currentData.workflow); + } catch { + return null; + } + } + return null; + }, [currentData]); if (!workflow) { return ; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CompareToolbar.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CompareToolbar.tsx new file mode 100644 index 0000000000..4f525bc670 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CompareToolbar.tsx @@ -0,0 +1,140 @@ +import { + Button, + ButtonGroup, + Flex, + Icon, + IconButton, + Kbd, + ListItem, + Tooltip, + UnorderedList, +} from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + comparedImagesSwapped, + comparisonFitChanged, + comparisonModeChanged, + comparisonModeCycled, + imageToCompareChanged, +} from 'features/gallery/store/gallerySlice'; +import { memo, useCallback } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { Trans, useTranslation } from 'react-i18next'; +import { PiArrowsOutBold, PiQuestion, PiSwapBold, PiXBold } from 'react-icons/pi'; + +export const CompareToolbar = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const comparisonMode = useAppSelector((s) => s.gallery.comparisonMode); + const comparisonFit = useAppSelector((s) => s.gallery.comparisonFit); + const setComparisonModeSlider = useCallback(() => { + dispatch(comparisonModeChanged('slider')); + }, [dispatch]); + const setComparisonModeSideBySide = useCallback(() => { + dispatch(comparisonModeChanged('side-by-side')); + }, [dispatch]); + const setComparisonModeHover = useCallback(() => { + dispatch(comparisonModeChanged('hover')); + }, [dispatch]); + const swapImages = useCallback(() => { + dispatch(comparedImagesSwapped()); + }, [dispatch]); + useHotkeys('c', swapImages, [swapImages]); + const toggleComparisonFit = useCallback(() => { + dispatch(comparisonFitChanged(comparisonFit === 'contain' ? 'fill' : 'contain')); + }, [dispatch, comparisonFit]); + const exitCompare = useCallback(() => { + dispatch(imageToCompareChanged(null)); + }, [dispatch]); + useHotkeys('esc', exitCompare, [exitCompare]); + const nextMode = useCallback(() => { + dispatch(comparisonModeCycled()); + }, [dispatch]); + useHotkeys('m', nextMode, [nextMode]); + + return ( + + + + } + aria-label={`${t('gallery.swapImages')} (C)`} + tooltip={`${t('gallery.swapImages')} (C)`} + onClick={swapImages} + /> + {comparisonMode !== 'side-by-side' && ( + } + /> + )} + + + + + + + + + + + + }> + + + + + } + aria-label={`${t('gallery.exitCompare')} (Esc)`} + tooltip={`${t('gallery.exitCompare')} (Esc)`} + onClick={exitCompare} + /> + + + + ); +}); + +CompareToolbar.displayName = 'CompareToolbar'; + +const CompareHelp = () => { + return ( + + + }}> + + + }}> + + + }}> + + + }}> + + + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index f93f48e51b..d500d692fe 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -1,4 +1,5 @@ import { ButtonGroup, IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { skipToken } from '@reduxjs/toolkit/query'; import { upscaleRequested } from 'app/store/middleware/listenerMiddleware/listeners/upscaleRequested'; @@ -12,11 +13,14 @@ import { sentImageToImg2Img } from 'features/gallery/store/actions'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import { selectGallerySlice } from 'features/gallery/store/gallerySlice'; import { parseAndRecallImageDimensions } from 'features/metadata/util/handlers'; +import { $templates } from 'features/nodes/store/nodesSlice'; import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUpscaleSettings'; import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { selectSystemSlice } from 'features/system/store/systemSlice'; +import { setActiveTab } from 'features/ui/store/uiSlice'; import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; +import { size } from 'lodash-es'; import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; @@ -47,7 +51,7 @@ const CurrentImageButtons = () => { const lastSelectedImage = useAppSelector(selectLastSelectedImage); const selection = useAppSelector((s) => s.gallery.selection); const shouldDisableToolbarButtons = useAppSelector(selectShouldDisableToolbarButtons); - + const templates = useStore($templates); const isUpscalingEnabled = useFeatureStatus('upscaling'); const isQueueMutationInProgress = useIsQueueMutationInProgress(); const { t } = useTranslation(); @@ -84,6 +88,7 @@ const CurrentImageButtons = () => { } dispatch(sentImageToImg2Img()); dispatch(iiLayerAdded(imageDTO)); + dispatch(setActiveTab('generation')); }, [dispatch, imageDTO]); useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]); @@ -141,7 +146,7 @@ const CurrentImageButtons = () => { icon={} tooltip={`${t('nodes.loadWorkflow')} (W)`} aria-label={`${t('nodes.loadWorkflow')} (W)`} - isDisabled={!imageDTO?.has_workflow} + isDisabled={!imageDTO?.has_workflow || !size(templates)} onClick={handleLoadWorkflow} isLoading={getAndLoadEmbeddedWorkflowResult.isLoading} /> diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx index 8e6eccbe73..a812391992 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx @@ -4,7 +4,7 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; +import type { TypesafeDraggableData } from 'features/dnd/types'; import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer'; import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; @@ -22,21 +22,7 @@ const selectLastSelectedImageName = createSelector( (lastSelectedImage) => lastSelectedImage?.image_name ); -type Props = { - isDragDisabled?: boolean; - isDropDisabled?: boolean; - withNextPrevButtons?: boolean; - withMetadata?: boolean; - alwaysShowProgress?: boolean; -}; - -const CurrentImagePreview = ({ - isDragDisabled = false, - isDropDisabled = false, - withNextPrevButtons = true, - withMetadata = true, - alwaysShowProgress = false, -}: Props) => { +const CurrentImagePreview = () => { const { t } = useTranslation(); const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails); const imageName = useAppSelector(selectLastSelectedImageName); @@ -55,14 +41,6 @@ const CurrentImagePreview = ({ } }, [imageDTO]); - const droppableData = useMemo( - () => ({ - id: 'current-image', - actionType: 'SET_CURRENT_IMAGE', - }), - [] - ); - // Show and hide the next/prev buttons on mouse move const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState(false); const timeoutId = useRef(0); @@ -86,43 +64,27 @@ const CurrentImagePreview = ({ justifyContent="center" position="relative" > - {hasDenoiseProgress && (shouldShowProgressInViewer || alwaysShowProgress) ? ( + {hasDenoiseProgress && shouldShowProgressInViewer ? ( ) : ( } dataTestId="image-preview" /> )} + {shouldShowImageDetails && imageDTO && ( + + + + )} - {shouldShowImageDetails && imageDTO && withMetadata && ( - - - - )} - - - {withNextPrevButtons && shouldShowNextPrevButtons && imageDTO && ( + {shouldShowNextPrevButtons && imageDTO && ( = { - generation: 'controlLayers.controlLayers', - canvas: 'ui.tabs.canvas', - workflows: 'ui.tabs.workflows', - models: 'ui.tabs.models', - queue: 'ui.tabs.queue', -}; - -export const EditorButton = () => { - const { t } = useTranslation(); - const { onClose } = useImageViewer(); - const activeTabName = useAppSelector(activeTabNameSelector); - const tooltip = useMemo( - () => t('gallery.switchTo', { tab: t(TAB_NAME_TO_TKEY_SHORT[activeTabName]) }), - [t, activeTabName] - ); - - return ( - - ); -}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/FloatingImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/FloatingImageViewer.tsx deleted file mode 100644 index 1107e86ff3..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/FloatingImageViewer.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { Flex, IconButton, Spacer, Text, useShiftModifier } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview'; -import { isFloatingImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; -import { useCallback, useLayoutEffect, useRef } from 'react'; -import { flushSync } from 'react-dom'; -import { useTranslation } from 'react-i18next'; -import { PiHourglassBold, PiXBold } from 'react-icons/pi'; -import { Rnd } from 'react-rnd'; - -const defaultDim = 256; -const maxDim = 512; -const defaultSize = { width: defaultDim, height: defaultDim + 24 }; -const maxSize = { width: maxDim, height: maxDim + 24 }; -const rndDefault = { x: 0, y: 0, ...defaultSize }; - -const rndStyles = { - zIndex: 11, -}; - -const enableResizing = { - top: false, - right: false, - bottom: false, - left: false, - topRight: false, - bottomRight: true, - bottomLeft: false, - topLeft: false, -}; - -const FloatingImageViewerComponent = () => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const shift = useShiftModifier(); - const rndRef = useRef(null); - const imagePreviewRef = useRef(null); - const onClose = useCallback(() => { - dispatch(isFloatingImageViewerOpenChanged(false)); - }, [dispatch]); - - const fitToScreen = useCallback(() => { - if (!imagePreviewRef.current || !rndRef.current) { - return; - } - const el = imagePreviewRef.current; - const rnd = rndRef.current; - - const { top, right, bottom, left, width, height } = el.getBoundingClientRect(); - const { innerWidth, innerHeight } = window; - - const newPosition = rnd.getDraggablePosition(); - - if (top < 0) { - newPosition.y = 0; - } - if (left < 0) { - newPosition.x = 0; - } - if (bottom > innerHeight) { - newPosition.y = innerHeight - height; - } - if (right > innerWidth) { - newPosition.x = innerWidth - width; - } - rnd.updatePosition(newPosition); - }, []); - - const onDoubleClick = useCallback(() => { - if (!rndRef.current || !imagePreviewRef.current) { - return; - } - const { width, height } = imagePreviewRef.current.getBoundingClientRect(); - if (width === defaultSize.width && height === defaultSize.height) { - rndRef.current.updateSize(maxSize); - } else { - rndRef.current.updateSize(defaultSize); - } - flushSync(fitToScreen); - }, [fitToScreen]); - - useLayoutEffect(() => { - window.addEventListener('resize', fitToScreen); - return () => { - window.removeEventListener('resize', fitToScreen); - }; - }, [fitToScreen]); - - useLayoutEffect(() => { - // Set the initial position - if (!imagePreviewRef.current || !rndRef.current) { - return; - } - - const { width, height } = imagePreviewRef.current.getBoundingClientRect(); - - const initialPosition = { - // 54 = width of left-hand vertical bar of tab icons - // 430 = width of parameters panel - x: 54 + 430 / 2 - width / 2, - // 16 = just a reasonable bottom padding - y: window.innerHeight - height - 16, - }; - - rndRef.current.updatePosition(initialPosition); - }, [fitToScreen]); - - return ( - - - - - {t('common.viewer')} - - - } size="sm" variant="link" onClick={onClose} /> - - - - - - - ); -}; - -export const FloatingImageViewer = () => { - const isOpen = useAppSelector((s) => s.gallery.isFloatingImageViewerOpen); - - if (!isOpen) { - return null; - } - - return ; -}; - -export const ToggleFloatingImageViewerButton = () => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isOpen = useAppSelector((s) => s.gallery.isFloatingImageViewerOpen); - - const onToggle = useCallback(() => { - dispatch(isFloatingImageViewerOpenChanged(!isOpen)); - }, [dispatch, isOpen]); - - return ( - } - size="sm" - onClick={onToggle} - variant="link" - colorScheme={isOpen ? 'invokeBlue' : 'base'} - boxSize={8} - /> - ); -}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx new file mode 100644 index 0000000000..5607d7dd4f --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx @@ -0,0 +1,41 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import type { Dimensions } from 'features/canvas/store/canvasTypes'; +import { selectComparisonImages } from 'features/gallery/components/ImageViewer/common'; +import { ImageComparisonHover } from 'features/gallery/components/ImageViewer/ImageComparisonHover'; +import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide'; +import { ImageComparisonSlider } from 'features/gallery/components/ImageViewer/ImageComparisonSlider'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiImagesBold } from 'react-icons/pi'; + +type Props = { + containerDims: Dimensions; +}; + +export const ImageComparison = memo(({ containerDims }: Props) => { + const { t } = useTranslation(); + const comparisonMode = useAppSelector((s) => s.gallery.comparisonMode); + const { firstImage, secondImage } = useAppSelector(selectComparisonImages); + + if (!firstImage || !secondImage) { + // Should rarely/never happen - we don't render this component unless we have images to compare + return ; + } + + if (comparisonMode === 'slider') { + return ; + } + + if (comparisonMode === 'side-by-side') { + return ( + + ); + } + + if (comparisonMode === 'hover') { + return ; + } +}); + +ImageComparison.displayName = 'ImageComparison'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonDroppable.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonDroppable.tsx new file mode 100644 index 0000000000..3678c920c0 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonDroppable.tsx @@ -0,0 +1,47 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import IAIDroppable from 'common/components/IAIDroppable'; +import type { CurrentImageDropData, SelectForCompareDropData } from 'features/dnd/types'; +import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { selectComparisonImages } from './common'; + +const setCurrentImageDropData: CurrentImageDropData = { + id: 'current-image', + actionType: 'SET_CURRENT_IMAGE', +}; + +export const ImageComparisonDroppable = memo(() => { + const { t } = useTranslation(); + const imageViewer = useImageViewer(); + const { firstImage, secondImage } = useAppSelector(selectComparisonImages); + const selectForCompareDropData = useMemo( + () => ({ + id: 'image-comparison', + actionType: 'SELECT_FOR_COMPARE', + context: { + firstImageName: firstImage?.image_name, + secondImageName: secondImage?.image_name, + }, + }), + [firstImage?.image_name, secondImage?.image_name] + ); + + if (!imageViewer.isOpen) { + return ( + + + + ); + } + + return ( + + + + ); +}); + +ImageComparisonDroppable.displayName = 'ImageComparisonDroppable'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx new file mode 100644 index 0000000000..a02e94b547 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonHover.tsx @@ -0,0 +1,117 @@ +import { Box, Flex, Image } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useBoolean } from 'common/hooks/useBoolean'; +import { preventDefault } from 'common/util/stopPropagation'; +import type { Dimensions } from 'features/canvas/store/canvasTypes'; +import { STAGE_BG_DATAURL } from 'features/controlLayers/util/renderers'; +import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel'; +import { memo, useMemo, useRef } from 'react'; + +import type { ComparisonProps } from './common'; +import { fitDimsToContainer, getSecondImageDims } from './common'; + +export const ImageComparisonHover = memo(({ firstImage, secondImage, containerDims }: ComparisonProps) => { + const comparisonFit = useAppSelector((s) => s.gallery.comparisonFit); + const imageContainerRef = useRef(null); + const mouseOver = useBoolean(false); + const fittedDims = useMemo( + () => fitDimsToContainer(containerDims, firstImage), + [containerDims, firstImage] + ); + const compareImageDims = useMemo( + () => getSecondImageDims(comparisonFit, fittedDims, firstImage, secondImage), + [comparisonFit, fittedDims, firstImage, secondImage] + ); + return ( + + + + + + + + + + + + + + + + ); +}); + +ImageComparisonHover.displayName = 'ImageComparisonHover'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonLabel.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonLabel.tsx new file mode 100644 index 0000000000..a5a40dfc9c --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonLabel.tsx @@ -0,0 +1,33 @@ +import type { TextProps } from '@invoke-ai/ui-library'; +import { Text } from '@invoke-ai/ui-library'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { DROP_SHADOW } from './common'; + +type Props = TextProps & { + type: 'first' | 'second'; +}; + +export const ImageComparisonLabel = memo(({ type, ...rest }: Props) => { + const { t } = useTranslation(); + return ( + + {type === 'first' ? t('gallery.viewerImage') : t('gallery.compareImage')} + + ); +}); + +ImageComparisonLabel.displayName = 'ImageComparisonLabel'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSideBySide.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSideBySide.tsx new file mode 100644 index 0000000000..8bac2bb45d --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSideBySide.tsx @@ -0,0 +1,70 @@ +import { Flex, Image } from '@invoke-ai/ui-library'; +import type { ComparisonProps } from 'features/gallery/components/ImageViewer/common'; +import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel'; +import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; +import { memo, useCallback, useRef } from 'react'; +import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; +import { Panel, PanelGroup } from 'react-resizable-panels'; + +export const ImageComparisonSideBySide = memo(({ firstImage, secondImage }: ComparisonProps) => { + const panelGroupRef = useRef(null); + const onDoubleClickHandle = useCallback(() => { + if (!panelGroupRef.current) { + return; + } + panelGroupRef.current.setLayout([50, 50]); + }, []); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); +}); + +ImageComparisonSideBySide.displayName = 'ImageComparisonSideBySide'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx new file mode 100644 index 0000000000..8972af7d4f --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonSlider.tsx @@ -0,0 +1,215 @@ +import { Box, Flex, Icon, Image } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { preventDefault } from 'common/util/stopPropagation'; +import type { Dimensions } from 'features/canvas/store/canvasTypes'; +import { STAGE_BG_DATAURL } from 'features/controlLayers/util/renderers'; +import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi'; + +import type { ComparisonProps } from './common'; +import { DROP_SHADOW, fitDimsToContainer, getSecondImageDims } from './common'; + +const INITIAL_POS = '50%'; +const HANDLE_WIDTH = 2; +const HANDLE_WIDTH_PX = `${HANDLE_WIDTH}px`; +const HANDLE_HITBOX = 20; +const HANDLE_HITBOX_PX = `${HANDLE_HITBOX}px`; +const HANDLE_INNER_LEFT_PX = `${HANDLE_HITBOX / 2 - HANDLE_WIDTH / 2}px`; +const HANDLE_LEFT_INITIAL_PX = `calc(${INITIAL_POS} - ${HANDLE_HITBOX / 2}px)`; + +export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerDims }: ComparisonProps) => { + const comparisonFit = useAppSelector((s) => s.gallery.comparisonFit); + // How far the handle is from the left - this will be a CSS calculation that takes into account the handle width + const [left, setLeft] = useState(HANDLE_LEFT_INITIAL_PX); + // How wide the first image is + const [width, setWidth] = useState(INITIAL_POS); + const handleRef = useRef(null); + // To manage aspect ratios, we need to know the size of the container + const imageContainerRef = useRef(null); + // To keep things smooth, we use RAF to update the handle position & gate it to 60fps + const rafRef = useRef(null); + const lastMoveTimeRef = useRef(0); + + const fittedDims = useMemo( + () => fitDimsToContainer(containerDims, firstImage), + [containerDims, firstImage] + ); + + const compareImageDims = useMemo( + () => getSecondImageDims(comparisonFit, fittedDims, firstImage, secondImage), + [comparisonFit, fittedDims, firstImage, secondImage] + ); + + const updateHandlePos = useCallback((clientX: number) => { + if (!handleRef.current || !imageContainerRef.current) { + return; + } + lastMoveTimeRef.current = performance.now(); + const { x, width } = imageContainerRef.current.getBoundingClientRect(); + const rawHandlePos = ((clientX - x) * 100) / width; + const handleWidthPct = (HANDLE_WIDTH * 100) / width; + const newHandlePos = Math.min(100 - handleWidthPct, Math.max(0, rawHandlePos)); + setWidth(`${newHandlePos}%`); + setLeft(`calc(${newHandlePos}% - ${HANDLE_HITBOX / 2}px)`); + }, []); + + const onMouseMove = useCallback( + (e: MouseEvent) => { + if (rafRef.current === null && performance.now() > lastMoveTimeRef.current + 1000 / 60) { + rafRef.current = window.requestAnimationFrame(() => { + updateHandlePos(e.clientX); + rafRef.current = null; + }); + } + }, + [updateHandlePos] + ); + + const onMouseUp = useCallback(() => { + window.removeEventListener('mousemove', onMouseMove); + }, [onMouseMove]); + + const onMouseDown = useCallback( + (e: React.MouseEvent) => { + // Update the handle position immediately on click + updateHandlePos(e.clientX); + window.addEventListener('mouseup', onMouseUp, { once: true }); + window.addEventListener('mousemove', onMouseMove); + }, + [onMouseMove, onMouseUp, updateHandlePos] + ); + + useEffect( + () => () => { + if (rafRef.current !== null) { + cancelAnimationFrame(rafRef.current); + } + }, + [] + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}); + +ImageComparisonSlider.displayName = 'ImageComparisonSlider'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx index 949e72fad1..530431fc4c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx @@ -1,91 +1,39 @@ -import { Flex } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton'; -import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; -import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; -import type { InvokeTabName } from 'features/ui/store/tabMap'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import type { AnimationProps } from 'framer-motion'; -import { AnimatePresence, motion } from 'framer-motion'; -import { memo, useMemo } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; +import { Box, Flex } from '@invoke-ai/ui-library'; +import { CompareToolbar } from 'features/gallery/components/ImageViewer/CompareToolbar'; +import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview'; +import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison'; +import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar'; +import { memo } from 'react'; +import { useMeasure } from 'react-use'; -import CurrentImageButtons from './CurrentImageButtons'; -import CurrentImagePreview from './CurrentImagePreview'; -import { EditorButton } from './EditorButton'; - -const initial: AnimationProps['initial'] = { - opacity: 0, -}; -const animate: AnimationProps['animate'] = { - opacity: 1, - transition: { duration: 0.07 }, -}; -const exit: AnimationProps['exit'] = { - opacity: 0, - transition: { duration: 0.07 }, -}; - -const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows']; +import { useImageViewer } from './useImageViewer'; export const ImageViewer = memo(() => { - const { isOpen, onToggle, onClose } = useImageViewer(); - const activeTabName = useAppSelector(activeTabNameSelector); - const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]); - const shouldShowViewer = useMemo(() => { - if (!isViewerEnabled) { - return false; - } - return isOpen; - }, [isOpen, isViewerEnabled]); + const imageViewer = useImageViewer(); + const [containerRef, containerDims] = useMeasure(); - useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]); - useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]); - - // The AnimatePresence mode must be wait - else framer can get confused if you spam the toggle button return ( - - {shouldShowViewer && ( - - - - - - - - - - - - - - - - - - - - )} - + + {imageViewer.isComparing && } + {!imageViewer.isComparing && } + + {!imageViewer.isComparing && } + {imageViewer.isComparing && } + + ); }); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx index a298ebda56..df3fbe2765 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleMetadataViewerButton.tsx @@ -1,6 +1,5 @@ import { IconButton } from '@invoke-ai/ui-library'; import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppToaster } from 'app/components/Toaster'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; @@ -14,7 +13,6 @@ export const ToggleMetadataViewerButton = memo(() => { const dispatch = useAppDispatch(); const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails); const lastSelectedImage = useAppSelector(selectLastSelectedImage); - const toaster = useAppToaster(); const { t } = useTranslation(); const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken); @@ -24,7 +22,7 @@ export const ToggleMetadataViewerButton = memo(() => { [dispatch, shouldShowImageDetails] ); - useHotkeys('i', toggleMetadataViewer, { enabled: Boolean(imageDTO) }, [imageDTO, shouldShowImageDetails, toaster]); + useHotkeys('i', toggleMetadataViewer, { enabled: Boolean(imageDTO) }, [imageDTO, shouldShowImageDetails]); return ( { isDisabled={!imageDTO} variant="outline" colorScheme={shouldShowImageDetails ? 'invokeBlue' : 'base'} + data-testid="toggle-show-metadata-button" /> ); }); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleProgressButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleProgressButton.tsx index 994a8bf10e..ee698130fb 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleProgressButton.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ToggleProgressButton.tsx @@ -22,6 +22,7 @@ export const ToggleProgressButton = memo(() => { onClick={onClick} variant="outline" colorScheme={shouldShowProgressInViewer ? 'invokeBlue' : 'base'} + data-testid="toggle-show-progress-button" /> ); }); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx deleted file mode 100644 index edceb5099c..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerButton.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Button } from '@invoke-ai/ui-library'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowsDownUpBold } from 'react-icons/pi'; - -import { useImageViewer } from './useImageViewer'; - -export const ViewerButton = () => { - const { t } = useTranslation(); - const { onOpen } = useImageViewer(); - const tooltip = useMemo(() => t('gallery.switchTo', { tab: t('common.viewer') }), [t]); - - return ( - - ); -}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx new file mode 100644 index 0000000000..7dc13afb48 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx @@ -0,0 +1,69 @@ +import { + Button, + Flex, + Icon, + Popover, + PopoverArrow, + PopoverBody, + PopoverContent, + PopoverTrigger, + Text, +} from '@invoke-ai/ui-library'; +import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { PiCaretDownBold, PiCheckBold, PiEyeBold, PiPencilBold } from 'react-icons/pi'; + +export const ViewerToggleMenu = () => { + const { t } = useTranslation(); + const imageViewer = useImageViewer(); + useHotkeys('z', imageViewer.onToggle, [imageViewer]); + useHotkeys('esc', imageViewer.onClose, [imageViewer]); + + return ( + + + + + + + + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx new file mode 100644 index 0000000000..e610ca0077 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx @@ -0,0 +1,33 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton'; +import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { memo } from 'react'; + +import CurrentImageButtons from './CurrentImageButtons'; +import { ViewerToggleMenu } from './ViewerToggleMenu'; + +export const ViewerToolbar = memo(() => { + const tab = useAppSelector(activeTabNameSelector); + return ( + + + + + + + + + + + + + {tab !== 'workflows' && } + + + + ); +}); + +ViewerToolbar.displayName = 'ViewerToolbar'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts new file mode 100644 index 0000000000..7f29e906ef --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts @@ -0,0 +1,64 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import type { Dimensions } from 'features/canvas/store/canvasTypes'; +import { selectGallerySlice } from 'features/gallery/store/gallerySlice'; +import type { ComparisonFit } from 'features/gallery/store/types'; +import type { ImageDTO } from 'services/api/types'; + +export const DROP_SHADOW = 'drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.3))'; + +export type ComparisonProps = { + firstImage: ImageDTO; + secondImage: ImageDTO; + containerDims: Dimensions; +}; + +export const fitDimsToContainer = (containerDims: Dimensions, imageDims: Dimensions): Dimensions => { + // Fall back to the image's dimensions if the container has no dimensions + if (containerDims.width === 0 || containerDims.height === 0) { + return { width: imageDims.width, height: imageDims.height }; + } + + // Fall back to the image's dimensions if the image fits within the container + if (imageDims.width <= containerDims.width && imageDims.height <= containerDims.height) { + return { width: imageDims.width, height: imageDims.height }; + } + + const targetAspectRatio = containerDims.width / containerDims.height; + const imageAspectRatio = imageDims.width / imageDims.height; + + let width: number; + let height: number; + + if (imageAspectRatio > targetAspectRatio) { + // Image is wider than container's aspect ratio + width = containerDims.width; + height = width / imageAspectRatio; + } else { + // Image is taller than container's aspect ratio + height = containerDims.height; + width = height * imageAspectRatio; + } + return { width, height }; +}; + +/** + * Gets the dimensions of the second image in a comparison based on the comparison fit mode. + */ +export const getSecondImageDims = ( + comparisonFit: ComparisonFit, + fittedDims: Dimensions, + firstImageDims: Dimensions, + secondImageDims: Dimensions +): Dimensions => { + const width = + comparisonFit === 'fill' ? fittedDims.width : (fittedDims.width * secondImageDims.width) / firstImageDims.width; + const height = + comparisonFit === 'fill' ? fittedDims.height : (fittedDims.height * secondImageDims.height) / firstImageDims.height; + + return { width, height }; +}; +export const selectComparisonImages = createMemoizedSelector(selectGallerySlice, (gallerySlice) => { + const firstImage = gallerySlice.selection.slice(-1)[0] ?? null; + const secondImage = gallerySlice.imageToCompare; + return { firstImage, secondImage }; +}); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts new file mode 100644 index 0000000000..1e1567e70a --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts @@ -0,0 +1,31 @@ +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; +import { useCallback } from 'react'; + +export const useImageViewer = () => { + const dispatch = useAppDispatch(); + const isComparing = useAppSelector((s) => s.gallery.imageToCompare !== null); + const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen); + + const onClose = useCallback(() => { + if (isComparing && isOpen) { + dispatch(imageToCompareChanged(null)); + } else { + dispatch(isImageViewerOpenChanged(false)); + } + }, [dispatch, isComparing, isOpen]); + + const onOpen = useCallback(() => { + dispatch(isImageViewerOpenChanged(true)); + }, [dispatch]); + + const onToggle = useCallback(() => { + if (isComparing && isOpen) { + dispatch(imageToCompareChanged(null)); + } else { + dispatch(isImageViewerOpenChanged(!isOpen)); + } + }, [dispatch, isComparing, isOpen]); + + return { isOpen, onOpen, onClose, onToggle, isComparing }; +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx deleted file mode 100644 index 57b3697b7e..0000000000 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; -import { useCallback } from 'react'; - -export const useImageViewer = () => { - const dispatch = useAppDispatch(); - const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen); - - const onClose = useCallback(() => { - dispatch(isImageViewerOpenChanged(false)); - }, [dispatch]); - - const onOpen = useCallback(() => { - dispatch(isImageViewerOpenChanged(true)); - }, [dispatch]); - - const onToggle = useCallback(() => { - dispatch(isImageViewerOpenChanged(!isOpen)); - }, [dispatch, isOpen]); - - return { isOpen, onOpen, onClose, onToggle }; -}; diff --git a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx index 9949fb5bd5..19368455e3 100644 --- a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx @@ -14,7 +14,7 @@ const nextPrevButtonStyles: ChakraProps['sx'] = { const NextPrevImageButtons = () => { const { t } = useTranslation(); - const { handleLeftImage, handleRightImage, isOnFirstImage, isOnLastImage } = useGalleryNavigation(); + const { prevImage, nextImage, isOnFirstImage, isOnLastImage } = useGalleryNavigation(); const { areMoreImagesAvailable, @@ -30,7 +30,7 @@ const NextPrevImageButtons = () => { aria-label={t('accessibility.previousImage')} icon={} variant="unstyled" - onClick={handleLeftImage} + onClick={prevImage} boxSize={16} sx={nextPrevButtonStyles} /> @@ -42,7 +42,7 @@ const NextPrevImageButtons = () => { aria-label={t('accessibility.nextImage')} icon={} variant="unstyled" - onClick={handleRightImage} + onClick={nextImage} boxSize={16} sx={nextPrevButtonStyles} /> diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts index 1efc317e3a..931d93272b 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryHotkeys.ts @@ -27,16 +27,16 @@ export const useGalleryHotkeys = () => { useGalleryNavigation(); useHotkeys( - 'left', - () => { - canNavigateGallery && handleLeftImage(); + ['left', 'alt+left'], + (e) => { + canNavigateGallery && handleLeftImage(e.altKey); }, [handleLeftImage, canNavigateGallery] ); useHotkeys( - 'right', - () => { + ['right', 'alt+right'], + (e) => { if (!canNavigateGallery) { return; } @@ -45,29 +45,29 @@ export const useGalleryHotkeys = () => { return; } if (!isOnLastImage) { - handleRightImage(); + handleRightImage(e.altKey); } }, [isOnLastImage, areMoreImagesAvailable, handleLoadMoreImages, isFetching, handleRightImage, canNavigateGallery] ); useHotkeys( - 'up', - () => { - handleUpImage(); + ['up', 'alt+up'], + (e) => { + handleUpImage(e.altKey); }, { preventDefault: true }, [handleUpImage] ); useHotkeys( - 'down', - () => { + ['down', 'alt+down'], + (e) => { if (!areImagesBelowCurrent && areMoreImagesAvailable && !isFetching) { handleLoadMoreImages(); return; } - handleDownImage(); + handleDownImage(e.altKey); }, { preventDefault: true }, [areImagesBelowCurrent, areMoreImagesAvailable, handleLoadMoreImages, isFetching, handleDownImage] diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryNavigation.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryNavigation.ts index 1464c23285..ce6b152577 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGalleryNavigation.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGalleryNavigation.ts @@ -1,11 +1,11 @@ +import { useAltModifier } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId'; import { imageItemContainerTestId } from 'features/gallery/components/ImageGrid/ImageGridItemContainer'; import { imageListContainerTestId } from 'features/gallery/components/ImageGrid/ImageGridListContainer'; import { virtuosoGridRefs } from 'features/gallery/components/ImageGrid/types'; import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages'; -import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; -import { imageSelected } from 'features/gallery/store/gallerySlice'; +import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice'; import { getIsVisible } from 'features/gallery/util/getIsVisible'; import { getScrollToIndexAlign } from 'features/gallery/util/getScrollToIndexAlign'; import { clamp } from 'lodash-es'; @@ -106,10 +106,12 @@ const getImageFuncs = { }; type UseGalleryNavigationReturn = { - handleLeftImage: () => void; - handleRightImage: () => void; - handleUpImage: () => void; - handleDownImage: () => void; + handleLeftImage: (alt?: boolean) => void; + handleRightImage: (alt?: boolean) => void; + handleUpImage: (alt?: boolean) => void; + handleDownImage: (alt?: boolean) => void; + prevImage: () => void; + nextImage: () => void; isOnFirstImage: boolean; isOnLastImage: boolean; areImagesBelowCurrent: boolean; @@ -123,7 +125,15 @@ type UseGalleryNavigationReturn = { */ export const useGalleryNavigation = (): UseGalleryNavigationReturn => { const dispatch = useAppDispatch(); - const lastSelectedImage = useAppSelector(selectLastSelectedImage); + const alt = useAltModifier(); + const lastSelectedImage = useAppSelector((s) => { + const lastSelected = s.gallery.selection.slice(-1)[0] ?? null; + if (alt) { + return s.gallery.imageToCompare ?? lastSelected; + } else { + return lastSelected; + } + }); const { queryResult: { data }, } = useGalleryImages(); @@ -136,7 +146,7 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => { }, [lastSelectedImage, data]); const handleNavigation = useCallback( - (direction: 'left' | 'right' | 'up' | 'down') => { + (direction: 'left' | 'right' | 'up' | 'down', alt?: boolean) => { if (!data) { return; } @@ -144,10 +154,14 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => { if (!image || index === lastSelectedImageIndex) { return; } - dispatch(imageSelected(image)); + if (alt) { + dispatch(imageToCompareChanged(image)); + } else { + dispatch(imageSelected(image)); + } scrollToImage(image.image_name, index); }, - [dispatch, lastSelectedImageIndex, data] + [data, lastSelectedImageIndex, dispatch] ); const isOnFirstImage = useMemo(() => lastSelectedImageIndex === 0, [lastSelectedImageIndex]); @@ -162,21 +176,41 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => { return lastSelectedImageIndex + imagesPerRow < loadedImagesCount; }, [lastSelectedImageIndex, loadedImagesCount]); - const handleLeftImage = useCallback(() => { - handleNavigation('left'); - }, [handleNavigation]); + const handleLeftImage = useCallback( + (alt?: boolean) => { + handleNavigation('left', alt); + }, + [handleNavigation] + ); - const handleRightImage = useCallback(() => { - handleNavigation('right'); - }, [handleNavigation]); + const handleRightImage = useCallback( + (alt?: boolean) => { + handleNavigation('right', alt); + }, + [handleNavigation] + ); - const handleUpImage = useCallback(() => { - handleNavigation('up'); - }, [handleNavigation]); + const handleUpImage = useCallback( + (alt?: boolean) => { + handleNavigation('up', alt); + }, + [handleNavigation] + ); - const handleDownImage = useCallback(() => { - handleNavigation('down'); - }, [handleNavigation]); + const handleDownImage = useCallback( + (alt?: boolean) => { + handleNavigation('down', alt); + }, + [handleNavigation] + ); + + const nextImage = useCallback(() => { + handleRightImage(); + }, [handleRightImage]); + + const prevImage = useCallback(() => { + handleLeftImage(); + }, [handleLeftImage]); return { handleLeftImage, @@ -186,5 +220,7 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => { isOnFirstImage, isOnLastImage, areImagesBelowCurrent, + nextImage, + prevImage, }; }; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts index 727752d79e..2978e08f4f 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts @@ -53,7 +53,7 @@ export const useImageActions = (image_name?: string) => { const recallSeed = useCallback(() => { handlers.seed.parse(metadata).then((seed) => { - handlers.seed.recall && handlers.seed.recall(seed); + handlers.seed.recall && handlers.seed.recall(seed, true); }); }, [metadata]); diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useMultiselect.ts b/invokeai/frontend/web/src/features/gallery/hooks/useMultiselect.ts index f84a349d2a..5f7c5e4da8 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useMultiselect.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useMultiselect.ts @@ -36,6 +36,7 @@ export const useMultiselect = (imageDTO?: ImageDTO) => { shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, + altKey: e.altKey, }) ); }, diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 892c5c954d..b1b673d10c 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,13 +1,12 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; -import { setActiveTab } from 'features/ui/store/uiSlice'; import { uniqBy } from 'lodash-es'; import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; -import type { BoardId, GalleryState, GalleryView } from './types'; +import type { BoardId, ComparisonMode, GalleryState, GalleryView } from './types'; import { IMAGE_LIMIT, INITIAL_IMAGE_LIMIT } from './types'; const initialGalleryState: GalleryState = { @@ -22,8 +21,10 @@ const initialGalleryState: GalleryState = { boardSearchText: '', limit: INITIAL_IMAGE_LIMIT, offset: 0, - isImageViewerOpen: false, - isFloatingImageViewerOpen: false, + isImageViewerOpen: true, + imageToCompare: null, + comparisonMode: 'slider', + comparisonFit: 'fill', }; export const gallerySlice = createSlice({ @@ -36,6 +37,28 @@ export const gallerySlice = createSlice({ selectionChanged: (state, action: PayloadAction) => { state.selection = uniqBy(action.payload, (i) => i.image_name); }, + imageToCompareChanged: (state, action: PayloadAction) => { + state.imageToCompare = action.payload; + if (action.payload) { + state.isImageViewerOpen = true; + } + }, + comparisonModeChanged: (state, action: PayloadAction) => { + state.comparisonMode = action.payload; + }, + comparisonModeCycled: (state) => { + switch (state.comparisonMode) { + case 'slider': + state.comparisonMode = 'side-by-side'; + break; + case 'side-by-side': + state.comparisonMode = 'hover'; + break; + case 'hover': + state.comparisonMode = 'slider'; + break; + } + }, shouldAutoSwitchChanged: (state, action: PayloadAction) => { state.shouldAutoSwitch = action.payload; }, @@ -81,14 +104,18 @@ export const gallerySlice = createSlice({ isImageViewerOpenChanged: (state, action: PayloadAction) => { state.isImageViewerOpen = action.payload; }, - isFloatingImageViewerOpenChanged: (state, action: PayloadAction) => { - state.isFloatingImageViewerOpen = action.payload; + comparedImagesSwapped: (state) => { + if (state.imageToCompare) { + const oldSelection = state.selection; + state.selection = [state.imageToCompare]; + state.imageToCompare = oldSelection[0] ?? null; + } + }, + comparisonFitChanged: (state, action: PayloadAction<'contain' | 'fill'>) => { + state.comparisonFit = action.payload; }, }, extraReducers: (builder) => { - builder.addCase(setActiveTab, (state) => { - state.isImageViewerOpen = false; - }); builder.addMatcher(isAnyBoardDeleted, (state, action) => { const deletedBoardId = action.meta.arg.originalArgs; if (deletedBoardId === state.selectedBoardId) { @@ -125,7 +152,11 @@ export const { moreImagesLoaded, alwaysShowImageSizeBadgeChanged, isImageViewerOpenChanged, - isFloatingImageViewerOpenChanged, + imageToCompareChanged, + comparisonModeChanged, + comparedImagesSwapped, + comparisonFitChanged, + comparisonModeCycled, } = gallerySlice.actions; const isAnyBoardDeleted = isAnyOf( @@ -147,5 +178,13 @@ export const galleryPersistConfig: PersistConfig = { name: gallerySlice.name, initialState: initialGalleryState, migrate: migrateGalleryState, - persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit', 'isImageViewerOpen'], + persistDenylist: [ + 'selection', + 'selectedBoardId', + 'galleryView', + 'offset', + 'limit', + 'isImageViewerOpen', + 'imageToCompare', + ], }; diff --git a/invokeai/frontend/web/src/features/gallery/store/types.ts b/invokeai/frontend/web/src/features/gallery/store/types.ts index 9c258060c9..a88715b0bd 100644 --- a/invokeai/frontend/web/src/features/gallery/store/types.ts +++ b/invokeai/frontend/web/src/features/gallery/store/types.ts @@ -7,6 +7,8 @@ export const IMAGE_LIMIT = 20; export type GalleryView = 'images' | 'assets'; export type BoardId = 'none' | (string & Record); +export type ComparisonMode = 'slider' | 'side-by-side' | 'hover'; +export type ComparisonFit = 'contain' | 'fill'; export type GalleryState = { selection: ImageDTO[]; @@ -20,6 +22,8 @@ export type GalleryState = { offset: number; limit: number; alwaysShowImageSizeBadge: boolean; + imageToCompare: ImageDTO | null; + comparisonMode: ComparisonMode; + comparisonFit: ComparisonFit; isImageViewerOpen: boolean; - isFloatingImageViewerOpen: boolean; }; diff --git a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx index ddcdd58e75..f7261b4608 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx @@ -75,8 +75,8 @@ export const LoRACard = memo((props: LoRACardProps) => { { - const [controlNets, setControlNets] = useState([]); - - useEffect(() => { - const parse = async () => { - try { - const parsed = await handlers.controlNetsV2.parse(metadata); - setControlNets(parsed); - } catch (e) { - setControlNets([]); - } - }; - parse(); - }, [metadata]); - - const label = useMemo(() => handlers.controlNetsV2.getLabel(), []); - - return ( - <> - {controlNets.map((controlNet) => ( - - ))} - - ); -}; - -const MetadataViewControlNet = ({ - label, - controlNet, - handlers, -}: { - label: string; - controlNet: ControlNetConfigV2Metadata; - handlers: MetadataHandlers; -}) => { - const onRecall = useCallback(() => { - if (!handlers.recallItem) { - return; - } - handlers.recallItem(controlNet, true); - }, [handlers, controlNet]); - - const [renderedValue, setRenderedValue] = useState(null); - useEffect(() => { - const _renderValue = async () => { - if (!handlers.renderItemValue) { - setRenderedValue(null); - return; - } - const rendered = await handlers.renderItemValue(controlNet); - setRenderedValue(rendered); - }; - - _renderValue(); - }, [handlers, controlNet]); - - return ; -}; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataItem.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataItem.tsx index 66d101f458..7489b05158 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataItem.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataItem.tsx @@ -3,6 +3,7 @@ import { MetadataItemView } from 'features/metadata/components/MetadataItemView' import { useMetadataItem } from 'features/metadata/hooks/useMetadataItem'; import type { MetadataHandlers } from 'features/metadata/types'; import { MetadataParseFailedToken } from 'features/metadata/util/parsers'; +import { isSymbol } from 'lodash-es'; type MetadataItemProps = { metadata: unknown; @@ -17,6 +18,10 @@ const _MetadataItem = typedMemo(({ metadata, handlers, direction = 'row' }: return null; } + if (handlers.getIsVisible && !isSymbol(value) && !handlers.getIsVisible(value)) { + return null; + } + return ( { - const [ipAdapters, setIPAdapters] = useState([]); +export const MetadataLayers = ({ metadata }: Props) => { + const [layers, setLayers] = useState([]); useEffect(() => { const parse = async () => { try { - const parsed = await handlers.ipAdaptersV2.parse(metadata); - setIPAdapters(parsed); + const parsed = await handlers.layers.parse(metadata); + setLayers(parsed); } catch (e) { - setIPAdapters([]); + setLayers([]); } }; parse(); }, [metadata]); - const label = useMemo(() => handlers.ipAdaptersV2.getLabel(), []); + const label = useMemo(() => handlers.layers.getLabel(), []); return ( <> - {ipAdapters.map((ipAdapter) => ( - + {layers.map((layer) => ( + ))} ); }; -const MetadataViewIPAdapter = ({ +const MetadataViewLayer = ({ label, - ipAdapter, + layer, handlers, }: { label: string; - ipAdapter: IPAdapterConfigV2Metadata; - handlers: MetadataHandlers; + layer: Layer; + handlers: MetadataHandlers; }) => { const onRecall = useCallback(() => { if (!handlers.recallItem) { return; } - handlers.recallItem(ipAdapter, true); - }, [handlers, ipAdapter]); + handlers.recallItem(layer, true); + }, [handlers, layer]); const [renderedValue, setRenderedValue] = useState(null); useEffect(() => { @@ -61,12 +57,12 @@ const MetadataViewIPAdapter = ({ setRenderedValue(null); return; } - const rendered = await handlers.renderItemValue(ipAdapter); + const rendered = await handlers.renderItemValue(layer); setRenderedValue(rendered); }; _renderValue(); - }, [handlers, ipAdapter]); + }, [handlers, layer]); return ; }; diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdaptersV2.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdaptersV2.tsx deleted file mode 100644 index 42d3de2ec2..0000000000 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataT2IAdaptersV2.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; -import type { MetadataHandlers, T2IAdapterConfigV2Metadata } from 'features/metadata/types'; -import { handlers } from 'features/metadata/util/handlers'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - -type Props = { - metadata: unknown; -}; - -export const MetadataT2IAdaptersV2 = ({ metadata }: Props) => { - const [t2iAdapters, setT2IAdapters] = useState([]); - - useEffect(() => { - const parse = async () => { - try { - const parsed = await handlers.t2iAdaptersV2.parse(metadata); - setT2IAdapters(parsed); - } catch (e) { - setT2IAdapters([]); - } - }; - parse(); - }, [metadata]); - - const label = useMemo(() => handlers.t2iAdaptersV2.getLabel(), []); - - return ( - <> - {t2iAdapters.map((t2iAdapter) => ( - - ))} - - ); -}; - -const MetadataViewT2IAdapter = ({ - label, - t2iAdapter, - handlers, -}: { - label: string; - t2iAdapter: T2IAdapterConfigV2Metadata; - handlers: MetadataHandlers; -}) => { - const onRecall = useCallback(() => { - if (!handlers.recallItem) { - return; - } - handlers.recallItem(t2iAdapter, true); - }, [handlers, t2iAdapter]); - - const [renderedValue, setRenderedValue] = useState(null); - useEffect(() => { - const _renderValue = async () => { - if (!handlers.renderItemValue) { - setRenderedValue(null); - return; - } - const rendered = await handlers.renderItemValue(t2iAdapter); - setRenderedValue(rendered); - }; - - _renderValue(); - }, [handlers, t2iAdapter]); - - return ; -}; diff --git a/invokeai/frontend/web/src/features/metadata/types.ts b/invokeai/frontend/web/src/features/metadata/types.ts index 30a34ec0c6..dfc1e828c9 100644 --- a/invokeai/frontend/web/src/features/metadata/types.ts +++ b/invokeai/frontend/web/src/features/metadata/types.ts @@ -1,9 +1,4 @@ import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlAdapters/store/types'; -import type { - ControlNetConfigV2, - IPAdapterConfigV2, - T2IAdapterConfigV2, -} from 'features/controlLayers/util/controlAdapters'; import type { O } from 'ts-toolbelt'; /** @@ -50,6 +45,14 @@ export type MetadataParseFunc = (metadata: unknown) => Promise; */ export type MetadataValidateFunc = (value: T) => Promise; +/** + * A function that determines whether a metadata item should be visible. + * + * @param value The value to check. + * @returns True if the item should be visible, false otherwise. + */ +type MetadataGetIsVisibleFunc = (value: T) => boolean; + export type MetadataHandlers = { /** * Gets the label of the current metadata item as a string. @@ -111,6 +114,14 @@ export type MetadataHandlers = { * @returns The rendered item. */ renderItemValue?: MetadataRenderValueFunc; + /** + * Checks if a parsed metadata value should be visible. + * If not provided, the item is always visible. + * + * @param value The value to check. + * @returns True if the item should be visible, false otherwise. + */ + getIsVisible?: MetadataGetIsVisibleFunc; }; // TODO(psyche): The types for item handlers should be able to be inferred from the type of the value: @@ -127,6 +138,7 @@ type BuildMetadataHandlersArg = { getLabel: MetadataGetLabelFunc; renderValue?: MetadataRenderValueFunc; renderItemValue?: MetadataRenderValueFunc; + getIsVisible?: MetadataGetIsVisibleFunc; }; export type BuildMetadataHandlers = ( @@ -140,11 +152,3 @@ export type AnyControlAdapterConfigMetadata = | ControlNetConfigMetadata | T2IAdapterConfigMetadata | IPAdapterConfigMetadata; - -export type ControlNetConfigV2Metadata = O.NonNullable; -export type T2IAdapterConfigV2Metadata = O.NonNullable; -export type IPAdapterConfigV2Metadata = O.NonNullable; -export type AnyControlAdapterConfigV2Metadata = - | ControlNetConfigV2Metadata - | T2IAdapterConfigV2Metadata - | IPAdapterConfigV2Metadata; diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 467f702cea..2829507dcd 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -1,9 +1,8 @@ import { objectKeys } from 'common/util/objectKeys'; -import { toast } from 'common/util/toast'; +import type { Layer } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { AnyControlAdapterConfigMetadata, - AnyControlAdapterConfigV2Metadata, BuildMetadataHandlers, MetadataGetLabelFunc, MetadataHandlers, @@ -15,7 +14,9 @@ import type { import { fetchModelConfig } from 'features/metadata/util/modelFetchingHelpers'; import { validators } from 'features/metadata/util/validators'; import type { ModelIdentifierField } from 'features/nodes/types/common'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; +import { assert } from 'tsafe'; import { parsers } from './parsers'; import { recallers } from './recallers'; @@ -44,55 +45,70 @@ const renderControlAdapterValue: MetadataRenderValueFunc = async (value) => { - try { - const modelConfig = await fetchModelConfig(value.model.key ?? 'none'); - return `${modelConfig.name} (${modelConfig.base.toUpperCase()}) - ${value.weight}`; - } catch { - return `${value.model.key} (${value.model.base.toUpperCase()}) - ${value.weight}`; +const renderLayerValue: MetadataRenderValueFunc = async (layer) => { + if (layer.type === 'initial_image_layer') { + let rendered = t('controlLayers.globalInitialImageLayer'); + if (layer.image) { + rendered += ` (${layer.image})`; + } + return rendered; } + if (layer.type === 'control_adapter_layer') { + let rendered = t('controlLayers.globalControlAdapterLayer'); + const model = layer.controlAdapter.model; + if (model) { + rendered += ` (${model.name} - ${model.base.toUpperCase()})`; + } + return rendered; + } + if (layer.type === 'ip_adapter_layer') { + let rendered = t('controlLayers.globalIPAdapterLayer'); + const model = layer.ipAdapter.model; + if (model) { + rendered += ` (${model.name} - ${model.base.toUpperCase()})`; + } + return rendered; + } + if (layer.type === 'regional_guidance_layer') { + const rendered = t('controlLayers.regionalGuidanceLayer'); + const items: string[] = []; + if (layer.positivePrompt) { + items.push(`Positive: ${layer.positivePrompt}`); + } + if (layer.negativePrompt) { + items.push(`Negative: ${layer.negativePrompt}`); + } + if (layer.ipAdapters.length > 0) { + items.push(`${layer.ipAdapters.length} IP Adapters`); + } + return `${rendered} (${items.join(', ')})`; + } + assert(false, 'Unknown layer type'); +}; +const renderLayersValue: MetadataRenderValueFunc = async (layers) => { + return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`; }; -const parameterSetToast = (parameter: string, description?: string) => { +const parameterSetToast = (parameter: string) => { toast({ - title: t('toast.parameterSet', { parameter }), - description, + id: 'PARAMETER_SET', + title: t('toast.parameterSet'), + description: t('toast.parameterSetDesc', { parameter }), status: 'info', - duration: 2500, - isClosable: true, }); }; -const parameterNotSetToast = (parameter: string, description?: string) => { +const parameterNotSetToast = (parameter: string, message?: string) => { toast({ - title: t('toast.parameterNotSet', { parameter }), - description, + id: 'PARAMETER_NOT_SET', + title: t('toast.parameterNotSet'), + description: message + ? t('toast.parameterNotSetDescWithMessage', { parameter, message }) + : t('toast.parameterNotSetDesc', { parameter }), status: 'warning', - duration: 2500, - isClosable: true, }); }; -// const allParameterSetToast = (description?: string) => { -// toast({ -// title: t('toast.parametersSet'), -// status: 'info', -// description, -// duration: 2500, -// isClosable: true, -// }); -// }; - -// const allParameterNotSetToast = (description?: string) => { -// toast({ -// title: t('toast.parametersNotSet'), -// status: 'warning', -// description, -// duration: 2500, -// isClosable: true, -// }); -// }; - const buildParse = (arg: { parser: MetadataParseFunc; @@ -171,6 +187,7 @@ const buildHandlers: BuildMetadataHandlers = ({ itemValidator, renderValue, renderItemValue, + getIsVisible, }) => ({ parse: buildParse({ parser, getLabel }), parseItem: itemParser ? buildParseItem({ itemParser, getLabel }) : undefined, @@ -179,6 +196,7 @@ const buildHandlers: BuildMetadataHandlers = ({ getLabel, renderValue: renderValue ?? resolveToString, renderItemValue: renderItemValue ?? resolveToString, + getIsVisible, }); export const handlers = { @@ -198,12 +216,6 @@ export const handlers = { recaller: recallers.cfgScale, }), height: buildHandlers({ getLabel: () => t('metadata.height'), parser: parsers.height, recaller: recallers.height }), - initialImage: buildHandlers({ - getLabel: () => t('metadata.initImage'), - parser: parsers.initialImage, - recaller: recallers.initialImage, - renderValue: async (imageDTO) => imageDTO.image_name, - }), negativePrompt: buildHandlers({ getLabel: () => t('metadata.negativePrompt'), parser: parsers.negativePrompt, @@ -350,35 +362,17 @@ export const handlers = { itemValidator: validators.t2iAdapter, renderItemValue: renderControlAdapterValue, }), - controlNetsV2: buildHandlers({ - getLabel: () => t('common.controlNet'), - parser: parsers.controlNetsV2, - itemParser: parsers.controlNetV2, - recaller: recallers.controlNetsV2, - itemRecaller: recallers.controlNetV2, - validator: validators.controlNetsV2, - itemValidator: validators.controlNetV2, - renderItemValue: renderControlAdapterValueV2, - }), - ipAdaptersV2: buildHandlers({ - getLabel: () => t('common.ipAdapter'), - parser: parsers.ipAdaptersV2, - itemParser: parsers.ipAdapterV2, - recaller: recallers.ipAdaptersV2, - itemRecaller: recallers.ipAdapterV2, - validator: validators.ipAdaptersV2, - itemValidator: validators.ipAdapterV2, - renderItemValue: renderControlAdapterValueV2, - }), - t2iAdaptersV2: buildHandlers({ - getLabel: () => t('common.t2iAdapter'), - parser: parsers.t2iAdaptersV2, - itemParser: parsers.t2iAdapterV2, - recaller: recallers.t2iAdaptersV2, - itemRecaller: recallers.t2iAdapterV2, - validator: validators.t2iAdaptersV2, - itemValidator: validators.t2iAdapterV2, - renderItemValue: renderControlAdapterValueV2, + layers: buildHandlers({ + getLabel: () => t('controlLayers.layers_one'), + parser: parsers.layers, + itemParser: parsers.layer, + recaller: recallers.layers, + itemRecaller: recallers.layer, + validator: validators.layers, + itemValidator: validators.layer, + renderItemValue: renderLayerValue, + renderValue: renderLayersValue, + getIsVisible: (value) => value.length > 0, }), } as const; @@ -435,9 +429,9 @@ export const parseAndRecallImageDimensions = async (metadata: unknown) => { }; // These handlers should be omitted when recalling to control layers -const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNets', 'ipAdapters', 't2iAdapters']; +const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNets', 'ipAdapters', 't2iAdapters', 'strength']; // These handlers should be omitted when recalling to the rest of the app -const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNetsV2', 'ipAdaptersV2', 't2iAdaptersV2']; +const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['layers']; export const parseAndRecallAllMetadata = async ( metadata: unknown, @@ -464,7 +458,18 @@ export const parseAndRecallAllMetadata = async ( }); }) ); + if (results.some((result) => result.status === 'fulfilled')) { - parameterSetToast(t('toast.parameters')); + toast({ + id: 'PARAMETER_SET', + title: t('toast.parametersSet'), + status: 'info', + }); + } else { + toast({ + id: 'PARAMETER_SET', + title: t('toast.parametersNotSet'), + status: 'warning', + }); } }; diff --git a/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts b/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts index a237582ed8..a2db414937 100644 --- a/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/modelFetchingHelpers.ts @@ -1,4 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; +import type { ModelIdentifierField } from 'features/nodes/types/common'; import { isModelIdentifier, isModelIdentifierV2 } from 'features/nodes/types/common'; import { modelsApi } from 'services/api/endpoints/models'; import type { AnyModelConfig, BaseModelType, ModelType } from 'services/api/types'; @@ -68,6 +69,24 @@ const fetchModelConfigByAttrs = async (name: string, base: BaseModelType, type: } }; +/** + * Fetches the model config given an identifier. First attempts to fetch by key, then falls back to fetching by attrs. + * @param identifier The model identifier. + * @returns A promise that resolves to the model config. + * @throws {ModelConfigNotFoundError} If the model config is unable to be fetched. + */ +export const fetchModelConfigByIdentifier = async (identifier: ModelIdentifierField): Promise => { + try { + return await fetchModelConfig(identifier.key); + } catch { + try { + return await fetchModelConfigByAttrs(identifier.name, identifier.base, identifier.type); + } catch { + throw new ModelConfigNotFoundError(`Unable to retrieve model config for identifier ${identifier}`); + } + } +}; + /** * Fetches the model config for a given model key and type, and ensures that the model config is of a specific type. * @param key The model key. diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 8641977b1f..0757d2e8db 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -1,10 +1,12 @@ -import { getStore } from 'app/store/nanostores/store'; import { initialControlNet, initialIPAdapter, initialT2IAdapter, } from 'features/controlAdapters/util/buildControlAdapter'; import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; +import { getCALayerId, getIPALayerId, INITIAL_IMAGE_LAYER_ID } from 'features/controlLayers/store/controlLayersSlice'; +import type { ControlAdapterLayer, InitialImageLayer, IPAdapterLayer, Layer } from 'features/controlLayers/store/types'; +import { zLayer } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA, imageDTOToImageWithDims, @@ -17,12 +19,9 @@ import type { LoRA } from 'features/lora/store/loraSlice'; import { defaultLoRAConfig } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, - ControlNetConfigV2Metadata, IPAdapterConfigMetadata, - IPAdapterConfigV2Metadata, MetadataParseFunc, T2IAdapterConfigMetadata, - T2IAdapterConfigV2Metadata, } from 'features/metadata/types'; import { fetchModelConfigWithTypeGuard, getModelKey } from 'features/metadata/util/modelFetchingHelpers'; import { zControlField, zIPAdapterField, zModelIdentifierField, zT2IAdapterField } from 'features/nodes/types/common'; @@ -69,8 +68,7 @@ import { isParameterWidth, } from 'features/parameters/types/parameterSchemas'; import { get, isArray, isString } from 'lodash-es'; -import { getImageDTO, imagesApi } from 'services/api/endpoints/images'; -import type { ImageDTO } from 'services/api/types'; +import { getImageDTO } from 'services/api/endpoints/images'; import { isControlNetModelConfig, isIPAdapterModelConfig, @@ -80,6 +78,7 @@ import { isT2IAdapterModelConfig, isVAEModelConfig, } from 'services/api/types'; +import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; export const MetadataParsePendingToken = Symbol('pending'); @@ -149,14 +148,6 @@ const parseCFGRescaleMultiplier: MetadataParseFunc = (metadata) => getProperty(metadata, 'scheduler', isParameterScheduler); -const parseInitialImage: MetadataParseFunc = async (metadata) => { - const imageName = await getProperty(metadata, 'init_image', isString); - const imageDTORequest = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName)); - const imageDTO = await imageDTORequest.unwrap(); - imageDTORequest.unsubscribe(); - return imageDTO; -}; - const parseWidth: MetadataParseFunc = (metadata) => getProperty(metadata, 'width', isParameterWidth); const parseHeight: MetadataParseFunc = (metadata) => @@ -309,7 +300,7 @@ const parseControlNet: MetadataParseFunc = async (meta const parseAllControlNets: MetadataParseFunc = async (metadata) => { try { - const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray || undefined); + const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray); const parseResults = await Promise.allSettled(controlNetsRaw.map((cn) => parseControlNet(cn))); const controlNets = parseResults .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') @@ -439,8 +430,103 @@ const parseAllIPAdapters: MetadataParseFunc = async ( } }; -//#region V2/Control Layers -const parseControlNetV2: MetadataParseFunc = async (metadataItem) => { +//#region Control Layers +const parseLayer: MetadataParseFunc = async (metadataItem) => zLayer.parseAsync(metadataItem); + +const parseLayers: MetadataParseFunc = async (metadata) => { + // We need to support recalling pre-Control Layers metadata into Control Layers. A separate set of parsers handles + // taking pre-CL metadata and parsing it into layers. It doesn't always map 1-to-1, so this is best-effort. For + // example, CL Control Adapters don't support resize mode, so we simply omit that property. + + try { + const layers: Layer[] = []; + + try { + const control_layers = await getProperty(metadata, 'control_layers'); + const controlLayersRaw = await getProperty(control_layers, 'layers', isArray); + const controlLayersParseResults = await Promise.allSettled(controlLayersRaw.map(parseLayer)); + const controlLayers = controlLayersParseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + layers.push(...controlLayers); + } catch { + // no-op + } + + try { + const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray); + const controlNetsParseResults = await Promise.allSettled( + controlNetsRaw.map(async (cn) => await parseControlNetToControlAdapterLayer(cn)) + ); + const controlNetsAsLayers = controlNetsParseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + layers.push(...controlNetsAsLayers); + } catch { + // no-op + } + + try { + const t2iAdaptersRaw = await getProperty(metadata, 't2iAdapters', isArray); + const t2iAdaptersParseResults = await Promise.allSettled( + t2iAdaptersRaw.map(async (cn) => await parseT2IAdapterToControlAdapterLayer(cn)) + ); + const t2iAdaptersAsLayers = t2iAdaptersParseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + layers.push(...t2iAdaptersAsLayers); + } catch { + // no-op + } + + try { + const ipAdaptersRaw = await getProperty(metadata, 'ipAdapters', isArray); + const ipAdaptersParseResults = await Promise.allSettled( + ipAdaptersRaw.map(async (cn) => await parseIPAdapterToIPAdapterLayer(cn)) + ); + const ipAdaptersAsLayers = ipAdaptersParseResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + layers.push(...ipAdaptersAsLayers); + } catch { + // no-op + } + + try { + const initialImageLayer = await parseInitialImageToInitialImageLayer(metadata); + layers.push(initialImageLayer); + } catch { + // no-op + } + + return layers; + } catch { + return []; + } +}; + +const parseInitialImageToInitialImageLayer: MetadataParseFunc = async (metadata) => { + const denoisingStrength = await getProperty(metadata, 'strength', isParameterStrength); + const imageName = await getProperty(metadata, 'init_image', isString); + const imageDTO = await getImageDTO(imageName); + assert(imageDTO, 'ImageDTO is null'); + const layer: InitialImageLayer = { + id: INITIAL_IMAGE_LAYER_ID, + type: 'initial_image_layer', + bbox: null, + bboxNeedsUpdate: true, + x: 0, + y: 0, + isEnabled: true, + opacity: 1, + image: imageDTOToImageWithDims(imageDTO), + isSelected: true, + denoisingStrength, + }; + return layer; +}; + +const parseControlNetToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { const control_model = await getProperty(metadataItem, 'control_model'); const key = await getModelKey(control_model, 'controlnet'); const controlNetModel = await fetchModelConfigWithTypeGuard(key, isControlNetModelConfig); @@ -469,7 +555,6 @@ const parseControlNetV2: MetadataParseFunc = async ( .catch(null) .parse(await getProperty(metadataItem, 'control_mode')); - const id = uuidv4(); const defaultPreprocessor = controlNetModel.default_settings?.preprocessor; const processorConfig = isProcessorTypeV2(defaultPreprocessor) ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults() @@ -481,36 +566,35 @@ const parseControlNetV2: MetadataParseFunc = async ( const imageDTO = image ? await getImageDTO(image.image_name) : null; const processedImageDTO = processedImage ? await getImageDTO(processedImage.image_name) : null; - const controlNet: ControlNetConfigV2Metadata = { - id, - type: 'controlnet', - model: zModelIdentifierField.parse(controlNetModel), - weight: typeof control_weight === 'number' ? control_weight : initialControlNetV2.weight, - beginEndStepPct, - controlMode: control_mode ?? initialControlNetV2.controlMode, - image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, - processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null, - processorConfig, - isProcessingImage: false, + const layer: ControlAdapterLayer = { + id: getCALayerId(uuidv4()), + bbox: null, + bboxNeedsUpdate: true, + isEnabled: true, + isFilterEnabled: true, + isSelected: true, + opacity: 1, + type: 'control_adapter_layer', + x: 0, + y: 0, + controlAdapter: { + id: uuidv4(), + type: 'controlnet', + model: zModelIdentifierField.parse(controlNetModel), + weight: typeof control_weight === 'number' ? control_weight : initialControlNetV2.weight, + beginEndStepPct, + controlMode: control_mode ?? initialControlNetV2.controlMode, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null, + processorConfig, + processorPendingBatchId: null, + }, }; - return controlNet; + return layer; }; -const parseAllControlNetsV2: MetadataParseFunc = async (metadata) => { - try { - const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray || undefined); - const parseResults = await Promise.allSettled(controlNetsRaw.map((cn) => parseControlNetV2(cn))); - const controlNets = parseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') - .map((result) => result.value); - return controlNets; - } catch { - return []; - } -}; - -const parseT2IAdapterV2: MetadataParseFunc = async (metadataItem) => { +const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc = async (metadataItem) => { const t2i_adapter_model = await getProperty(metadataItem, 't2i_adapter_model'); const key = await getModelKey(t2i_adapter_model, 't2i_adapter'); const t2iAdapterModel = await fetchModelConfigWithTypeGuard(key, isT2IAdapterModelConfig); @@ -536,7 +620,6 @@ const parseT2IAdapterV2: MetadataParseFunc = async ( .catch(null) .parse(await getProperty(metadataItem, 'end_step_percent')); - const id = uuidv4(); const defaultPreprocessor = t2iAdapterModel.default_settings?.preprocessor; const processorConfig = isProcessorTypeV2(defaultPreprocessor) ? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults() @@ -548,35 +631,34 @@ const parseT2IAdapterV2: MetadataParseFunc = async ( const imageDTO = image ? await getImageDTO(image.image_name) : null; const processedImageDTO = processedImage ? await getImageDTO(processedImage.image_name) : null; - const t2iAdapter: T2IAdapterConfigV2Metadata = { - id, - type: 't2i_adapter', - model: zModelIdentifierField.parse(t2iAdapterModel), - weight: typeof weight === 'number' ? weight : initialT2IAdapterV2.weight, - beginEndStepPct, - image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, - processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null, - processorConfig, - isProcessingImage: false, + const layer: ControlAdapterLayer = { + id: getCALayerId(uuidv4()), + bbox: null, + bboxNeedsUpdate: true, + isEnabled: true, + isFilterEnabled: true, + isSelected: true, + opacity: 1, + type: 'control_adapter_layer', + x: 0, + y: 0, + controlAdapter: { + id: uuidv4(), + type: 't2i_adapter', + model: zModelIdentifierField.parse(t2iAdapterModel), + weight: typeof weight === 'number' ? weight : initialT2IAdapterV2.weight, + beginEndStepPct, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null, + processorConfig, + processorPendingBatchId: null, + }, }; - return t2iAdapter; + return layer; }; -const parseAllT2IAdaptersV2: MetadataParseFunc = async (metadata) => { - try { - const t2iAdaptersRaw = await getProperty(metadata, 't2iAdapters', isArray); - const parseResults = await Promise.allSettled(t2iAdaptersRaw.map((t2iAdapter) => parseT2IAdapterV2(t2iAdapter))); - const t2iAdapters = parseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') - .map((result) => result.value); - return t2iAdapters; - } catch { - return []; - } -}; - -const parseIPAdapterV2: MetadataParseFunc = async (metadataItem) => { +const parseIPAdapterToIPAdapterLayer: MetadataParseFunc = async (metadataItem) => { const ip_adapter_model = await getProperty(metadataItem, 'ip_adapter_model'); const key = await getModelKey(ip_adapter_model, 'ip_adapter'); const ipAdapterModel = await fetchModelConfigWithTypeGuard(key, isIPAdapterModelConfig); @@ -602,39 +684,32 @@ const parseIPAdapterV2: MetadataParseFunc = async (me .catch(null) .parse(await getProperty(metadataItem, 'end_step_percent')); - const id = uuidv4(); const beginEndStepPct: [number, number] = [ begin_step_percent ?? initialIPAdapterV2.beginEndStepPct[0], end_step_percent ?? initialIPAdapterV2.beginEndStepPct[1], ]; const imageDTO = image ? await getImageDTO(image.image_name) : null; - const ipAdapter: IPAdapterConfigV2Metadata = { - id, - type: 'ip_adapter', - model: zModelIdentifierField.parse(ipAdapterModel), - weight: typeof weight === 'number' ? weight : initialIPAdapterV2.weight, - beginEndStepPct, - image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, - clipVisionModel: initialIPAdapterV2.clipVisionModel, // TODO: This needs to be added to the zIPAdapterField... - method: method ?? initialIPAdapterV2.method, + const layer: IPAdapterLayer = { + id: getIPALayerId(uuidv4()), + type: 'ip_adapter_layer', + isEnabled: true, + isSelected: true, + ipAdapter: { + id: uuidv4(), + type: 'ip_adapter', + model: zModelIdentifierField.parse(ipAdapterModel), + weight: typeof weight === 'number' ? weight : initialIPAdapterV2.weight, + beginEndStepPct, + image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, + clipVisionModel: initialIPAdapterV2.clipVisionModel, // TODO: This needs to be added to the zIPAdapterField... + method: method ?? initialIPAdapterV2.method, + }, }; - return ipAdapter; -}; - -const parseAllIPAdaptersV2: MetadataParseFunc = async (metadata) => { - try { - const ipAdaptersRaw = await getProperty(metadata, 'ipAdapters', isArray); - const parseResults = await Promise.allSettled(ipAdaptersRaw.map((ipAdapter) => parseIPAdapterV2(ipAdapter))); - const ipAdapters = parseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') - .map((result) => result.value); - return ipAdapters; - } catch { - return []; - } + return layer; }; +//#endregion export const parsers = { createdBy: parseCreatedBy, @@ -647,7 +722,6 @@ export const parsers = { cfgScale: parseCFGScale, cfgRescaleMultiplier: parseCFGRescaleMultiplier, scheduler: parseScheduler, - initialImage: parseInitialImage, width: parseWidth, height: parseHeight, steps: parseSteps, @@ -672,10 +746,9 @@ export const parsers = { t2iAdapters: parseAllT2IAdapters, ipAdapter: parseIPAdapter, ipAdapters: parseAllIPAdapters, - controlNetV2: parseControlNetV2, - controlNetsV2: parseAllControlNetsV2, - t2iAdapterV2: parseT2IAdapterV2, - t2iAdaptersV2: parseAllT2IAdaptersV2, - ipAdapterV2: parseIPAdapterV2, - ipAdaptersV2: parseAllIPAdaptersV2, + controlNetToControlLayer: parseControlNetToControlAdapterLayer, + t2iAdapterToControlAdapterLayer: parseT2IAdapterToControlAdapterLayer, + ipAdapterToIPAdapterLayer: parseIPAdapterToIPAdapterLayer, + layer: parseLayer, + layers: parseLayers, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index b29d937159..5a17fd4b5d 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -1,4 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; +import { deepClone } from 'common/util/deepClone'; import { controlAdapterRecalled, controlNetsReset, @@ -6,31 +7,32 @@ import { t2iAdaptersReset, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { - caLayerAdded, - caLayerControlNetsDeleted, - caLayerT2IAdaptersDeleted, + allLayersDeleted, + caLayerRecalled, + getCALayerId, + getIPALayerId, + getRGLayerId, heightChanged, - iiLayerAdded, - ipaLayerAdded, - ipaLayersDeleted, + iiLayerRecalled, + ipaLayerRecalled, negativePrompt2Changed, negativePromptChanged, positivePrompt2Changed, positivePromptChanged, + rgLayerRecalled, widthChanged, } from 'features/controlLayers/store/controlLayersSlice'; +import type { Layer } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import type { LoRA } from 'features/lora/store/loraSlice'; import { loraRecalled, lorasReset } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, - ControlNetConfigV2Metadata, IPAdapterConfigMetadata, - IPAdapterConfigV2Metadata, MetadataRecallFunc, T2IAdapterConfigMetadata, - T2IAdapterConfigV2Metadata, } from 'features/metadata/types'; +import { fetchModelConfigByIdentifier } from 'features/metadata/util/modelFetchingHelpers'; import { modelSelected } from 'features/parameters/store/actions'; import { setCfgRescaleMultiplier, @@ -72,7 +74,8 @@ import { setRefinerStart, setRefinerSteps, } from 'features/sdxl/store/sdxlSlice'; -import type { ImageDTO } from 'services/api/types'; +import { getImageDTO } from 'services/api/endpoints/images'; +import { v4 as uuidv4 } from 'uuid'; const recallPositivePrompt: MetadataRecallFunc = (positivePrompt) => { getStore().dispatch(positivePromptChanged(positivePrompt)); @@ -106,10 +109,6 @@ const recallScheduler: MetadataRecallFunc = (scheduler) => { getStore().dispatch(setScheduler(scheduler)); }; -const recallInitialImage: MetadataRecallFunc = async (imageDTO) => { - getStore().dispatch(iiLayerAdded(imageDTO)); -}; - const setSizeOptions = { updateAspectRatio: true, clamp: true }; const recallWidth: MetadataRecallFunc = (width) => { @@ -244,50 +243,96 @@ const recallIPAdapters: MetadataRecallFunc = (ipAdapt }); }; -//#region V2/Control Layer -const recallControlNetV2: MetadataRecallFunc = (controlNet) => { - getStore().dispatch(caLayerAdded(controlNet)); -}; - -const recallControlNetsV2: MetadataRecallFunc = (controlNets) => { +//#region Control Layers +const recallLayer: MetadataRecallFunc = async (layer) => { const { dispatch } = getStore(); - dispatch(caLayerControlNetsDeleted()); - if (!controlNets.length) { + // We need to check for the existence of all images and models when recalling. If they do not exist, SMITE THEM! + // Also, we need fresh IDs for all objects when recalling, to prevent multiple layers with the same ID. + if (layer.type === 'control_adapter_layer') { + const clone = deepClone(layer); + if (clone.controlAdapter.image) { + const imageDTO = await getImageDTO(clone.controlAdapter.image.name); + if (!imageDTO) { + clone.controlAdapter.image = null; + } + } + if (clone.controlAdapter.processedImage) { + const imageDTO = await getImageDTO(clone.controlAdapter.processedImage.name); + if (!imageDTO) { + clone.controlAdapter.processedImage = null; + } + } + if (clone.controlAdapter.model) { + try { + await fetchModelConfigByIdentifier(clone.controlAdapter.model); + } catch { + clone.controlAdapter.model = null; + } + } + clone.id = getCALayerId(uuidv4()); + clone.controlAdapter.id = uuidv4(); + dispatch(caLayerRecalled(clone)); return; } - controlNets.forEach((controlNet) => { - dispatch(caLayerAdded(controlNet)); - }); -}; - -const recallT2IAdapterV2: MetadataRecallFunc = (t2iAdapter) => { - getStore().dispatch(caLayerAdded(t2iAdapter)); -}; - -const recallT2IAdaptersV2: MetadataRecallFunc = (t2iAdapters) => { - const { dispatch } = getStore(); - dispatch(caLayerT2IAdaptersDeleted()); - if (!t2iAdapters.length) { + if (layer.type === 'ip_adapter_layer') { + const clone = deepClone(layer); + if (clone.ipAdapter.image) { + const imageDTO = await getImageDTO(clone.ipAdapter.image.name); + if (!imageDTO) { + clone.ipAdapter.image = null; + } + } + if (clone.ipAdapter.model) { + try { + await fetchModelConfigByIdentifier(clone.ipAdapter.model); + } catch { + clone.ipAdapter.model = null; + } + } + clone.id = getIPALayerId(uuidv4()); + clone.ipAdapter.id = uuidv4(); + dispatch(ipaLayerRecalled(clone)); return; } - t2iAdapters.forEach((t2iAdapters) => { - dispatch(caLayerAdded(t2iAdapters)); - }); -}; -const recallIPAdapterV2: MetadataRecallFunc = (ipAdapter) => { - getStore().dispatch(ipaLayerAdded(ipAdapter)); -}; + if (layer.type === 'regional_guidance_layer') { + const clone = deepClone(layer); + // Strip out the uploaded mask image property - this is an intermediate image + clone.uploadedMaskImage = null; -const recallIPAdaptersV2: MetadataRecallFunc = (ipAdapters) => { - const { dispatch } = getStore(); - dispatch(ipaLayersDeleted()); - if (!ipAdapters.length) { + for (const ipAdapter of clone.ipAdapters) { + if (ipAdapter.image) { + const imageDTO = await getImageDTO(ipAdapter.image.name); + if (!imageDTO) { + ipAdapter.image = null; + } + } + if (ipAdapter.model) { + try { + await fetchModelConfigByIdentifier(ipAdapter.model); + } catch { + ipAdapter.model = null; + } + } + ipAdapter.id = uuidv4(); + } + clone.id = getRGLayerId(uuidv4()); + dispatch(rgLayerRecalled(clone)); return; } - ipAdapters.forEach((ipAdapter) => { - dispatch(ipaLayerAdded(ipAdapter)); - }); + + if (layer.type === 'initial_image_layer') { + dispatch(iiLayerRecalled(layer)); + return; + } +}; + +const recallLayers: MetadataRecallFunc = (layers) => { + const { dispatch } = getStore(); + dispatch(allLayersDeleted()); + for (const l of layers) { + recallLayer(l); + } }; export const recallers = { @@ -299,7 +344,6 @@ export const recallers = { cfgScale: recallCFGScale, cfgRescaleMultiplier: recallCFGRescaleMultiplier, scheduler: recallScheduler, - initialImage: recallInitialImage, width: recallWidth, height: recallHeight, steps: recallSteps, @@ -324,10 +368,6 @@ export const recallers = { t2iAdapter: recallT2IAdapter, ipAdapters: recallIPAdapters, ipAdapter: recallIPAdapter, - controlNetV2: recallControlNetV2, - controlNetsV2: recallControlNetsV2, - t2iAdapterV2: recallT2IAdapterV2, - t2iAdaptersV2: recallT2IAdaptersV2, - ipAdapterV2: recallIPAdapterV2, - ipAdaptersV2: recallIPAdaptersV2, + layer: recallLayer, + layers: recallLayers, } as const; diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index d09321003f..759e8ba561 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -1,17 +1,16 @@ import { getStore } from 'app/store/nanostores/store'; +import type { Layer } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, - ControlNetConfigV2Metadata, IPAdapterConfigMetadata, - IPAdapterConfigV2Metadata, MetadataValidateFunc, T2IAdapterConfigMetadata, - T2IAdapterConfigV2Metadata, } from 'features/metadata/types'; import { InvalidModelConfigError } from 'features/metadata/util/modelFetchingHelpers'; import type { ParameterSDXLRefinerModel, ParameterVAEModel } from 'features/parameters/types/parameterSchemas'; import type { BaseModelType } from 'services/api/types'; +import { assert } from 'tsafe'; /** * Checks the given base model type against the currently-selected model's base type and throws an error if they are @@ -111,58 +110,39 @@ const validateIPAdapters: MetadataValidateFunc = (ipA return new Promise((resolve) => resolve(validatedIPAdapters)); }; -const validateControlNetV2: MetadataValidateFunc = (controlNet) => { - validateBaseCompatibility(controlNet.model?.base, 'ControlNet incompatible with currently-selected model'); - return new Promise((resolve) => resolve(controlNet)); -}; - -const validateControlNetsV2: MetadataValidateFunc = (controlNets) => { - const validatedControlNets: ControlNetConfigV2Metadata[] = []; - controlNets.forEach((controlNet) => { - try { - validateBaseCompatibility(controlNet.model?.base, 'ControlNet incompatible with currently-selected model'); - validatedControlNets.push(controlNet); - } catch { - // This is a no-op - we want to continue validating the rest of the ControlNets, and an empty list is valid. +const validateLayer: MetadataValidateFunc = async (layer) => { + if (layer.type === 'control_adapter_layer') { + const model = layer.controlAdapter.model; + assert(model, 'Control Adapter layer missing model'); + validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model'); + } + if (layer.type === 'ip_adapter_layer') { + const model = layer.ipAdapter.model; + assert(model, 'IP Adapter layer missing model'); + validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model'); + } + if (layer.type === 'regional_guidance_layer') { + for (const ipa of layer.ipAdapters) { + const model = ipa.model; + assert(model, 'IP Adapter layer missing model'); + validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model'); } - }); - return new Promise((resolve) => resolve(validatedControlNets)); + } + + return layer; }; -const validateT2IAdapterV2: MetadataValidateFunc = (t2iAdapter) => { - validateBaseCompatibility(t2iAdapter.model?.base, 'T2I Adapter incompatible with currently-selected model'); - return new Promise((resolve) => resolve(t2iAdapter)); -}; - -const validateT2IAdaptersV2: MetadataValidateFunc = (t2iAdapters) => { - const validatedT2IAdapters: T2IAdapterConfigV2Metadata[] = []; - t2iAdapters.forEach((t2iAdapter) => { +const validateLayers: MetadataValidateFunc = async (layers) => { + const validatedLayers: Layer[] = []; + for (const l of layers) { try { - validateBaseCompatibility(t2iAdapter.model?.base, 'T2I Adapter incompatible with currently-selected model'); - validatedT2IAdapters.push(t2iAdapter); + const validated = await validateLayer(l); + validatedLayers.push(validated); } catch { - // This is a no-op - we want to continue validating the rest of the T2I Adapters, and an empty list is valid. + // This is a no-op - we want to continue validating the rest of the layers, and an empty list is valid. } - }); - return new Promise((resolve) => resolve(validatedT2IAdapters)); -}; - -const validateIPAdapterV2: MetadataValidateFunc = (ipAdapter) => { - validateBaseCompatibility(ipAdapter.model?.base, 'IP Adapter incompatible with currently-selected model'); - return new Promise((resolve) => resolve(ipAdapter)); -}; - -const validateIPAdaptersV2: MetadataValidateFunc = (ipAdapters) => { - const validatedIPAdapters: IPAdapterConfigV2Metadata[] = []; - ipAdapters.forEach((ipAdapter) => { - try { - validateBaseCompatibility(ipAdapter.model?.base, 'IP Adapter incompatible with currently-selected model'); - validatedIPAdapters.push(ipAdapter); - } catch { - // This is a no-op - we want to continue validating the rest of the IP Adapters, and an empty list is valid. - } - }); - return new Promise((resolve) => resolve(validatedIPAdapters)); + } + return new Promise((resolve) => resolve(validatedLayers)); }; export const validators = { @@ -176,10 +156,6 @@ export const validators = { t2iAdapters: validateT2IAdapters, ipAdapter: validateIPAdapter, ipAdapters: validateIPAdapters, - controlNetV2: validateControlNetV2, - controlNetsV2: validateControlNetsV2, - t2iAdapterV2: validateT2IAdapterV2, - t2iAdaptersV2: validateT2IAdaptersV2, - ipAdapterV2: validateIPAdapterV2, - ipAdaptersV2: validateIPAdaptersV2, + layer: validateLayer, + layers: validateLayers, } as const; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useInstallModel.ts b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useInstallModel.ts new file mode 100644 index 0000000000..7636b9f314 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useInstallModel.ts @@ -0,0 +1,48 @@ +import { toast } from 'features/toast/toast'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useInstallModelMutation } from 'services/api/endpoints/models'; + +type InstallModelArg = { + source: string; + inplace?: boolean; + onSuccess?: () => void; + onError?: (error: unknown) => void; +}; + +export const useInstallModel = () => { + const { t } = useTranslation(); + const [_installModel, request] = useInstallModelMutation(); + + const installModel = useCallback( + ({ source, inplace, onSuccess, onError }: InstallModelArg) => { + _installModel({ source, inplace }) + .unwrap() + .then((_) => { + if (onSuccess) { + onSuccess(); + } + toast({ + id: 'MODEL_INSTALL_QUEUED', + title: t('toast.modelAddedSimple'), + status: 'success', + }); + }) + .catch((error) => { + if (onError) { + onError(error); + } + if (error) { + toast({ + id: 'MODEL_INSTALL_QUEUE_FAILED', + title: `${error.data.detail} `, + status: 'error', + }); + } + }); + }, + [_installModel, t] + ); + + return [installModel, request] as const; +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx index 6106264b78..6da320aa0b 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useStarterModelsToast.tsx @@ -17,7 +17,11 @@ export const useStarterModelsToast = () => { useEffect(() => { if (toast.isActive(TOAST_ID)) { - return; + if (mainModels.length === 0) { + return; + } else { + toast.close(TOAST_ID); + } } if (data && mainModels.length === 0 && !didToast && isEnabled) { toast({ diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceForm.tsx index 184429478e..ee5960f7d2 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceForm.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceForm.tsx @@ -1,11 +1,9 @@ import { Button, Flex, FormControl, FormErrorMessage, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel'; import type { ChangeEventHandler } from 'react'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useInstallModelMutation, useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models'; +import { useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models'; import { HuggingFaceResults } from './HuggingFaceResults'; @@ -14,50 +12,19 @@ export const HuggingFaceForm = () => { const [displayResults, setDisplayResults] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const { t } = useTranslation(); - const dispatch = useAppDispatch(); const [_getHuggingFaceModels, { isLoading, data }] = useLazyGetHuggingFaceModelsQuery(); - const [installModel] = useInstallModelMutation(); - - const handleInstallModel = useCallback( - (source: string) => { - installModel({ source }) - .unwrap() - .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('toast.modelAddedSimple'), - status: 'success', - }) - ) - ); - }) - .catch((error) => { - if (error) { - dispatch( - addToast( - makeToast({ - title: `${error.data.detail} `, - status: 'error', - }) - ) - ); - } - }); - }, - [installModel, dispatch, t] - ); + const [installModel] = useInstallModel(); const getModels = useCallback(async () => { _getHuggingFaceModels(huggingFaceRepo) .unwrap() .then((response) => { if (response.is_diffusers) { - handleInstallModel(huggingFaceRepo); + installModel({ source: huggingFaceRepo }); setDisplayResults(false); } else if (response.urls?.length === 1 && response.urls[0]) { - handleInstallModel(response.urls[0]); + installModel({ source: response.urls[0] }); setDisplayResults(false); } else { setDisplayResults(true); @@ -66,7 +33,7 @@ export const HuggingFaceForm = () => { .catch((error) => { setErrorMessage(error.data.detail || ''); }); - }, [_getHuggingFaceModels, handleInstallModel, huggingFaceRepo]); + }, [_getHuggingFaceModels, installModel, huggingFaceRepo]); const handleSetHuggingFaceRepo: ChangeEventHandler = useCallback((e) => { setHuggingFaceRepo(e.target.value); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceResultItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceResultItem.tsx index 1595a61147..32970a3666 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceResultItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceResultItem.tsx @@ -1,47 +1,20 @@ import { Flex, IconButton, Text } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; -import { useInstallModelMutation } from 'services/api/endpoints/models'; type Props = { result: string; }; export const HuggingFaceResultItem = ({ result }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const [installModel] = useInstallModelMutation(); + const [installModel] = useInstallModel(); - const handleInstall = useCallback(() => { - installModel({ source: result }) - .unwrap() - .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('toast.modelAddedSimple'), - status: 'success', - }) - ) - ); - }) - .catch((error) => { - if (error) { - dispatch( - addToast( - makeToast({ - title: `${error.data.detail} `, - status: 'error', - }) - ) - ); - } - }); - }, [installModel, result, dispatch, t]); + const onClick = useCallback(() => { + installModel({ source: result }); + }, [installModel, result]); return ( @@ -51,7 +24,7 @@ export const HuggingFaceResultItem = ({ result }: Props) => { {result} - } onClick={handleInstall} size="sm" /> + } onClick={onClick} size="sm" /> ); }; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceResults.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceResults.tsx index 8144accf3f..826fd177ea 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceResults.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/HuggingFaceFolder/HuggingFaceResults.tsx @@ -8,15 +8,12 @@ import { InputGroup, InputRightElement, } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel'; import type { ChangeEventHandler } from 'react'; import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PiXBold } from 'react-icons/pi'; -import { useInstallModelMutation } from 'services/api/endpoints/models'; import { HuggingFaceResultItem } from './HuggingFaceResultItem'; @@ -27,9 +24,8 @@ type HuggingFaceResultsProps = { export const HuggingFaceResults = ({ results }: HuggingFaceResultsProps) => { const { t } = useTranslation(); const [searchTerm, setSearchTerm] = useState(''); - const dispatch = useAppDispatch(); - const [installModel] = useInstallModelMutation(); + const [installModel] = useInstallModel(); const filteredResults = useMemo(() => { return results.filter((result) => { @@ -46,34 +42,11 @@ export const HuggingFaceResults = ({ results }: HuggingFaceResultsProps) => { setSearchTerm(''); }, []); - const handleAddAll = useCallback(() => { + const onClickAddAll = useCallback(() => { for (const result of filteredResults) { - installModel({ source: result }) - .unwrap() - .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('toast.modelAddedSimple'), - status: 'success', - }) - ) - ); - }) - .catch((error) => { - if (error) { - dispatch( - addToast( - makeToast({ - title: `${error.data.detail} `, - status: 'error', - }) - ) - ); - } - }); + installModel({ source: result }); } - }, [filteredResults, installModel, dispatch, t]); + }, [filteredResults, installModel]); return ( <> @@ -82,7 +55,7 @@ export const HuggingFaceResults = ({ results }: HuggingFaceResultsProps) => { {t('modelManager.availableModels')} - diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/InstallModelForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/InstallModelForm.tsx index 282e07ee27..cc052878bf 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/InstallModelForm.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/InstallModelForm.tsx @@ -1,12 +1,9 @@ import { Button, Checkbox, Flex, FormControl, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel'; import { t } from 'i18next'; import { useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; -import { useInstallModelMutation } from 'services/api/endpoints/models'; type SimpleImportModelConfig = { location: string; @@ -14,9 +11,7 @@ type SimpleImportModelConfig = { }; export const InstallModelForm = () => { - const dispatch = useAppDispatch(); - - const [installModel, { isLoading }] = useInstallModelMutation(); + const [installModel, { isLoading }] = useInstallModel(); const { register, handleSubmit, formState, reset } = useForm({ defaultValues: { @@ -26,40 +21,22 @@ export const InstallModelForm = () => { mode: 'onChange', }); + const resetForm = useCallback(() => reset(undefined, { keepValues: true }), [reset]); + const onSubmit = useCallback>( (values) => { if (!values?.location) { return; } - installModel({ source: values.location, inplace: values.inplace }) - .unwrap() - .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('toast.modelAddedSimple'), - status: 'success', - }) - ) - ); - reset(undefined, { keepValues: true }); - }) - .catch((error) => { - reset(undefined, { keepValues: true }); - if (error) { - dispatch( - addToast( - makeToast({ - title: `${error.data.detail} `, - status: 'error', - }) - ) - ); - } - }); + installModel({ + source: values.location, + inplace: values.inplace, + onSuccess: resetForm, + onError: resetForm, + }); }, - [dispatch, reset, installModel] + [installModel, resetForm] ); return ( diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueue.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueue.tsx index 5db2743669..b3544af5b3 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueue.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueue.tsx @@ -1,8 +1,6 @@ import { Box, Button, Flex, Heading } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { useCallback, useMemo } from 'react'; import { useListModelInstallsQuery, usePruneCompletedModelInstallsMutation } from 'services/api/endpoints/models'; @@ -10,8 +8,6 @@ import { useListModelInstallsQuery, usePruneCompletedModelInstallsMutation } fro import { ModelInstallQueueItem } from './ModelInstallQueueItem'; export const ModelInstallQueue = () => { - const dispatch = useAppDispatch(); - const { data } = useListModelInstallsQuery(); const [_pruneCompletedModelInstalls] = usePruneCompletedModelInstallsMutation(); @@ -20,28 +16,22 @@ export const ModelInstallQueue = () => { _pruneCompletedModelInstalls() .unwrap() .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('toast.prunedQueue'), - status: 'success', - }) - ) - ); + toast({ + id: 'MODEL_INSTALL_QUEUE_PRUNED', + title: t('toast.prunedQueue'), + status: 'success', + }); }) .catch((error) => { if (error) { - dispatch( - addToast( - makeToast({ - title: `${error.data.detail} `, - status: 'error', - }) - ) - ); + toast({ + id: 'MODEL_INSTALL_QUEUE_PRUNE_FAILED', + title: `${error.data.detail} `, + status: 'error', + }); } }); - }, [_pruneCompletedModelInstalls, dispatch]); + }, [_pruneCompletedModelInstalls]); const pruneAvailable = useMemo(() => { return data?.some( diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueueItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueueItem.tsx index d1fc600510..82a28b2d75 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueueItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueueItem.tsx @@ -1,7 +1,5 @@ import { Flex, IconButton, Progress, Text, Tooltip } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { isNil } from 'lodash-es'; import { useCallback, useMemo } from 'react'; @@ -29,7 +27,6 @@ const formatBytes = (bytes: number) => { export const ModelInstallQueueItem = (props: ModelListItemProps) => { const { installJob } = props; - const dispatch = useAppDispatch(); const [deleteImportModel] = useCancelModelInstallMutation(); @@ -37,28 +34,22 @@ export const ModelInstallQueueItem = (props: ModelListItemProps) => { deleteImportModel(installJob.id) .unwrap() .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('toast.modelImportCanceled'), - status: 'success', - }) - ) - ); + toast({ + id: 'MODEL_INSTALL_CANCELED', + title: t('toast.modelImportCanceled'), + status: 'success', + }); }) .catch((error) => { if (error) { - dispatch( - addToast( - makeToast({ - title: `${error.data.detail} `, - status: 'error', - }) - ) - ); + toast({ + id: 'MODEL_INSTALL_CANCEL_FAILED', + title: `${error.data.detail} `, + status: 'error', + }); } }); - }, [deleteImportModel, installJob, dispatch]); + }, [deleteImportModel, installJob]); const sourceLocation = useMemo(() => { switch (installJob.source.type) { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResults.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResults.tsx index 360d6c1403..749ef4c8e0 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResults.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderResults.tsx @@ -11,15 +11,13 @@ import { InputGroup, InputRightElement, } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel'; import type { ChangeEvent, ChangeEventHandler } from 'react'; import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PiXBold } from 'react-icons/pi'; -import { type ScanFolderResponse, useInstallModelMutation } from 'services/api/endpoints/models'; +import type { ScanFolderResponse } from 'services/api/endpoints/models'; import { ScanModelResultItem } from './ScanFolderResultItem'; @@ -30,9 +28,8 @@ type ScanModelResultsProps = { export const ScanModelsResults = ({ results }: ScanModelResultsProps) => { const { t } = useTranslation(); const [searchTerm, setSearchTerm] = useState(''); - const dispatch = useAppDispatch(); const [inplace, setInplace] = useState(true); - const [installModel] = useInstallModelMutation(); + const [installModel] = useInstallModel(); const filteredResults = useMemo(() => { return results.filter((result) => { @@ -58,61 +55,15 @@ export const ScanModelsResults = ({ results }: ScanModelResultsProps) => { if (result.is_installed) { continue; } - installModel({ source: result.path, inplace }) - .unwrap() - .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('toast.modelAddedSimple'), - status: 'success', - }) - ) - ); - }) - .catch((error) => { - if (error) { - dispatch( - addToast( - makeToast({ - title: `${error.data.detail} `, - status: 'error', - }) - ) - ); - } - }); + installModel({ source: result.path, inplace }); } - }, [filteredResults, installModel, inplace, dispatch, t]); + }, [filteredResults, installModel, inplace]); const handleInstallOne = useCallback( (source: string) => { - installModel({ source, inplace }) - .unwrap() - .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('toast.modelAddedSimple'), - status: 'success', - }) - ) - ); - }) - .catch((error) => { - if (error) { - dispatch( - addToast( - makeToast({ - title: `${error.data.detail} `, - status: 'error', - }) - ) - ); - } - }); + installModel({ source, inplace }); }, - [installModel, inplace, dispatch, t] + [installModel, inplace] ); return ( diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StartModelsResultItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StartModelsResultItem.tsx index a32fad810f..98e1e39640 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StartModelsResultItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StartModelsResultItem.tsx @@ -1,20 +1,16 @@ import { Badge, Box, Flex, IconButton, Text } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel'; import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; import type { GetStarterModelsResponse } from 'services/api/endpoints/models'; -import { useInstallModelMutation } from 'services/api/endpoints/models'; type Props = { result: GetStarterModelsResponse[number]; }; export const StarterModelsResultItem = ({ result }: Props) => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); const allSources = useMemo(() => { const _allSources = [result.source]; if (result.dependencies) { @@ -22,36 +18,13 @@ export const StarterModelsResultItem = ({ result }: Props) => { } return _allSources; }, [result]); - const [installModel] = useInstallModelMutation(); + const [installModel] = useInstallModel(); - const handleQuickAdd = useCallback(() => { + const onClick = useCallback(() => { for (const source of allSources) { - installModel({ source }) - .unwrap() - .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('toast.modelAddedSimple'), - status: 'success', - }) - ) - ); - }) - .catch((error) => { - if (error) { - dispatch( - addToast( - makeToast({ - title: `${error.data.detail} `, - status: 'error', - }) - ) - ); - } - }); + installModel({ source }); } - }, [allSources, installModel, dispatch, t]); + }, [allSources, installModel]); return ( @@ -67,7 +40,7 @@ export const StarterModelsResultItem = ({ result }: Props) => { {result.is_installed ? ( {t('common.installed')} ) : ( - } onClick={handleQuickAdd} size="sm" /> + } onClick={onClick} size="sm" /> )} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx index c9d0c03ed8..a4a6d5c833 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx @@ -4,8 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge'; import ModelFormatBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { toast } from 'features/toast/toast'; import type { MouseEvent } from 'react'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -53,25 +52,19 @@ const ModelListItem = (props: ModelListItemProps) => { deleteModel({ key: model.key }) .unwrap() .then((_) => { - dispatch( - addToast( - makeToast({ - title: `${t('modelManager.modelDeleted')}: ${model.name}`, - status: 'success', - }) - ) - ); + toast({ + id: 'MODEL_DELETED', + title: `${t('modelManager.modelDeleted')}: ${model.name}`, + status: 'success', + }); }) .catch((error) => { if (error) { - dispatch( - addToast( - makeToast({ - title: `${t('modelManager.modelDeleteFailed')}: ${model.name}`, - status: 'error', - }) - ) - ); + toast({ + id: 'MODEL_DELETE_FAILED', + title: `${t('modelManager.modelDeleteFailed')}: ${model.name}`, + status: 'error', + }); } }); dispatch(setSelectedModelKey(null)); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/ControlNetOrT2IAdapterDefaultSettings.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/ControlNetOrT2IAdapterDefaultSettings.tsx index dcdc4e2a36..9a84fbc726 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/ControlNetOrT2IAdapterDefaultSettings.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/ControlNetOrT2IAdapterDefaultSettings.tsx @@ -1,10 +1,9 @@ import { Button, Flex, Heading, SimpleGrid, Text } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { useControlNetOrT2IAdapterDefaultSettings } from 'features/modelManagerV2/hooks/useControlNetOrT2IAdapterDefaultSettings'; import { DefaultPreprocessor } from 'features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/DefaultPreprocessor'; import type { FormField } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; @@ -19,7 +18,6 @@ export type ControlNetOrT2IAdapterDefaultSettingsFormData = { export const ControlNetOrT2IAdapterDefaultSettings = () => { const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); const { t } = useTranslation(); - const dispatch = useAppDispatch(); const { defaultSettingsDefaults, isLoading: isLoadingDefaultSettings } = useControlNetOrT2IAdapterDefaultSettings(selectedModelKey); @@ -46,30 +44,24 @@ export const ControlNetOrT2IAdapterDefaultSettings = () => { }) .unwrap() .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('modelManager.defaultSettingsSaved'), - status: 'success', - }) - ) - ); + toast({ + id: 'DEFAULT_SETTINGS_SAVED', + title: t('modelManager.defaultSettingsSaved'), + status: 'success', + }); reset(data); }) .catch((error) => { if (error) { - dispatch( - addToast( - makeToast({ - title: `${error.data.detail} `, - status: 'error', - }) - ) - ); + toast({ + id: 'DEFAULT_SETTINGS_SAVE_FAILED', + title: `${error.data.detail} `, + status: 'error', + }); } }); }, - [selectedModelKey, dispatch, reset, updateModel, t] + [selectedModelKey, reset, updateModel, t] ); if (isLoadingDefaultSettings) { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx index 0d7920ef77..292835a7b7 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelImageUpload.tsx @@ -1,8 +1,6 @@ import { Box, Button, Flex, Icon, IconButton, Image, Tooltip } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; import { typedMemo } from 'common/util/typedMemo'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { toast } from 'features/toast/toast'; import { useCallback, useState } from 'react'; import { useDropzone } from 'react-dropzone'; import { useTranslation } from 'react-i18next'; @@ -15,7 +13,6 @@ type Props = { }; const ModelImageUpload = ({ model_key, model_image }: Props) => { - const dispatch = useAppDispatch(); const [image, setImage] = useState(model_image || null); const { t } = useTranslation(); @@ -34,27 +31,21 @@ const ModelImageUpload = ({ model_key, model_image }: Props) => { .unwrap() .then(() => { setImage(URL.createObjectURL(file)); - dispatch( - addToast( - makeToast({ - title: t('modelManager.modelImageUpdated'), - status: 'success', - }) - ) - ); + toast({ + id: 'MODEL_IMAGE_UPDATED', + title: t('modelManager.modelImageUpdated'), + status: 'success', + }); }) - .catch((_) => { - dispatch( - addToast( - makeToast({ - title: t('modelManager.modelImageUpdateFailed'), - status: 'error', - }) - ) - ); + .catch(() => { + toast({ + id: 'MODEL_IMAGE_UPDATE_FAILED', + title: t('modelManager.modelImageUpdateFailed'), + status: 'error', + }); }); }, - [dispatch, model_key, t, updateModelImage] + [model_key, t, updateModelImage] ); const handleResetImage = useCallback(() => { @@ -65,26 +56,20 @@ const ModelImageUpload = ({ model_key, model_image }: Props) => { deleteModelImage(model_key) .unwrap() .then(() => { - dispatch( - addToast( - makeToast({ - title: t('modelManager.modelImageDeleted'), - status: 'success', - }) - ) - ); + toast({ + id: 'MODEL_IMAGE_DELETED', + title: t('modelManager.modelImageDeleted'), + status: 'success', + }); }) - .catch((_) => { - dispatch( - addToast( - makeToast({ - title: t('modelManager.modelImageDeleteFailed'), - status: 'error', - }) - ) - ); + .catch(() => { + toast({ + id: 'MODEL_IMAGE_DELETE_FAILED', + title: t('modelManager.modelImageDeleteFailed'), + status: 'error', + }); }); - }, [dispatch, model_key, t, deleteModelImage]); + }, [model_key, t, deleteModelImage]); const { getInputProps, getRootProps } = useDropzone({ accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] }, diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx index e096b11209..233fc7bc6b 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx @@ -1,11 +1,10 @@ import { Button, Flex, Heading, SimpleGrid, Text } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector } from 'app/store/storeHooks'; import { useMainModelDefaultSettings } from 'features/modelManagerV2/hooks/useMainModelDefaultSettings'; import { DefaultHeight } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultHeight'; import { DefaultWidth } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultWidth'; import type { ParameterScheduler } from 'features/parameters/types/parameterSchemas'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; @@ -39,7 +38,6 @@ export type MainModelDefaultSettingsFormData = { export const MainModelDefaultSettings = () => { const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); const { t } = useTranslation(); - const dispatch = useAppDispatch(); const { defaultSettingsDefaults, @@ -76,30 +74,24 @@ export const MainModelDefaultSettings = () => { }) .unwrap() .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('modelManager.defaultSettingsSaved'), - status: 'success', - }) - ) - ); + toast({ + id: 'DEFAULT_SETTINGS_SAVED', + title: t('modelManager.defaultSettingsSaved'), + status: 'success', + }); reset(data); }) .catch((error) => { if (error) { - dispatch( - addToast( - makeToast({ - title: `${error.data.detail} `, - status: 'error', - }) - ) - ); + toast({ + id: 'DEFAULT_SETTINGS_SAVE_FAILED', + title: `${error.data.detail} `, + status: 'error', + }); } }); }, - [selectedModelKey, dispatch, reset, updateModel, t] + [selectedModelKey, reset, updateModel, t] ); if (isLoadingDefaultSettings) { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx index d95eed8d24..fa7ca4c394 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx @@ -4,8 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { ModelConvertButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton'; import { ModelEditButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelEditButton'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; @@ -47,25 +46,19 @@ export const Model = () => { .then((payload) => { form.reset(payload, { keepDefaultValues: true }); dispatch(setSelectedModelMode('view')); - dispatch( - addToast( - makeToast({ - title: t('modelManager.modelUpdated'), - status: 'success', - }) - ) - ); + toast({ + id: 'MODEL_UPDATED', + title: t('modelManager.modelUpdated'), + status: 'success', + }); }) .catch((_) => { form.reset(); - dispatch( - addToast( - makeToast({ - title: t('modelManager.modelUpdateFailed'), - status: 'error', - }) - ) - ); + toast({ + id: 'MODEL_UPDATE_FAILED', + title: t('modelManager.modelUpdateFailed'), + status: 'error', + }); }); }, [dispatch, data?.key, form, t, updateModel] diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx index bcff0451d6..40ffca76b4 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx @@ -9,9 +9,7 @@ import { useDisclosure, } from '@invoke-ai/ui-library'; import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useConvertModelMutation, useGetModelConfigQuery } from 'services/api/endpoints/models'; @@ -22,7 +20,6 @@ interface ModelConvertProps { export const ModelConvertButton = (props: ModelConvertProps) => { const { modelKey } = props; - const dispatch = useAppDispatch(); const { t } = useTranslation(); const { data } = useGetModelConfigQuery(modelKey ?? skipToken); const [convertModel, { isLoading }] = useConvertModelMutation(); @@ -33,38 +30,26 @@ export const ModelConvertButton = (props: ModelConvertProps) => { return; } - dispatch( - addToast( - makeToast({ - title: `${t('modelManager.convertingModelBegin')}: ${data?.name}`, - status: 'info', - }) - ) - ); + const toastId = `CONVERTING_MODEL_${data.key}`; + toast({ + id: toastId, + title: `${t('modelManager.convertingModelBegin')}: ${data?.name}`, + status: 'info', + }); convertModel(data?.key) .unwrap() .then(() => { - dispatch( - addToast( - makeToast({ - title: `${t('modelManager.modelConverted')}: ${data?.name}`, - status: 'success', - }) - ) - ); + toast({ id: toastId, title: `${t('modelManager.modelConverted')}: ${data?.name}`, status: 'success' }); }) .catch(() => { - dispatch( - addToast( - makeToast({ - title: `${t('modelManager.modelConversionFailed')}: ${data?.name}`, - status: 'error', - }) - ) - ); + toast({ + id: toastId, + title: `${t('modelManager.modelConversionFailed')}: ${data?.name}`, + status: 'error', + }); }); - }, [data, isLoading, dispatch, t, convertModel]); + }, [data, isLoading, t, convertModel]); if (data?.format !== 'checkpoint') { return; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx index 1f8e50b9da..8bc775c872 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx @@ -72,10 +72,12 @@ export const ModelEdit = ({ form }: Props) => { {t('modelManager.baseModel')} - - {t('modelManager.variant')} - - + {data.type === 'main' && ( + + {t('modelManager.variant')} + + + )} {data.type === 'main' && data.format === 'checkpoint' && ( <> diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 061209cafc..6da87f4e98 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -2,26 +2,36 @@ import 'reactflow/dist/style.css'; import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } from '@invoke-ai/ui-library'; -import { useAppToaster } from 'app/components/Toaster'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { useAppDispatch, useAppStore } from 'app/store/storeHooks'; import type { SelectInstance } from 'chakra-react-select'; import { useBuildNode } from 'features/nodes/hooks/useBuildNode'; import { - addNodePopoverClosed, - addNodePopoverOpened, - nodeAdded, - selectNodesSlice, + $cursorPos, + $edgePendingUpdate, + $isAddNodePopoverOpen, + $pendingConnection, + $templates, + closeAddNodePopover, + edgesChanged, + nodesChanged, + openAddNodePopover, } from 'features/nodes/store/nodesSlice'; -import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes'; +import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition'; +import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; +import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil'; +import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; +import type { AnyNode } from 'features/nodes/types/invocation'; +import { isInvocationNode } from 'features/nodes/types/invocation'; +import { toast } from 'features/toast/toast'; import { filter, map, memoize, some } from 'lodash-es'; -import type { KeyboardEventHandler } from 'react'; -import { memo, useCallback, useRef } from 'react'; +import { memo, useCallback, useMemo, useRef } from 'react'; import { flushSync } from 'react-dom'; import { useHotkeys } from 'react-hotkeys-hook'; import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types'; import { useTranslation } from 'react-i18next'; import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters'; +import type { EdgeChange, NodeChange } from 'reactflow'; const createRegex = memoize( (inputValue: string) => @@ -50,30 +60,35 @@ const filterOption = memoize((option: FilterOptionOption, inputV const AddNodePopover = () => { const dispatch = useAppDispatch(); const buildInvocation = useBuildNode(); - const toaster = useAppToaster(); const { t } = useTranslation(); const selectRef = useRef | null>(null); const inputRef = useRef(null); + const templates = useStore($templates); + const pendingConnection = useStore($pendingConnection); + const isOpen = useStore($isAddNodePopoverOpen); + const store = useAppStore(); - const fieldFilter = useAppSelector((s) => s.nodes.connectionStartFieldType); - const handleFilter = useAppSelector((s) => s.nodes.connectionStartParams?.handleType); - - const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { + const filteredTemplates = useMemo(() => { // If we have a connection in progress, we need to filter the node choices - const filteredNodeTemplates = fieldFilter - ? filter(nodes.templates, (template) => { - const handles = handleFilter === 'source' ? template.inputs : template.outputs; + const templatesArray = map(templates); + if (!pendingConnection) { + return templatesArray; + } - return some(handles, (handle) => { - const sourceType = handleFilter === 'source' ? fieldFilter : handle.type; - const targetType = handleFilter === 'target' ? fieldFilter : handle.type; + return filter(templates, (template) => { + const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs; + return some(candidateFields, (field) => { + const sourceType = + pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type; + const targetType = + pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type; + return validateConnectionTypes(sourceType, targetType); + }); + }); + }, [templates, pendingConnection]); - return validateSourceAndTargetTypes(sourceType, targetType); - }); - }) - : map(nodes.templates); - - const options: ComboboxOption[] = map(filteredNodeTemplates, (template) => { + const options = useMemo(() => { + const _options: ComboboxOption[] = map(filteredTemplates, (template) => { return { label: template.title, value: template.type, @@ -83,15 +98,15 @@ const AddNodePopover = () => { }); //We only want these nodes if we're not filtered - if (fieldFilter === null) { - options.push({ + if (!pendingConnection) { + _options.push({ label: t('nodes.currentImage'), value: 'current_image', description: t('nodes.currentImageDescription'), tags: ['progress'], }); - options.push({ + _options.push({ label: t('nodes.notes'), value: 'notes', description: t('nodes.notesDescription'), @@ -99,31 +114,55 @@ const AddNodePopover = () => { }); } - options.sort((a, b) => a.label.localeCompare(b.label)); + _options.sort((a, b) => a.label.localeCompare(b.label)); - return { options }; - }); - - const { options } = useAppSelector(selector); - const isOpen = useAppSelector((s) => s.nodes.isAddNodePopoverOpen); + return _options; + }, [filteredTemplates, pendingConnection, t]); const addNode = useCallback( - (nodeType: string) => { - const invocation = buildInvocation(nodeType); - if (!invocation) { + (nodeType: string): AnyNode | null => { + const node = buildInvocation(nodeType); + if (!node) { const errorMessage = t('nodes.unknownNode', { nodeType: nodeType, }); - toaster({ + toast({ status: 'error', title: errorMessage, }); - return; + return null; } - dispatch(nodeAdded(invocation)); + // Find a cozy spot for the node + const cursorPos = $cursorPos.get(); + const { nodes, edges } = store.getState().nodes.present; + node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y); + node.selected = true; + + // Deselect all other nodes and edges + const nodeChanges: NodeChange[] = [{ type: 'add', item: node }]; + const edgeChanges: EdgeChange[] = []; + nodes.forEach(({ id, selected }) => { + if (selected) { + nodeChanges.push({ type: 'select', id, selected: false }); + } + }); + edges.forEach(({ id, selected }) => { + if (selected) { + edgeChanges.push({ type: 'select', id, selected: false }); + } + }); + + // Onwards! + if (nodeChanges.length > 0) { + dispatch(nodesChanged(nodeChanges)); + } + if (edgeChanges.length > 0) { + dispatch(edgesChanged(edgeChanges)); + } + return node; }, - [dispatch, buildInvocation, toaster, t] + [buildInvocation, store, dispatch, t] ); const onChange = useCallback( @@ -131,52 +170,65 @@ const AddNodePopover = () => { if (!v) { return; } - addNode(v.value); - dispatch(addNodePopoverClosed()); + const node = addNode(v.value); + + // Auto-connect an edge if we just added a node and have a pending connection + if (pendingConnection && isInvocationNode(node)) { + const edgePendingUpdate = $edgePendingUpdate.get(); + const { handleType } = pendingConnection; + + const source = handleType === 'source' ? pendingConnection.nodeId : node.id; + const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null; + const target = handleType === 'target' ? pendingConnection.nodeId : node.id; + const targetHandle = handleType === 'target' ? pendingConnection.handleId : null; + + const { nodes, edges } = store.getState().nodes.present; + const connection = getFirstValidConnection( + source, + sourceHandle, + target, + targetHandle, + nodes, + edges, + templates, + edgePendingUpdate + ); + if (connection) { + const newEdge = connectionToEdge(connection); + dispatch(edgesChanged([{ type: 'add', item: newEdge }])); + } + } + + closeAddNodePopover(); }, - [addNode, dispatch] + [addNode, dispatch, pendingConnection, store, templates] ); - const onClose = useCallback(() => { - dispatch(addNodePopoverClosed()); - }, [dispatch]); - - const onOpen = useCallback(() => { - dispatch(addNodePopoverOpened()); - }, [dispatch]); - - const handleHotkeyOpen: HotkeyCallback = useCallback( - (e) => { + const handleHotkeyOpen: HotkeyCallback = useCallback((e) => { + if (!$isAddNodePopoverOpen.get()) { e.preventDefault(); - onOpen(); + openAddNodePopover(); flushSync(() => { selectRef.current?.inputRef?.focus(); }); - }, - [onOpen] - ); + } + }, []); const handleHotkeyClose: HotkeyCallback = useCallback(() => { - onClose(); - }, [onClose]); + if ($isAddNodePopoverOpen.get()) { + closeAddNodePopover(); + } + }, []); useHotkeys(['shift+a', 'space'], handleHotkeyOpen); - useHotkeys(['escape'], handleHotkeyClose); - const onKeyDown: KeyboardEventHandler = useCallback( - (e) => { - if (e.key === 'Escape') { - onClose(); - } - }, - [onClose] - ); + useHotkeys(['escape'], handleHotkeyClose, { enableOnFormTags: ['TEXTAREA'] }); const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]); return ( { noOptionsMessage={noOptionsMessage} filterOption={filterOption} onChange={onChange} - onMenuClose={onClose} - onKeyDown={onKeyDown} + onMenuClose={closeAddNodePopover} inputRef={inputRef} closeMenuOnSelect={false} /> diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 4b9249e94f..727dad9617 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -1,47 +1,42 @@ import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useStore } from '@nanostores/react'; +import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks'; +import { useConnection } from 'features/nodes/hooks/useConnection'; +import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste'; +import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; -import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { - connectionEnded, - connectionMade, - connectionStarted, - edgeAdded, - edgeChangeStarted, - edgeDeleted, + $cursorPos, + $didUpdateEdge, + $edgePendingUpdate, + $isAddNodePopoverOpen, + $lastEdgeUpdateMouseEvent, + $pendingConnection, + $viewport, edgesChanged, - edgesDeleted, nodesChanged, - nodesDeleted, - selectedAll, - selectedEdgesChanged, - selectedNodesChanged, - selectionCopied, - selectionPasted, - viewportChanged, + redo, + undo, } from 'features/nodes/store/nodesSlice'; -import { $flow } from 'features/nodes/store/reactFlowInstance'; +import { $flow, $needsFit } from 'features/nodes/store/reactFlowInstance'; +import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil'; import type { CSSProperties, MouseEvent } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import type { - OnConnect, - OnConnectEnd, - OnConnectStart, + EdgeChange, + NodeChange, OnEdgesChange, - OnEdgesDelete, OnEdgeUpdateFunc, OnInit, OnMoveEnd, OnNodesChange, - OnNodesDelete, - OnSelectionChangeFunc, ProOptions, ReactFlowProps, - XYPosition, + ReactFlowState, } from 'reactflow'; -import { Background, ReactFlow } from 'reactflow'; +import { Background, ReactFlow, useStore as useReactFlowStore, useUpdateNodeInternals } from 'reactflow'; import CustomConnectionLine from './connectionLines/CustomConnectionLine'; import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge'; @@ -50,8 +45,6 @@ import CurrentImageNode from './nodes/CurrentImage/CurrentImageNode'; import InvocationNodeWrapper from './nodes/Invocation/InvocationNodeWrapper'; import NotesNode from './nodes/Notes/NotesNode'; -const DELETE_KEYS = ['Delete', 'Backspace']; - const edgeTypes = { collapsed: InvocationCollapsedEdge, default: InvocationDefaultEdge, @@ -68,17 +61,26 @@ const proOptions: ProOptions = { hideAttribution: true }; const snapGrid: [number, number] = [25, 25]; +const selectCancelConnection = (state: ReactFlowState) => state.cancelConnection; + export const Flow = memo(() => { const dispatch = useAppDispatch(); - const nodes = useAppSelector((s) => s.nodes.nodes); - const edges = useAppSelector((s) => s.nodes.edges); - const viewport = useAppSelector((s) => s.nodes.viewport); - const shouldSnapToGrid = useAppSelector((s) => s.nodes.shouldSnapToGrid); - const selectionMode = useAppSelector((s) => s.nodes.selectionMode); + const nodes = useAppSelector((s) => s.nodes.present.nodes); + const edges = useAppSelector((s) => s.nodes.present.edges); + const viewport = useStore($viewport); + const needsFit = useStore($needsFit); + const mayUndo = useAppSelector((s) => s.nodes.past.length > 0); + const mayRedo = useAppSelector((s) => s.nodes.future.length > 0); + const shouldSnapToGrid = useAppSelector((s) => s.workflowSettings.shouldSnapToGrid); + const selectionMode = useAppSelector((s) => s.workflowSettings.selectionMode); + const { onConnectStart, onConnect, onConnectEnd } = useConnection(); const flowWrapper = useRef(null); - const cursorPosition = useRef(null); const isValidConnection = useIsValidConnection(); + const cancelConnection = useReactFlowStore(selectCancelConnection); + const updateNodeInternals = useUpdateNodeInternals(); + const store = useAppStore(); useWorkflowWatcher(); + useSyncExecutionState(); const [borderRadius] = useToken('radii', ['base']); const flowStyles = useMemo( @@ -89,73 +91,32 @@ export const Flow = memo(() => { ); const onNodesChange: OnNodesChange = useCallback( - (changes) => { - dispatch(nodesChanged(changes)); + (nodeChanges) => { + dispatch(nodesChanged(nodeChanges)); + const flow = $flow.get(); + if (!flow) { + return; + } + if (needsFit) { + $needsFit.set(false); + flow.fitView(); + } }, - [dispatch] + [dispatch, needsFit] ); const onEdgesChange: OnEdgesChange = useCallback( (changes) => { - dispatch(edgesChanged(changes)); + if (changes.length > 0) { + dispatch(edgesChanged(changes)); + } }, [dispatch] ); - const onConnectStart: OnConnectStart = useCallback( - (event, params) => { - dispatch(connectionStarted(params)); - }, - [dispatch] - ); - - const onConnect: OnConnect = useCallback( - (connection) => { - dispatch(connectionMade(connection)); - }, - [dispatch] - ); - - const onConnectEnd: OnConnectEnd = useCallback(() => { - if (!cursorPosition.current) { - return; - } - dispatch( - connectionEnded({ - cursorPosition: cursorPosition.current, - mouseOverNodeId: $mouseOverNode.get(), - }) - ); - }, [dispatch]); - - const onEdgesDelete: OnEdgesDelete = useCallback( - (edges) => { - dispatch(edgesDeleted(edges)); - }, - [dispatch] - ); - - const onNodesDelete: OnNodesDelete = useCallback( - (nodes) => { - dispatch(nodesDeleted(nodes)); - }, - [dispatch] - ); - - const handleSelectionChange: OnSelectionChangeFunc = useCallback( - ({ nodes, edges }) => { - dispatch(selectedNodesChanged(nodes ? nodes.map((n) => n.id) : [])); - dispatch(selectedEdgesChanged(edges ? edges.map((e) => e.id) : [])); - }, - [dispatch] - ); - - const handleMoveEnd: OnMoveEnd = useCallback( - (e, viewport) => { - dispatch(viewportChanged(viewport)); - }, - [dispatch] - ); + const handleMoveEnd: OnMoveEnd = useCallback((e, viewport) => { + $viewport.set(viewport); + }, []); const { onCloseGlobal } = useGlobalMenuClose(); const handlePaneClick = useCallback(() => { @@ -169,11 +130,12 @@ export const Flow = memo(() => { const onMouseMove = useCallback((event: MouseEvent) => { if (flowWrapper.current?.getBoundingClientRect()) { - cursorPosition.current = + $cursorPos.set( $flow.get()?.screenToFlowPosition({ x: event.clientX, y: event.clientY, - }) ?? null; + }) ?? null + ); } }, []); @@ -189,67 +151,157 @@ export const Flow = memo(() => { * where the edge is deleted if you click it accidentally). */ - // We have a ref for cursor position, but it is the *projected* cursor position. - // Easiest to just keep track of the last mouse event for this particular feature - const edgeUpdateMouseEvent = useRef(); - - const onEdgeUpdateStart: NonNullable = useCallback( - (e, edge, _handleType) => { - // update mouse event - edgeUpdateMouseEvent.current = e; - // always delete the edge when starting an updated - dispatch(edgeDeleted(edge.id)); - dispatch(edgeChangeStarted()); - }, - [dispatch] - ); + const onEdgeUpdateStart: NonNullable = useCallback((e, edge, _handleType) => { + $edgePendingUpdate.set(edge); + $didUpdateEdge.set(false); + $lastEdgeUpdateMouseEvent.set(e); + }, []); const onEdgeUpdate: OnEdgeUpdateFunc = useCallback( - (_oldEdge, newConnection) => { - // instead of updating the edge (we deleted it earlier), we instead create - // a new one. - dispatch(connectionMade(newConnection)); + (oldEdge, newConnection) => { + // This event is fired when an edge update is successful + $didUpdateEdge.set(true); + // When an edge update is successful, we need to delete the old edge and create a new one + const newEdge = connectionToEdge(newConnection); + dispatch( + edgesChanged([ + { type: 'remove', id: oldEdge.id }, + { type: 'add', item: newEdge }, + ]) + ); + // Because we shift the position of handles depending on whether a field is connected or not, we must use + // updateNodeInternals to tell reactflow to recalculate the positions of the handles + updateNodeInternals([oldEdge.source, oldEdge.target, newEdge.source, newEdge.target]); }, - [dispatch] + [dispatch, updateNodeInternals] ); const onEdgeUpdateEnd: NonNullable = useCallback( (e, edge, _handleType) => { - // Handle the case where user begins a drag but didn't move the cursor - - // bc we deleted the edge, we need to add it back - if ( - // ignore touch events - !('touches' in e) && - edgeUpdateMouseEvent.current?.clientX === e.clientX && - edgeUpdateMouseEvent.current?.clientY === e.clientY - ) { - dispatch(edgeAdded(edge)); + const didUpdateEdge = $didUpdateEdge.get(); + // Fall back to a reasonable default event + const lastEvent = $lastEdgeUpdateMouseEvent.get() ?? { clientX: 0, clientY: 0 }; + // We have to narrow this event down to MouseEvents - could be TouchEvent + const didMouseMove = + !('touches' in e) && Math.hypot(e.clientX - lastEvent.clientX, e.clientY - lastEvent.clientY) > 5; + + // If we got this far and did not successfully update an edge, and the mouse moved away from the handle, + // the user probably intended to delete the edge + if (!didUpdateEdge && didMouseMove) { + dispatch(edgesChanged([{ type: 'remove', id: edge.id }])); } - // reset mouse event - edgeUpdateMouseEvent.current = undefined; + + $edgePendingUpdate.set(null); + $didUpdateEdge.set(false); + $pendingConnection.set(null); + $lastEdgeUpdateMouseEvent.set(null); }, [dispatch] ); // #endregion - useHotkeys(['Ctrl+c', 'Meta+c'], (e) => { - e.preventDefault(); - dispatch(selectionCopied()); - }); + const { copySelection, pasteSelection } = useCopyPaste(); - useHotkeys(['Ctrl+a', 'Meta+a'], (e) => { - e.preventDefault(); - dispatch(selectedAll()); - }); + const onCopyHotkey = useCallback( + (e: KeyboardEvent) => { + e.preventDefault(); + copySelection(); + }, + [copySelection] + ); + useHotkeys(['Ctrl+c', 'Meta+c'], onCopyHotkey); - useHotkeys(['Ctrl+v', 'Meta+v'], (e) => { - if (!cursorPosition.current) { - return; + const onSelectAllHotkey = useCallback( + (e: KeyboardEvent) => { + e.preventDefault(); + const { nodes, edges } = store.getState().nodes.present; + const nodeChanges: NodeChange[] = []; + const edgeChanges: EdgeChange[] = []; + nodes.forEach(({ id, selected }) => { + if (!selected) { + nodeChanges.push({ type: 'select', id, selected: true }); + } + }); + edges.forEach(({ id, selected }) => { + if (!selected) { + edgeChanges.push({ type: 'select', id, selected: true }); + } + }); + if (nodeChanges.length > 0) { + dispatch(nodesChanged(nodeChanges)); + } + if (edgeChanges.length > 0) { + dispatch(edgesChanged(edgeChanges)); + } + }, + [dispatch, store] + ); + useHotkeys(['Ctrl+a', 'Meta+a'], onSelectAllHotkey); + + const onPasteHotkey = useCallback( + (e: KeyboardEvent) => { + e.preventDefault(); + pasteSelection(); + }, + [pasteSelection] + ); + useHotkeys(['Ctrl+v', 'Meta+v'], onPasteHotkey); + + const onPasteWithEdgesToNodesHotkey = useCallback( + (e: KeyboardEvent) => { + e.preventDefault(); + pasteSelection(true); + }, + [pasteSelection] + ); + useHotkeys(['Ctrl+shift+v', 'Meta+shift+v'], onPasteWithEdgesToNodesHotkey); + + const onUndoHotkey = useCallback(() => { + if (mayUndo) { + dispatch(undo()); } - e.preventDefault(); - dispatch(selectionPasted({ cursorPosition: cursorPosition.current })); - }); + }, [dispatch, mayUndo]); + useHotkeys(['meta+z', 'ctrl+z'], onUndoHotkey); + + const onRedoHotkey = useCallback(() => { + if (mayRedo) { + dispatch(redo()); + } + }, [dispatch, mayRedo]); + useHotkeys(['meta+shift+z', 'ctrl+shift+z'], onRedoHotkey); + + const onEscapeHotkey = useCallback(() => { + if (!$edgePendingUpdate.get()) { + $pendingConnection.set(null); + $isAddNodePopoverOpen.set(false); + cancelConnection(); + } + }, [cancelConnection]); + useHotkeys('esc', onEscapeHotkey); + + const onDeleteHotkey = useCallback(() => { + const { nodes, edges } = store.getState().nodes.present; + const nodeChanges: NodeChange[] = []; + const edgeChanges: EdgeChange[] = []; + nodes + .filter((n) => n.selected) + .forEach(({ id }) => { + nodeChanges.push({ type: 'remove', id }); + }); + edges + .filter((e) => e.selected) + .forEach(({ id }) => { + edgeChanges.push({ type: 'remove', id }); + }); + if (nodeChanges.length > 0) { + dispatch(nodesChanged(nodeChanges)); + } + if (edgeChanges.length > 0) { + dispatch(edgesChanged(edgeChanges)); + } + }, [dispatch, store]); + useHotkeys(['delete', 'backspace'], onDeleteHotkey); return ( { onMouseMove={onMouseMove} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} - onEdgesDelete={onEdgesDelete} onEdgeUpdate={onEdgeUpdate} onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateEnd={onEdgeUpdateEnd} - onNodesDelete={onNodesDelete} onConnectStart={onConnectStart} onConnect={onConnect} onConnectEnd={onConnectEnd} onMoveEnd={handleMoveEnd} connectionLineComponent={CustomConnectionLine} - onSelectionChange={handleSelectionChange} isValidConnection={isValidConnection} minZoom={0.1} snapToGrid={shouldSnapToGrid} @@ -283,8 +332,10 @@ export const Flow = memo(() => { proOptions={proOptions} style={flowStyles} onPaneClick={handlePaneClick} - deleteKeyCode={DELETE_KEYS} + deleteKeyCode={null} selectionMode={selectionMode} + elevateEdgesOnSelect + nodeDragThreshold={1} > diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx index 61efcea06a..09c88c713b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx @@ -1,26 +1,33 @@ -import { createSelector } from '@reduxjs/toolkit'; +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $pendingConnection } from 'features/nodes/store/nodesSlice'; import type { CSSProperties } from 'react'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import type { ConnectionLineComponentProps } from 'reactflow'; import { getBezierPath } from 'reactflow'; -const selectStroke = createSelector(selectNodesSlice, (nodes) => - nodes.shouldColorEdges ? getFieldColor(nodes.connectionStartFieldType) : colorTokenToCssVar('base.500') -); - -const selectClassName = createSelector(selectNodesSlice, (nodes) => - nodes.shouldAnimateEdges ? 'react-flow__custom_connection-path animated' : 'react-flow__custom_connection-path' -); - const pathStyles: CSSProperties = { opacity: 0.8 }; const CustomConnectionLine = ({ fromX, fromY, fromPosition, toX, toY, toPosition }: ConnectionLineComponentProps) => { - const stroke = useAppSelector(selectStroke); - const className = useAppSelector(selectClassName); + const pendingConnection = useStore($pendingConnection); + const shouldColorEdges = useAppSelector((state) => state.workflowSettings.shouldColorEdges); + const shouldAnimateEdges = useAppSelector((state) => state.workflowSettings.shouldAnimateEdges); + const stroke = useMemo(() => { + if (shouldColorEdges && pendingConnection) { + return getFieldColor(pendingConnection.fieldTemplate.type); + } else { + return colorTokenToCssVar('base.500'); + } + }, [pendingConnection, shouldColorEdges]); + const className = useMemo(() => { + if (shouldAnimateEdges) { + return 'react-flow__custom_connection-path animated'; + } else { + return 'react-flow__custom_connection-path'; + } + }, [shouldAnimateEdges]); const pathParams = { sourceX: fromX, diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx index eae7970804..0d7e7b7d5e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationCollapsedEdge.tsx @@ -1,12 +1,14 @@ import { Badge, Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; +import { getEdgeStyles } from 'features/nodes/components/flow/edges/util/getEdgeColor'; +import { makeEdgeSelector } from 'features/nodes/components/flow/edges/util/makeEdgeSelector'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { memo, useMemo } from 'react'; import type { EdgeProps } from 'reactflow'; import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow'; -import { makeEdgeSelector } from './util/makeEdgeSelector'; - const InvocationCollapsedEdge = ({ sourceX, sourceY, @@ -16,18 +18,19 @@ const InvocationCollapsedEdge = ({ targetPosition, markerEnd, data, - selected, + selected = false, source, - target, sourceHandleId, + target, targetHandleId, }: EdgeProps<{ count: number }>) => { + const templates = useStore($templates); const selector = useMemo( - () => makeEdgeSelector(source, sourceHandleId, target, targetHandleId, selected), - [selected, source, sourceHandleId, target, targetHandleId] + () => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId), + [templates, source, sourceHandleId, target, targetHandleId] ); - const { isSelected, shouldAnimate } = useAppSelector(selector); + const { shouldAnimateEdges, areConnectedNodesSelected } = useAppSelector(selector); const [edgePath, labelX, labelY] = getBezierPath({ sourceX, @@ -41,14 +44,8 @@ const InvocationCollapsedEdge = ({ const { base500 } = useChakraThemeTokens(); const edgeStyles = useMemo( - () => ({ - strokeWidth: isSelected ? 3 : 2, - stroke: base500, - opacity: isSelected ? 0.8 : 0.5, - animation: shouldAnimate ? 'dashdraw 0.5s linear infinite' : undefined, - strokeDasharray: shouldAnimate ? 5 : 'none', - }), - [base500, isSelected, shouldAnimate] + () => getEdgeStyles(base500, selected, shouldAnimateEdges, areConnectedNodesSelected), + [areConnectedNodesSelected, base500, selected, shouldAnimateEdges] ); return ( @@ -57,11 +54,15 @@ const InvocationCollapsedEdge = ({ {data?.count && data.count > 1 && ( - + {data.count} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx index 77be60a945..5a27e974e5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx @@ -1,6 +1,8 @@ import { Flex, Text } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; -import type { CSSProperties } from 'react'; +import { getEdgeStyles } from 'features/nodes/components/flow/edges/util/getEdgeColor'; +import { $templates } from 'features/nodes/store/nodesSlice'; import { memo, useMemo } from 'react'; import type { EdgeProps } from 'reactflow'; import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow'; @@ -15,19 +17,20 @@ const InvocationDefaultEdge = ({ sourcePosition, targetPosition, markerEnd, - selected, + selected = false, source, target, sourceHandleId, targetHandleId, }: EdgeProps) => { + const templates = useStore($templates); const selector = useMemo( - () => makeEdgeSelector(source, sourceHandleId, target, targetHandleId, selected), - [source, sourceHandleId, target, targetHandleId, selected] + () => makeEdgeSelector(templates, source, sourceHandleId, target, targetHandleId), + [templates, source, sourceHandleId, target, targetHandleId] ); - const { isSelected, shouldAnimate, stroke, label } = useAppSelector(selector); - const shouldShowEdgeLabels = useAppSelector((s) => s.nodes.shouldShowEdgeLabels); + const { shouldAnimateEdges, areConnectedNodesSelected, stroke, label } = useAppSelector(selector); + const shouldShowEdgeLabels = useAppSelector((s) => s.workflowSettings.shouldShowEdgeLabels); const [edgePath, labelX, labelY] = getBezierPath({ sourceX, @@ -38,15 +41,9 @@ const InvocationDefaultEdge = ({ targetPosition, }); - const edgeStyles = useMemo( - () => ({ - strokeWidth: isSelected ? 3 : 2, - stroke, - opacity: isSelected ? 0.8 : 0.5, - animation: shouldAnimate ? 'dashdraw 0.5s linear infinite' : undefined, - strokeDasharray: shouldAnimate ? 5 : 'none', - }), - [isSelected, shouldAnimate, stroke] + const edgeStyles = useMemo( + () => getEdgeStyles(stroke, selected, shouldAnimateEdges, areConnectedNodesSelected), + [areConnectedNodesSelected, stroke, selected, shouldAnimateEdges] ); return ( @@ -62,13 +59,13 @@ const InvocationDefaultEdge = ({ bg="base.800" borderRadius="base" borderWidth={1} - borderColor={isSelected ? 'undefined' : 'transparent'} - opacity={isSelected ? 1 : 0.5} + borderColor={selected ? 'undefined' : 'transparent'} + opacity={selected ? 1 : 0.5} py={1} px={3} shadow="md" > - + {label} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts index e7fa43015b..b5801c45ed 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/getEdgeColor.ts @@ -1,6 +1,7 @@ import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { FIELD_COLORS } from 'features/nodes/types/constants'; import type { FieldType } from 'features/nodes/types/field'; +import type { CSSProperties } from 'react'; export const getFieldColor = (fieldType: FieldType | null): string => { if (!fieldType) { @@ -10,3 +11,16 @@ export const getFieldColor = (fieldType: FieldType | null): string => { return color ? colorTokenToCssVar(color) : colorTokenToCssVar('base.500'); }; + +export const getEdgeStyles = ( + stroke: string, + selected: boolean, + shouldAnimateEdges: boolean, + areConnectedNodesSelected: boolean +): CSSProperties => ({ + strokeWidth: 3, + stroke, + opacity: selected ? 1 : 0.5, + animation: shouldAnimateEdges ? 'dashdraw 0.5s linear infinite' : undefined, + strokeDasharray: selected || areConnectedNodesSelected ? 5 : 'none', +}); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts index a485bf64c1..9c67728722 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts @@ -1,53 +1,58 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; +import { deepClone } from 'common/util/deepClone'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectFieldOutputTemplate, selectNodeTemplate } from 'features/nodes/store/selectors'; +import type { Templates } from 'features/nodes/store/types'; +import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getFieldColor } from './getEdgeColor'; const defaultReturnValue = { - isSelected: false, - shouldAnimate: false, + areConnectedNodesSelected: false, + shouldAnimateEdges: false, stroke: colorTokenToCssVar('base.500'), label: '', }; export const makeEdgeSelector = ( + templates: Templates, source: string, sourceHandleId: string | null | undefined, target: string, - targetHandleId: string | null | undefined, - selected?: boolean + targetHandleId: string | null | undefined ) => createMemoizedSelector( selectNodesSlice, - (nodes): { isSelected: boolean; shouldAnimate: boolean; stroke: string; label: string } => { + selectWorkflowSettingsSlice, + ( + nodes, + workflowSettings + ): { areConnectedNodesSelected: boolean; shouldAnimateEdges: boolean; stroke: string; label: string } => { + const { shouldAnimateEdges, shouldColorEdges } = workflowSettings; const sourceNode = nodes.nodes.find((node) => node.id === source); const targetNode = nodes.nodes.find((node) => node.id === target); + const returnValue = deepClone(defaultReturnValue); + returnValue.shouldAnimateEdges = shouldAnimateEdges; + const isInvocationToInvocationEdge = isInvocationNode(sourceNode) && isInvocationNode(targetNode); - const isSelected = Boolean(sourceNode?.selected || targetNode?.selected || selected); + returnValue.areConnectedNodesSelected = Boolean(sourceNode?.selected || targetNode?.selected); if (!sourceNode || !sourceHandleId || !targetNode || !targetHandleId) { - return defaultReturnValue; + return returnValue; } - const outputFieldTemplate = selectFieldOutputTemplate(nodes, sourceNode.id, sourceHandleId); + const sourceNodeTemplate = templates[sourceNode.data.type]; + const targetNodeTemplate = templates[targetNode.data.type]; + + const outputFieldTemplate = sourceNodeTemplate?.outputs[sourceHandleId]; const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined; - const stroke = sourceType && nodes.shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500'); + returnValue.stroke = sourceType && shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500'); - const sourceNodeTemplate = selectNodeTemplate(nodes, sourceNode.id); - const targetNodeTemplate = selectNodeTemplate(nodes, targetNode.id); + returnValue.label = `${sourceNodeTemplate?.title || sourceNode.data?.label} -> ${targetNodeTemplate?.title || targetNode.data?.label}`; - const label = `${sourceNodeTemplate?.title || sourceNode.data?.label} -> ${targetNodeTemplate?.title || targetNode.data?.label}`; - - return { - isSelected, - shouldAnimate: nodes.shouldAnimateEdges && isSelected, - stroke, - label, - }; + return returnValue; } ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx index 0147bcaed2..baa7fc262a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx @@ -1,7 +1,7 @@ import { Flex, Grid, GridItem } from '@invoke-ai/ui-library'; import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper'; -import { useAnyOrDirectInputFieldNames } from 'features/nodes/hooks/useAnyOrDirectInputFieldNames'; -import { useConnectionInputFieldNames } from 'features/nodes/hooks/useConnectionInputFieldNames'; +import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck'; +import { useFieldNames } from 'features/nodes/hooks/useFieldNames'; import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames'; import { useWithFooter } from 'features/nodes/hooks/useWithFooter'; import { memo } from 'react'; @@ -20,8 +20,7 @@ type Props = { }; const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { - const inputConnectionFieldNames = useConnectionInputFieldNames(nodeId); - const inputAnyOrDirectFieldNames = useAnyOrDirectInputFieldNames(nodeId); + const fieldNames = useFieldNames(nodeId); const withFooter = useWithFooter(nodeId); const outputFieldNames = useOutputFieldNames(nodeId); @@ -41,9 +40,11 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { > - {inputConnectionFieldNames.map((fieldName, i) => ( + {fieldNames.connectionFields.map((fieldName, i) => ( - + + + ))} {outputFieldNames.map((fieldName, i) => ( @@ -52,8 +53,23 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { ))} - {inputAnyOrDirectFieldNames.map((fieldName) => ( - + {fieldNames.anyOrDirectFields.map((fieldName) => ( + + + + ))} + {fieldNames.missingFields.map((fieldName) => ( + + + ))} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx index 3138cb32fe..b58f6fe8ba 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeStatusIndicator.tsx @@ -1,12 +1,10 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Badge, CircularProgress, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { useExecutionState } from 'features/nodes/hooks/useExecutionState'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; import type { NodeExecutionState } from 'features/nodes/types/invocation'; import { zNodeStatus } from 'features/nodes/types/invocation'; -import { memo, useMemo } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCheckBold, PiDotsThreeOutlineFill, PiWarningBold } from 'react-icons/pi'; @@ -24,12 +22,7 @@ const circleStyles: SystemStyleObject = { }; const InvocationNodeStatusIndicator = ({ nodeId }: Props) => { - const selectNodeExecutionState = useMemo( - () => createMemoizedSelector(selectNodesSlice, (nodes) => nodes.nodeExecutionStates[nodeId]), - [nodeId] - ); - - const nodeExecutionState = useAppSelector(selectNodeExecutionState); + const nodeExecutionState = useExecutionState(nodeId); if (!nodeExecutionState) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx index 0fe81c0882..1d12b6a454 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx @@ -1,7 +1,7 @@ -import { createSelector } from '@reduxjs/toolkit'; +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import InvocationNode from 'features/nodes/components/flow/nodes/Invocation/InvocationNode'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $templates } from 'features/nodes/store/nodesSlice'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; import { memo, useMemo } from 'react'; import type { NodeProps } from 'reactflow'; @@ -11,13 +11,13 @@ import InvocationNodeUnknownFallback from './InvocationNodeUnknownFallback'; const InvocationNodeWrapper = (props: NodeProps) => { const { data, selected } = props; const { id: nodeId, type, isOpen, label } = data; + const templates = useStore($templates); + const hasTemplate = useMemo(() => Boolean(templates[type]), [templates, type]); + const nodeExists = useAppSelector((s) => Boolean(s.nodes.present.nodes.find((n) => n.id === nodeId))); - const hasTemplateSelector = useMemo( - () => createSelector(selectNodesSlice, (nodes) => Boolean(nodes.templates[type])), - [type] - ); - - const hasTemplate = useAppSelector(hasTemplateSelector); + if (!nodeExists) { + return null; + } if (!hasTemplate) { return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx index e02b1a1474..617b6141c8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx @@ -25,10 +25,11 @@ interface Props { kind: 'inputs' | 'outputs'; isMissingInput?: boolean; withTooltip?: boolean; + shouldDim?: boolean; } const EditableFieldTitle = forwardRef((props: Props, ref) => { - const { nodeId, fieldName, kind, isMissingInput = false, withTooltip = false } = props; + const { nodeId, fieldName, kind, isMissingInput = false, withTooltip = false, shouldDim = false } = props; const label = useFieldLabel(nodeId, fieldName); const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind); const { t } = useTranslation(); @@ -37,14 +38,13 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => { const [localTitle, setLocalTitle] = useState(label || fieldTemplateTitle || t('nodes.unknownField')); const handleSubmit = useCallback( - async (newTitle: string) => { - if (newTitle && (newTitle === label || newTitle === fieldTemplateTitle)) { - return; - } - setLocalTitle(newTitle || fieldTemplateTitle || t('nodes.unknownField')); - dispatch(fieldLabelChanged({ nodeId, fieldName, label: newTitle })); + async (newTitleRaw: string) => { + const newTitle = newTitleRaw.trim(); + const finalTitle = newTitle || fieldTemplateTitle || t('nodes.unknownField'); + setLocalTitle(finalTitle); + dispatch(fieldLabelChanged({ nodeId, fieldName, label: finalTitle })); }, - [label, fieldTemplateTitle, dispatch, nodeId, fieldName, t] + [fieldTemplateTitle, dispatch, nodeId, fieldName, t] ); const handleChange = useCallback((newTitle: string) => { @@ -57,33 +57,34 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => { }, [label, fieldTemplateTitle, t]); return ( - : undefined} - openDelay={HANDLE_TOOLTIP_OPEN_DELAY} + - : undefined} + openDelay={HANDLE_TOOLTIP_OPEN_DELAY} > - - - - + + + + ); }); @@ -127,7 +128,15 @@ const EditableControls = memo(() => { } return ( - + ); }); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx index 959b13c2d0..143dee983f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx @@ -2,10 +2,12 @@ import { Tooltip } from '@invoke-ai/ui-library'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor'; import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType'; +import type { ValidationResult } from 'features/nodes/store/util/validateConnection'; import { HANDLE_TOOLTIP_OPEN_DELAY, MODEL_TYPES } from 'features/nodes/types/constants'; -import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field'; +import { type FieldInputTemplate, type FieldOutputTemplate, isSingle } from 'features/nodes/types/field'; import type { CSSProperties } from 'react'; import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import type { HandleType } from 'reactflow'; import { Handle, Position } from 'reactflow'; @@ -14,11 +16,12 @@ type FieldHandleProps = { handleType: HandleType; isConnectionInProgress: boolean; isConnectionStartField: boolean; - connectionError?: string; + validationResult: ValidationResult; }; const FieldHandle = (props: FieldHandleProps) => { - const { fieldTemplate, handleType, isConnectionInProgress, isConnectionStartField, connectionError } = props; + const { fieldTemplate, handleType, isConnectionInProgress, isConnectionStartField, validationResult } = props; + const { t } = useTranslation(); const { name } = fieldTemplate; const type = fieldTemplate.type; const fieldTypeName = useFieldTypeName(type); @@ -26,11 +29,11 @@ const FieldHandle = (props: FieldHandleProps) => { const isModelType = MODEL_TYPES.some((t) => t === type.name); const color = getFieldColor(type); const s: CSSProperties = { - backgroundColor: type.isCollection || type.isCollectionOrScalar ? colorTokenToCssVar('base.900') : color, + backgroundColor: !isSingle(type) ? colorTokenToCssVar('base.900') : color, position: 'absolute', width: '1rem', height: '1rem', - borderWidth: type.isCollection || type.isCollectionOrScalar ? 4 : 0, + borderWidth: !isSingle(type) ? 4 : 0, borderStyle: 'solid', borderColor: color, borderRadius: isModelType ? 4 : '100%', @@ -43,11 +46,11 @@ const FieldHandle = (props: FieldHandleProps) => { s.insetInlineEnd = '-1rem'; } - if (isConnectionInProgress && !isConnectionStartField && connectionError) { + if (isConnectionInProgress && !isConnectionStartField && !validationResult.isValid) { s.filter = 'opacity(0.4) grayscale(0.7)'; } - if (isConnectionInProgress && connectionError) { + if (isConnectionInProgress && !validationResult.isValid) { if (isConnectionStartField) { s.cursor = 'grab'; } else { @@ -58,14 +61,14 @@ const FieldHandle = (props: FieldHandleProps) => { } return s; - }, [connectionError, handleType, isConnectionInProgress, isConnectionStartField, type]); + }, [handleType, isConnectionInProgress, isConnectionStartField, type, validationResult.isValid]); const tooltip = useMemo(() => { - if (isConnectionInProgress && connectionError) { - return connectionError; + if (isConnectionInProgress && validationResult.messageTKey) { + return t(validationResult.messageTKey); } return fieldTypeName; - }, [connectionError, fieldTypeName, isConnectionInProgress]); + }, [fieldTypeName, isConnectionInProgress, t, validationResult.messageTKey]); return ( { - const { t } = useTranslation(); const fieldTemplate = useFieldInputTemplate(nodeId, fieldName); - const fieldInstance = useFieldInputInstance(nodeId, fieldName); const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName); const [isHovered, setIsHovered] = useState(false); - const { isConnected, isConnectionInProgress, isConnectionStartField, connectionError, shouldDim } = + const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } = useConnectionState({ nodeId, fieldName, kind: 'inputs' }); const isMissingInput = useMemo(() => { @@ -55,21 +51,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { setIsHovered(false); }, []); - if (!fieldTemplate || !fieldInstance) { - return ( - - - - {t('nodes.unknownInput', { - name: fieldInstance?.label ?? fieldTemplate?.title ?? fieldName, - })} - - - - ); - } - - if (fieldTemplate.input === 'connection') { + if (fieldTemplate.input === 'connection' || isConnected) { return ( @@ -79,6 +61,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { kind="inputs" isMissingInput={isMissingInput} withTooltip + shouldDim /> @@ -87,7 +70,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { handleType="target" isConnectionInProgress={isConnectionInProgress} isConnectionStartField={isConnectionStartField} - connectionError={connectionError} + validationResult={validationResult} /> ); @@ -95,7 +78,15 @@ const InputField = ({ nodeId, fieldName }: Props) => { return ( - + { handleType="target" isConnectionInProgress={isConnectionInProgress} isConnectionStartField={isConnectionStartField} - connectionError={connectionError} + validationResult={validationResult} /> )} @@ -125,27 +116,3 @@ const InputField = ({ nodeId, fieldName }: Props) => { }; export default memo(InputField); - -type InputFieldWrapperProps = PropsWithChildren<{ - shouldDim: boolean; -}>; - -const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => { - return ( - - {children} - - ); -}); - -InputFieldWrapper.displayName = 'InputFieldWrapper'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx index b6e331c114..99937ceec4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx @@ -1,3 +1,4 @@ +import ModelIdentifierFieldInputComponent from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent'; import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance'; import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate'; import { @@ -23,6 +24,8 @@ import { isLoRAModelFieldInputTemplate, isMainModelFieldInputInstance, isMainModelFieldInputTemplate, + isModelIdentifierFieldInputInstance, + isModelIdentifierFieldInputTemplate, isSchedulerFieldInputInstance, isSchedulerFieldInputTemplate, isSDXLMainModelFieldInputInstance, @@ -95,6 +98,10 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => { return ; } + if (isModelIdentifierFieldInputInstance(fieldInstance) && isModelIdentifierFieldInputTemplate(fieldTemplate)) { + return ; + } + if (isSDXLRefinerModelFieldInputInstance(fieldInstance) && isSDXLRefinerModelFieldInputTemplate(fieldTemplate)) { return ; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx new file mode 100644 index 0000000000..8723538f85 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx @@ -0,0 +1,27 @@ +import { Flex } from '@invoke-ai/ui-library'; +import type { PropsWithChildren } from 'react'; +import { memo } from 'react'; + +type InputFieldWrapperProps = PropsWithChildren<{ + shouldDim: boolean; +}>; + +export const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => { + return ( + + {children} + + ); +}); + +InputFieldWrapper.displayName = 'InputFieldWrapper'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx new file mode 100644 index 0000000000..f4b6be0cd6 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx @@ -0,0 +1,59 @@ +import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectInvocationNode } from 'features/nodes/store/selectors'; +import type { PropsWithChildren } from 'react'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = PropsWithChildren<{ + nodeId: string; + fieldName: string; +}>; + +export const InvocationInputFieldCheck = memo(({ nodeId, fieldName, children }: Props) => { + const { t } = useTranslation(); + const templates = useStore($templates); + const selector = useMemo( + () => + createSelector(selectNodesSlice, (nodesSlice) => { + const node = selectInvocationNode(nodesSlice, nodeId); + const instance = node.data.inputs[fieldName]; + const template = templates[node.data.type]; + const fieldTemplate = template?.inputs[fieldName]; + return { + name: instance?.label || fieldTemplate?.title || fieldName, + hasInstance: Boolean(instance), + hasTemplate: Boolean(fieldTemplate), + }; + }), + [fieldName, nodeId, templates] + ); + const { hasInstance, hasTemplate, name } = useAppSelector(selector); + + if (!hasTemplate || !hasInstance) { + return ( + + + + {t('nodes.unknownInput', { name })} + + + + ); + } + + return children; +}); + +InvocationInputFieldCheck.displayName = 'InvocationInputFieldCheck'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx index 0cd199f7a4..ef466b2882 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx @@ -3,6 +3,7 @@ import { CSS } from '@dnd-kit/utilities'; import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay'; +import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck'; import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue'; import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice'; @@ -20,7 +21,7 @@ type Props = { fieldName: string; }; -const LinearViewField = ({ nodeId, fieldName }: Props) => { +const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => { const dispatch = useAppDispatch(); const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName); const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId); @@ -99,4 +100,12 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => { ); }; +const LinearViewField = ({ nodeId, fieldName }: Props) => { + return ( + + + + ); +}; + export default memo(LinearViewField); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx index f2d776a2da..94e8b62744 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx @@ -18,7 +18,7 @@ const OutputField = ({ nodeId, fieldName }: Props) => { const { t } = useTranslation(); const fieldTemplate = useFieldOutputTemplate(nodeId, fieldName); - const { isConnected, isConnectionInProgress, isConnectionStartField, connectionError, shouldDim } = + const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } = useConnectionState({ nodeId, fieldName, kind: 'outputs' }); if (!fieldTemplate) { @@ -52,7 +52,7 @@ const OutputField = ({ nodeId, fieldName }: Props) => { handleType="source" isConnectionInProgress={isConnectionInProgress} isConnectionStartField={isConnectionStartField} - connectionError={connectionError} + validationResult={validationResult} /> ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent.tsx new file mode 100644 index 0000000000..4019689978 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ModelIdentifierFieldInputComponent.tsx @@ -0,0 +1,66 @@ +import { Combobox, Flex, FormControl } from '@invoke-ai/ui-library'; +import { EMPTY_ARRAY } from 'app/store/constants'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; +import { fieldModelIdentifierValueChanged } from 'features/nodes/store/nodesSlice'; +import type { ModelIdentifierFieldInputInstance, ModelIdentifierFieldInputTemplate } from 'features/nodes/types/field'; +import { memo, useCallback, useMemo } from 'react'; +import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/api/endpoints/models'; +import type { AnyModelConfig } from 'services/api/types'; + +import type { FieldComponentProps } from './types'; + +type Props = FieldComponentProps; + +const ModelIdentifierFieldInputComponent = (props: Props) => { + const { nodeId, field } = props; + const dispatch = useAppDispatch(); + const { data, isLoading } = useGetModelConfigsQuery(); + const _onChange = useCallback( + (value: AnyModelConfig | null) => { + if (!value) { + return; + } + dispatch( + fieldModelIdentifierValueChanged({ + nodeId, + fieldName: field.name, + value, + }) + ); + }, + [dispatch, field.name, nodeId] + ); + + const modelConfigs = useMemo(() => { + if (!data) { + return EMPTY_ARRAY; + } + + return modelConfigsAdapterSelectors.selectAll(data); + }, [data]); + + const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({ + modelConfigs, + onChange: _onChange, + isLoading, + selectedModel: field.value, + groupByType: true, + }); + + return ( + + + + + + ); +}; + +export default memo(ModelIdentifierFieldInputComponent); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx index 966809cb0e..76666af396 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx @@ -48,7 +48,7 @@ const NotesNode = (props: NodeProps) => { gap={1} > -