Compare commits

..

2 Commits

Author SHA1 Message Date
Mary Hipp
caa74f070c lint 2024-05-20 16:07:12 -04:00
Mary Hipp
aca7fb3aad update copy for OOM toast, add reference session ID, dont dismiss toast but also dedupe 2024-05-20 15:52:37 -04:00
140 changed files with 1847 additions and 2551 deletions

View File

@@ -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: 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. 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 [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. 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.
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. 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 #### Using IP-Adapter

View File

@@ -10,7 +10,8 @@ set INVOKEAI_ROOT=.
echo Desired action: echo Desired action:
echo 1. Generate images with the browser-based interface echo 1. Generate images with the browser-based interface
echo 2. Open the developer console echo 2. Open the developer console
echo 3. Command-line help echo 3. Run the InvokeAI image database maintenance script
echo 4. Command-line help
echo Q - Quit echo Q - Quit
echo. echo.
echo To update, download and run the installer from https://github.com/invoke-ai/InvokeAI/releases/latest. echo To update, download and run the installer from https://github.com/invoke-ai/InvokeAI/releases/latest.
@@ -33,6 +34,9 @@ IF /I "%choice%" == "1" (
echo *** Type `exit` to quit this shell and deactivate the Python virtual environment *** echo *** Type `exit` to quit this shell and deactivate the Python virtual environment ***
call cmd /k call cmd /k
) ELSE IF /I "%choice%" == "3" ( ) 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... echo Displaying command line help...
python .venv\Scripts\invokeai-web.exe --help %* python .venv\Scripts\invokeai-web.exe --help %*
pause pause

View File

@@ -47,6 +47,11 @@ do_choice() {
bash --init-file "$file_name" bash --init-file "$file_name"
;; ;;
3) 3)
clear
printf "Running the db maintenance script\n"
invokeai-db-maintenance --root ${INVOKEAI_ROOT}
;;
4)
clear clear
printf "Command-line help\n" printf "Command-line help\n"
invokeai-web --help invokeai-web --help
@@ -66,7 +71,8 @@ do_line_input() {
printf "What would you like to do?\n" printf "What would you like to do?\n"
printf "1: Generate images using the browser-based interface\n" printf "1: Generate images using the browser-based interface\n"
printf "2: Open the developer console\n" printf "2: Open the developer console\n"
printf "3: Command-line help\n" printf "3: Run the InvokeAI image database maintenance script\n"
printf "4: Command-line help\n"
printf "Q: Quit\n\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" 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 read -p "Please enter 1-4, Q: [1] " yn

View File

@@ -29,7 +29,7 @@ from ..services.model_images.model_images_default import ModelImageFileStorageDi
from ..services.model_manager.model_manager_default import ModelManagerService from ..services.model_manager.model_manager_default import ModelManagerService
from ..services.model_records import ModelRecordServiceSQL from ..services.model_records import ModelRecordServiceSQL
from ..services.names.names_default import SimpleNameService from ..services.names.names_default import SimpleNameService
from ..services.session_processor.session_processor_default import DefaultSessionProcessor, DefaultSessionRunner from ..services.session_processor.session_processor_default import DefaultSessionProcessor
from ..services.session_queue.session_queue_sqlite import SqliteSessionQueue from ..services.session_queue.session_queue_sqlite import SqliteSessionQueue
from ..services.urls.urls_default import LocalUrlService from ..services.urls.urls_default import LocalUrlService
from ..services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage from ..services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
@@ -103,8 +103,7 @@ class ApiDependencies:
) )
names = SimpleNameService() names = SimpleNameService()
performance_statistics = InvocationStatsService() performance_statistics = InvocationStatsService()
session_processor = DefaultSessionProcessor()
session_processor = DefaultSessionProcessor(session_runner=DefaultSessionRunner())
session_queue = SqliteSessionQueue(db=db) session_queue = SqliteSessionQueue(db=db)
urls = LocalUrlService() urls = LocalUrlService()
workflow_records = SqliteWorkflowRecordsStorage(db=db) workflow_records = SqliteWorkflowRecordsStorage(db=db)

View File

@@ -69,7 +69,7 @@ async def upload_image(
if isinstance(metadata_raw, str): if isinstance(metadata_raw, str):
_metadata = metadata_raw _metadata = metadata_raw
else: else:
ApiDependencies.invoker.services.logger.debug("Failed to parse metadata for uploaded image") ApiDependencies.invoker.services.logger.warn("Failed to parse metadata for uploaded image")
pass pass
# attempt to parse workflow from image # attempt to parse workflow from image
@@ -77,7 +77,7 @@ async def upload_image(
if isinstance(workflow_raw, str): if isinstance(workflow_raw, str):
_workflow = workflow_raw _workflow = workflow_raw
else: else:
ApiDependencies.invoker.services.logger.debug("Failed to parse workflow for uploaded image") ApiDependencies.invoker.services.logger.warn("Failed to parse workflow for uploaded image")
pass pass
# attempt to extract graph from image # attempt to extract graph from image
@@ -85,7 +85,7 @@ async def upload_image(
if isinstance(graph_raw, str): if isinstance(graph_raw, str):
_graph = graph_raw _graph = graph_raw
else: else:
ApiDependencies.invoker.services.logger.debug("Failed to parse graph for uploaded image") ApiDependencies.invoker.services.logger.warn("Failed to parse graph for uploaded image")
pass pass
try: try:

View File

@@ -203,7 +203,6 @@ async def get_batch_status(
responses={ responses={
200: {"model": SessionQueueItem}, 200: {"model": SessionQueueItem},
}, },
response_model_exclude_none=True,
) )
async def get_queue_item( async def get_queue_item(
queue_id: str = Path(description="The queue id to perform this operation on"), queue_id: str = Path(description="The queue id to perform this operation on"),

View File

@@ -121,8 +121,7 @@ class EventServiceBase:
node: dict, node: dict,
source_node_id: str, source_node_id: str,
error_type: str, error_type: str,
error_message: str, error: str,
error_traceback: str,
user_id: str | None, user_id: str | None,
project_id: str | None, project_id: str | None,
) -> None: ) -> None:
@@ -137,8 +136,7 @@ class EventServiceBase:
"node": node, "node": node,
"source_node_id": source_node_id, "source_node_id": source_node_id,
"error_type": error_type, "error_type": error_type,
"error_message": error_message, "error": error,
"error_traceback": error_traceback,
"user_id": user_id, "user_id": user_id,
"project_id": project_id, "project_id": project_id,
}, },
@@ -259,9 +257,7 @@ class EventServiceBase:
"status": session_queue_item.status, "status": session_queue_item.status,
"batch_id": session_queue_item.batch_id, "batch_id": session_queue_item.batch_id,
"session_id": session_queue_item.session_id, "session_id": session_queue_item.session_id,
"error_type": session_queue_item.error_type, "error": session_queue_item.error,
"error_message": session_queue_item.error_message,
"error_traceback": session_queue_item.error_traceback,
"created_at": str(session_queue_item.created_at) if session_queue_item.created_at else None, "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, "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, "started_at": str(session_queue_item.started_at) if session_queue_item.started_at else None,

View File

@@ -1,49 +1,6 @@
from abc import ABC, abstractmethod 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_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): class SessionProcessorBase(ABC):
@@ -69,85 +26,3 @@ class SessionProcessorBase(ABC):
def get_status(self) -> SessionProcessorStatus: def get_status(self) -> SessionProcessorStatus:
"""Gets the status of the session processor""" """Gets the status of the session processor"""
pass 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.
"""
...

View File

@@ -7,305 +7,21 @@ from typing import Optional
from fastapi_events.handlers.local import local_handler from fastapi_events.handlers.local import local_handler
from fastapi_events.typing import Event as FastAPIEvent from fastapi_events.typing import Event as FastAPIEvent
from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput from invokeai.app.invocations.baseinvocation import BaseInvocation
from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.events.events_base import EventServiceBase
from invokeai.app.services.invocation_stats.invocation_stats_common import GESStatsNotFoundError from invokeai.app.services.invocation_stats.invocation_stats_common import GESStatsNotFoundError
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_processor.session_processor_common import CanceledException
from invokeai.app.services.session_queue.session_queue_common import SessionQueueItem, SessionQueueItemNotFoundError from invokeai.app.services.session_queue.session_queue_common import SessionQueueItem
from invokeai.app.services.shared.graph import NodeInputError
from invokeai.app.services.shared.invocation_context import InvocationContextData, build_invocation_context from invokeai.app.services.shared.invocation_context import InvocationContextData, build_invocation_context
from invokeai.app.util.profiler import Profiler from invokeai.app.util.profiler import Profiler
from ..invoker import Invoker from ..invoker import Invoker
from .session_processor_base import InvocationServices, SessionProcessorBase, SessionRunnerBase from .session_processor_base import SessionProcessorBase
from .session_processor_common import SessionProcessorStatus 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 []
def start(self, services: InvocationServices, cancel_event: ThreadEvent, profiler: Optional[Profiler] = None):
self._services = services
self._cancel_event = cancel_event
self._profiler = profiler
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:
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._cancel_event.is_set():
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.
if queue_item.session.is_complete() or self._cancel_event.is_set():
break
self._on_after_run_session(queue_item=queue_item)
def run_node(self, invocation: BaseInvocation, queue_item: SessionQueueItem):
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,
cancel_event=self._cancel_event,
)
# 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:
# 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_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:
"""Run before a session is executed"""
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:
"""Run after a session is executed"""
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)
# TODO(psyche): This feels jumbled - we should review separation of concerns here.
# Send complete event. The events service will receive this and update the queue item's status.
self._services.events.emit_graph_execution_complete(
queue_batch_id=queue_item.batch_id,
queue_item_id=queue_item.item_id,
queue_id=queue_item.queue_id,
graph_execution_state_id=queue_item.session.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):
"""Run before a node is executed"""
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_batch_id=queue_item.batch_id,
queue_item_id=queue_item.item_id,
queue_id=queue_item.queue_id,
graph_execution_state_id=queue_item.session_id,
node=invocation.model_dump(),
source_node_id=queue_item.session.prepared_source_mapping[invocation.id],
)
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
):
"""Run after a node is executed"""
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(
queue_batch_id=queue_item.batch_id,
queue_item_id=queue_item.item_id,
queue_id=queue_item.queue_id,
graph_execution_state_id=queue_item.session.id,
node=invocation.model_dump(),
source_node_id=queue_item.session.prepared_source_mapping[invocation.id],
result=output.model_dump(),
)
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,
):
"""Run when a node errors"""
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)
# Send error event
self._services.events.emit_invocation_error(
queue_batch_id=queue_item.session_id,
queue_item_id=queue_item.item_id,
queue_id=queue_item.queue_id,
graph_execution_state_id=queue_item.session.id,
node=invocation.model_dump(),
source_node_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),
)
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): class DefaultSessionProcessor(SessionProcessorBase):
def __init__( def start(self, invoker: Invoker, thread_limit: int = 1, polling_interval: int = 1) -> None:
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._invoker: Invoker = invoker
self._queue_item: Optional[SessionQueueItem] = None self._queue_item: Optional[SessionQueueItem] = None
self._invocation: Optional[BaseInvocation] = None self._invocation: Optional[BaseInvocation] = None
@@ -317,7 +33,9 @@ class DefaultSessionProcessor(SessionProcessorBase):
local_handler.register(event_name=EventServiceBase.queue_event, _func=self._on_queue_event) local_handler.register(event_name=EventServiceBase.queue_event, _func=self._on_queue_event)
self._thread_semaphore = BoundedSemaphore(self._thread_limit) self._thread_limit = thread_limit
self._thread_semaphore = BoundedSemaphore(thread_limit)
self._polling_interval = polling_interval
# If profiling is enabled, create a profiler. The same profiler will be used for all sessions. Internally, # 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. # the profiler will create a new profile for each session.
@@ -331,7 +49,6 @@ class DefaultSessionProcessor(SessionProcessorBase):
else None else None
) )
self.session_runner.start(services=invoker.services, cancel_event=self._cancel_event, profiler=self._profiler)
self._thread = Thread( self._thread = Thread(
name="session_processor", name="session_processor",
target=self._process, target=self._process,
@@ -374,7 +91,6 @@ class DefaultSessionProcessor(SessionProcessorBase):
"failed", "failed",
"canceled", "canceled",
]: ]:
self._cancel_event.set()
self._poll_now() self._poll_now()
def resume(self) -> SessionProcessorStatus: def resume(self) -> SessionProcessorStatus:
@@ -400,8 +116,8 @@ class DefaultSessionProcessor(SessionProcessorBase):
resume_event: ThreadEvent, resume_event: ThreadEvent,
cancel_event: ThreadEvent, cancel_event: ThreadEvent,
): ):
# Outermost processor try block; any unhandled exception is a fatal processor error
try: try:
# Any unhandled exception in this block is a fatal processor error and will stop the processor.
self._thread_semaphore.acquire() self._thread_semaphore.acquire()
stop_event.clear() stop_event.clear()
resume_event.set() resume_event.set()
@@ -409,8 +125,8 @@ class DefaultSessionProcessor(SessionProcessorBase):
while not stop_event.is_set(): while not stop_event.is_set():
poll_now_event.clear() poll_now_event.clear()
# Middle processor try block; any unhandled exception is a non-fatal processor error
try: try:
# Any unhandled exception in this block is a nonfatal processor error and will be handled.
# If we are paused, wait for resume event # If we are paused, wait for resume event
resume_event.wait() resume_event.wait()
@@ -426,62 +142,159 @@ class DefaultSessionProcessor(SessionProcessorBase):
self._invoker.services.logger.debug(f"Executing queue item {self._queue_item.item_id}") self._invoker.services.logger.debug(f"Executing queue item {self._queue_item.item_id}")
cancel_event.clear() cancel_event.clear()
# Run the graph # If profiling is enabled, start the profiler
self.session_runner.run(queue_item=self._queue_item) if self._profiler is not None:
self._profiler.start(profile_id=self._queue_item.session_id)
except Exception as e: # Prepare invocations and take the first
error_type = e.__class__.__name__ self._invocation = self._queue_item.session.next()
error_message = str(e)
error_traceback = traceback.format_exc() # Loop over invocations until the session is complete or canceled
self._on_non_fatal_processor_error( while self._invocation is not None and not cancel_event.is_set():
queue_item=self._queue_item, # get the source node id to provide to clients (the prepared node id is not as useful)
error_type=error_type, source_invocation_id = self._queue_item.session.prepared_source_mapping[self._invocation.id]
error_message=error_message,
error_traceback=error_traceback, # Send starting event
self._invoker.services.events.emit_invocation_started(
queue_batch_id=self._queue_item.batch_id,
queue_item_id=self._queue_item.item_id,
queue_id=self._queue_item.queue_id,
graph_execution_state_id=self._queue_item.session_id,
node=self._invocation.model_dump(),
source_node_id=source_invocation_id,
)
# Innermost processor try block; any unhandled exception is an invocation error & will fail the graph
try:
with self._invoker.services.performance_statistics.collect_stats(
self._invocation, self._queue_item.session.id
):
# Build invocation context (the node-facing API)
data = InvocationContextData(
invocation=self._invocation,
source_invocation_id=source_invocation_id,
queue_item=self._queue_item,
)
context = build_invocation_context(
data=data,
services=self._invoker.services,
cancel_event=self._cancel_event,
)
# Invoke the node
outputs = self._invocation.invoke_internal(
context=context, services=self._invoker.services
)
# Save outputs and history
self._queue_item.session.complete(self._invocation.id, outputs)
# Send complete event
self._invoker.services.events.emit_invocation_complete(
queue_batch_id=self._queue_item.batch_id,
queue_item_id=self._queue_item.item_id,
queue_id=self._queue_item.queue_id,
graph_execution_state_id=self._queue_item.session.id,
node=self._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
self._queue_item.session.set_node_error(self._invocation.id, error)
self._invoker.services.logger.error(
f"Error while invoking session {self._queue_item.session_id}, invocation {self._invocation.id} ({self._invocation.get_type()}):\n{e}"
)
self._invoker.services.logger.error(error)
# Send error event
self._invoker.services.events.emit_invocation_error(
queue_batch_id=self._queue_item.session_id,
queue_item_id=self._queue_item.item_id,
queue_id=self._queue_item.queue_id,
graph_execution_state_id=self._queue_item.session.id,
node=self._invocation.model_dump(),
source_node_id=source_invocation_id,
error_type=e.__class__.__name__,
error=error,
user_id=None,
project_id=None,
)
pass
# The session is complete if the all invocations are complete or there was an error
if self._queue_item.session.is_complete() or cancel_event.is_set():
# Send complete event
self._invoker.services.events.emit_graph_execution_complete(
queue_batch_id=self._queue_item.batch_id,
queue_item_id=self._queue_item.item_id,
queue_id=self._queue_item.queue_id,
graph_execution_state_id=self._queue_item.session.id,
)
# 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")
self._invoker.services.performance_statistics.dump_stats(
graph_execution_state_id=self._queue_item.session.id, output_path=stats_path
)
# 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._invoker.services.performance_statistics.log_stats(self._queue_item.session.id)
self._invoker.services.performance_statistics.reset_stats()
# Set the invocation to None to prepare for the next session
self._invocation = None
else:
# Prepare the next invocation
self._invocation = self._queue_item.session.next()
else:
# 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
except Exception:
# Non-fatal error in processor
self._invoker.services.logger.error(
f"Non-fatal error in session processor:\n{traceback.format_exc()}"
) )
# Wait for next polling interval or event to try again # Cancel the queue item
if self._queue_item is not None:
self._invoker.services.session_queue.cancel_queue_item(
self._queue_item.item_id, error=traceback.format_exc()
)
# Reset the invocation to None to prepare for the next session
self._invocation = None
# Immediately poll for next queue item
poll_now_event.wait(self._polling_interval) poll_now_event.wait(self._polling_interval)
continue continue
except Exception as e: except Exception:
# Fatal error in processor, log and pass - we're done here # Fatal error in processor, log and pass - we're done here
error_type = e.__class__.__name__ self._invoker.services.logger.error(f"Fatal Error in session processor:\n{traceback.format_exc()}")
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 pass
finally: finally:
stop_event.clear() stop_event.clear()
poll_now_event.clear() poll_now_event.clear()
self._queue_item = None self._queue_item = None
self._thread_semaphore.release() self._thread_semaphore.release()
def _on_non_fatal_processor_error(
self,
queue_item: Optional[SessionQueueItem],
error_type: str,
error_message: str,
error_traceback: str,
) -> None:
# Non-fatal error in processor
self._invoker.services.logger.error(f"Non-fatal error in session processor {error_type}: {error_message}")
self._invoker.services.logger.error(error_traceback)
if queue_item is not None:
# Update the queue item with the completed session
self._invoker.services.session_queue.set_queue_item_session(queue_item.item_id, queue_item.session)
# Fail the 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,
)
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,
)

View File

@@ -16,7 +16,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
SessionQueueItemDTO, SessionQueueItemDTO,
SessionQueueStatus, SessionQueueStatus,
) )
from invokeai.app.services.shared.graph import GraphExecutionState
from invokeai.app.services.shared.pagination import CursorPaginatedResults from invokeai.app.services.shared.pagination import CursorPaginatedResults
@@ -74,17 +73,10 @@ class SessionQueueBase(ABC):
pass pass
@abstractmethod @abstractmethod
def cancel_queue_item(self, item_id: int) -> SessionQueueItem: def cancel_queue_item(self, item_id: int, error: Optional[str] = None) -> SessionQueueItem:
"""Cancels a session queue item""" """Cancels a session queue item"""
pass 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 @abstractmethod
def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBatchIDsResult: def cancel_by_batch_ids(self, queue_id: str, batch_ids: list[str]) -> CancelByBatchIDsResult:
"""Cancels all queue items with matching batch IDs""" """Cancels all queue items with matching batch IDs"""
@@ -111,8 +103,3 @@ class SessionQueueBase(ABC):
def get_queue_item(self, item_id: int) -> SessionQueueItem: def get_queue_item(self, item_id: int) -> SessionQueueItem:
"""Gets a session queue item by ID""" """Gets a session queue item by ID"""
pass 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

View File

@@ -3,16 +3,7 @@ import json
from itertools import chain, product from itertools import chain, product
from typing import Generator, Iterable, Literal, NamedTuple, Optional, TypeAlias, Union, cast from typing import Generator, Iterable, Literal, NamedTuple, Optional, TypeAlias, Union, cast
from pydantic import ( from pydantic import BaseModel, ConfigDict, Field, StrictStr, TypeAdapter, field_validator, model_validator
AliasChoices,
BaseModel,
ConfigDict,
Field,
StrictStr,
TypeAdapter,
field_validator,
model_validator,
)
from pydantic_core import to_jsonable_python from pydantic_core import to_jsonable_python
from invokeai.app.invocations.baseinvocation import BaseInvocation from invokeai.app.invocations.baseinvocation import BaseInvocation
@@ -198,13 +189,7 @@ class SessionQueueItemWithoutGraph(BaseModel):
session_id: str = Field( 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." 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_type: Optional[str] = Field(default=None, description="The error type if this queue item errored") error: Optional[str] = Field(default=None, description="The error message 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") 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") 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") started_at: Optional[Union[datetime.datetime, str]] = Field(description="When this queue item was started")

View File

@@ -27,7 +27,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
calc_session_count, calc_session_count,
prepare_values_to_insert, 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.pagination import CursorPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
@@ -82,18 +81,10 @@ class SqliteSessionQueue(SessionQueueBase):
async def _handle_error_event(self, event: FastAPIEvent) -> None: async def _handle_error_event(self, event: FastAPIEvent) -> None:
try: try:
item_id = event[1]["data"]["queue_item_id"] item_id = event[1]["data"]["queue_item_id"]
error_type = event[1]["data"]["error_type"] error = event[1]["data"]["error"]
error_message = event[1]["data"]["error_message"]
error_traceback = event[1]["data"]["error_traceback"]
queue_item = self.get_queue_item(item_id) 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 # 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( queue_item = self._set_queue_item_status(item_id=queue_item.item_id, status="failed", error=error)
item_id=queue_item.item_id,
status="failed",
error_type=error_type,
error_message=error_message,
error_traceback=error_traceback,
)
except SessionQueueItemNotFoundError: except SessionQueueItemNotFoundError:
return return
@@ -280,22 +271,17 @@ class SqliteSessionQueue(SessionQueueBase):
return SessionQueueItem.queue_item_from_dict(dict(result)) return SessionQueueItem.queue_item_from_dict(dict(result))
def _set_queue_item_status( def _set_queue_item_status(
self, self, item_id: int, status: QUEUE_ITEM_STATUS, error: Optional[str] = None
item_id: int,
status: QUEUE_ITEM_STATUS,
error_type: Optional[str] = None,
error_message: Optional[str] = None,
error_traceback: Optional[str] = None,
) -> SessionQueueItem: ) -> SessionQueueItem:
try: try:
self.__lock.acquire() self.__lock.acquire()
self.__cursor.execute( self.__cursor.execute(
"""--sql """--sql
UPDATE session_queue UPDATE session_queue
SET status = ?, error_type = ?, error_message = ?, error_traceback = ? SET status = ?, error = ?
WHERE item_id = ? WHERE item_id = ?
""", """,
(status, error_type, error_message, error_traceback, item_id), (status, error, item_id),
) )
self.__conn.commit() self.__conn.commit()
except Exception: except Exception:
@@ -352,6 +338,26 @@ class SqliteSessionQueue(SessionQueueBase):
self.__lock.release() self.__lock.release()
return IsFullResult(is_full=is_full) 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: def clear(self, queue_id: str) -> ClearResult:
try: try:
self.__lock.acquire() self.__lock.acquire()
@@ -418,34 +424,11 @@ class SqliteSessionQueue(SessionQueueBase):
self.__lock.release() self.__lock.release()
return PruneResult(deleted=count) return PruneResult(deleted=count)
def cancel_queue_item(self, item_id: int) -> SessionQueueItem: def cancel_queue_item(self, item_id: int, error: Optional[str] = None) -> SessionQueueItem:
queue_item = self.get_queue_item(item_id) queue_item = self.get_queue_item(item_id)
if queue_item.status not in ["canceled", "failed", "completed"]: if queue_item.status not in ["canceled", "failed", "completed"]:
queue_item = self._set_queue_item_status(item_id=item_id, status="canceled") status = "failed" if error is not None else "canceled"
self.__invoker.services.events.emit_session_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
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,
)
return queue_item
def fail_queue_item(
self,
item_id: int,
error_type: str,
error_message: str,
error_traceback: str,
) -> SessionQueueItem:
queue_item = self.get_queue_item(item_id)
if queue_item.status not in ["canceled", "failed", "completed"]:
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,
)
self.__invoker.services.events.emit_session_canceled( self.__invoker.services.events.emit_session_canceled(
queue_item_id=queue_item.item_id, queue_item_id=queue_item.item_id,
queue_id=queue_item.queue_id, queue_id=queue_item.queue_id,
@@ -579,29 +562,6 @@ class SqliteSessionQueue(SessionQueueBase):
raise SessionQueueItemNotFoundError(f"No queue item with id {item_id}") raise SessionQueueItemNotFoundError(f"No queue item with id {item_id}")
return SessionQueueItem.queue_item_from_dict(dict(result)) 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( def list_queue_items(
self, self,
queue_id: str, queue_id: str,
@@ -618,9 +578,7 @@ class SqliteSessionQueue(SessionQueueBase):
status, status,
priority, priority,
field_values, field_values,
error_type, error,
error_message,
error_traceback,
created_at, created_at,
updated_at, updated_at,
completed_at, completed_at,

View File

@@ -8,7 +8,6 @@ import networkx as nx
from pydantic import ( from pydantic import (
BaseModel, BaseModel,
GetJsonSchemaHandler, GetJsonSchemaHandler,
ValidationError,
field_validator, field_validator,
) )
from pydantic.fields import Field from pydantic.fields import Field
@@ -191,39 +190,6 @@ class UnknownGraphValidationError(ValueError):
pass 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") @invocation_output("iterate_output")
class IterateInvocationOutput(BaseInvocationOutput): class IterateInvocationOutput(BaseInvocationOutput):
"""Used to connect iteration outputs. Will be expanded to a specific output.""" """Used to connect iteration outputs. Will be expanded to a specific output."""
@@ -855,10 +821,7 @@ class GraphExecutionState(BaseModel):
# Get values from edges # Get values from edges
if next_node is not None: if next_node is not None:
try: self._prepare_inputs(next_node)
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 # If next is still none, there's no next node, return None
return next_node return next_node

View File

@@ -12,7 +12,6 @@ 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_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_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_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 from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
@@ -42,7 +41,6 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto
migrator.register_migration(build_migration_7()) migrator.register_migration(build_migration_7())
migrator.register_migration(build_migration_8(app_config=config)) migrator.register_migration(build_migration_8(app_config=config))
migrator.register_migration(build_migration_9()) migrator.register_migration(build_migration_9())
migrator.register_migration(build_migration_10())
migrator.run_migrations() migrator.run_migrations()
return db return db

View File

@@ -1,35 +0,0 @@
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

View File

@@ -2,7 +2,6 @@
"accessibility": { "accessibility": {
"about": "About", "about": "About",
"createIssue": "Create Issue", "createIssue": "Create Issue",
"submitSupportTicket": "Submit Support Ticket",
"invokeProgressBar": "Invoke progress bar", "invokeProgressBar": "Invoke progress bar",
"menu": "Menu", "menu": "Menu",
"mode": "Mode", "mode": "Mode",
@@ -147,9 +146,7 @@
"viewing": "Viewing", "viewing": "Viewing",
"viewingDesc": "Review images in a large gallery view", "viewingDesc": "Review images in a large gallery view",
"editing": "Editing", "editing": "Editing",
"editingDesc": "Edit on the Control Layers canvas", "editingDesc": "Edit on the Control Layers canvas"
"enabled": "Enabled",
"disabled": "Disabled"
}, },
"controlnet": { "controlnet": {
"controlAdapter_one": "Control Adapter", "controlAdapter_one": "Control Adapter",
@@ -900,10 +897,7 @@
"zoomInNodes": "Zoom In", "zoomInNodes": "Zoom In",
"zoomOutNodes": "Zoom Out", "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.", "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": { "parameters": {
"aspect": "Aspect", "aspect": "Aspect",
@@ -1076,9 +1070,8 @@
}, },
"toast": { "toast": {
"addedToBoard": "Added to board", "addedToBoard": "Added to board",
"baseModelChanged": "Base Model Changed", "baseModelChangedCleared_one": "Base model changed, cleared or disabled {{count}} incompatible submodel",
"baseModelChangedCleared_one": "Cleared or disabled {{count}} incompatible submodel", "baseModelChangedCleared_other": "Base model changed, cleared or disabled {{count}} incompatible submodels",
"baseModelChangedCleared_other": "Cleared or disabled {{count}} incompatible submodels",
"canceled": "Processing Canceled", "canceled": "Processing Canceled",
"canvasCopiedClipboard": "Canvas Copied to Clipboard", "canvasCopiedClipboard": "Canvas Copied to Clipboard",
"canvasDownloaded": "Canvas Downloaded", "canvasDownloaded": "Canvas Downloaded",
@@ -1100,16 +1093,11 @@
"modelAddedSimple": "Model Added to Queue", "modelAddedSimple": "Model Added to Queue",
"modelImportCanceled": "Model Import Canceled", "modelImportCanceled": "Model Import Canceled",
"outOfMemoryError": "Out of Memory Error", "outOfMemoryError": "Out of Memory Error",
"outOfMemoryErrorDesc": "Your current generation settings exceed system capacity. Please adjust your settings and try again.", "outOfMemoryDescription": "Your current generation settings exceed system capacity. Please adjust your settings and try again.",
"parameters": "Parameters", "parameters": "Parameters",
"parameterSet": "Parameter Recalled", "parameterNotSet": "{{parameter}} not set",
"parameterSetDesc": "Recalled {{parameter}}", "parameterSet": "{{parameter}} set",
"parameterNotSet": "Parameter Recalled", "parametersNotSet": "Parameters Not Set",
"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", "problemCopyingCanvas": "Problem Copying Canvas",
"problemCopyingCanvasDesc": "Unable to export base layer", "problemCopyingCanvasDesc": "Unable to export base layer",
"problemCopyingImage": "Unable to Copy Image", "problemCopyingImage": "Unable to Copy Image",
@@ -1128,14 +1116,13 @@
"resetInitialImage": "Reset Initial Image", "resetInitialImage": "Reset Initial Image",
"sentToImageToImage": "Sent To Image To Image", "sentToImageToImage": "Sent To Image To Image",
"sentToUnifiedCanvas": "Sent to Unified Canvas", "sentToUnifiedCanvas": "Sent to Unified Canvas",
"sessionReference": "Session Reference",
"serverError": "Server Error", "serverError": "Server Error",
"sessionRef": "Session: {{sessionId}}",
"setAsCanvasInitialImage": "Set as canvas initial image", "setAsCanvasInitialImage": "Set as canvas initial image",
"setCanvasInitialImage": "Set canvas initial image", "setCanvasInitialImage": "Set canvas initial image",
"setControlImage": "Set as control image", "setControlImage": "Set as control image",
"setInitialImage": "Set as initial image", "setInitialImage": "Set as initial image",
"setNodeField": "Set as node field", "setNodeField": "Set as node field",
"somethingWentWrong": "Something Went Wrong",
"uploadFailed": "Upload failed", "uploadFailed": "Upload failed",
"uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image", "uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image",
"uploadInitialImage": "Upload Initial Image", "uploadInitialImage": "Upload Initial Image",
@@ -1575,6 +1562,7 @@
"controlLayers": "Control Layers", "controlLayers": "Control Layers",
"globalMaskOpacity": "Global Mask Opacity", "globalMaskOpacity": "Global Mask Opacity",
"autoNegative": "Auto Negative", "autoNegative": "Auto Negative",
"toggleVisibility": "Toggle Layer Visibility",
"deletePrompt": "Delete Prompt", "deletePrompt": "Delete Prompt",
"resetRegion": "Reset Region", "resetRegion": "Reset Region",
"debugLayers": "Debug Layers", "debugLayers": "Debug Layers",

View File

@@ -382,7 +382,7 @@
"canvasMerged": "Lienzo consolidado", "canvasMerged": "Lienzo consolidado",
"sentToImageToImage": "Enviar hacia Imagen a Imagen", "sentToImageToImage": "Enviar hacia Imagen a Imagen",
"sentToUnifiedCanvas": "Enviar hacia Lienzo Consolidado", "sentToUnifiedCanvas": "Enviar hacia Lienzo Consolidado",
"parametersNotSet": "Parámetros no recuperados", "parametersNotSet": "Parámetros no establecidos",
"metadataLoadFailed": "Error al cargar metadatos", "metadataLoadFailed": "Error al cargar metadatos",
"serverError": "Error en el servidor", "serverError": "Error en el servidor",
"canceled": "Procesando la cancelación", "canceled": "Procesando la cancelación",
@@ -390,8 +390,7 @@
"uploadFailedInvalidUploadDesc": "Debe ser una sola imagen PNG o JPEG", "uploadFailedInvalidUploadDesc": "Debe ser una sola imagen PNG o JPEG",
"parameterSet": "Conjunto de parámetros", "parameterSet": "Conjunto de parámetros",
"parameterNotSet": "Parámetro no configurado", "parameterNotSet": "Parámetro no configurado",
"problemCopyingImage": "No se puede copiar la imagen", "problemCopyingImage": "No se puede copiar la imagen"
"errorCopied": "Error al copiar"
}, },
"tooltip": { "tooltip": {
"feature": { "feature": {

View File

@@ -524,20 +524,7 @@
"missingNodeTemplate": "Modello di nodo mancante", "missingNodeTemplate": "Modello di nodo mancante",
"missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} ingresso 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", "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", "useCpuNoise": "Usa la CPU per generare rumore",
"iterations": "Iterazioni", "iterations": "Iterazioni",
@@ -837,8 +824,8 @@
"unableToUpdateNodes_other": "Impossibile aggiornare {{count}} nodi", "unableToUpdateNodes_other": "Impossibile aggiornare {{count}} nodi",
"addLinearView": "Aggiungi alla vista Lineare", "addLinearView": "Aggiungi alla vista Lineare",
"unknownErrorValidatingWorkflow": "Errore sconosciuto durante la convalida del flusso di lavoro", "unknownErrorValidatingWorkflow": "Errore sconosciuto durante la convalida del flusso di lavoro",
"collectionFieldType": "{{name}} (Raccolta)", "collectionFieldType": "{{name}} Raccolta",
"collectionOrScalarFieldType": "{{name}} (Singola o Raccolta)", "collectionOrScalarFieldType": "{{name}} Raccolta|Scalare",
"nodeVersion": "Versione Nodo", "nodeVersion": "Versione Nodo",
"inputFieldTypeParseError": "Impossibile analizzare il tipo di campo di input {{node}}.{{field}} ({{message}})", "inputFieldTypeParseError": "Impossibile analizzare il tipo di campo di input {{node}}.{{field}} ({{message}})",
"unsupportedArrayItemType": "Tipo di elemento dell'array non supportato \"{{type}}\"", "unsupportedArrayItemType": "Tipo di elemento dell'array non supportato \"{{type}}\"",
@@ -876,13 +863,7 @@
"edit": "Modifica", "edit": "Modifica",
"graph": "Grafico", "graph": "Grafico",
"showEdgeLabelsHelp": "Mostra etichette sui collegamenti, che indicano i nodi collegati", "showEdgeLabelsHelp": "Mostra etichette sui collegamenti, che indicano i nodi collegati",
"showEdgeLabels": "Mostra le etichette del collegamento", "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": { "boards": {
"autoAddBoard": "Aggiungi automaticamente bacheca", "autoAddBoard": "Aggiungi automaticamente bacheca",
@@ -1053,16 +1034,7 @@
"graphFailedToQueue": "Impossibile mettere in coda il grafico", "graphFailedToQueue": "Impossibile mettere in coda il grafico",
"batchFieldValues": "Valori Campi Lotto", "batchFieldValues": "Valori Campi Lotto",
"time": "Tempo", "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": { "models": {
"noMatchingModels": "Nessun modello corrispondente", "noMatchingModels": "Nessun modello corrispondente",
@@ -1591,6 +1563,7 @@
"brushSize": "Dimensioni del pennello", "brushSize": "Dimensioni del pennello",
"globalMaskOpacity": "Opacità globale della maschera", "globalMaskOpacity": "Opacità globale della maschera",
"autoNegative": "Auto Negativo", "autoNegative": "Auto Negativo",
"toggleVisibility": "Attiva/disattiva la visibilità dei livelli",
"deletePrompt": "Cancella il prompt", "deletePrompt": "Cancella il prompt",
"debugLayers": "Debug dei Livelli", "debugLayers": "Debug dei Livelli",
"rectangle": "Rettangolo", "rectangle": "Rettangolo",

View File

@@ -6,7 +6,7 @@
"settingsLabel": "Instellingen", "settingsLabel": "Instellingen",
"img2img": "Afbeelding naar afbeelding", "img2img": "Afbeelding naar afbeelding",
"unifiedCanvas": "Centraal canvas", "unifiedCanvas": "Centraal canvas",
"nodes": "Werkstromen", "nodes": "Werkstroom-editor",
"upload": "Upload", "upload": "Upload",
"load": "Laad", "load": "Laad",
"statusDisconnected": "Niet verbonden", "statusDisconnected": "Niet verbonden",
@@ -34,60 +34,7 @@
"controlNet": "ControlNet", "controlNet": "ControlNet",
"imageFailedToLoad": "Kan afbeelding niet laden", "imageFailedToLoad": "Kan afbeelding niet laden",
"learnMore": "Meer informatie", "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": { "gallery": {
"galleryImageSize": "Afbeeldingsgrootte", "galleryImageSize": "Afbeeldingsgrootte",
@@ -363,41 +310,10 @@
"modelSyncFailed": "Synchronisatie modellen mislukt", "modelSyncFailed": "Synchronisatie modellen mislukt",
"modelDeleteFailed": "Model kon niet verwijderd worden", "modelDeleteFailed": "Model kon niet verwijderd worden",
"convertingModelBegin": "Model aan het converteren. Even geduld.", "convertingModelBegin": "Model aan het converteren. Even geduld.",
"predictionType": "Soort voorspelling", "predictionType": "Soort voorspelling (voor Stable Diffusion 2.x-modellen en incidentele Stable Diffusion 1.x-modellen)",
"advanced": "Uitgebreid", "advanced": "Uitgebreid",
"modelType": "Soort model", "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": { "parameters": {
"images": "Afbeeldingen", "images": "Afbeeldingen",
@@ -437,13 +353,13 @@
"copyImage": "Kopieer afbeelding", "copyImage": "Kopieer afbeelding",
"denoisingStrength": "Sterkte ontruisen", "denoisingStrength": "Sterkte ontruisen",
"scheduler": "Planner", "scheduler": "Planner",
"seamlessXAxis": "Naadloze tegels in x-as", "seamlessXAxis": "X-as",
"seamlessYAxis": "Naadloze tegels in y-as", "seamlessYAxis": "Y-as",
"clipSkip": "Overslaan CLIP", "clipSkip": "Overslaan CLIP",
"negativePromptPlaceholder": "Negatieve prompt", "negativePromptPlaceholder": "Negatieve prompt",
"controlNetControlMode": "Aansturingsmodus", "controlNetControlMode": "Aansturingsmodus",
"positivePromptPlaceholder": "Positieve prompt", "positivePromptPlaceholder": "Positieve prompt",
"maskBlur": "Vervaging van masker", "maskBlur": "Vervaag",
"invoke": { "invoke": {
"noNodesInGraph": "Geen knooppunten in graaf", "noNodesInGraph": "Geen knooppunten in graaf",
"noModelSelected": "Geen model ingesteld", "noModelSelected": "Geen model ingesteld",
@@ -453,25 +369,11 @@
"missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} invoer ontbreekt", "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} invoer ontbreekt",
"noControlImageForControlAdapter": "Controle-adapter #{{number}} heeft geen controle-afbeelding", "noControlImageForControlAdapter": "Controle-adapter #{{number}} heeft geen controle-afbeelding",
"noModelForControlAdapter": "Control-adapter #{{number}} heeft geen model ingesteld staan.", "noModelForControlAdapter": "Control-adapter #{{number}} heeft geen model ingesteld staan.",
"incompatibleBaseModelForControlAdapter": "Model van controle-adapter #{{number}} is niet compatibel met het hoofdmodel.", "incompatibleBaseModelForControlAdapter": "Model van controle-adapter #{{number}} is ongeldig in combinatie met het hoofdmodel.",
"systemDisconnected": "Systeem is niet verbonden", "systemDisconnected": "Systeem is niet verbonden",
"missingNodeTemplate": "Knooppuntsjabloon ontbreekt", "missingNodeTemplate": "Knooppuntsjabloon ontbreekt",
"missingFieldTemplate": "Veldsjabloon 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": { "isAllowedToUpscale": {
"useX2Model": "Afbeelding is te groot om te vergroten met het x4-model. Gebruik hiervoor het x2-model", "useX2Model": "Afbeelding is te groot om te vergroten met het x4-model. Gebruik hiervoor het x2-model",
@@ -481,26 +383,7 @@
"useCpuNoise": "Gebruik CPU-ruis", "useCpuNoise": "Gebruik CPU-ruis",
"imageActions": "Afbeeldingshandeling", "imageActions": "Afbeeldingshandeling",
"iterations": "Iteraties", "iterations": "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": { "settings": {
"models": "Modellen", "models": "Modellen",
@@ -527,12 +410,7 @@
"intermediatesCleared_one": "{{count}} tussentijdse afbeelding gewist", "intermediatesCleared_one": "{{count}} tussentijdse afbeelding gewist",
"intermediatesCleared_other": "{{count}} tussentijdse afbeeldingen gewist", "intermediatesCleared_other": "{{count}} tussentijdse afbeeldingen gewist",
"clearIntermediatesDesc1": "Als je tussentijdse afbeeldingen wist, dan wordt de staat hersteld van je canvas en van ControlNet.", "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": { "toast": {
"uploadFailed": "Upload mislukt", "uploadFailed": "Upload mislukt",
@@ -547,8 +425,8 @@
"connected": "Verbonden met server", "connected": "Verbonden met server",
"canceled": "Verwerking geannuleerd", "canceled": "Verwerking geannuleerd",
"uploadFailedInvalidUploadDesc": "Moet een enkele PNG- of JPEG-afbeelding zijn", "uploadFailedInvalidUploadDesc": "Moet een enkele PNG- of JPEG-afbeelding zijn",
"parameterNotSet": "{{parameter}} niet ingesteld", "parameterNotSet": "Parameter niet ingesteld",
"parameterSet": "{{parameter}} ingesteld", "parameterSet": "Instellen parameters",
"problemCopyingImage": "Kan Afbeelding Niet Kopiëren", "problemCopyingImage": "Kan Afbeelding Niet Kopiëren",
"baseModelChangedCleared_one": "Basismodel is gewijzigd: {{count}} niet-compatibel submodel weggehaald of uitgeschakeld", "baseModelChangedCleared_one": "Basismodel is gewijzigd: {{count}} niet-compatibel submodel weggehaald of uitgeschakeld",
"baseModelChangedCleared_other": "Basismodel is gewijzigd: {{count}} niet-compatibele submodellen weggehaald of uitgeschakeld", "baseModelChangedCleared_other": "Basismodel is gewijzigd: {{count}} niet-compatibele submodellen weggehaald of uitgeschakeld",
@@ -565,11 +443,11 @@
"maskSavedAssets": "Masker bewaard in Assets", "maskSavedAssets": "Masker bewaard in Assets",
"problemDownloadingCanvas": "Fout bij downloaden van canvas", "problemDownloadingCanvas": "Fout bij downloaden van canvas",
"problemMergingCanvas": "Fout bij samenvoegen canvas", "problemMergingCanvas": "Fout bij samenvoegen canvas",
"setCanvasInitialImage": "Initiële canvasafbeelding ingesteld", "setCanvasInitialImage": "Ingesteld als initiële canvasafbeelding",
"imageUploaded": "Afbeelding geüpload", "imageUploaded": "Afbeelding geüpload",
"addedToBoard": "Toegevoegd aan bord", "addedToBoard": "Toegevoegd aan bord",
"workflowLoaded": "Werkstroom geladen", "workflowLoaded": "Werkstroom geladen",
"modelAddedSimple": "Model toegevoegd aan wachtrij", "modelAddedSimple": "Model toegevoegd",
"problemImportingMaskDesc": "Kan masker niet exporteren", "problemImportingMaskDesc": "Kan masker niet exporteren",
"problemCopyingCanvas": "Fout bij kopiëren canvas", "problemCopyingCanvas": "Fout bij kopiëren canvas",
"problemSavingCanvas": "Fout bij bewaren canvas", "problemSavingCanvas": "Fout bij bewaren canvas",
@@ -581,18 +459,7 @@
"maskSentControlnetAssets": "Masker gestuurd naar ControlNet en Assets", "maskSentControlnetAssets": "Masker gestuurd naar ControlNet en Assets",
"canvasSavedGallery": "Canvas bewaard in galerij", "canvasSavedGallery": "Canvas bewaard in galerij",
"imageUploadFailed": "Fout bij uploaden afbeelding", "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": { "tooltip": {
"feature": { "feature": {
@@ -666,11 +533,7 @@
"showOptionsPanel": "Toon zijscherm", "showOptionsPanel": "Toon zijscherm",
"menu": "Menu", "menu": "Menu",
"showGalleryPanel": "Toon deelscherm Galerij", "showGalleryPanel": "Toon deelscherm Galerij",
"loadMore": "Laad meer", "loadMore": "Laad meer"
"about": "Over",
"mode": "Modus",
"resetUI": "$t(accessibility.reset) UI",
"createIssue": "Maak probleem aan"
}, },
"nodes": { "nodes": {
"zoomOutNodes": "Uitzoomen", "zoomOutNodes": "Uitzoomen",
@@ -684,7 +547,7 @@
"loadWorkflow": "Laad werkstroom", "loadWorkflow": "Laad werkstroom",
"downloadWorkflow": "Download JSON van werkstroom", "downloadWorkflow": "Download JSON van werkstroom",
"scheduler": "Planner", "scheduler": "Planner",
"missingTemplate": "Ongeldig knooppunt: knooppunt {{node}} van het soort {{type}} heeft een ontbrekend sjabloon (niet geïnstalleerd?)", "missingTemplate": "Ontbrekende sjabloon",
"workflowDescription": "Korte beschrijving", "workflowDescription": "Korte beschrijving",
"versionUnknown": " Versie onbekend", "versionUnknown": " Versie onbekend",
"noNodeSelected": "Geen knooppunt gekozen", "noNodeSelected": "Geen knooppunt gekozen",
@@ -700,7 +563,7 @@
"integer": "Geheel getal", "integer": "Geheel getal",
"nodeTemplate": "Sjabloon knooppunt", "nodeTemplate": "Sjabloon knooppunt",
"nodeOpacity": "Dekking knooppunt", "nodeOpacity": "Dekking knooppunt",
"unableToLoadWorkflow": "Fout bij laden werkstroom", "unableToLoadWorkflow": "Kan werkstroom niet valideren",
"snapToGrid": "Lijn uit op raster", "snapToGrid": "Lijn uit op raster",
"noFieldsLinearview": "Geen velden toegevoegd aan lineaire weergave", "noFieldsLinearview": "Geen velden toegevoegd aan lineaire weergave",
"nodeSearch": "Zoek naar knooppunten", "nodeSearch": "Zoek naar knooppunten",
@@ -751,56 +614,11 @@
"unknownField": "Onbekend veld", "unknownField": "Onbekend veld",
"colorCodeEdges": "Kleurgecodeerde randen", "colorCodeEdges": "Kleurgecodeerde randen",
"unknownNode": "Onbekend knooppunt", "unknownNode": "Onbekend knooppunt",
"mismatchedVersion": "Ongeldig knooppunt: knooppunt {{node}} van het soort {{type}} heeft een niet-overeenkomende versie (probeer het bij te werken?)", "mismatchedVersion": "Heeft niet-overeenkomende versie",
"addNodeToolTip": "Voeg knooppunt toe (Shift+A, spatie)", "addNodeToolTip": "Voeg knooppunt toe (Shift+A, spatie)",
"loadingNodes": "Bezig met laden van knooppunten...", "loadingNodes": "Bezig met laden van knooppunten...",
"snapToGridHelp": "Lijn knooppunten uit op raster bij verplaatsing", "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": { "controlnet": {
"amult": "a_mult", "amult": "a_mult",
@@ -873,28 +691,9 @@
"canny": "Canny", "canny": "Canny",
"depthZoeDescription": "Genereer diepteblad via Zoe", "depthZoeDescription": "Genereer diepteblad via Zoe",
"hedDescription": "Herkenning van holistisch-geneste randen", "hedDescription": "Herkenning van holistisch-geneste randen",
"setControlImageDimensions": "Kopieer grootte naar B/H (optimaliseer voor model)", "setControlImageDimensions": "Stel afmetingen controle-afbeelding in op B/H",
"scribble": "Krabbel", "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": { "dynamicPrompts": {
"seedBehaviour": { "seedBehaviour": {
@@ -907,10 +706,7 @@
"maxPrompts": "Max. prompts", "maxPrompts": "Max. prompts",
"promptsWithCount_one": "{{count}} prompt", "promptsWithCount_one": "{{count}} prompt",
"promptsWithCount_other": "{{count}} prompts", "promptsWithCount_other": "{{count}} prompts",
"dynamicPrompts": "Dynamische prompts", "dynamicPrompts": "Dynamische prompts"
"showDynamicPrompts": "Toon dynamische prompts",
"loading": "Genereren van dynamische prompts...",
"promptsPreview": "Voorvertoning prompts"
}, },
"popovers": { "popovers": {
"noiseUseCPU": { "noiseUseCPU": {
@@ -923,7 +719,7 @@
}, },
"paramScheduler": { "paramScheduler": {
"paragraphs": [ "paragraphs": [
"De planner gebruikt gedurende het genereringsproces." "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."
], ],
"heading": "Planner" "heading": "Planner"
}, },
@@ -1010,8 +806,8 @@
}, },
"clipSkip": { "clipSkip": {
"paragraphs": [ "paragraphs": [
"Aantal over te slaan CLIP-modellagen.", "Kies hoeveel CLIP-modellagen je wilt overslaan.",
"Bepaalde modellen zijn beter geschikt met bepaalde Overslaan CLIP-instellingen." "Bepaalde modellen werken beter met bepaalde Overslaan CLIP-instellingen."
], ],
"heading": "Overslaan CLIP" "heading": "Overslaan CLIP"
}, },
@@ -1195,26 +991,17 @@
"denoisingStrength": "Sterkte ontruising", "denoisingStrength": "Sterkte ontruising",
"refinermodel": "Verfijningsmodel", "refinermodel": "Verfijningsmodel",
"posAestheticScore": "Positieve esthetische score", "posAestheticScore": "Positieve esthetische score",
"concatPromptStyle": "Koppelen van prompt en stijl", "concatPromptStyle": "Plak prompt- en stijltekst aan elkaar",
"loading": "Bezig met laden...", "loading": "Bezig met laden...",
"steps": "Stappen", "steps": "Stappen",
"posStylePrompt": "Positieve-stijlprompt", "posStylePrompt": "Positieve-stijlprompt"
"freePromptStyle": "Handmatige stijlprompt",
"refinerSteps": "Aantal stappen verfijner"
}, },
"models": { "models": {
"noMatchingModels": "Geen overeenkomend modellen", "noMatchingModels": "Geen overeenkomend modellen",
"loading": "bezig met laden", "loading": "bezig met laden",
"noMatchingLoRAs": "Geen overeenkomende LoRA's", "noMatchingLoRAs": "Geen overeenkomende LoRA's",
"noModelsAvailable": "Geen modellen beschikbaar", "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": { "boards": {
"autoAddBoard": "Voeg automatisch bord toe", "autoAddBoard": "Voeg automatisch bord toe",
@@ -1232,13 +1019,7 @@
"downloadBoard": "Download bord", "downloadBoard": "Download bord",
"changeBoard": "Wijzig bord", "changeBoard": "Wijzig bord",
"loading": "Bezig met laden...", "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": { "invocationCache": {
"disable": "Schakel uit", "disable": "Schakel uit",
@@ -1255,39 +1036,5 @@
"clear": "Wis", "clear": "Wis",
"maxCacheSize": "Max. grootte cache", "maxCacheSize": "Max. grootte cache",
"cacheSize": "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"
} }
} }

View File

@@ -1594,6 +1594,7 @@
"deleteAll": "Удалить всё", "deleteAll": "Удалить всё",
"addLayer": "Добавить слой", "addLayer": "Добавить слой",
"moveToFront": "На передний план", "moveToFront": "На передний план",
"toggleVisibility": "Переключить видимость слоя",
"addPositivePrompt": "Добавить $t(common.positivePrompt)", "addPositivePrompt": "Добавить $t(common.positivePrompt)",
"addIPAdapter": "Добавить $t(common.ipAdapter)", "addIPAdapter": "Добавить $t(common.ipAdapter)",
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)", "regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",

View File

@@ -25,6 +25,7 @@ import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback'; import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
import PreselectedImage from './PreselectedImage'; import PreselectedImage from './PreselectedImage';
import Toaster from './Toaster';
const DEFAULT_CONFIG = {}; const DEFAULT_CONFIG = {};
@@ -95,6 +96,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
<DeleteImageModal /> <DeleteImageModal />
<ChangeBoardModal /> <ChangeBoardModal />
<DynamicPromptsModal /> <DynamicPromptsModal />
<Toaster />
<PreselectedImage selectedImage={selectedImage} /> <PreselectedImage selectedImage={selectedImage} />
</ErrorBoundary> </ErrorBoundary>
); );

View File

@@ -1,8 +1,5 @@
import { Button, Flex, Heading, Image, Link, Text } from '@invoke-ai/ui-library'; import { Button, Flex, Heading, Link, Text, useToast } 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 newGithubIssueUrl from 'new-github-issue-url';
import InvokeLogoYellow from 'public/assets/images/invoke-symbol-ylw-lrg.svg';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiArrowSquareOutBold, PiCopyBold } from 'react-icons/pi'; import { PiArrowCounterClockwiseBold, PiArrowSquareOutBold, PiCopyBold } from 'react-icons/pi';
@@ -14,39 +11,31 @@ type Props = {
}; };
const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => { const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
const toast = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const isLocal = useAppSelector((s) => s.config.isLocal);
const handleCopy = useCallback(() => { const handleCopy = useCallback(() => {
const text = JSON.stringify(serializeError(error), null, 2); const text = JSON.stringify(serializeError(error), null, 2);
navigator.clipboard.writeText(`\`\`\`\n${text}\n\`\`\``); navigator.clipboard.writeText(`\`\`\`\n${text}\n\`\`\``);
toast({ toast({
id: 'ERROR_COPIED', title: 'Error Copied',
title: t('toast.errorCopied'),
}); });
}, [error, t]); }, [error, toast]);
const url = useMemo(() => { const url = useMemo(
if (isLocal) { () =>
return newGithubIssueUrl({ newGithubIssueUrl({
user: 'invoke-ai', user: 'invoke-ai',
repo: 'InvokeAI', repo: 'InvokeAI',
template: 'BUG_REPORT.yml', template: 'BUG_REPORT.yml',
title: `[bug]: ${error.name}: ${error.message}`, title: `[bug]: ${error.name}: ${error.message}`,
}); }),
} else { [error.message, error.name]
return 'https://support.invoke.ai/support/tickets/new'; );
}
}, [error.message, error.name, isLocal]);
return ( return (
<Flex layerStyle="body" w="100vw" h="100vh" alignItems="center" justifyContent="center" p={4}> <Flex layerStyle="body" w="100vw" h="100vh" alignItems="center" justifyContent="center" p={4}>
<Flex layerStyle="first" flexDir="column" borderRadius="base" justifyContent="center" gap={8} p={16}> <Flex layerStyle="first" flexDir="column" borderRadius="base" justifyContent="center" gap={8} p={16}>
<Flex alignItems="center" gap="2"> <Heading>{t('common.somethingWentWrong')}</Heading>
<Image src={InvokeLogoYellow} alt="invoke-logo" w="24px" h="24px" minW="24px" minH="24px" userSelect="none" />
<Heading fontSize="2xl">{t('common.somethingWentWrong')}</Heading>
</Flex>
<Flex <Flex
layerStyle="second" layerStyle="second"
px={8} px={8}
@@ -68,9 +57,7 @@ const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
{t('common.copyError')} {t('common.copyError')}
</Button> </Button>
<Link href={url} isExternal> <Link href={url} isExternal>
<Button leftIcon={<PiArrowSquareOutBold />}> <Button leftIcon={<PiArrowSquareOutBold />}>{t('accessibility.createIssue')}</Button>
{isLocal ? t('accessibility.createIssue') : t('accessibility.submitSupportTicket')}
</Button>
</Link> </Link>
</Flex> </Flex>
</Flex> </Flex>

View File

@@ -0,0 +1,44 @@
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);

View File

@@ -41,10 +41,12 @@ import { addGeneratorProgressEventListener } from 'app/store/middleware/listener
import { addGraphExecutionStateCompleteEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketGraphExecutionStateComplete'; import { addGraphExecutionStateCompleteEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketGraphExecutionStateComplete';
import { addInvocationCompleteEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete'; import { addInvocationCompleteEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete';
import { addInvocationErrorEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError'; 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 { addInvocationStartedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted';
import { addModelInstallEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall'; import { addModelInstallEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall';
import { addModelLoadEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad'; import { addModelLoadEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad';
import { addSocketQueueItemStatusChangedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged'; 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 { addSocketSubscribedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketSubscribed';
import { addSocketUnsubscribedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketUnsubscribed'; import { addSocketUnsubscribedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketUnsubscribed';
import { addStagingAreaImageSavedListener } from 'app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved'; import { addStagingAreaImageSavedListener } from 'app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved';
@@ -112,6 +114,8 @@ addSocketSubscribedEventListener(startAppListening);
addSocketUnsubscribedEventListener(startAppListening); addSocketUnsubscribedEventListener(startAppListening);
addModelLoadEventListener(startAppListening); addModelLoadEventListener(startAppListening);
addModelInstallEventListener(startAppListening); addModelInstallEventListener(startAppListening);
addSessionRetrievalErrorEventListener(startAppListening);
addInvocationRetrievalErrorEventListener(startAppListening);
addSocketQueueItemStatusChangedEventListener(startAppListening); addSocketQueueItemStatusChangedEventListener(startAppListening);
addBulkDownloadListeners(startAppListening); addBulkDownloadListeners(startAppListening);

View File

@@ -8,7 +8,7 @@ import {
resetCanvas, resetCanvas,
setInitialCanvasImage, setInitialCanvasImage,
} from 'features/canvas/store/canvasSlice'; } from 'features/canvas/store/canvasSlice';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { queueApi } from 'services/api/endpoints/queue'; import { queueApi } from 'services/api/endpoints/queue';
@@ -30,20 +30,22 @@ export const addCommitStagingAreaImageListener = (startAppListening: AppStartLis
req.reset(); req.reset();
if (canceled > 0) { if (canceled > 0) {
log.debug(`Canceled ${canceled} canvas batches`); log.debug(`Canceled ${canceled} canvas batches`);
toast({ dispatch(
id: 'CANCEL_BATCH_SUCCEEDED', addToast({
title: t('queue.cancelBatchSucceeded'), title: t('queue.cancelBatchSucceeded'),
status: 'success', status: 'success',
}); })
);
} }
dispatch(canvasBatchIdsReset()); dispatch(canvasBatchIdsReset());
} catch { } catch {
log.error('Failed to cancel canvas batches'); log.error('Failed to cancel canvas batches');
toast({ dispatch(
id: 'CANCEL_BATCH_FAILED', addToast({
title: t('queue.cancelBatchFailed'), title: t('queue.cancelBatchFailed'),
status: 'error', status: 'error',
}); })
);
} }
}, },
}); });

View File

@@ -1,8 +1,8 @@
import { logger } from 'app/logging/logger'; import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize'; import { parseify } from 'common/util/serialize';
import { toast } from 'common/util/toast';
import { zPydanticValidationError } from 'features/system/store/zodSchemas'; import { zPydanticValidationError } from 'features/system/store/zodSchemas';
import { toast } from 'features/toast/toast';
import { t } from 'i18next'; import { t } from 'i18next';
import { truncate, upperFirst } from 'lodash-es'; import { truncate, upperFirst } from 'lodash-es';
import { queueApi } from 'services/api/endpoints/queue'; import { queueApi } from 'services/api/endpoints/queue';
@@ -16,15 +16,18 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
const arg = action.meta.arg.originalArgs; const arg = action.meta.arg.originalArgs;
logger('queue').debug({ enqueueResult: parseify(response) }, 'Batch enqueued'); logger('queue').debug({ enqueueResult: parseify(response) }, 'Batch enqueued');
toast({ if (!toast.isActive('batch-queued')) {
id: 'QUEUE_BATCH_SUCCEEDED', toast({
title: t('queue.batchQueued'), id: 'batch-queued',
status: 'success', title: t('queue.batchQueued'),
description: t('queue.batchQueuedDesc', { description: t('queue.batchQueuedDesc', {
count: response.enqueued, count: response.enqueued,
direction: arg.prepend ? t('queue.front') : t('queue.back'), direction: arg.prepend ? t('queue.front') : t('queue.back'),
}), }),
}); duration: 1000,
status: 'success',
});
}
}, },
}); });
@@ -37,10 +40,9 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
if (!response) { if (!response) {
toast({ toast({
id: 'QUEUE_BATCH_FAILED',
title: t('queue.batchFailedToQueue'), title: t('queue.batchFailedToQueue'),
status: 'error', status: 'error',
description: t('common.unknownError'), description: 'Unknown Error',
}); });
logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue')); logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
return; return;
@@ -50,7 +52,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
if (result.success) { if (result.success) {
result.data.data.detail.map((e) => { result.data.data.detail.map((e) => {
toast({ toast({
id: 'QUEUE_BATCH_FAILED', id: 'batch-failed-to-queue',
title: truncate(upperFirst(e.msg), { length: 128 }), title: truncate(upperFirst(e.msg), { length: 128 }),
status: 'error', status: 'error',
description: truncate( description: truncate(
@@ -62,10 +64,9 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
}); });
} else if (response.status !== 403) { } else if (response.status !== 403) {
toast({ toast({
id: 'QUEUE_BATCH_FAILED',
title: t('queue.batchFailedToQueue'), title: t('queue.batchFailedToQueue'),
status: 'error',
description: t('common.unknownError'), description: t('common.unknownError'),
status: 'error',
}); });
} }
logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue')); logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));

View File

@@ -1,7 +1,8 @@
import type { UseToastOptions } from '@invoke-ai/ui-library';
import { ExternalLink } from '@invoke-ai/ui-library'; import { ExternalLink } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger'; import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { toast } from 'features/toast/toast'; import { toast } from 'common/util/toast';
import { t } from 'i18next'; import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
import { import {
@@ -27,6 +28,7 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) =
// Show the response message if it exists, otherwise show the default message // Show the response message if it exists, otherwise show the default message
description: action.payload.response || t('gallery.bulkDownloadRequestedDesc'), description: action.payload.response || t('gallery.bulkDownloadRequestedDesc'),
duration: null, duration: null,
isClosable: true,
}); });
}, },
}); });
@@ -38,9 +40,9 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) =
// There isn't any toast to update if we get this event. // There isn't any toast to update if we get this event.
toast({ toast({
id: 'BULK_DOWNLOAD_REQUEST_FAILED',
title: t('gallery.bulkDownloadRequestFailed'), title: t('gallery.bulkDownloadRequestFailed'),
status: 'error', status: 'success',
isClosable: true,
}); });
}, },
}); });
@@ -63,7 +65,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 // 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 url = `/api/v1/images/download/${bulk_download_item_name}`;
toast({ const toastOptions: UseToastOptions = {
id: bulk_download_item_name, id: bulk_download_item_name,
title: t('gallery.bulkDownloadReady', 'Download ready'), title: t('gallery.bulkDownloadReady', 'Download ready'),
status: 'success', status: 'success',
@@ -75,7 +77,14 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) =
/> />
), ),
duration: null, duration: null,
}); isClosable: true,
};
if (toast.isActive(bulk_download_item_name)) {
toast.update(bulk_download_item_name, toastOptions);
} else {
toast(toastOptions);
}
}, },
}); });
@@ -86,13 +95,20 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) =
const { bulk_download_item_name } = action.payload.data; const { bulk_download_item_name } = action.payload.data;
toast({ const toastOptions: UseToastOptions = {
id: bulk_download_item_name, id: bulk_download_item_name,
title: t('gallery.bulkDownloadFailed'), title: t('gallery.bulkDownloadFailed'),
status: 'error', status: 'error',
description: action.payload.data.error, description: action.payload.data.error,
duration: null, duration: null,
}); isClosable: true,
};
if (toast.isActive(bulk_download_item_name)) {
toast.update(bulk_download_item_name, toastOptions);
} else {
toast(toastOptions);
}
}, },
}); });
}; };

View File

@@ -2,14 +2,14 @@ import { $logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasCopiedToClipboard } from 'features/canvas/store/actions'; import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice';
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard'; import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
import { toast } from 'features/toast/toast';
import { t } from 'i18next'; import { t } from 'i18next';
export const addCanvasCopiedToClipboardListener = (startAppListening: AppStartListening) => { export const addCanvasCopiedToClipboardListener = (startAppListening: AppStartListening) => {
startAppListening({ startAppListening({
actionCreator: canvasCopiedToClipboard, actionCreator: canvasCopiedToClipboard,
effect: async (action, { getState }) => { effect: async (action, { dispatch, getState }) => {
const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' }); const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' });
const state = getState(); const state = getState();
@@ -19,20 +19,22 @@ export const addCanvasCopiedToClipboardListener = (startAppListening: AppStartLi
copyBlobToClipboard(blob); copyBlobToClipboard(blob);
} catch (err) { } catch (err) {
moduleLog.error(String(err)); moduleLog.error(String(err));
toast({ dispatch(
id: 'CANVAS_COPY_FAILED', addToast({
title: t('toast.problemCopyingCanvas'), title: t('toast.problemCopyingCanvas'),
description: t('toast.problemCopyingCanvasDesc'), description: t('toast.problemCopyingCanvasDesc'),
status: 'error', status: 'error',
}); })
);
return; return;
} }
toast({ dispatch(
id: 'CANVAS_COPY_SUCCEEDED', addToast({
title: t('toast.canvasCopiedClipboard'), title: t('toast.canvasCopiedClipboard'),
status: 'success', status: 'success',
}); })
);
}, },
}); });
}; };

View File

@@ -3,13 +3,13 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { canvasDownloadedAsImage } from 'features/canvas/store/actions'; import { canvasDownloadedAsImage } from 'features/canvas/store/actions';
import { downloadBlob } from 'features/canvas/util/downloadBlob'; import { downloadBlob } from 'features/canvas/util/downloadBlob';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
export const addCanvasDownloadedAsImageListener = (startAppListening: AppStartListening) => { export const addCanvasDownloadedAsImageListener = (startAppListening: AppStartListening) => {
startAppListening({ startAppListening({
actionCreator: canvasDownloadedAsImage, actionCreator: canvasDownloadedAsImage,
effect: async (action, { getState }) => { effect: async (action, { dispatch, getState }) => {
const moduleLog = $logger.get().child({ namespace: 'canvasSavedToGalleryListener' }); const moduleLog = $logger.get().child({ namespace: 'canvasSavedToGalleryListener' });
const state = getState(); const state = getState();
@@ -18,17 +18,18 @@ export const addCanvasDownloadedAsImageListener = (startAppListening: AppStartLi
blob = await getBaseLayerBlob(state); blob = await getBaseLayerBlob(state);
} catch (err) { } catch (err) {
moduleLog.error(String(err)); moduleLog.error(String(err));
toast({ dispatch(
id: 'CANVAS_DOWNLOAD_FAILED', addToast({
title: t('toast.problemDownloadingCanvas'), title: t('toast.problemDownloadingCanvas'),
description: t('toast.problemDownloadingCanvasDesc'), description: t('toast.problemDownloadingCanvasDesc'),
status: 'error', status: 'error',
}); })
);
return; return;
} }
downloadBlob(blob, 'canvas.png'); downloadBlob(blob, 'canvas.png');
toast({ id: 'CANVAS_DOWNLOAD_SUCCEEDED', title: t('toast.canvasDownloaded'), status: 'success' }); dispatch(addToast({ title: t('toast.canvasDownloaded'), status: 'success' }));
}, },
}); });
}; };

View File

@@ -3,7 +3,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { canvasImageToControlAdapter } from 'features/canvas/store/actions'; import { canvasImageToControlAdapter } from 'features/canvas/store/actions';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
@@ -20,12 +20,13 @@ export const addCanvasImageToControlNetListener = (startAppListening: AppStartLi
blob = await getBaseLayerBlob(state, true); blob = await getBaseLayerBlob(state, true);
} catch (err) { } catch (err) {
log.error(String(err)); log.error(String(err));
toast({ dispatch(
id: 'PROBLEM_SAVING_CANVAS', addToast({
title: t('toast.problemSavingCanvas'), title: t('toast.problemSavingCanvas'),
description: t('toast.problemSavingCanvasDesc'), description: t('toast.problemSavingCanvasDesc'),
status: 'error', status: 'error',
}); })
);
return; return;
} }
@@ -42,7 +43,7 @@ export const addCanvasImageToControlNetListener = (startAppListening: AppStartLi
crop_visible: false, crop_visible: false,
postUploadAction: { postUploadAction: {
type: 'TOAST', type: 'TOAST',
title: t('toast.canvasSentControlnetAssets'), toastOptions: { title: t('toast.canvasSentControlnetAssets') },
}, },
}) })
).unwrap(); ).unwrap();

View File

@@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasMaskSavedToGallery } from 'features/canvas/store/actions'; import { canvasMaskSavedToGallery } from 'features/canvas/store/actions';
import { getCanvasData } from 'features/canvas/util/getCanvasData'; import { getCanvasData } from 'features/canvas/util/getCanvasData';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
@@ -29,12 +29,13 @@ export const addCanvasMaskSavedToGalleryListener = (startAppListening: AppStartL
if (!maskBlob) { if (!maskBlob) {
log.error('Problem getting mask layer blob'); log.error('Problem getting mask layer blob');
toast({ dispatch(
id: 'PROBLEM_SAVING_MASK', addToast({
title: t('toast.problemSavingMask'), title: t('toast.problemSavingMask'),
description: t('toast.problemSavingMaskDesc'), description: t('toast.problemSavingMaskDesc'),
status: 'error', status: 'error',
}); })
);
return; return;
} }
@@ -51,7 +52,7 @@ export const addCanvasMaskSavedToGalleryListener = (startAppListening: AppStartL
crop_visible: true, crop_visible: true,
postUploadAction: { postUploadAction: {
type: 'TOAST', type: 'TOAST',
title: t('toast.maskSavedAssets'), toastOptions: { title: t('toast.maskSavedAssets') },
}, },
}) })
); );

View File

@@ -3,7 +3,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { canvasMaskToControlAdapter } from 'features/canvas/store/actions'; import { canvasMaskToControlAdapter } from 'features/canvas/store/actions';
import { getCanvasData } from 'features/canvas/util/getCanvasData'; import { getCanvasData } from 'features/canvas/util/getCanvasData';
import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
@@ -30,12 +30,13 @@ export const addCanvasMaskToControlNetListener = (startAppListening: AppStartLis
if (!maskBlob) { if (!maskBlob) {
log.error('Problem getting mask layer blob'); log.error('Problem getting mask layer blob');
toast({ dispatch(
id: 'PROBLEM_IMPORTING_MASK', addToast({
title: t('toast.problemImportingMask'), title: t('toast.problemImportingMask'),
description: t('toast.problemImportingMaskDesc'), description: t('toast.problemImportingMaskDesc'),
status: 'error', status: 'error',
}); })
);
return; return;
} }
@@ -52,7 +53,7 @@ export const addCanvasMaskToControlNetListener = (startAppListening: AppStartLis
crop_visible: false, crop_visible: false,
postUploadAction: { postUploadAction: {
type: 'TOAST', type: 'TOAST',
title: t('toast.maskSentControlnetAssets'), toastOptions: { title: t('toast.maskSentControlnetAssets') },
}, },
}) })
).unwrap(); ).unwrap();

View File

@@ -4,7 +4,7 @@ import { canvasMerged } from 'features/canvas/store/actions';
import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore'; import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore';
import { setMergedCanvas } from 'features/canvas/store/canvasSlice'; import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob'; import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
@@ -17,12 +17,13 @@ export const addCanvasMergedListener = (startAppListening: AppStartListening) =>
if (!blob) { if (!blob) {
moduleLog.error('Problem getting base layer blob'); moduleLog.error('Problem getting base layer blob');
toast({ dispatch(
id: 'PROBLEM_MERGING_CANVAS', addToast({
title: t('toast.problemMergingCanvas'), title: t('toast.problemMergingCanvas'),
description: t('toast.problemMergingCanvasDesc'), description: t('toast.problemMergingCanvasDesc'),
status: 'error', status: 'error',
}); })
);
return; return;
} }
@@ -30,12 +31,13 @@ export const addCanvasMergedListener = (startAppListening: AppStartListening) =>
if (!canvasBaseLayer) { if (!canvasBaseLayer) {
moduleLog.error('Problem getting canvas base layer'); moduleLog.error('Problem getting canvas base layer');
toast({ dispatch(
id: 'PROBLEM_MERGING_CANVAS', addToast({
title: t('toast.problemMergingCanvas'), title: t('toast.problemMergingCanvas'),
description: t('toast.problemMergingCanvasDesc'), description: t('toast.problemMergingCanvasDesc'),
status: 'error', status: 'error',
}); })
);
return; return;
} }
@@ -52,7 +54,7 @@ export const addCanvasMergedListener = (startAppListening: AppStartListening) =>
is_intermediate: true, is_intermediate: true,
postUploadAction: { postUploadAction: {
type: 'TOAST', type: 'TOAST',
title: t('toast.canvasMerged'), toastOptions: { title: t('toast.canvasMerged') },
}, },
}) })
).unwrap(); ).unwrap();

View File

@@ -3,7 +3,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { parseify } from 'common/util/serialize'; import { parseify } from 'common/util/serialize';
import { canvasSavedToGallery } from 'features/canvas/store/actions'; import { canvasSavedToGallery } from 'features/canvas/store/actions';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob'; import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
@@ -19,12 +19,13 @@ export const addCanvasSavedToGalleryListener = (startAppListening: AppStartListe
blob = await getBaseLayerBlob(state); blob = await getBaseLayerBlob(state);
} catch (err) { } catch (err) {
log.error(String(err)); log.error(String(err));
toast({ dispatch(
id: 'CANVAS_SAVE_FAILED', addToast({
title: t('toast.problemSavingCanvas'), title: t('toast.problemSavingCanvas'),
description: t('toast.problemSavingCanvasDesc'), description: t('toast.problemSavingCanvasDesc'),
status: 'error', status: 'error',
}); })
);
return; return;
} }
@@ -41,7 +42,7 @@ export const addCanvasSavedToGalleryListener = (startAppListening: AppStartListe
crop_visible: true, crop_visible: true,
postUploadAction: { postUploadAction: {
type: 'TOAST', type: 'TOAST',
title: t('toast.canvasSavedGallery'), toastOptions: { title: t('toast.canvasSavedGallery') },
}, },
metadata: { metadata: {
_canvas_objects: parseify(state.canvas.layerState.objects), _canvas_objects: parseify(state.canvas.layerState.objects),

View File

@@ -14,7 +14,7 @@ import {
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
import { isImageOutput } from 'features/nodes/types/common'; import { isImageOutput } from 'features/nodes/types/common';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { getImageDTO } from 'services/api/endpoints/images'; import { getImageDTO } from 'services/api/endpoints/images';
@@ -174,11 +174,12 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
} }
} }
toast({ dispatch(
id: 'GRAPH_QUEUE_FAILED', addToast({
title: t('queue.graphFailedToQueue'), title: t('queue.graphFailedToQueue'),
status: 'error', status: 'error',
}); })
);
} }
} finally { } finally {
req.reset(); req.reset();

View File

@@ -10,7 +10,7 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice'; } from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { isImageOutput } from 'features/nodes/types/common'; import { isImageOutput } from 'features/nodes/types/common';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
import { queueApi } from 'services/api/endpoints/queue'; import { queueApi } from 'services/api/endpoints/queue';
@@ -108,11 +108,12 @@ export const addControlNetImageProcessedListener = (startAppListening: AppStartL
} }
} }
toast({ dispatch(
id: 'GRAPH_QUEUE_FAILED', addToast({
title: t('queue.graphFailedToQueue'), title: t('queue.graphFailedToQueue'),
status: 'error', status: 'error',
}); })
);
} }
}, },
}); });

View File

@@ -1,3 +1,4 @@
import type { UseToastOptions } from '@invoke-ai/ui-library';
import { logger } from 'app/logging/logger'; import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
@@ -13,7 +14,7 @@ import {
} from 'features/controlLayers/store/controlLayersSlice'; } from 'features/controlLayers/store/controlLayersSlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { omit } from 'lodash-es'; import { omit } from 'lodash-es';
import { boardsApi } from 'services/api/endpoints/boards'; import { boardsApi } from 'services/api/endpoints/boards';
@@ -41,17 +42,16 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
return; return;
} }
const DEFAULT_UPLOADED_TOAST = { const DEFAULT_UPLOADED_TOAST: UseToastOptions = {
id: 'IMAGE_UPLOADED',
title: t('toast.imageUploaded'), title: t('toast.imageUploaded'),
status: 'success', status: 'success',
} as const; };
// default action - just upload and alert user // default action - just upload and alert user
if (postUploadAction?.type === 'TOAST') { if (postUploadAction?.type === 'TOAST') {
const { toastOptions } = postUploadAction;
if (!autoAddBoardId || autoAddBoardId === 'none') { if (!autoAddBoardId || autoAddBoardId === 'none') {
const title = postUploadAction.title || DEFAULT_UPLOADED_TOAST.title; dispatch(addToast({ ...DEFAULT_UPLOADED_TOAST, ...toastOptions }));
toast({ ...DEFAULT_UPLOADED_TOAST, title });
} else { } else {
// Add this image to the board // Add this image to the board
dispatch( dispatch(
@@ -70,20 +70,24 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
? `${t('toast.addedToBoard')} ${board.board_name}` ? `${t('toast.addedToBoard')} ${board.board_name}`
: `${t('toast.addedToBoard')} ${autoAddBoardId}`; : `${t('toast.addedToBoard')} ${autoAddBoardId}`;
toast({ dispatch(
...DEFAULT_UPLOADED_TOAST, addToast({
description, ...DEFAULT_UPLOADED_TOAST,
}); description,
})
);
} }
return; return;
} }
if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') { if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') {
dispatch(setInitialCanvasImage(imageDTO, selectOptimalDimension(state))); dispatch(setInitialCanvasImage(imageDTO, selectOptimalDimension(state)));
toast({ dispatch(
...DEFAULT_UPLOADED_TOAST, addToast({
description: t('toast.setAsCanvasInitialImage'), ...DEFAULT_UPLOADED_TOAST,
}); description: t('toast.setAsCanvasInitialImage'),
})
);
return; return;
} }
@@ -101,56 +105,68 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
controlImage: imageDTO.image_name, controlImage: imageDTO.image_name,
}) })
); );
toast({ dispatch(
...DEFAULT_UPLOADED_TOAST, addToast({
description: t('toast.setControlImage'), ...DEFAULT_UPLOADED_TOAST,
}); description: t('toast.setControlImage'),
})
);
return; return;
} }
if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') { if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') {
const { layerId } = postUploadAction; const { layerId } = postUploadAction;
dispatch(caLayerImageChanged({ layerId, imageDTO })); dispatch(caLayerImageChanged({ layerId, imageDTO }));
toast({ dispatch(
...DEFAULT_UPLOADED_TOAST, addToast({
description: t('toast.setControlImage'), ...DEFAULT_UPLOADED_TOAST,
}); description: t('toast.setControlImage'),
})
);
} }
if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') { if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') {
const { layerId } = postUploadAction; const { layerId } = postUploadAction;
dispatch(ipaLayerImageChanged({ layerId, imageDTO })); dispatch(ipaLayerImageChanged({ layerId, imageDTO }));
toast({ dispatch(
...DEFAULT_UPLOADED_TOAST, addToast({
description: t('toast.setControlImage'), ...DEFAULT_UPLOADED_TOAST,
}); description: t('toast.setControlImage'),
})
);
} }
if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') { if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') {
const { layerId, ipAdapterId } = postUploadAction; const { layerId, ipAdapterId } = postUploadAction;
dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
toast({ dispatch(
...DEFAULT_UPLOADED_TOAST, addToast({
description: t('toast.setControlImage'), ...DEFAULT_UPLOADED_TOAST,
}); description: t('toast.setControlImage'),
})
);
} }
if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') { if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') {
const { layerId } = postUploadAction; const { layerId } = postUploadAction;
dispatch(iiLayerImageChanged({ layerId, imageDTO })); dispatch(iiLayerImageChanged({ layerId, imageDTO }));
toast({ dispatch(
...DEFAULT_UPLOADED_TOAST, addToast({
description: t('toast.setControlImage'), ...DEFAULT_UPLOADED_TOAST,
}); description: t('toast.setControlImage'),
})
);
} }
if (postUploadAction?.type === 'SET_NODES_IMAGE') { if (postUploadAction?.type === 'SET_NODES_IMAGE') {
const { nodeId, fieldName } = postUploadAction; const { nodeId, fieldName } = postUploadAction;
dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO })); dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO }));
toast({ dispatch(
...DEFAULT_UPLOADED_TOAST, addToast({
description: `${t('toast.setNodeField')} ${fieldName}`, ...DEFAULT_UPLOADED_TOAST,
}); description: `${t('toast.setNodeField')} ${fieldName}`,
})
);
return; return;
} }
}, },
@@ -158,7 +174,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
startAppListening({ startAppListening({
matcher: imagesApi.endpoints.uploadImage.matchRejected, matcher: imagesApi.endpoints.uploadImage.matchRejected,
effect: (action) => { effect: (action, { dispatch }) => {
const log = logger('images'); const log = logger('images');
const sanitizedData = { const sanitizedData = {
arg: { arg: {
@@ -167,11 +183,13 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
}, },
}; };
log.error({ ...sanitizedData }, 'Image upload failed'); log.error({ ...sanitizedData }, 'Image upload failed');
toast({ dispatch(
title: t('toast.imageUploadFailed'), addToast({
description: action.error.message, title: t('toast.imageUploadFailed'),
status: 'error', description: action.error.message,
}); status: 'error',
})
);
}, },
}); });
}; };

View File

@@ -8,7 +8,8 @@ import { loraRemoved } from 'features/lora/store/loraSlice';
import { modelSelected } from 'features/parameters/store/actions'; import { modelSelected } from 'features/parameters/store/actions';
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice'; import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
import { zParameterModel } from 'features/parameters/types/parameterSchemas'; import { zParameterModel } from 'features/parameters/types/parameterSchemas';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next'; import { t } from 'i18next';
import { forEach } from 'lodash-es'; import { forEach } from 'lodash-es';
@@ -59,14 +60,16 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
}); });
if (modelsCleared > 0) { if (modelsCleared > 0) {
toast({ dispatch(
id: 'BASE_MODEL_CHANGED', addToast(
title: t('toast.baseModelChanged'), makeToast({
description: t('toast.baseModelChangedCleared', { title: t('toast.baseModelChangedCleared', {
count: modelsCleared, count: modelsCleared,
}), }),
status: 'warning', status: 'warning',
}); })
)
);
} }
} }

View File

@@ -19,7 +19,8 @@ import {
isParameterWidth, isParameterWidth,
zParameterVAEModel, zParameterVAEModel,
} from 'features/parameters/types/parameterSchemas'; } from 'features/parameters/types/parameterSchemas';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next'; import { t } from 'i18next';
import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models'; import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models';
import { isNonRefinerMainModelConfig } from 'services/api/types'; import { isNonRefinerMainModelConfig } from 'services/api/types';
@@ -108,7 +109,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
} }
} }
toast({ id: 'PARAMETER_SET', title: t('toast.parameterSet', { parameter: 'Default settings' }) }); dispatch(addToast(makeToast({ title: t('toast.parameterSet', { parameter: 'Default settings' }) })));
} }
}, },
}); });

View File

@@ -3,70 +3,24 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { deepClone } from 'common/util/deepClone'; import { deepClone } from 'common/util/deepClone';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
import { zNodeStatus } from 'features/nodes/types/invocation'; import { zNodeStatus } from 'features/nodes/types/invocation';
import { toast } from 'features/toast/toast';
import ToastWithSessionRefDescription from 'features/toast/ToastWithSessionRefDescription';
import { t } from 'i18next';
import { startCase } from 'lodash-es';
import { socketInvocationError } from 'services/events/actions'; import { socketInvocationError } from 'services/events/actions';
const log = logger('socketio'); const log = logger('socketio');
const getTitle = (errorType: string) => {
if (errorType === 'OutOfMemoryError') {
return t('toast.outOfMemoryError');
}
return t('toast.serverError');
};
const getDescription = (errorType: string, sessionId: string, isLocal?: boolean) => {
if (!isLocal) {
if (errorType === 'OutOfMemoryError') {
return ToastWithSessionRefDescription({
message: t('toast.outOfMemoryDescription'),
sessionId,
});
}
return ToastWithSessionRefDescription({
message: errorType,
sessionId,
});
}
return errorType;
};
export const addInvocationErrorEventListener = (startAppListening: AppStartListening) => { export const addInvocationErrorEventListener = (startAppListening: AppStartListening) => {
startAppListening({ startAppListening({
actionCreator: socketInvocationError, actionCreator: socketInvocationError,
effect: (action, { getState }) => { effect: (action) => {
log.error(action.payload, `Invocation error (${action.payload.data.node.type})`); log.error(action.payload, `Invocation error (${action.payload.data.node.type})`);
const { source_node_id, error_type, error_message, error_traceback, graph_execution_state_id } = const { source_node_id } = action.payload.data;
action.payload.data;
const nes = deepClone($nodeExecutionStates.get()[source_node_id]); const nes = deepClone($nodeExecutionStates.get()[source_node_id]);
if (nes) { if (nes) {
nes.status = zNodeStatus.enum.FAILED; nes.status = zNodeStatus.enum.FAILED;
nes.error = action.payload.data.error;
nes.progress = null; nes.progress = null;
nes.progressImage = null; nes.progressImage = null;
nes.error = {
error_type,
error_message,
error_traceback,
};
upsertExecutionState(nes.nodeId, nes); upsertExecutionState(nes.nodeId, nes);
} }
const errorType = startCase(error_type);
const sessionId = graph_execution_state_id;
const { isLocal } = getState().config;
toast({
id: `INVOCATION_ERROR_${errorType}`,
title: getTitle(errorType),
status: 'error',
duration: null,
description: getDescription(errorType, sessionId, isLocal),
updateDescription: isLocal ? true : false,
});
}, },
}); });
}; };

View File

@@ -0,0 +1,14 @@
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})`);
},
});
};

View File

@@ -43,15 +43,20 @@ export const addSocketQueueItemStatusChangedEventListener = (startAppListening:
queueApi.util.updateQueryData('getBatchStatus', { batch_id: batch_status.batch_id }, () => batch_status) 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 // 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 // TODO: technically, we could possibly update the current session queue item, but feels safer to just request it again
dispatch( dispatch(
queueApi.util.invalidateTags([ queueApi.util.invalidateTags(['CurrentSessionQueueItem', 'NextSessionQueueItem', 'InvocationCacheStatus'])
'CurrentSessionQueueItem',
'NextSessionQueueItem',
'InvocationCacheStatus',
{ type: 'SessionQueueItem', id: queue_item.item_id },
])
); );
if (['in_progress'].includes(action.payload.data.queue_item.status)) { if (['in_progress'].includes(action.payload.data.queue_item.status)) {

View File

@@ -0,0 +1,14 @@
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})`);
},
});
};

View File

@@ -1,6 +1,6 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { stagingAreaImageSaved } from 'features/canvas/store/actions'; import { stagingAreaImageSaved } from 'features/canvas/store/actions';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
@@ -29,14 +29,15 @@ export const addStagingAreaImageSavedListener = (startAppListening: AppStartList
}) })
); );
} }
toast({ id: 'IMAGE_SAVED', title: t('toast.imageSaved'), status: 'success' }); dispatch(addToast({ title: t('toast.imageSaved'), status: 'success' }));
} catch (error) { } catch (error) {
toast({ dispatch(
id: 'IMAGE_SAVE_FAILED', addToast({
title: t('toast.imageSavingFailed'), title: t('toast.imageSavingFailed'),
description: (error as Error)?.message, description: (error as Error)?.message,
status: 'error', status: 'error',
}); })
);
} }
}, },
}); });

View File

@@ -5,7 +5,8 @@ import { $templates, nodesChanged } from 'features/nodes/store/nodesSlice';
import { NodeUpdateError } from 'features/nodes/types/error'; import { NodeUpdateError } from 'features/nodes/types/error';
import { isInvocationNode } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation';
import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate'; import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next'; import { t } from 'i18next';
export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartListening) => { export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartListening) => {
@@ -49,18 +50,24 @@ export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartLi
count: unableToUpdateCount, count: unableToUpdateCount,
}) })
); );
toast({ dispatch(
id: 'UNABLE_TO_UPDATE_NODES', addToast(
title: t('nodes.unableToUpdateNodes', { makeToast({
count: unableToUpdateCount, title: t('nodes.unableToUpdateNodes', {
}), count: unableToUpdateCount,
}); }),
})
)
);
} else { } else {
toast({ dispatch(
id: 'ALL_NODES_UPDATED', addToast(
title: t('nodes.allNodesUpdated'), makeToast({
status: 'success', title: t('nodes.allNodesUpdated'),
}); status: 'success',
})
)
);
} }
}, },
}); });

View File

@@ -4,7 +4,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { parseify } from 'common/util/serialize'; import { parseify } from 'common/util/serialize';
import { buildAdHocUpscaleGraph } from 'features/nodes/util/graph/buildAdHocUpscaleGraph'; import { buildAdHocUpscaleGraph } from 'features/nodes/util/graph/buildAdHocUpscaleGraph';
import { createIsAllowedToUpscaleSelector } from 'features/parameters/hooks/useIsAllowedToUpscale'; import { createIsAllowedToUpscaleSelector } from 'features/parameters/hooks/useIsAllowedToUpscale';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { t } from 'i18next'; import { t } from 'i18next';
import { queueApi } from 'services/api/endpoints/queue'; import { queueApi } from 'services/api/endpoints/queue';
import type { BatchConfig, ImageDTO } from 'services/api/types'; import type { BatchConfig, ImageDTO } from 'services/api/types';
@@ -29,11 +29,12 @@ export const addUpscaleRequestedListener = (startAppListening: AppStartListening
{ imageDTO }, { imageDTO },
t(detailTKey ?? 'parameters.isAllowedToUpscale.tooLarge') // should never coalesce t(detailTKey ?? 'parameters.isAllowedToUpscale.tooLarge') // should never coalesce
); );
toast({ dispatch(
id: 'NOT_ALLOWED_TO_UPSCALE', addToast({
title: t(detailTKey ?? 'parameters.isAllowedToUpscale.tooLarge'), // should never coalesce title: t(detailTKey ?? 'parameters.isAllowedToUpscale.tooLarge'), // should never coalesce
status: 'error', status: 'error',
}); })
);
return; return;
} }
@@ -64,11 +65,12 @@ export const addUpscaleRequestedListener = (startAppListening: AppStartListening
if (error instanceof Object && 'status' in error && error.status === 403) { if (error instanceof Object && 'status' in error && error.status === 403) {
return; return;
} else { } else {
toast({ dispatch(
id: 'GRAPH_QUEUE_FAILED', addToast({
title: t('queue.graphFailedToQueue'), title: t('queue.graphFailedToQueue'),
status: 'error', status: 'error',
}); })
);
} }
} }
}, },

View File

@@ -8,23 +8,23 @@ import type { Templates } from 'features/nodes/store/types';
import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error'; import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error';
import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow'; import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow';
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow'; import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next'; import { t } from 'i18next';
import { checkBoardAccess, checkImageAccess, checkModelAccess } from 'services/api/hooks/accessChecks';
import type { GraphAndWorkflowResponse, NonNullableGraph } from 'services/api/types'; import type { GraphAndWorkflowResponse, NonNullableGraph } from 'services/api/types';
import { z } from 'zod'; import { z } from 'zod';
import { fromZodError } from 'zod-validation-error'; import { fromZodError } from 'zod-validation-error';
const getWorkflow = async (data: GraphAndWorkflowResponse, templates: Templates) => { const getWorkflow = (data: GraphAndWorkflowResponse, templates: Templates) => {
if (data.workflow) { if (data.workflow) {
// Prefer to load the workflow if it's available - it has more information // Prefer to load the workflow if it's available - it has more information
const parsed = JSON.parse(data.workflow); const parsed = JSON.parse(data.workflow);
return await validateWorkflow(parsed, templates, checkImageAccess, checkBoardAccess, checkModelAccess); return validateWorkflow(parsed, templates);
} else if (data.graph) { } else if (data.graph) {
// Else we fall back on the graph, using the graphToWorkflow function to convert and do layout // Else we fall back on the graph, using the graphToWorkflow function to convert and do layout
const parsed = JSON.parse(data.graph); const parsed = JSON.parse(data.graph);
const workflow = graphToWorkflow(parsed as NonNullableGraph, true); const workflow = graphToWorkflow(parsed as NonNullableGraph, true);
return await validateWorkflow(workflow, templates, checkImageAccess, checkBoardAccess, checkModelAccess); return validateWorkflow(workflow, templates);
} else { } else {
throw new Error('No workflow or graph provided'); throw new Error('No workflow or graph provided');
} }
@@ -33,13 +33,13 @@ const getWorkflow = async (data: GraphAndWorkflowResponse, templates: Templates)
export const addWorkflowLoadRequestedListener = (startAppListening: AppStartListening) => { export const addWorkflowLoadRequestedListener = (startAppListening: AppStartListening) => {
startAppListening({ startAppListening({
actionCreator: workflowLoadRequested, actionCreator: workflowLoadRequested,
effect: async (action, { dispatch }) => { effect: (action, { dispatch }) => {
const log = logger('nodes'); const log = logger('nodes');
const { data, asCopy } = action.payload; const { data, asCopy } = action.payload;
const nodeTemplates = $templates.get(); const nodeTemplates = $templates.get();
try { try {
const { workflow, warnings } = await getWorkflow(data, nodeTemplates); const { workflow, warnings } = getWorkflow(data, nodeTemplates);
if (asCopy) { if (asCopy) {
// If we're loading a copy, we need to remove the ID so that the backend will create a new workflow // If we're loading a copy, we need to remove the ID so that the backend will create a new workflow
@@ -48,18 +48,23 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
dispatch(workflowLoaded(workflow)); dispatch(workflowLoaded(workflow));
if (!warnings.length) { if (!warnings.length) {
toast({ dispatch(
id: 'WORKFLOW_LOADED', addToast(
title: t('toast.workflowLoaded'), makeToast({
status: 'success', title: t('toast.workflowLoaded'),
}); status: 'success',
})
)
);
} else { } else {
toast({ dispatch(
id: 'WORKFLOW_LOADED', addToast(
title: t('toast.loadedWithWarnings'), makeToast({
status: 'warning', title: t('toast.loadedWithWarnings'),
}); status: 'warning',
})
)
);
warnings.forEach(({ message, ...rest }) => { warnings.forEach(({ message, ...rest }) => {
log.warn(rest, message); log.warn(rest, message);
}); });
@@ -72,42 +77,54 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
if (e instanceof WorkflowVersionError) { if (e instanceof WorkflowVersionError) {
// The workflow version was not recognized in the valid list of versions // The workflow version was not recognized in the valid list of versions
log.error({ error: parseify(e) }, e.message); log.error({ error: parseify(e) }, e.message);
toast({ dispatch(
id: 'UNABLE_TO_VALIDATE_WORKFLOW', addToast(
title: t('nodes.unableToValidateWorkflow'), makeToast({
status: 'error', title: t('nodes.unableToValidateWorkflow'),
description: e.message, status: 'error',
}); description: e.message,
})
)
);
} else if (e instanceof WorkflowMigrationError) { } else if (e instanceof WorkflowMigrationError) {
// There was a problem migrating the workflow to the latest version // There was a problem migrating the workflow to the latest version
log.error({ error: parseify(e) }, e.message); log.error({ error: parseify(e) }, e.message);
toast({ dispatch(
id: 'UNABLE_TO_VALIDATE_WORKFLOW', addToast(
title: t('nodes.unableToValidateWorkflow'), makeToast({
status: 'error', title: t('nodes.unableToValidateWorkflow'),
description: e.message, status: 'error',
}); description: e.message,
})
)
);
} else if (e instanceof z.ZodError) { } else if (e instanceof z.ZodError) {
// There was a problem validating the workflow itself // There was a problem validating the workflow itself
const { message } = fromZodError(e, { const { message } = fromZodError(e, {
prefix: t('nodes.workflowValidation'), prefix: t('nodes.workflowValidation'),
}); });
log.error({ error: parseify(e) }, message); log.error({ error: parseify(e) }, message);
toast({ dispatch(
id: 'UNABLE_TO_VALIDATE_WORKFLOW', addToast(
title: t('nodes.unableToValidateWorkflow'), makeToast({
status: 'error', title: t('nodes.unableToValidateWorkflow'),
description: message, status: 'error',
}); description: message,
})
)
);
} else { } else {
// Some other error occurred // Some other error occurred
log.error({ error: parseify(e) }, t('nodes.unknownErrorValidatingWorkflow')); log.error({ error: parseify(e) }, t('nodes.unknownErrorValidatingWorkflow'));
toast({ dispatch(
id: 'UNABLE_TO_VALIDATE_WORKFLOW', addToast(
title: t('nodes.unableToValidateWorkflow'), makeToast({
status: 'error', title: t('nodes.unableToValidateWorkflow'),
description: t('nodes.unknownErrorValidatingWorkflow'), status: 'error',
}); description: t('nodes.unknownErrorValidatingWorkflow'),
})
)
);
} }
} }
}, },

View File

@@ -74,7 +74,6 @@ export type AppConfig = {
maxUpscalePixels?: number; maxUpscalePixels?: number;
metadataFetchDebounce?: number; metadataFetchDebounce?: number;
workflowFetchDebounce?: number; workflowFetchDebounce?: number;
isLocal?: boolean;
sd: { sd: {
defaultModel?: string; defaultModel?: string;
disabledControlNetModels: string[]; disabledControlNetModels: string[];

View File

@@ -1,10 +1,11 @@
import { useAppToaster } from 'app/components/Toaster';
import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob'; import { useImageUrlToBlob } from 'common/hooks/useImageUrlToBlob';
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard'; import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const useCopyImageToClipboard = () => { export const useCopyImageToClipboard = () => {
const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
const imageUrlToBlob = useImageUrlToBlob(); const imageUrlToBlob = useImageUrlToBlob();
@@ -15,11 +16,12 @@ export const useCopyImageToClipboard = () => {
const copyImageToClipboard = useCallback( const copyImageToClipboard = useCallback(
async (image_url: string) => { async (image_url: string) => {
if (!isClipboardAPIAvailable) { if (!isClipboardAPIAvailable) {
toast({ toaster({
id: 'PROBLEM_COPYING_IMAGE',
title: t('toast.problemCopyingImage'), title: t('toast.problemCopyingImage'),
description: "Your browser doesn't support the Clipboard API.", description: "Your browser doesn't support the Clipboard API.",
status: 'error', status: 'error',
duration: 2500,
isClosable: true,
}); });
} }
try { try {
@@ -31,21 +33,23 @@ export const useCopyImageToClipboard = () => {
copyBlobToClipboard(blob); copyBlobToClipboard(blob);
toast({ toaster({
id: 'IMAGE_COPIED',
title: t('toast.imageCopied'), title: t('toast.imageCopied'),
status: 'success', status: 'success',
duration: 2500,
isClosable: true,
}); });
} catch (err) { } catch (err) {
toast({ toaster({
id: 'PROBLEM_COPYING_IMAGE',
title: t('toast.problemCopyingImage'), title: t('toast.problemCopyingImage'),
description: String(err), description: String(err),
status: 'error', status: 'error',
duration: 2500,
isClosable: true,
}); });
} }
}, },
[imageUrlToBlob, isClipboardAPIAvailable, t] [imageUrlToBlob, isClipboardAPIAvailable, t, toaster]
); );
return { isClipboardAPIAvailable, copyImageToClipboard }; return { isClipboardAPIAvailable, copyImageToClipboard };

View File

@@ -1,12 +1,13 @@
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { useAppToaster } from 'app/components/Toaster';
import { $authToken } from 'app/store/nanostores/authToken'; import { $authToken } from 'app/store/nanostores/authToken';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { imageDownloaded } from 'features/gallery/store/actions'; import { imageDownloaded } from 'features/gallery/store/actions';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export const useDownloadImage = () => { export const useDownloadImage = () => {
const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const authToken = useStore($authToken); const authToken = useStore($authToken);
@@ -36,15 +37,16 @@ export const useDownloadImage = () => {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
dispatch(imageDownloaded()); dispatch(imageDownloaded());
} catch (err) { } catch (err) {
toast({ toaster({
id: 'PROBLEM_DOWNLOADING_IMAGE',
title: t('toast.problemDownloadingImage'), title: t('toast.problemDownloadingImage'),
description: String(err), description: String(err),
status: 'error', status: 'error',
duration: 2500,
isClosable: true,
}); });
} }
}, },
[t, dispatch, authToken] [t, toaster, dispatch, authToken]
); );
return { downloadImage }; return { downloadImage };

View File

@@ -1,6 +1,6 @@
import { useAppToaster } from 'app/components/Toaster';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { toast } from 'features/toast/toast';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import type { Accept, FileRejection } from 'react-dropzone'; import type { Accept, FileRejection } from 'react-dropzone';
@@ -26,6 +26,7 @@ const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (ac
export const useFullscreenDropzone = () => { export const useFullscreenDropzone = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const toaster = useAppToaster();
const postUploadAction = useAppSelector(selectPostUploadAction); const postUploadAction = useAppSelector(selectPostUploadAction);
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false); const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
@@ -36,14 +37,13 @@ export const useFullscreenDropzone = () => {
(rejection: FileRejection) => { (rejection: FileRejection) => {
setIsHandlingUpload(true); setIsHandlingUpload(true);
toast({ toaster({
id: 'UPLOAD_FAILED',
title: t('toast.uploadFailed'), title: t('toast.uploadFailed'),
description: rejection.errors.map((error) => error.message).join('\n'), description: rejection.errors.map((error) => error.message).join('\n'),
status: 'error', status: 'error',
}); });
}, },
[t] [t, toaster]
); );
const fileAcceptedCallback = useCallback( const fileAcceptedCallback = useCallback(
@@ -62,8 +62,7 @@ export const useFullscreenDropzone = () => {
const onDrop = useCallback( const onDrop = useCallback(
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => { (acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
if (fileRejections.length > 1) { if (fileRejections.length > 1) {
toast({ toaster({
id: 'UPLOAD_FAILED',
title: t('toast.uploadFailed'), title: t('toast.uploadFailed'),
description: t('toast.uploadFailedInvalidUploadDesc'), description: t('toast.uploadFailedInvalidUploadDesc'),
status: 'error', status: 'error',
@@ -79,7 +78,7 @@ export const useFullscreenDropzone = () => {
fileAcceptedCallback(file); fileAcceptedCallback(file);
}); });
}, },
[t, fileAcceptedCallback, fileRejectionCallback] [t, toaster, fileAcceptedCallback, fileRejectionCallback]
); );
const onDragOver = useCallback(() => { const onDragOver = useCallback(() => {

View File

@@ -0,0 +1,6 @@
import { createStandaloneToast, theme, TOAST_OPTIONS } from '@invoke-ai/ui-library';
export const { toast } = createStandaloneToast({
theme: theme,
defaultOptions: TOAST_OPTIONS.defaultOptions,
});

View File

@@ -4,7 +4,7 @@ import { CALayerControlAdapterWrapper } from 'features/controlLayers/components/
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { layerSelected, selectCALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; import { layerSelected, selectCALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
@@ -26,7 +26,7 @@ export const CALayer = memo(({ layerId }: Props) => {
return ( return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}> <LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}> <Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerIsEnabledToggle layerId={layerId} /> <LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="control_adapter_layer" /> <LayerTitle type="control_adapter_layer" />
<Spacer /> <Spacer />
<CALayerOpacity layerId={layerId} /> <CALayerOpacity layerId={layerId} />

View File

@@ -5,7 +5,7 @@ import { InitialImagePreview } from 'features/controlLayers/components/IILayer/I
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { import {
iiLayerDenoisingStrengthChanged, iiLayerDenoisingStrengthChanged,
@@ -66,7 +66,7 @@ export const IILayer = memo(({ layerId }: Props) => {
return ( return (
<LayerWrapper onClick={onClick} borderColor={layer.isSelected ? 'base.400' : 'base.800'}> <LayerWrapper onClick={onClick} borderColor={layer.isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}> <Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerIsEnabledToggle layerId={layerId} /> <LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="initial_image_layer" /> <LayerTitle type="initial_image_layer" />
<Spacer /> <Spacer />
<IILayerOpacity layerId={layerId} /> <IILayerOpacity layerId={layerId} />

View File

@@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper'; import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { layerSelected, selectIPALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice'; import { layerSelected, selectIPALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
@@ -22,7 +22,7 @@ export const IPALayer = memo(({ layerId }: Props) => {
return ( return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}> <LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}> <Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerIsEnabledToggle layerId={layerId} /> <LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="ip_adapter_layer" /> <LayerTitle type="ip_adapter_layer" />
<Spacer /> <Spacer />
<LayerDeleteButton layerId={layerId} /> <LayerDeleteButton layerId={layerId} />

View File

@@ -1,8 +1,8 @@
import { IconButton } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation'; import { stopPropagation } from 'common/util/stopPropagation';
import { useLayerIsEnabled } from 'features/controlLayers/hooks/layerStateHooks'; import { useLayerIsVisible } from 'features/controlLayers/hooks/layerStateHooks';
import { layerIsEnabledToggled } from 'features/controlLayers/store/controlLayersSlice'; import { layerVisibilityToggled } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiCheckBold } from 'react-icons/pi'; import { PiCheckBold } from 'react-icons/pi';
@@ -11,21 +11,21 @@ type Props = {
layerId: string; layerId: string;
}; };
export const LayerIsEnabledToggle = memo(({ layerId }: Props) => { export const LayerVisibilityToggle = memo(({ layerId }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isEnabled = useLayerIsEnabled(layerId); const isVisible = useLayerIsVisible(layerId);
const onClick = useCallback(() => { const onClick = useCallback(() => {
dispatch(layerIsEnabledToggled(layerId)); dispatch(layerVisibilityToggled(layerId));
}, [dispatch, layerId]); }, [dispatch, layerId]);
return ( return (
<IconButton <IconButton
size="sm" size="sm"
aria-label={t(isEnabled ? 'common.enabled' : 'common.disabled')} aria-label={t('controlLayers.toggleVisibility')}
tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')} tooltip={t('controlLayers.toggleVisibility')}
variant="outline" variant="outline"
icon={isEnabled ? <PiCheckBold /> : undefined} icon={isVisible ? <PiCheckBold /> : undefined}
onClick={onClick} onClick={onClick}
colorScheme="base" colorScheme="base"
onDoubleClick={stopPropagation} // double click expands the layer onDoubleClick={stopPropagation} // double click expands the layer
@@ -33,4 +33,4 @@ export const LayerIsEnabledToggle = memo(({ layerId }: Props) => {
); );
}); });
LayerIsEnabledToggle.displayName = 'LayerVisibilityToggle'; LayerVisibilityToggle.displayName = 'LayerVisibilityToggle';

View File

@@ -6,7 +6,7 @@ import { AddPromptButtons } from 'features/controlLayers/components/AddPromptBut
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton'; import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu'; import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { import {
isRegionalGuidanceLayer, isRegionalGuidanceLayer,
@@ -55,7 +55,7 @@ export const RGLayer = memo(({ layerId }: Props) => {
return ( return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? color : 'base.800'}> <LayerWrapper onClick={onClick} borderColor={isSelected ? color : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}> <Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<LayerIsEnabledToggle layerId={layerId} /> <LayerVisibilityToggle layerId={layerId} />
<LayerTitle type="regional_guidance_layer" /> <LayerTitle type="regional_guidance_layer" />
<Spacer /> <Spacer />
{autoNegative === 'invert' && ( {autoNegative === 'invert' && (

View File

@@ -45,6 +45,7 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => {
variant="darkFilled" variant="darkFilled"
paddingRight={30} paddingRight={30}
fontSize="sm" fontSize="sm"
spellCheck={false}
/> />
<PromptOverlayButtonWrapper> <PromptOverlayButtonWrapper>
<RGLayerPromptDeleteButton layerId={layerId} polarity="negative" /> <RGLayerPromptDeleteButton layerId={layerId} polarity="negative" />

View File

@@ -45,6 +45,7 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => {
variant="darkFilled" variant="darkFilled"
paddingRight={30} paddingRight={30}
minH={28} minH={28}
spellCheck={false}
/> />
<PromptOverlayButtonWrapper> <PromptOverlayButtonWrapper>
<RGLayerPromptDeleteButton layerId={layerId} polarity="positive" /> <RGLayerPromptDeleteButton layerId={layerId} polarity="positive" />

View File

@@ -39,7 +39,7 @@ export const useLayerNegativePrompt = (layerId: string) => {
return prompt; return prompt;
}; };
export const useLayerIsEnabled = (layerId: string) => { export const useLayerIsVisible = (layerId: string) => {
const selectLayer = useMemo( const selectLayer = useMemo(
() => () =>
createSelector(selectControlLayersSlice, (controlLayers) => { createSelector(selectControlLayersSlice, (controlLayers) => {

View File

@@ -139,7 +139,7 @@ export const controlLayersSlice = createSlice({
layerSelected: (state, action: PayloadAction<string>) => { layerSelected: (state, action: PayloadAction<string>) => {
exclusivelySelectLayer(state, action.payload); exclusivelySelectLayer(state, action.payload);
}, },
layerIsEnabledToggled: (state, action: PayloadAction<string>) => { layerVisibilityToggled: (state, action: PayloadAction<string>) => {
const layer = state.layers.find((l) => l.id === action.payload); const layer = state.layers.find((l) => l.id === action.payload);
if (layer) { if (layer) {
layer.isEnabled = !layer.isEnabled; layer.isEnabled = !layer.isEnabled;
@@ -791,7 +791,7 @@ class LayerColors {
export const { export const {
// Any Layer Type // Any Layer Type
layerSelected, layerSelected,
layerIsEnabledToggled, layerVisibilityToggled,
layerTranslated, layerTranslated,
layerBboxChanged, layerBboxChanged,
layerReset, layerReset,

View File

@@ -1,5 +1,6 @@
import { Flex, MenuDivider, MenuItem, Spinner } from '@invoke-ai/ui-library'; import { Flex, MenuDivider, MenuItem, Spinner } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { useAppToaster } from 'app/components/Toaster';
import { $customStarUI } from 'app/store/nanostores/customStarUI'; import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard'; import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
@@ -13,7 +14,6 @@ import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/ac
import { $templates } from 'features/nodes/store/nodesSlice'; import { $templates } from 'features/nodes/store/nodesSlice';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice'; import { setActiveTab } from 'features/ui/store/uiSlice';
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow'; import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
import { size } from 'lodash-es'; import { size } from 'lodash-es';
@@ -46,6 +46,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const optimalDimension = useAppSelector(selectOptimalDimension); const optimalDimension = useAppSelector(selectOptimalDimension);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const toaster = useAppToaster();
const isCanvasEnabled = useFeatureStatus('canvas'); const isCanvasEnabled = useFeatureStatus('canvas');
const customStarUi = useStore($customStarUI); const customStarUi = useStore($customStarUI);
const { downloadImage } = useDownloadImage(); const { downloadImage } = useDownloadImage();
@@ -85,12 +86,13 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
}); });
dispatch(setInitialCanvasImage(imageDTO, optimalDimension)); dispatch(setInitialCanvasImage(imageDTO, optimalDimension));
toast({ toaster({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToUnifiedCanvas'), title: t('toast.sentToUnifiedCanvas'),
status: 'success', status: 'success',
duration: 2500,
isClosable: true,
}); });
}, [dispatch, imageDTO, t, optimalDimension]); }, [dispatch, imageDTO, t, toaster, optimalDimension]);
const handleChangeBoard = useCallback(() => { const handleChangeBoard = useCallback(() => {
dispatch(imagesToChangeSelected([imageDTO])); dispatch(imagesToChangeSelected([imageDTO]));

View File

@@ -1,5 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query'; import { skipToken } from '@reduxjs/toolkit/query';
import { useAppToaster } from 'app/components/Toaster';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; import { setShouldShowImageDetails } from 'features/ui/store/uiSlice';
@@ -13,6 +14,7 @@ export const ToggleMetadataViewerButton = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails); const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
const lastSelectedImage = useAppSelector(selectLastSelectedImage); const lastSelectedImage = useAppSelector(selectLastSelectedImage);
const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken); const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
@@ -22,7 +24,7 @@ export const ToggleMetadataViewerButton = memo(() => {
[dispatch, shouldShowImageDetails] [dispatch, shouldShowImageDetails]
); );
useHotkeys('i', toggleMetadataViewer, { enabled: Boolean(imageDTO) }, [imageDTO, shouldShowImageDetails]); useHotkeys('i', toggleMetadataViewer, { enabled: Boolean(imageDTO) }, [imageDTO, shouldShowImageDetails, toaster]);
return ( return (
<IconButton <IconButton

View File

@@ -53,7 +53,7 @@ export const useImageActions = (image_name?: string) => {
const recallSeed = useCallback(() => { const recallSeed = useCallback(() => {
handlers.seed.parse(metadata).then((seed) => { handlers.seed.parse(metadata).then((seed) => {
handlers.seed.recall && handlers.seed.recall(seed, true); handlers.seed.recall && handlers.seed.recall(seed);
}); });
}, [metadata]); }, [metadata]);

View File

@@ -1,4 +1,5 @@
import { objectKeys } from 'common/util/objectKeys'; import { objectKeys } from 'common/util/objectKeys';
import { toast } from 'common/util/toast';
import type { Layer } from 'features/controlLayers/store/types'; import type { Layer } from 'features/controlLayers/store/types';
import type { LoRA } from 'features/lora/store/loraSlice'; import type { LoRA } from 'features/lora/store/loraSlice';
import type { import type {
@@ -14,7 +15,6 @@ import type {
import { fetchModelConfig } from 'features/metadata/util/modelFetchingHelpers'; import { fetchModelConfig } from 'features/metadata/util/modelFetchingHelpers';
import { validators } from 'features/metadata/util/validators'; import { validators } from 'features/metadata/util/validators';
import type { ModelIdentifierField } from 'features/nodes/types/common'; import type { ModelIdentifierField } from 'features/nodes/types/common';
import { toast } from 'features/toast/toast';
import { t } from 'i18next'; import { t } from 'i18next';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
@@ -89,23 +89,23 @@ const renderLayersValue: MetadataRenderValueFunc<Layer[]> = async (layers) => {
return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`; return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`;
}; };
const parameterSetToast = (parameter: string) => { const parameterSetToast = (parameter: string, description?: string) => {
toast({ toast({
id: 'PARAMETER_SET', title: t('toast.parameterSet', { parameter }),
title: t('toast.parameterSet'), description,
description: t('toast.parameterSetDesc', { parameter }),
status: 'info', status: 'info',
duration: 2500,
isClosable: true,
}); });
}; };
const parameterNotSetToast = (parameter: string, message?: string) => { const parameterNotSetToast = (parameter: string, description?: string) => {
toast({ toast({
id: 'PARAMETER_NOT_SET', title: t('toast.parameterNotSet', { parameter }),
title: t('toast.parameterNotSet'), description,
description: message
? t('toast.parameterNotSetDescWithMessage', { parameter, message })
: t('toast.parameterNotSetDesc', { parameter }),
status: 'warning', status: 'warning',
duration: 2500,
isClosable: true,
}); });
}; };
@@ -458,18 +458,7 @@ export const parseAndRecallAllMetadata = async (
}); });
}) })
); );
if (results.some((result) => result.status === 'fulfilled')) { if (results.some((result) => result.status === 'fulfilled')) {
toast({ parameterSetToast(t('toast.parameters'));
id: 'PARAMETER_SET',
title: t('toast.parametersSet'),
status: 'info',
});
} else {
toast({
id: 'PARAMETER_SET',
title: t('toast.parametersNotSet'),
status: 'warning',
});
} }
}; };

View File

@@ -1,48 +0,0 @@
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;
};

View File

@@ -17,11 +17,7 @@ export const useStarterModelsToast = () => {
useEffect(() => { useEffect(() => {
if (toast.isActive(TOAST_ID)) { if (toast.isActive(TOAST_ID)) {
if (mainModels.length === 0) { return;
return;
} else {
toast.close(TOAST_ID);
}
} }
if (data && mainModels.length === 0 && !didToast && isEnabled) { if (data && mainModels.length === 0 && !didToast && isEnabled) {
toast({ toast({

View File

@@ -1,9 +1,11 @@
import { Button, Flex, FormControl, FormErrorMessage, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library'; import { Button, Flex, FormControl, FormErrorMessage, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library';
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel'; import { useAppDispatch } from 'app/store/storeHooks';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import type { ChangeEventHandler } from 'react'; import type { ChangeEventHandler } from 'react';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models'; import { useInstallModelMutation, useLazyGetHuggingFaceModelsQuery } from 'services/api/endpoints/models';
import { HuggingFaceResults } from './HuggingFaceResults'; import { HuggingFaceResults } from './HuggingFaceResults';
@@ -12,19 +14,50 @@ export const HuggingFaceForm = () => {
const [displayResults, setDisplayResults] = useState(false); const [displayResults, setDisplayResults] = useState(false);
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const [_getHuggingFaceModels, { isLoading, data }] = useLazyGetHuggingFaceModelsQuery(); const [_getHuggingFaceModels, { isLoading, data }] = useLazyGetHuggingFaceModelsQuery();
const [installModel] = useInstallModel(); 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 getModels = useCallback(async () => { const getModels = useCallback(async () => {
_getHuggingFaceModels(huggingFaceRepo) _getHuggingFaceModels(huggingFaceRepo)
.unwrap() .unwrap()
.then((response) => { .then((response) => {
if (response.is_diffusers) { if (response.is_diffusers) {
installModel({ source: huggingFaceRepo }); handleInstallModel(huggingFaceRepo);
setDisplayResults(false); setDisplayResults(false);
} else if (response.urls?.length === 1 && response.urls[0]) { } else if (response.urls?.length === 1 && response.urls[0]) {
installModel({ source: response.urls[0] }); handleInstallModel(response.urls[0]);
setDisplayResults(false); setDisplayResults(false);
} else { } else {
setDisplayResults(true); setDisplayResults(true);
@@ -33,7 +66,7 @@ export const HuggingFaceForm = () => {
.catch((error) => { .catch((error) => {
setErrorMessage(error.data.detail || ''); setErrorMessage(error.data.detail || '');
}); });
}, [_getHuggingFaceModels, installModel, huggingFaceRepo]); }, [_getHuggingFaceModels, handleInstallModel, huggingFaceRepo]);
const handleSetHuggingFaceRepo: ChangeEventHandler<HTMLInputElement> = useCallback((e) => { const handleSetHuggingFaceRepo: ChangeEventHandler<HTMLInputElement> = useCallback((e) => {
setHuggingFaceRepo(e.target.value); setHuggingFaceRepo(e.target.value);

View File

@@ -1,20 +1,47 @@
import { Flex, IconButton, Text } from '@invoke-ai/ui-library'; import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel'; import { useAppDispatch } from 'app/store/storeHooks';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi'; import { PiPlusBold } from 'react-icons/pi';
import { useInstallModelMutation } from 'services/api/endpoints/models';
type Props = { type Props = {
result: string; result: string;
}; };
export const HuggingFaceResultItem = ({ result }: Props) => { export const HuggingFaceResultItem = ({ result }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const [installModel] = useInstallModel(); const [installModel] = useInstallModelMutation();
const onClick = useCallback(() => { const handleInstall = useCallback(() => {
installModel({ source: result }); installModel({ source: result })
}, [installModel, 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]);
return ( return (
<Flex alignItems="center" justifyContent="space-between" w="100%" gap={3}> <Flex alignItems="center" justifyContent="space-between" w="100%" gap={3}>
@@ -24,7 +51,7 @@ export const HuggingFaceResultItem = ({ result }: Props) => {
{result} {result}
</Text> </Text>
</Flex> </Flex>
<IconButton aria-label={t('modelManager.install')} icon={<PiPlusBold />} onClick={onClick} size="sm" /> <IconButton aria-label={t('modelManager.install')} icon={<PiPlusBold />} onClick={handleInstall} size="sm" />
</Flex> </Flex>
); );
}; };

View File

@@ -8,12 +8,15 @@ import {
InputGroup, InputGroup,
InputRightElement, InputRightElement,
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import type { ChangeEventHandler } from 'react'; import type { ChangeEventHandler } from 'react';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi'; import { PiXBold } from 'react-icons/pi';
import { useInstallModelMutation } from 'services/api/endpoints/models';
import { HuggingFaceResultItem } from './HuggingFaceResultItem'; import { HuggingFaceResultItem } from './HuggingFaceResultItem';
@@ -24,8 +27,9 @@ type HuggingFaceResultsProps = {
export const HuggingFaceResults = ({ results }: HuggingFaceResultsProps) => { export const HuggingFaceResults = ({ results }: HuggingFaceResultsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const dispatch = useAppDispatch();
const [installModel] = useInstallModel(); const [installModel] = useInstallModelMutation();
const filteredResults = useMemo(() => { const filteredResults = useMemo(() => {
return results.filter((result) => { return results.filter((result) => {
@@ -42,11 +46,34 @@ export const HuggingFaceResults = ({ results }: HuggingFaceResultsProps) => {
setSearchTerm(''); setSearchTerm('');
}, []); }, []);
const onClickAddAll = useCallback(() => { const handleAddAll = useCallback(() => {
for (const result of filteredResults) { for (const result of filteredResults) {
installModel({ source: result }); 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',
})
)
);
}
});
} }
}, [filteredResults, installModel]); }, [filteredResults, installModel, dispatch, t]);
return ( return (
<> <>
@@ -55,7 +82,7 @@ export const HuggingFaceResults = ({ results }: HuggingFaceResultsProps) => {
<Flex justifyContent="space-between" alignItems="center"> <Flex justifyContent="space-between" alignItems="center">
<Heading size="sm">{t('modelManager.availableModels')}</Heading> <Heading size="sm">{t('modelManager.availableModels')}</Heading>
<Flex alignItems="center" gap={3}> <Flex alignItems="center" gap={3}>
<Button size="sm" onClick={onClickAddAll} isDisabled={results.length === 0} flexShrink={0}> <Button size="sm" onClick={handleAddAll} isDisabled={results.length === 0} flexShrink={0}>
{t('modelManager.installAll')} {t('modelManager.installAll')}
</Button> </Button>
<InputGroup w={64} size="xs"> <InputGroup w={64} size="xs">

View File

@@ -1,9 +1,12 @@
import { Button, Checkbox, Flex, FormControl, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library'; import { Button, Checkbox, Flex, FormControl, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library';
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel'; import { useAppDispatch } from 'app/store/storeHooks';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next'; import { t } from 'i18next';
import { useCallback } from 'react'; import { useCallback } from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useInstallModelMutation } from 'services/api/endpoints/models';
type SimpleImportModelConfig = { type SimpleImportModelConfig = {
location: string; location: string;
@@ -11,7 +14,9 @@ type SimpleImportModelConfig = {
}; };
export const InstallModelForm = () => { export const InstallModelForm = () => {
const [installModel, { isLoading }] = useInstallModel(); const dispatch = useAppDispatch();
const [installModel, { isLoading }] = useInstallModelMutation();
const { register, handleSubmit, formState, reset } = useForm<SimpleImportModelConfig>({ const { register, handleSubmit, formState, reset } = useForm<SimpleImportModelConfig>({
defaultValues: { defaultValues: {
@@ -21,22 +26,40 @@ export const InstallModelForm = () => {
mode: 'onChange', mode: 'onChange',
}); });
const resetForm = useCallback(() => reset(undefined, { keepValues: true }), [reset]);
const onSubmit = useCallback<SubmitHandler<SimpleImportModelConfig>>( const onSubmit = useCallback<SubmitHandler<SimpleImportModelConfig>>(
(values) => { (values) => {
if (!values?.location) { if (!values?.location) {
return; return;
} }
installModel({ installModel({ source: values.location, inplace: values.inplace })
source: values.location, .unwrap()
inplace: values.inplace, .then((_) => {
onSuccess: resetForm, dispatch(
onError: resetForm, 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, resetForm] [dispatch, reset, installModel]
); );
return ( return (

View File

@@ -1,6 +1,8 @@
import { Box, Button, Flex, Heading } from '@invoke-ai/ui-library'; import { Box, Button, Flex, Heading } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next'; import { t } from 'i18next';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useListModelInstallsQuery, usePruneCompletedModelInstallsMutation } from 'services/api/endpoints/models'; import { useListModelInstallsQuery, usePruneCompletedModelInstallsMutation } from 'services/api/endpoints/models';
@@ -8,6 +10,8 @@ import { useListModelInstallsQuery, usePruneCompletedModelInstallsMutation } fro
import { ModelInstallQueueItem } from './ModelInstallQueueItem'; import { ModelInstallQueueItem } from './ModelInstallQueueItem';
export const ModelInstallQueue = () => { export const ModelInstallQueue = () => {
const dispatch = useAppDispatch();
const { data } = useListModelInstallsQuery(); const { data } = useListModelInstallsQuery();
const [_pruneCompletedModelInstalls] = usePruneCompletedModelInstallsMutation(); const [_pruneCompletedModelInstalls] = usePruneCompletedModelInstallsMutation();
@@ -16,22 +20,28 @@ export const ModelInstallQueue = () => {
_pruneCompletedModelInstalls() _pruneCompletedModelInstalls()
.unwrap() .unwrap()
.then((_) => { .then((_) => {
toast({ dispatch(
id: 'MODEL_INSTALL_QUEUE_PRUNED', addToast(
title: t('toast.prunedQueue'), makeToast({
status: 'success', title: t('toast.prunedQueue'),
}); status: 'success',
})
)
);
}) })
.catch((error) => { .catch((error) => {
if (error) { if (error) {
toast({ dispatch(
id: 'MODEL_INSTALL_QUEUE_PRUNE_FAILED', addToast(
title: `${error.data.detail} `, makeToast({
status: 'error', title: `${error.data.detail} `,
}); status: 'error',
})
)
);
} }
}); });
}, [_pruneCompletedModelInstalls]); }, [_pruneCompletedModelInstalls, dispatch]);
const pruneAvailable = useMemo(() => { const pruneAvailable = useMemo(() => {
return data?.some( return data?.some(

View File

@@ -1,5 +1,7 @@
import { Flex, IconButton, Progress, Text, Tooltip } from '@invoke-ai/ui-library'; import { Flex, IconButton, Progress, Text, Tooltip } from '@invoke-ai/ui-library';
import { toast } from 'features/toast/toast'; import { useAppDispatch } from 'app/store/storeHooks';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next'; import { t } from 'i18next';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
@@ -27,6 +29,7 @@ const formatBytes = (bytes: number) => {
export const ModelInstallQueueItem = (props: ModelListItemProps) => { export const ModelInstallQueueItem = (props: ModelListItemProps) => {
const { installJob } = props; const { installJob } = props;
const dispatch = useAppDispatch();
const [deleteImportModel] = useCancelModelInstallMutation(); const [deleteImportModel] = useCancelModelInstallMutation();
@@ -34,22 +37,28 @@ export const ModelInstallQueueItem = (props: ModelListItemProps) => {
deleteImportModel(installJob.id) deleteImportModel(installJob.id)
.unwrap() .unwrap()
.then((_) => { .then((_) => {
toast({ dispatch(
id: 'MODEL_INSTALL_CANCELED', addToast(
title: t('toast.modelImportCanceled'), makeToast({
status: 'success', title: t('toast.modelImportCanceled'),
}); status: 'success',
})
)
);
}) })
.catch((error) => { .catch((error) => {
if (error) { if (error) {
toast({ dispatch(
id: 'MODEL_INSTALL_CANCEL_FAILED', addToast(
title: `${error.data.detail} `, makeToast({
status: 'error', title: `${error.data.detail} `,
}); status: 'error',
})
)
);
} }
}); });
}, [deleteImportModel, installJob]); }, [deleteImportModel, installJob, dispatch]);
const sourceLocation = useMemo(() => { const sourceLocation = useMemo(() => {
switch (installJob.source.type) { switch (installJob.source.type) {

View File

@@ -11,13 +11,15 @@ import {
InputGroup, InputGroup,
InputRightElement, InputRightElement,
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import type { ChangeEvent, ChangeEventHandler } from 'react'; import type { ChangeEvent, ChangeEventHandler } from 'react';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi'; import { PiXBold } from 'react-icons/pi';
import type { ScanFolderResponse } from 'services/api/endpoints/models'; import { type ScanFolderResponse, useInstallModelMutation } from 'services/api/endpoints/models';
import { ScanModelResultItem } from './ScanFolderResultItem'; import { ScanModelResultItem } from './ScanFolderResultItem';
@@ -28,8 +30,9 @@ type ScanModelResultsProps = {
export const ScanModelsResults = ({ results }: ScanModelResultsProps) => { export const ScanModelsResults = ({ results }: ScanModelResultsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const dispatch = useAppDispatch();
const [inplace, setInplace] = useState(true); const [inplace, setInplace] = useState(true);
const [installModel] = useInstallModel(); const [installModel] = useInstallModelMutation();
const filteredResults = useMemo(() => { const filteredResults = useMemo(() => {
return results.filter((result) => { return results.filter((result) => {
@@ -55,15 +58,61 @@ export const ScanModelsResults = ({ results }: ScanModelResultsProps) => {
if (result.is_installed) { if (result.is_installed) {
continue; continue;
} }
installModel({ source: result.path, inplace }); 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',
})
)
);
}
});
} }
}, [filteredResults, installModel, inplace]); }, [filteredResults, installModel, inplace, dispatch, t]);
const handleInstallOne = useCallback( const handleInstallOne = useCallback(
(source: string) => { (source: string) => {
installModel({ source, inplace }); 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, inplace] [installModel, inplace, dispatch, t]
); );
return ( return (

View File

@@ -1,16 +1,20 @@
import { Badge, Box, Flex, IconButton, Text } from '@invoke-ai/ui-library'; import { Badge, Box, Flex, IconButton, Text } from '@invoke-ai/ui-library';
import { useInstallModel } from 'features/modelManagerV2/hooks/useInstallModel'; import { useAppDispatch } from 'app/store/storeHooks';
import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge'; 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 { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi'; import { PiPlusBold } from 'react-icons/pi';
import type { GetStarterModelsResponse } from 'services/api/endpoints/models'; import type { GetStarterModelsResponse } from 'services/api/endpoints/models';
import { useInstallModelMutation } from 'services/api/endpoints/models';
type Props = { type Props = {
result: GetStarterModelsResponse[number]; result: GetStarterModelsResponse[number];
}; };
export const StarterModelsResultItem = ({ result }: Props) => { export const StarterModelsResultItem = ({ result }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const allSources = useMemo(() => { const allSources = useMemo(() => {
const _allSources = [result.source]; const _allSources = [result.source];
if (result.dependencies) { if (result.dependencies) {
@@ -18,13 +22,36 @@ export const StarterModelsResultItem = ({ result }: Props) => {
} }
return _allSources; return _allSources;
}, [result]); }, [result]);
const [installModel] = useInstallModel(); const [installModel] = useInstallModelMutation();
const onClick = useCallback(() => { const handleQuickAdd = useCallback(() => {
for (const source of allSources) { for (const source of allSources) {
installModel({ source }); 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',
})
)
);
}
});
} }
}, [allSources, installModel]); }, [allSources, installModel, dispatch, t]);
return ( return (
<Flex alignItems="center" justifyContent="space-between" w="100%" gap={3}> <Flex alignItems="center" justifyContent="space-between" w="100%" gap={3}>
@@ -40,7 +67,7 @@ export const StarterModelsResultItem = ({ result }: Props) => {
{result.is_installed ? ( {result.is_installed ? (
<Badge>{t('common.installed')}</Badge> <Badge>{t('common.installed')}</Badge>
) : ( ) : (
<IconButton aria-label={t('modelManager.install')} icon={<PiPlusBold />} onClick={onClick} size="sm" /> <IconButton aria-label={t('modelManager.install')} icon={<PiPlusBold />} onClick={handleQuickAdd} size="sm" />
)} )}
</Box> </Box>
</Flex> </Flex>

View File

@@ -4,7 +4,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge'; import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
import ModelFormatBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge'; import ModelFormatBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -52,19 +53,25 @@ const ModelListItem = (props: ModelListItemProps) => {
deleteModel({ key: model.key }) deleteModel({ key: model.key })
.unwrap() .unwrap()
.then((_) => { .then((_) => {
toast({ dispatch(
id: 'MODEL_DELETED', addToast(
title: `${t('modelManager.modelDeleted')}: ${model.name}`, makeToast({
status: 'success', title: `${t('modelManager.modelDeleted')}: ${model.name}`,
}); status: 'success',
})
)
);
}) })
.catch((error) => { .catch((error) => {
if (error) { if (error) {
toast({ dispatch(
id: 'MODEL_DELETE_FAILED', addToast(
title: `${t('modelManager.modelDeleteFailed')}: ${model.name}`, makeToast({
status: 'error', title: `${t('modelManager.modelDeleteFailed')}: ${model.name}`,
}); status: 'error',
})
)
);
} }
}); });
dispatch(setSelectedModelKey(null)); dispatch(setSelectedModelKey(null));

View File

@@ -1,9 +1,10 @@
import { Button, Flex, Heading, SimpleGrid, Text } from '@invoke-ai/ui-library'; import { Button, Flex, Heading, SimpleGrid, Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useControlNetOrT2IAdapterDefaultSettings } from 'features/modelManagerV2/hooks/useControlNetOrT2IAdapterDefaultSettings'; import { useControlNetOrT2IAdapterDefaultSettings } from 'features/modelManagerV2/hooks/useControlNetOrT2IAdapterDefaultSettings';
import { DefaultPreprocessor } from 'features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/DefaultPreprocessor'; import { DefaultPreprocessor } from 'features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/DefaultPreprocessor';
import type { FormField } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings'; import type { FormField } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { useCallback } from 'react'; import { useCallback } from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@@ -18,6 +19,7 @@ export type ControlNetOrT2IAdapterDefaultSettingsFormData = {
export const ControlNetOrT2IAdapterDefaultSettings = () => { export const ControlNetOrT2IAdapterDefaultSettings = () => {
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const { defaultSettingsDefaults, isLoading: isLoadingDefaultSettings } = const { defaultSettingsDefaults, isLoading: isLoadingDefaultSettings } =
useControlNetOrT2IAdapterDefaultSettings(selectedModelKey); useControlNetOrT2IAdapterDefaultSettings(selectedModelKey);
@@ -44,24 +46,30 @@ export const ControlNetOrT2IAdapterDefaultSettings = () => {
}) })
.unwrap() .unwrap()
.then((_) => { .then((_) => {
toast({ dispatch(
id: 'DEFAULT_SETTINGS_SAVED', addToast(
title: t('modelManager.defaultSettingsSaved'), makeToast({
status: 'success', title: t('modelManager.defaultSettingsSaved'),
}); status: 'success',
})
)
);
reset(data); reset(data);
}) })
.catch((error) => { .catch((error) => {
if (error) { if (error) {
toast({ dispatch(
id: 'DEFAULT_SETTINGS_SAVE_FAILED', addToast(
title: `${error.data.detail} `, makeToast({
status: 'error', title: `${error.data.detail} `,
}); status: 'error',
})
)
);
} }
}); });
}, },
[selectedModelKey, reset, updateModel, t] [selectedModelKey, dispatch, reset, updateModel, t]
); );
if (isLoadingDefaultSettings) { if (isLoadingDefaultSettings) {

View File

@@ -1,6 +1,8 @@
import { Box, Button, Flex, Icon, IconButton, Image, Tooltip } from '@invoke-ai/ui-library'; 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 { typedMemo } from 'common/util/typedMemo';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -13,6 +15,7 @@ type Props = {
}; };
const ModelImageUpload = ({ model_key, model_image }: Props) => { const ModelImageUpload = ({ model_key, model_image }: Props) => {
const dispatch = useAppDispatch();
const [image, setImage] = useState<string | null>(model_image || null); const [image, setImage] = useState<string | null>(model_image || null);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -31,21 +34,27 @@ const ModelImageUpload = ({ model_key, model_image }: Props) => {
.unwrap() .unwrap()
.then(() => { .then(() => {
setImage(URL.createObjectURL(file)); setImage(URL.createObjectURL(file));
toast({ dispatch(
id: 'MODEL_IMAGE_UPDATED', addToast(
title: t('modelManager.modelImageUpdated'), makeToast({
status: 'success', title: t('modelManager.modelImageUpdated'),
}); status: 'success',
})
)
);
}) })
.catch(() => { .catch((_) => {
toast({ dispatch(
id: 'MODEL_IMAGE_UPDATE_FAILED', addToast(
title: t('modelManager.modelImageUpdateFailed'), makeToast({
status: 'error', title: t('modelManager.modelImageUpdateFailed'),
}); status: 'error',
})
)
);
}); });
}, },
[model_key, t, updateModelImage] [dispatch, model_key, t, updateModelImage]
); );
const handleResetImage = useCallback(() => { const handleResetImage = useCallback(() => {
@@ -56,20 +65,26 @@ const ModelImageUpload = ({ model_key, model_image }: Props) => {
deleteModelImage(model_key) deleteModelImage(model_key)
.unwrap() .unwrap()
.then(() => { .then(() => {
toast({ dispatch(
id: 'MODEL_IMAGE_DELETED', addToast(
title: t('modelManager.modelImageDeleted'), makeToast({
status: 'success', title: t('modelManager.modelImageDeleted'),
}); status: 'success',
})
)
);
}) })
.catch(() => { .catch((_) => {
toast({ dispatch(
id: 'MODEL_IMAGE_DELETE_FAILED', addToast(
title: t('modelManager.modelImageDeleteFailed'), makeToast({
status: 'error', title: t('modelManager.modelImageDeleteFailed'),
}); status: 'error',
})
)
);
}); });
}, [model_key, t, deleteModelImage]); }, [dispatch, model_key, t, deleteModelImage]);
const { getInputProps, getRootProps } = useDropzone({ const { getInputProps, getRootProps } = useDropzone({
accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] }, accept: { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'] },

View File

@@ -1,10 +1,11 @@
import { Button, Flex, Heading, SimpleGrid, Text } from '@invoke-ai/ui-library'; import { Button, Flex, Heading, SimpleGrid, Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useMainModelDefaultSettings } from 'features/modelManagerV2/hooks/useMainModelDefaultSettings'; import { useMainModelDefaultSettings } from 'features/modelManagerV2/hooks/useMainModelDefaultSettings';
import { DefaultHeight } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultHeight'; import { DefaultHeight } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultHeight';
import { DefaultWidth } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultWidth'; import { DefaultWidth } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultWidth';
import type { ParameterScheduler } from 'features/parameters/types/parameterSchemas'; import type { ParameterScheduler } from 'features/parameters/types/parameterSchemas';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { useCallback } from 'react'; import { useCallback } from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@@ -38,6 +39,7 @@ export type MainModelDefaultSettingsFormData = {
export const MainModelDefaultSettings = () => { export const MainModelDefaultSettings = () => {
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const { const {
defaultSettingsDefaults, defaultSettingsDefaults,
@@ -74,24 +76,30 @@ export const MainModelDefaultSettings = () => {
}) })
.unwrap() .unwrap()
.then((_) => { .then((_) => {
toast({ dispatch(
id: 'DEFAULT_SETTINGS_SAVED', addToast(
title: t('modelManager.defaultSettingsSaved'), makeToast({
status: 'success', title: t('modelManager.defaultSettingsSaved'),
}); status: 'success',
})
)
);
reset(data); reset(data);
}) })
.catch((error) => { .catch((error) => {
if (error) { if (error) {
toast({ dispatch(
id: 'DEFAULT_SETTINGS_SAVE_FAILED', addToast(
title: `${error.data.detail} `, makeToast({
status: 'error', title: `${error.data.detail} `,
}); status: 'error',
})
)
);
} }
}); });
}, },
[selectedModelKey, reset, updateModel, t] [selectedModelKey, dispatch, reset, updateModel, t]
); );
if (isLoadingDefaultSettings) { if (isLoadingDefaultSettings) {

View File

@@ -4,7 +4,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice';
import { ModelConvertButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton'; import { ModelConvertButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton';
import { ModelEditButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelEditButton'; import { ModelEditButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelEditButton';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { useCallback } from 'react'; import { useCallback } from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@@ -46,19 +47,25 @@ export const Model = () => {
.then((payload) => { .then((payload) => {
form.reset(payload, { keepDefaultValues: true }); form.reset(payload, { keepDefaultValues: true });
dispatch(setSelectedModelMode('view')); dispatch(setSelectedModelMode('view'));
toast({ dispatch(
id: 'MODEL_UPDATED', addToast(
title: t('modelManager.modelUpdated'), makeToast({
status: 'success', title: t('modelManager.modelUpdated'),
}); status: 'success',
})
)
);
}) })
.catch((_) => { .catch((_) => {
form.reset(); form.reset();
toast({ dispatch(
id: 'MODEL_UPDATE_FAILED', addToast(
title: t('modelManager.modelUpdateFailed'), makeToast({
status: 'error', title: t('modelManager.modelUpdateFailed'),
}); status: 'error',
})
)
);
}); });
}, },
[dispatch, data?.key, form, t, updateModel] [dispatch, data?.key, form, t, updateModel]

View File

@@ -9,7 +9,9 @@ import {
useDisclosure, useDisclosure,
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query'; import { skipToken } from '@reduxjs/toolkit/query';
import { toast } from 'features/toast/toast'; import { useAppDispatch } from 'app/store/storeHooks';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useConvertModelMutation, useGetModelConfigQuery } from 'services/api/endpoints/models'; import { useConvertModelMutation, useGetModelConfigQuery } from 'services/api/endpoints/models';
@@ -20,6 +22,7 @@ interface ModelConvertProps {
export const ModelConvertButton = (props: ModelConvertProps) => { export const ModelConvertButton = (props: ModelConvertProps) => {
const { modelKey } = props; const { modelKey } = props;
const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const { data } = useGetModelConfigQuery(modelKey ?? skipToken); const { data } = useGetModelConfigQuery(modelKey ?? skipToken);
const [convertModel, { isLoading }] = useConvertModelMutation(); const [convertModel, { isLoading }] = useConvertModelMutation();
@@ -30,26 +33,38 @@ export const ModelConvertButton = (props: ModelConvertProps) => {
return; return;
} }
const toastId = `CONVERTING_MODEL_${data.key}`; dispatch(
toast({ addToast(
id: toastId, makeToast({
title: `${t('modelManager.convertingModelBegin')}: ${data?.name}`, title: `${t('modelManager.convertingModelBegin')}: ${data?.name}`,
status: 'info', status: 'info',
}); })
)
);
convertModel(data?.key) convertModel(data?.key)
.unwrap() .unwrap()
.then(() => { .then(() => {
toast({ id: toastId, title: `${t('modelManager.modelConverted')}: ${data?.name}`, status: 'success' }); dispatch(
addToast(
makeToast({
title: `${t('modelManager.modelConverted')}: ${data?.name}`,
status: 'success',
})
)
);
}) })
.catch(() => { .catch(() => {
toast({ dispatch(
id: toastId, addToast(
title: `${t('modelManager.modelConversionFailed')}: ${data?.name}`, makeToast({
status: 'error', title: `${t('modelManager.modelConversionFailed')}: ${data?.name}`,
}); status: 'error',
})
)
);
}); });
}, [data, isLoading, t, convertModel]); }, [data, isLoading, dispatch, t, convertModel]);
if (data?.format !== 'checkpoint') { if (data?.format !== 'checkpoint') {
return; return;

View File

@@ -3,6 +3,7 @@ import 'reactflow/dist/style.css';
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } from '@invoke-ai/ui-library'; import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { useAppToaster } from 'app/components/Toaster';
import { useAppDispatch, useAppStore } from 'app/store/storeHooks'; import { useAppDispatch, useAppStore } from 'app/store/storeHooks';
import type { SelectInstance } from 'chakra-react-select'; import type { SelectInstance } from 'chakra-react-select';
import { useBuildNode } from 'features/nodes/hooks/useBuildNode'; import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
@@ -23,7 +24,6 @@ import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes'; import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
import type { AnyNode } from 'features/nodes/types/invocation'; import type { AnyNode } from 'features/nodes/types/invocation';
import { isInvocationNode } 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 { filter, map, memoize, some } from 'lodash-es';
import { memo, useCallback, useMemo, useRef } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react';
import { flushSync } from 'react-dom'; import { flushSync } from 'react-dom';
@@ -60,6 +60,7 @@ const filterOption = memoize((option: FilterOptionOption<ComboboxOption>, inputV
const AddNodePopover = () => { const AddNodePopover = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const buildInvocation = useBuildNode(); const buildInvocation = useBuildNode();
const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
const selectRef = useRef<SelectInstance<ComboboxOption> | null>(null); const selectRef = useRef<SelectInstance<ComboboxOption> | null>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@@ -126,7 +127,7 @@ const AddNodePopover = () => {
const errorMessage = t('nodes.unknownNode', { const errorMessage = t('nodes.unknownNode', {
nodeType: nodeType, nodeType: nodeType,
}); });
toast({ toaster({
status: 'error', status: 'error',
title: errorMessage, title: errorMessage,
}); });
@@ -162,7 +163,7 @@ const AddNodePopover = () => {
} }
return node; return node;
}, },
[buildInvocation, store, dispatch, t] [buildInvocation, store, dispatch, t, toaster]
); );
const onChange = useCallback<ComboboxOnChange>( const onChange = useCallback<ComboboxOnChange>(

View File

@@ -1,7 +1,7 @@
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library'; import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper'; import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck'; import { useAnyOrDirectInputFieldNames } from 'features/nodes/hooks/useAnyOrDirectInputFieldNames';
import { useFieldNames } from 'features/nodes/hooks/useFieldNames'; import { useConnectionInputFieldNames } from 'features/nodes/hooks/useConnectionInputFieldNames';
import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames'; import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
import { useWithFooter } from 'features/nodes/hooks/useWithFooter'; import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
import { memo } from 'react'; import { memo } from 'react';
@@ -20,7 +20,8 @@ type Props = {
}; };
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
const fieldNames = useFieldNames(nodeId); const inputConnectionFieldNames = useConnectionInputFieldNames(nodeId);
const inputAnyOrDirectFieldNames = useAnyOrDirectInputFieldNames(nodeId);
const withFooter = useWithFooter(nodeId); const withFooter = useWithFooter(nodeId);
const outputFieldNames = useOutputFieldNames(nodeId); const outputFieldNames = useOutputFieldNames(nodeId);
@@ -40,11 +41,9 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
> >
<Flex flexDir="column" px={2} w="full" h="full"> <Flex flexDir="column" px={2} w="full" h="full">
<Grid gridTemplateColumns="1fr auto" gridAutoRows="1fr"> <Grid gridTemplateColumns="1fr auto" gridAutoRows="1fr">
{fieldNames.connectionFields.map((fieldName, i) => ( {inputConnectionFieldNames.map((fieldName, i) => (
<GridItem gridColumnStart={1} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.input-field`}> <GridItem gridColumnStart={1} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.input-field`}>
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}> <InputField nodeId={nodeId} fieldName={fieldName} />
<InputField nodeId={nodeId} fieldName={fieldName} />
</InvocationInputFieldCheck>
</GridItem> </GridItem>
))} ))}
{outputFieldNames.map((fieldName, i) => ( {outputFieldNames.map((fieldName, i) => (
@@ -53,23 +52,8 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
</GridItem> </GridItem>
))} ))}
</Grid> </Grid>
{fieldNames.anyOrDirectFields.map((fieldName) => ( {inputAnyOrDirectFieldNames.map((fieldName) => (
<InvocationInputFieldCheck <InputField key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName} />
key={`${nodeId}.${fieldName}.input-field`}
nodeId={nodeId}
fieldName={fieldName}
>
<InputField nodeId={nodeId} fieldName={fieldName} />
</InvocationInputFieldCheck>
))}
{fieldNames.missingFields.map((fieldName) => (
<InvocationInputFieldCheck
key={`${nodeId}.${fieldName}.input-field`}
nodeId={nodeId}
fieldName={fieldName}
>
<InputField nodeId={nodeId} fieldName={fieldName} />
</InvocationInputFieldCheck>
))} ))}
</Flex> </Flex>
</Flex> </Flex>

View File

@@ -0,0 +1,20 @@
import { useDoesFieldExist } from 'features/nodes/hooks/useDoesFieldExist';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
type Props = PropsWithChildren<{
nodeId: string;
fieldName?: string;
}>;
export const MissingFallback = memo((props: Props) => {
// We must be careful here to avoid race conditions where a deleted node is still referenced as an exposed field
const exists = useDoesFieldExist(props.nodeId, props.fieldName);
if (!exists) {
return null;
}
return props.children;
});
MissingFallback.displayName = 'MissingFallback';

View File

@@ -1,14 +1,16 @@
import { Flex, FormControl } from '@invoke-ai/ui-library'; import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState'; import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue'; import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate'; import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import type { PropsWithChildren } from 'react';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import EditableFieldTitle from './EditableFieldTitle'; import EditableFieldTitle from './EditableFieldTitle';
import FieldHandle from './FieldHandle'; import FieldHandle from './FieldHandle';
import FieldLinearViewToggle from './FieldLinearViewToggle'; import FieldLinearViewToggle from './FieldLinearViewToggle';
import InputFieldRenderer from './InputFieldRenderer'; import InputFieldRenderer from './InputFieldRenderer';
import { InputFieldWrapper } from './InputFieldWrapper';
interface Props { interface Props {
nodeId: string; nodeId: string;
@@ -16,7 +18,9 @@ interface Props {
} }
const InputField = ({ nodeId, fieldName }: Props) => { const InputField = ({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName); const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
const fieldInstance = useFieldInputInstance(nodeId, fieldName);
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName); const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
@@ -51,6 +55,20 @@ const InputField = ({ nodeId, fieldName }: Props) => {
setIsHovered(false); setIsHovered(false);
}, []); }, []);
if (!fieldTemplate || !fieldInstance) {
return (
<InputFieldWrapper shouldDim={shouldDim}>
<FormControl alignItems="stretch" justifyContent="space-between" flexDir="column" gap={2} h="full" w="full">
<FormLabel display="flex" alignItems="center" mb={0} px={1} gap={2} h="full">
{t('nodes.unknownInput', {
name: fieldInstance?.label ?? fieldTemplate?.title ?? fieldName,
})}
</FormLabel>
</FormControl>
</InputFieldWrapper>
);
}
if (fieldTemplate.input === 'connection' || isConnected) { if (fieldTemplate.input === 'connection' || isConnected) {
return ( return (
<InputFieldWrapper shouldDim={shouldDim}> <InputFieldWrapper shouldDim={shouldDim}>
@@ -116,3 +134,27 @@ const InputField = ({ nodeId, fieldName }: Props) => {
}; };
export default memo(InputField); export default memo(InputField);
type InputFieldWrapperProps = PropsWithChildren<{
shouldDim: boolean;
}>;
const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => {
return (
<Flex
position="relative"
minH={8}
py={0.5}
alignItems="center"
opacity={shouldDim ? 0.5 : 1}
transitionProperty="opacity"
transitionDuration="0.1s"
w="full"
h="full"
>
{children}
</Flex>
);
});
InputFieldWrapper.displayName = 'InputFieldWrapper';

View File

@@ -1,27 +0,0 @@
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 (
<Flex
position="relative"
minH={8}
py={0.5}
alignItems="center"
opacity={shouldDim ? 0.5 : 1}
transitionProperty="opacity"
transitionDuration="0.1s"
w="full"
h="full"
>
{children}
</Flex>
);
});
InputFieldWrapper.displayName = 'InputFieldWrapper';

View File

@@ -1,59 +0,0 @@
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 (
<Flex position="relative" minH={8} py={0.5} alignItems="center" w="full" h="full">
<FormControl
isInvalid={true}
alignItems="stretch"
justifyContent="center"
flexDir="column"
gap={2}
h="full"
w="full"
>
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
{t('nodes.unknownInput', { name })}
</FormLabel>
</FormControl>
</Flex>
);
}
return children;
});
InvocationInputFieldCheck.displayName = 'InvocationInputFieldCheck';

View File

@@ -3,7 +3,7 @@ import { CSS } from '@dnd-kit/utilities';
import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library'; import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay'; import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck'; import { MissingFallback } from 'features/nodes/components/flow/nodes/Invocation/MissingFallback';
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue'; import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice'; import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
@@ -102,9 +102,9 @@ const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => {
const LinearViewField = ({ nodeId, fieldName }: Props) => { const LinearViewField = ({ nodeId, fieldName }: Props) => {
return ( return (
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}> <MissingFallback nodeId={nodeId} fieldName={fieldName}>
<LinearViewFieldInternal nodeId={nodeId} fieldName={fieldName} /> <LinearViewFieldInternal nodeId={nodeId} fieldName={fieldName} />
</InvocationInputFieldCheck> </MissingFallback>
); );
}; };

View File

@@ -48,7 +48,7 @@ const NotesNode = (props: NodeProps<NotesNodeData>) => {
gap={1} gap={1}
> >
<Flex className="nopan" w="full" h="full" flexDir="column"> <Flex className="nopan" w="full" h="full" flexDir="column">
<Textarea className="nodrag" value={notes} onChange={handleChange} rows={8} resize="none" fontSize="sm" /> <Textarea value={notes} onChange={handleChange} rows={8} resize="none" fontSize="sm" />
</Flex> </Flex>
</Flex> </Flex>
</> </>

View File

@@ -1,7 +1,8 @@
import { ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library'; import { ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { toast } from 'features/toast/toast'; import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiTrashSimpleFill } from 'react-icons/pi'; import { PiTrashSimpleFill } from 'react-icons/pi';
@@ -15,11 +16,14 @@ const ClearFlowButton = () => {
const handleNewWorkflow = useCallback(() => { const handleNewWorkflow = useCallback(() => {
dispatch(nodeEditorReset()); dispatch(nodeEditorReset());
toast({ dispatch(
id: 'WORKFLOW_CLEARED', addToast(
title: t('workflows.workflowCleared'), makeToast({
status: 'success', title: t('workflows.workflowCleared'),
}); status: 'success',
})
)
);
onClose(); onClose();
}, [dispatch, onClose, t]); }, [dispatch, onClose, t]);

View File

@@ -1,7 +1,7 @@
import { Flex, FormLabel, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library'; import { Flex, FormLabel, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
import FieldTooltipContent from 'features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent'; import FieldTooltipContent from 'features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent';
import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer'; import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck'; import { MissingFallback } from 'features/nodes/components/flow/nodes/Invocation/MissingFallback';
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel'; import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue'; import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle'; import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
@@ -53,9 +53,9 @@ const WorkflowFieldInternal = ({ nodeId, fieldName }: Props) => {
const WorkflowField = ({ nodeId, fieldName }: Props) => { const WorkflowField = ({ nodeId, fieldName }: Props) => {
return ( return (
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}> <MissingFallback nodeId={nodeId} fieldName={fieldName}>
<WorkflowFieldInternal nodeId={nodeId} fieldName={fieldName} /> <WorkflowFieldInternal nodeId={nodeId} fieldName={fieldName} />
</InvocationInputFieldCheck> </MissingFallback>
); );
}; };

View File

@@ -0,0 +1,27 @@
import { EMPTY_ARRAY } from 'app/store/constants';
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import { isSingleOrCollection } from 'features/nodes/types/field';
import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
import { keys, map } from 'lodash-es';
import { useMemo } from 'react';
export const useAnyOrDirectInputFieldNames = (nodeId: string): string[] => {
const template = useNodeTemplate(nodeId);
const fieldNames = useMemo(() => {
const fields = map(template.inputs).filter((field) => {
return (
(['any', 'direct'].includes(field.input) || isSingleOrCollection(field.type)) &&
keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
);
});
const _fieldNames = getSortedFilteredFieldNames(fields);
if (_fieldNames.length === 0) {
return EMPTY_ARRAY;
}
return _fieldNames;
}, [template.inputs]);
return fieldNames;
};

View File

@@ -0,0 +1,29 @@
import { EMPTY_ARRAY } from 'app/store/constants';
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import { isSingleOrCollection } from 'features/nodes/types/field';
import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
import { keys, map } from 'lodash-es';
import { useMemo } from 'react';
export const useConnectionInputFieldNames = (nodeId: string): string[] => {
const template = useNodeTemplate(nodeId);
const fieldNames = useMemo(() => {
// get the visible fields
const fields = map(template.inputs).filter(
(field) =>
(field.input === 'connection' && !isSingleOrCollection(field.type)) ||
!keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
);
const _fieldNames = getSortedFilteredFieldNames(fields);
if (_fieldNames.length === 0) {
return EMPTY_ARRAY;
}
return _fieldNames;
}, [template.inputs]);
return fieldNames;
};

View File

@@ -0,0 +1,20 @@
import { useAppSelector } from 'app/store/storeHooks';
import { isInvocationNode } from 'features/nodes/types/invocation';
export const useDoesFieldExist = (nodeId: string, fieldName?: string) => {
const doesFieldExist = useAppSelector((s) => {
const node = s.nodes.present.nodes.find((n) => n.id === nodeId);
if (!isInvocationNode(node)) {
return false;
}
if (fieldName === undefined) {
return true;
}
if (!node.data.inputs[fieldName]) {
return false;
}
return true;
});
return doesFieldExist;
};

View File

@@ -1,14 +1,9 @@
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import type { FieldInputTemplate } from 'features/nodes/types/field'; import type { FieldInputTemplate } from 'features/nodes/types/field';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { assert } from 'tsafe';
export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate => { export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate | null => {
const template = useNodeTemplate(nodeId); const template = useNodeTemplate(nodeId);
const fieldTemplate = useMemo(() => { const fieldTemplate = useMemo(() => template.inputs[fieldName] ?? null, [fieldName, template.inputs]);
const _fieldTemplate = template.inputs[fieldName];
assert(_fieldTemplate, `Field template for field ${fieldName} not found`);
return _fieldTemplate;
}, [fieldName, template.inputs]);
return fieldTemplate; return fieldTemplate;
}; };

View File

@@ -1,39 +0,0 @@
import { useNodeData } from 'features/nodes/hooks/useNodeData';
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import type { FieldInputTemplate } from 'features/nodes/types/field';
import { isSingleOrCollection } from 'features/nodes/types/field';
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
import { difference, filter, keys } from 'lodash-es';
import { useMemo } from 'react';
const isConnectionInputField = (field: FieldInputTemplate) => {
return (
(field.input === 'connection' && !isSingleOrCollection(field.type)) ||
!keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
);
};
const isAnyOrDirectInputField = (field: FieldInputTemplate) => {
return (
(['any', 'direct'].includes(field.input) || isSingleOrCollection(field.type)) &&
keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
);
};
export const useFieldNames = (nodeId: string) => {
const template = useNodeTemplate(nodeId);
const node = useNodeData(nodeId);
const fieldNames = useMemo(() => {
const instanceFields = keys(node.inputs);
const allTemplateFields = keys(template.inputs);
const missingFields = difference(instanceFields, allTemplateFields);
const connectionFields = filter(template.inputs, isConnectionInputField).map((f) => f.name);
const anyOrDirectFields = filter(template.inputs, isAnyOrDirectInputField).map((f) => f.name);
return {
missingFields,
connectionFields,
anyOrDirectFields,
};
}, [node.inputs, template.inputs]);
return fieldNames;
};

View File

@@ -1,14 +1,14 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeData } from 'features/nodes/store/selectors';
import { useMemo } from 'react'; import { useMemo } from 'react';
export const useNodeLabel = (nodeId: string) => { export const useNodeLabel = (nodeId: string) => {
const selector = useMemo( const selector = useMemo(
() => () =>
createSelector(selectNodesSlice, (nodesSlice) => { createSelector(selectNodesSlice, (nodes) => {
const node = nodesSlice.nodes.find((node) => node.id === nodeId); return selectNodeData(nodes, nodeId)?.label ?? null;
return node?.data.label;
}), }),
[nodeId] [nodeId]
); );

View File

@@ -1,24 +1,8 @@
import { useStore } from '@nanostores/react'; import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react'; import { useMemo } from 'react';
export const useNodeTemplateTitle = (nodeId: string): string | null => { export const useNodeTemplateTitle = (nodeId: string): string | null => {
const templates = useStore($templates); const template = useNodeTemplate(nodeId);
const selector = useMemo( const title = useMemo(() => template.title, [template.title]);
() =>
createSelector(selectNodesSlice, (nodesSlice) => {
const node = nodesSlice.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return null;
}
const template = templates[node.data.type];
return template?.title ?? null;
}),
[nodeId, templates]
);
const title = useAppSelector(selector);
return title; return title;
}; };

View File

@@ -275,9 +275,10 @@ export const nodesSlice = createSlice({
const { nodeId, label } = action.payload; const { nodeId, label } = action.payload;
const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId); const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
const node = state.nodes?.[nodeIndex]; const node = state.nodes?.[nodeIndex];
if (isInvocationNode(node) || isNotesNode(node)) { if (!isInvocationNode(node)) {
node.data.label = label; return;
} }
node.data.label = label;
}, },
nodeNotesChanged: (state, action: PayloadAction<{ nodeId: string; notes: string }>) => { nodeNotesChanged: (state, action: PayloadAction<{ nodeId: string; notes: string }>) => {
const { nodeId, notes } = action.payload; const { nodeId, notes } = action.payload;

View File

@@ -4,7 +4,7 @@ import type { InvocationNode, InvocationNodeData } from 'features/nodes/types/in
import { isInvocationNode } from 'features/nodes/types/invocation'; import { isInvocationNode } from 'features/nodes/types/invocation';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
export const selectInvocationNode = (nodesSlice: NodesState, nodeId: string): InvocationNode => { const selectInvocationNode = (nodesSlice: NodesState, nodeId: string): InvocationNode => {
const node = nodesSlice.nodes.find((node) => node.id === nodeId); const node = nodesSlice.nodes.find((node) => node.id === nodeId);
assert(isInvocationNode(node), `Node ${nodeId} is not an invocation node`); assert(isInvocationNode(node), `Node ${nodeId} is not an invocation node`);
return node; return node;

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