mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-31 02:17:58 -05:00
Compare commits
2 Commits
psyche/fea
...
maryhipp/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caa74f070c | ||
|
|
aca7fb3aad |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
44
invokeai/frontend/web/src/app/components/Toaster.ts
Normal file
44
invokeai/frontend/web/src/app/components/Toaster.ts
Normal 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);
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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' }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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') },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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' }) })));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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})`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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})`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
6
invokeai/frontend/web/src/common/util/toast.ts
Normal file
6
invokeai/frontend/web/src/common/util/toast.ts
Normal 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,
|
||||||
|
});
|
||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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' && (
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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'] },
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
|
||||||
@@ -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';
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -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]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user